Skip to content

Commit

Permalink
feat: Supports searching webview content and quickly opening document…
Browse files Browse the repository at this point in the history
…s from code (#144)

* fix: styles

* feat: 文档增强-可通过hover代码中的API打开API文档

* fix: 优化敏感信息 Linter

* feat: 支持搜索并高亮定位webview中的内容

* test: 新增alicloud.api.quickOpenDocument命令测试

* test: 新增alicloud.api.quickOpenDocument命令测试
  • Loading branch information
yini-chen authored Sep 5, 2024
1 parent 4968ed4 commit abd2528
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 78 deletions.
7 changes: 4 additions & 3 deletions media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,25 @@
"@monaco-editor/react": "^4.6.0",
"@vercel/ncc": "^0.38.1",
"@vscode-elements/elements": "^1.3.0",
"ahooks": "3.8.1",
"antd": "^5.12.3",
"intl-format": "^1.2.0",
"load-script": "^2.0.0",
"lodash": "^4.17.20",
"next": "^14.2.6",
"pontx-semix-table": "0.5.3",
"pontx-ui": "latest",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-use-observer": "^2.2.4",
"sass": "^1.77.8",
"semix-schema-table": "^0.1.3",
"semix-schema-table": "0.1.3",
"styled-components": "^6.1.1",
"xml2js": "^0.6.2"
},
"resolutions": {
"semix-schema-table": "^0.1.3"
"semix-schema-table": "0.1.3"
},
"version": "0.3.70",
"repository": "[email protected]:aliyun/alibabacloud-api-vscode-toolkit.git",
Expand All @@ -47,7 +49,6 @@
"@types/vscode": "^1.91.0",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react": "^1.3.2",
"ahooks": "^3.8.1",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.5",
Expand Down
139 changes: 84 additions & 55 deletions media/src/components/APIPage/API.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import TrySDK from "./TrySDK/TrySDK";
import { APIPageContext } from "./context";
import { PontUIService } from "../../service/UIService";
import ApiResponseDoc from "./APIDocument/ApiResponseDoc";
import Searcher from "../common/Searcher";

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

const contentRef = React.useRef(null);

const [isSearchVisible, setIsSearchVisible] = React.useState(false);

const handleKeyDown = (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "f") {
setIsSearchVisible((prev) => !prev); // 切换 DOM 可见性
const input = document.getElementById("page-search-input");
input.focus();
}
};

React.useEffect(() => {
window.addEventListener("keydown", handleKeyDown);

// 清除事件监听器
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);

return (
<div className="h-full bg-[var(--vscode-textBlockQuote-background)] pb-4" ref={pageEl}>
{/* */}
<APIPageContext.Provider
initialState={{
apiMeta: selectedApi,
schemaForm: form,
product: props.product,
version: props.version,
mode: mode,
changeMode: changeMode,
}}
>
<RootContext.Provider initialState={initValue}>
{selectedApi ? (
<>
<div className="bg-[var(--vscode-editor-background)] p-4">
<div className="flex justify-between">
<div>
<div className="flex">
{/* {selectedApi.method ? (
<div>
<Searcher
contentRef={contentRef}
isVisible={isSearchVisible}
setIsVisible={setIsSearchVisible}
mode={mode}
></Searcher>
<div className="bg-[var(--vscode-textBlockQuote-background)] pb-4" ref={contentRef}>
<APIPageContext.Provider
initialState={{
apiMeta: selectedApi,
schemaForm: form,
product: props.product,
version: props.version,
mode: mode,
changeMode: changeMode,
}}
>
<RootContext.Provider initialState={initValue}>
{selectedApi ? (
<>
<div className="bg-[var(--vscode-editor-background)] p-4">
<div className="flex justify-between">
<div>
<div className="flex">
{/* {selectedApi.method ? (
<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 ">
{selectedApi.method?.toUpperCase()}
</div>
) : null} */}
{selectedApi.deprecated ? (
<Tag className="my-auto ml-2" color="var(--vscode-textSeparator-foreground)">
<span className="text-[$primary-2-font-color]">deprecated</span>
</Tag>
) : null}
<div className="my-auto ml-2 text-base font-medium text-[var(--vscode-editorWidget-foreground)]">
{apiNameEle}
{selectedApi?.title ? <span> - {selectedApi.title}</span> : null}
{selectedApi.deprecated ? (
<Tag className="my-auto ml-2" color="var(--vscode-textSeparator-foreground)">
<span className="text-[$primary-2-font-color]">deprecated</span>
</Tag>
) : null}
<div className="my-auto ml-2 text-base font-medium text-[var(--vscode-editorWidget-foreground)]">
{apiNameEle}
{selectedApi?.title ? <span> - {selectedApi.title}</span> : null}
</div>
</div>
{selectedApi?.summary ? (
<div
className="ml-2 py-2 text-sm font-normal text-[$primary-2-font-color] opacity-70"
style={{ width: "100%" }}
>
{selectedApi?.summary}
</div>
) : null}
</div>
<div className="my-auto">
<Segmented
className="document-segmented"
value={mode}
onChange={(val) => changeMode(val)}
options={tabs.map((teb) => {
return {
label: teb.tab,
value: teb.key,
};
})}
></Segmented>
</div>
{selectedApi?.summary ? (
<div
className="ml-2 py-2 text-sm font-normal text-[$primary-2-font-color] opacity-70"
style={{ width: "100%" }}
>
{selectedApi?.summary}
</div>
) : null}
</div>
<div className="my-auto">
<Segmented
className="document-segmented"
value={mode}
onChange={(val) => changeMode(val)}
options={tabs.map((teb) => {
return {
label: teb.tab,
value: teb.key,
};
})}
></Segmented>
</div>
</div>
</div>
<div className="m-4">{renderContent}</div>
</>
) : null}
</RootContext.Provider>
</APIPageContext.Provider>
<div className="m-4">{renderContent}</div>
</>
) : null}
</RootContext.Provider>
</APIPageContext.Provider>
</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion media/src/components/APIPage/APIDocument/BaseClass.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const BaseClass: React.FC<BaseClassProps> = (props) => {
return (
<div className={classNames("pontx-ui-baseclass", (schema as any)?.type)}>
<div className="header">
<div className="title text-base">
<div className="title p-4 text-base">
数据结构 - {name}
{schema?.templateArgs?.length
? `<${schema?.templateArgs.map((arg, argIndex) => "T" + argIndex).join(", ")}>`
Expand Down
169 changes: 169 additions & 0 deletions media/src/components/common/Searcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* @author yini-chen
* @description
*/
import { CloseOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
import { Button, Input } from "antd";
import * as React from "react";

export class SearcherProps {
contentRef: any;
isVisible: boolean;
setIsVisible: (visible: boolean) => void;
mode?: string;
}

export const Searcher: React.FC<SearcherProps> = (props) => {
const [searchTerm, setSearchTerm] = React.useState("");
const { contentRef } = props;
const contentElement = contentRef.current;

// 清除高亮
const clearHighlights = () => {
setHighlightDoms([]);
setCurrentIndex(0);
const highlightedElements = document.querySelectorAll('span[style*="background-color: yellow"]');
highlightedElements.forEach((element) => {
const parent = element.parentNode;
if (parent) {
const textNode = document.createTextNode(element.textContent || "");
parent.insertBefore(textNode, element);
parent.removeChild(element);
}
});
};

const [highlightDoms, setHighlightDoms] = React.useState([]);

React.useEffect(() => {
// 清除之前的高亮
clearHighlights();
}, [searchTerm, props.mode]);

const handleSearch = () => {
const matchDoms = [];
function highlightSampleText(searchTerm) {
// 遍历所有节点的递归函数
function traverseNodes(node: Node) {
// 如果是文本节点
if (node.nodeType === Node.TEXT_NODE) {
const textContent = node.textContent || "";
const index = textContent?.toLocaleLowerCase()?.indexOf(searchTerm?.toLocaleLowerCase());

if (index !== -1) {
const span = document.createElement("span");
span.style.backgroundColor = "yellow";
span.style.color = "#59636E";
span.textContent = textContent.substring(index, index + searchTerm.length); // 高亮的文本
span.id = "highlighted-text";
matchDoms.push(span);

// 创建一个新的文本节点,包含高亮部分和其他部分
const beforeText = document.createTextNode(textContent.substring(0, index));
const afterText = document.createTextNode(textContent.substring(index + searchTerm.length));

// 替换当前的文本节点
const parent = node.parentNode;
if (parent) {
parent.insertBefore(beforeText, node);
parent.insertBefore(span, node);
parent.insertBefore(afterText, node);
parent.removeChild(node);
}
}
} else {
// 除文本节点外,递归遍历子节点
node.childNodes.forEach(traverseNodes);
}
}

// 从文档体开始遍历
traverseNodes(contentElement);
setHighlightDoms(matchDoms);
setCurrentIndex(matchDoms?.length ? 1 : 0);
}
if (searchTerm?.length) {
if (highlightDoms?.length) {
// 回车时移动到下一个高亮元素
if (highlightDoms[curIndex]) {
setCurrentIndex(curIndex + 1);
} else {
setCurrentIndex(1);
}
} else {
highlightSampleText(searchTerm);
}
}
};

const [curIndex, setCurrentIndex] = React.useState(highlightDoms?.length ? 1 : 0);

// 滚动到当前高亮元素
React.useEffect(() => {
if (highlightDoms[curIndex - 1]) {
highlightDoms[curIndex - 1].scrollIntoView({ behavior: "smooth", block: "center" });
}
}, [highlightDoms, curIndex]);

if (!props.isVisible) {
return null;
}

return (
<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)]">
<Input
style={{ width: "200px" }}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onPressEnter={handleSearch}
placeholder="Search..."
size="small"
className="h-8"
id="page-search-input"
/>
<span className="m-auto text-nowrap p-2">
{curIndex}/{highlightDoms.length}
</span>
<div>
<Button
type="text"
className="h-8 w-8 p-1"
onClick={() => {
if (curIndex !== 1) {
setCurrentIndex(curIndex - 1);
} else {
setCurrentIndex(highlightDoms.length);
}
}}
>
<UpOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
</Button>
<Button
type="text"
className="h-8 w-8 p-1"
onClick={() => {
if (highlightDoms[curIndex]) {
setCurrentIndex(curIndex + 1);
} else {
setCurrentIndex(1);
}
}}
>
<DownOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
</Button>
<Button
type="text"
className="h-8 w-8 p-1"
onClick={() => {
clearHighlights();
props.setIsVisible(false);
}}
>
<CloseOutlined style={{ color: "var(--vscode-editor-foreground)", verticalAlign: "middle" }} />
</Button>
</div>
</div>
);
};
Searcher.defaultProps = new SearcherProps();
export default Searcher;
4 changes: 2 additions & 2 deletions media/src/components/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const App: React.FC<AppProps> = (props) => {
);
} else if (schemaType === "struct") {
return (
<div className="vscode-page">
<div className="vscode-page bg-[var(--vscode-editor-background)]">
<StructDocument
name={name}
schema={itemMeta}
Expand All @@ -112,7 +112,7 @@ export const App: React.FC<AppProps> = (props) => {
return <ProfileManagerIndex />;
}

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

Expand Down
Loading

0 comments on commit abd2528

Please sign in to comment.