Skip to content

Commit abd2528

Browse files
authored
feat: Supports searching webview content and quickly opening documents from code (#144)
* fix: styles * feat: 文档增强-可通过hover代码中的API打开API文档 * fix: 优化敏感信息 Linter * feat: 支持搜索并高亮定位webview中的内容 * test: 新增alicloud.api.quickOpenDocument命令测试 * test: 新增alicloud.api.quickOpenDocument命令测试
1 parent 4968ed4 commit abd2528

File tree

11 files changed

+338
-78
lines changed

11 files changed

+338
-78
lines changed

media/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,25 @@
1717
"@monaco-editor/react": "^4.6.0",
1818
"@vercel/ncc": "^0.38.1",
1919
"@vscode-elements/elements": "^1.3.0",
20+
"ahooks": "3.8.1",
2021
"antd": "^5.12.3",
2122
"intl-format": "^1.2.0",
2223
"load-script": "^2.0.0",
2324
"lodash": "^4.17.20",
2425
"next": "^14.2.6",
26+
"pontx-semix-table": "0.5.3",
2527
"pontx-ui": "latest",
2628
"react": "^18.3.1",
2729
"react-copy-to-clipboard": "^5.1.0",
2830
"react-dom": "^18.3.1",
2931
"react-use-observer": "^2.2.4",
3032
"sass": "^1.77.8",
31-
"semix-schema-table": "^0.1.3",
33+
"semix-schema-table": "0.1.3",
3234
"styled-components": "^6.1.1",
3335
"xml2js": "^0.6.2"
3436
},
3537
"resolutions": {
36-
"semix-schema-table": "^0.1.3"
38+
"semix-schema-table": "0.1.3"
3739
},
3840
"version": "0.3.70",
3941
"repository": "git@github.com:aliyun/alibabacloud-api-vscode-toolkit.git",
@@ -47,7 +49,6 @@
4749
"@types/vscode": "^1.91.0",
4850
"@types/vscode-webview": "^1.57.5",
4951
"@vitejs/plugin-react": "^1.3.2",
50-
"ahooks": "^3.8.1",
5152
"autoprefixer": "^10.0.1",
5253
"eslint": "^8",
5354
"eslint-config-next": "14.2.5",

media/src/components/APIPage/API.tsx

Lines changed: 84 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import TrySDK from "./TrySDK/TrySDK";
2121
import { APIPageContext } from "./context";
2222
import { PontUIService } from "../../service/UIService";
2323
import ApiResponseDoc from "./APIDocument/ApiResponseDoc";
24+
import Searcher from "../common/Searcher";
2425

2526
export class APIProps {
2627
selectedApi?: PontSpec.PontAPI;
@@ -228,70 +229,98 @@ export const API: React.FC<APIProps> = (props) => {
228229
});
229230
}, []);
230231

232+
const contentRef = React.useRef(null);
233+
234+
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
235+
236+
const handleKeyDown = (event) => {
237+
if ((event.ctrlKey || event.metaKey) && event.key === "f") {
238+
setIsSearchVisible((prev) => !prev); // 切换 DOM 可见性
239+
const input = document.getElementById("page-search-input");
240+
input.focus();
241+
}
242+
};
243+
244+
React.useEffect(() => {
245+
window.addEventListener("keydown", handleKeyDown);
246+
247+
// 清除事件监听器
248+
return () => {
249+
window.removeEventListener("keydown", handleKeyDown);
250+
};
251+
}, []);
252+
231253
return (
232-
<div className="h-full bg-[var(--vscode-textBlockQuote-background)] pb-4" ref={pageEl}>
233-
{/* */}
234-
<APIPageContext.Provider
235-
initialState={{
236-
apiMeta: selectedApi,
237-
schemaForm: form,
238-
product: props.product,
239-
version: props.version,
240-
mode: mode,
241-
changeMode: changeMode,
242-
}}
243-
>
244-
<RootContext.Provider initialState={initValue}>
245-
{selectedApi ? (
246-
<>
247-
<div className="bg-[var(--vscode-editor-background)] p-4">
248-
<div className="flex justify-between">
249-
<div>
250-
<div className="flex">
251-
{/* {selectedApi.method ? (
254+
<div>
255+
<Searcher
256+
contentRef={contentRef}
257+
isVisible={isSearchVisible}
258+
setIsVisible={setIsSearchVisible}
259+
mode={mode}
260+
></Searcher>
261+
<div className="bg-[var(--vscode-textBlockQuote-background)] pb-4" ref={contentRef}>
262+
<APIPageContext.Provider
263+
initialState={{
264+
apiMeta: selectedApi,
265+
schemaForm: form,
266+
product: props.product,
267+
version: props.version,
268+
mode: mode,
269+
changeMode: changeMode,
270+
}}
271+
>
272+
<RootContext.Provider initialState={initValue}>
273+
{selectedApi ? (
274+
<>
275+
<div className="bg-[var(--vscode-editor-background)] p-4">
276+
<div className="flex justify-between">
277+
<div>
278+
<div className="flex">
279+
{/* {selectedApi.method ? (
252280
<div className="h-6 w-16 rounded-sm border-2 border-solid border-emerald-100 bg-emerald-100 text-center text-base font-medium leading-5 text-teal-500 ">
253281
{selectedApi.method?.toUpperCase()}
254282
</div>
255283
) : null} */}
256-
{selectedApi.deprecated ? (
257-
<Tag className="my-auto ml-2" color="var(--vscode-textSeparator-foreground)">
258-
<span className="text-[$primary-2-font-color]">deprecated</span>
259-
</Tag>
260-
) : null}
261-
<div className="my-auto ml-2 text-base font-medium text-[var(--vscode-editorWidget-foreground)]">
262-
{apiNameEle}
263-
{selectedApi?.title ? <span> - {selectedApi.title}</span> : null}
284+
{selectedApi.deprecated ? (
285+
<Tag className="my-auto ml-2" color="var(--vscode-textSeparator-foreground)">
286+
<span className="text-[$primary-2-font-color]">deprecated</span>
287+
</Tag>
288+
) : null}
289+
<div className="my-auto ml-2 text-base font-medium text-[var(--vscode-editorWidget-foreground)]">
290+
{apiNameEle}
291+
{selectedApi?.title ? <span> - {selectedApi.title}</span> : null}
292+
</div>
264293
</div>
294+
{selectedApi?.summary ? (
295+
<div
296+
className="ml-2 py-2 text-sm font-normal text-[$primary-2-font-color] opacity-70"
297+
style={{ width: "100%" }}
298+
>
299+
{selectedApi?.summary}
300+
</div>
301+
) : null}
302+
</div>
303+
<div className="my-auto">
304+
<Segmented
305+
className="document-segmented"
306+
value={mode}
307+
onChange={(val) => changeMode(val)}
308+
options={tabs.map((teb) => {
309+
return {
310+
label: teb.tab,
311+
value: teb.key,
312+
};
313+
})}
314+
></Segmented>
265315
</div>
266-
{selectedApi?.summary ? (
267-
<div
268-
className="ml-2 py-2 text-sm font-normal text-[$primary-2-font-color] opacity-70"
269-
style={{ width: "100%" }}
270-
>
271-
{selectedApi?.summary}
272-
</div>
273-
) : null}
274-
</div>
275-
<div className="my-auto">
276-
<Segmented
277-
className="document-segmented"
278-
value={mode}
279-
onChange={(val) => changeMode(val)}
280-
options={tabs.map((teb) => {
281-
return {
282-
label: teb.tab,
283-
value: teb.key,
284-
};
285-
})}
286-
></Segmented>
287316
</div>
288317
</div>
289-
</div>
290-
<div className="m-4">{renderContent}</div>
291-
</>
292-
) : null}
293-
</RootContext.Provider>
294-
</APIPageContext.Provider>
318+
<div className="m-4">{renderContent}</div>
319+
</>
320+
) : null}
321+
</RootContext.Provider>
322+
</APIPageContext.Provider>
323+
</div>
295324
</div>
296325
);
297326
};

media/src/components/APIPage/APIDocument/BaseClass.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const BaseClass: React.FC<BaseClassProps> = (props) => {
2121
return (
2222
<div className={classNames("pontx-ui-baseclass", (schema as any)?.type)}>
2323
<div className="header">
24-
<div className="title text-base">
24+
<div className="title p-4 text-base">
2525
数据结构 - {name}
2626
{schema?.templateArgs?.length
2727
? `<${schema?.templateArgs.map((arg, argIndex) => "T" + argIndex).join(", ")}>`
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @author yini-chen
3+
* @description
4+
*/
5+
import { CloseOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
6+
import { Button, Input } from "antd";
7+
import * as React from "react";
8+
9+
export class SearcherProps {
10+
contentRef: any;
11+
isVisible: boolean;
12+
setIsVisible: (visible: boolean) => void;
13+
mode?: string;
14+
}
15+
16+
export const Searcher: React.FC<SearcherProps> = (props) => {
17+
const [searchTerm, setSearchTerm] = React.useState("");
18+
const { contentRef } = props;
19+
const contentElement = contentRef.current;
20+
21+
// 清除高亮
22+
const clearHighlights = () => {
23+
setHighlightDoms([]);
24+
setCurrentIndex(0);
25+
const highlightedElements = document.querySelectorAll('span[style*="background-color: yellow"]');
26+
highlightedElements.forEach((element) => {
27+
const parent = element.parentNode;
28+
if (parent) {
29+
const textNode = document.createTextNode(element.textContent || "");
30+
parent.insertBefore(textNode, element);
31+
parent.removeChild(element);
32+
}
33+
});
34+
};
35+
36+
const [highlightDoms, setHighlightDoms] = React.useState([]);
37+
38+
React.useEffect(() => {
39+
// 清除之前的高亮
40+
clearHighlights();
41+
}, [searchTerm, props.mode]);
42+
43+
const handleSearch = () => {
44+
const matchDoms = [];
45+
function highlightSampleText(searchTerm) {
46+
// 遍历所有节点的递归函数
47+
function traverseNodes(node: Node) {
48+
// 如果是文本节点
49+
if (node.nodeType === Node.TEXT_NODE) {
50+
const textContent = node.textContent || "";
51+
const index = textContent?.toLocaleLowerCase()?.indexOf(searchTerm?.toLocaleLowerCase());
52+
53+
if (index !== -1) {
54+
const span = document.createElement("span");
55+
span.style.backgroundColor = "yellow";
56+
span.style.color = "#59636E";
57+
span.textContent = textContent.substring(index, index + searchTerm.length); // 高亮的文本
58+
span.id = "highlighted-text";
59+
matchDoms.push(span);
60+
61+
// 创建一个新的文本节点,包含高亮部分和其他部分
62+
const beforeText = document.createTextNode(textContent.substring(0, index));
63+
const afterText = document.createTextNode(textContent.substring(index + searchTerm.length));
64+
65+
// 替换当前的文本节点
66+
const parent = node.parentNode;
67+
if (parent) {
68+
parent.insertBefore(beforeText, node);
69+
parent.insertBefore(span, node);
70+
parent.insertBefore(afterText, node);
71+
parent.removeChild(node);
72+
}
73+
}
74+
} else {
75+
// 除文本节点外,递归遍历子节点
76+
node.childNodes.forEach(traverseNodes);
77+
}
78+
}
79+
80+
// 从文档体开始遍历
81+
traverseNodes(contentElement);
82+
setHighlightDoms(matchDoms);
83+
setCurrentIndex(matchDoms?.length ? 1 : 0);
84+
}
85+
if (searchTerm?.length) {
86+
if (highlightDoms?.length) {
87+
// 回车时移动到下一个高亮元素
88+
if (highlightDoms[curIndex]) {
89+
setCurrentIndex(curIndex + 1);
90+
} else {
91+
setCurrentIndex(1);
92+
}
93+
} else {
94+
highlightSampleText(searchTerm);
95+
}
96+
}
97+
};
98+
99+
const [curIndex, setCurrentIndex] = React.useState(highlightDoms?.length ? 1 : 0);
100+
101+
// 滚动到当前高亮元素
102+
React.useEffect(() => {
103+
if (highlightDoms[curIndex - 1]) {
104+
highlightDoms[curIndex - 1].scrollIntoView({ behavior: "smooth", block: "center" });
105+
}
106+
}, [highlightDoms, curIndex]);
107+
108+
if (!props.isVisible) {
109+
return null;
110+
}
111+
112+
return (
113+
<div className="searcher-wrapper fixed right-[24px] top-[74px] z-50 flex rounded-sm bg-[var(--vscode-badge-background)] p-1 shadow-sm shadow-[var(--vscode-badge-background)]">
114+
<Input
115+
style={{ width: "200px" }}
116+
value={searchTerm}
117+
onChange={(e) => setSearchTerm(e.target.value)}
118+
onPressEnter={handleSearch}
119+
placeholder="Search..."
120+
size="small"
121+
className="h-8"
122+
id="page-search-input"
123+
/>
124+
<span className="m-auto text-nowrap p-2">
125+
{curIndex}/{highlightDoms.length}
126+
</span>
127+
<div>
128+
<Button
129+
type="text"
130+
className="h-8 w-8 p-1"
131+
onClick={() => {
132+
if (curIndex !== 1) {
133+
setCurrentIndex(curIndex - 1);
134+
} else {
135+
setCurrentIndex(highlightDoms.length);
136+
}
137+
}}
138+
>
139+
<UpOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
140+
</Button>
141+
<Button
142+
type="text"
143+
className="h-8 w-8 p-1"
144+
onClick={() => {
145+
if (highlightDoms[curIndex]) {
146+
setCurrentIndex(curIndex + 1);
147+
} else {
148+
setCurrentIndex(1);
149+
}
150+
}}
151+
>
152+
<DownOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
153+
</Button>
154+
<Button
155+
type="text"
156+
className="h-8 w-8 p-1"
157+
onClick={() => {
158+
clearHighlights();
159+
props.setIsVisible(false);
160+
}}
161+
>
162+
<CloseOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
163+
</Button>
164+
</div>
165+
</div>
166+
);
167+
};
168+
Searcher.defaultProps = new SearcherProps();
169+
export default Searcher;

media/src/components/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export const App: React.FC<AppProps> = (props) => {
9595
);
9696
} else if (schemaType === "struct") {
9797
return (
98-
<div className="vscode-page">
98+
<div className="vscode-page bg-[var(--vscode-editor-background)]">
9999
<StructDocument
100100
name={name}
101101
schema={itemMeta}
@@ -112,7 +112,7 @@ export const App: React.FC<AppProps> = (props) => {
112112
return <ProfileManagerIndex />;
113113
}
114114

115-
return <div className="vscode-page"></div>;
115+
return <div className="vscode-page bg-[var(--vscode-editor-background)]"></div>;
116116
}, [itemMeta, defs, popcode, version]);
117117
};
118118

0 commit comments

Comments
 (0)