diff --git a/doc/image-1.png b/doc/image-1.png new file mode 100644 index 0000000..c3f1888 Binary files /dev/null and b/doc/image-1.png differ diff --git a/doc/image.png b/doc/image.png new file mode 100644 index 0000000..cf665ee Binary files /dev/null and b/doc/image.png differ diff --git a/doc/文档tab页.md b/doc/文档tab页.md new file mode 100644 index 0000000..be4d015 --- /dev/null +++ b/doc/文档tab页.md @@ -0,0 +1,132 @@ +# 文档页面开发记录 + +## 文档页面实现功能 +1. 文档分页获取文件为markdown格式 +2. 布局: + ![alt 布局](image.png) + - 左侧为标题目录 + 1. 支持折叠展开 + 1. 支持点击时 右侧文档滚动到对应位置 + - 右侧为文档主题 + 1. 支持展示图片 + 1. 支持特殊标记实现点击时打开工作区内文件中的某个函数 + 目前暂定格式与示例\ + 示例: + [打开vscode中的src/test.js:testFunction](__workspace/src/test.js?functionName=resolveTripleslashReference) + 格式: + [显示文字](__workspace/[文件的相对路径]?functionName=[函数名]) + 关键代码: + ``` js + ///webview\src\components\MarkdownViewer.vue + + /// ... + const renderLink = renderer.link + const handleDelegateClick = (event) => { + // 通过事件冒泡捕获目标元素 + const target = event.target.closest('.openFileAndGoToFunction') + if (target) { + const { filepath, functionname } = target.dataset + console.log(filepath, functionname, '_**=== filepath, functionname'); + console.log(target.dataset, '_**=== target.dataset'); + if (window.acquireVsCodeApi && window.vscodeApi) { + window.vscodeApi.postMessage({ + type: 'openFileAndGoToFunction', + filePath: filepath, + functionName: functionname + }); + } + } + } + + renderer.link = function (src) { + // 工作区跳转 + if (src.href.startsWith("__workspace/")) { + const { href, title, tokens } = src + const text = renderer.parser.parseInline(tokens); + // const cleanHref = cleanUrl(href); + const titleTip = href.slice("__workspace/".length); + const [filePath, functionName] = titleTip.split('?functionName=') + console.log(filePath, functionName, '_**=== filePath, functionName'); + return `${text}` + } + return renderLink.call(renderer, src); + } + ``` + + ```typescript + // src\extension\ChatViewProvider.ts + async openFileAndGoToFunction(message: ListenerParam): Promise { + if (message.type !== 'openFileAndGoToFunction') { return false }; + const rootUri = vscode.workspace.workspaceFolders?.[0]?.uri; + const filePath = message.filePath; // 确保路径正确,相对于工作区 + this._logger(rootUri + '#####' + filePath) + const targetFunc = message.functionName + if (!filePath || !targetFunc) { + vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`) + return + } + try { + // 打开文件 + const docInfo = await this.openWorkspaceFile(filePath); + if (!docInfo) { + vscode.window.showWarningMessage(`无法打开或创建文件: ${filePath}`) + return + } + const { editor, document } = docInfo + // 获取文档符号 + const symbols = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + document.uri + ) + + if (symbols) { + const funcSymbol = this.findFunction(symbols, targetFunc) + if (funcSymbol) { + // 跳转到目标位置 + editor.revealRange(funcSymbol.range, vscode.TextEditorRevealType.InCenter) + editor.selection = new vscode.Selection(funcSymbol.range.start, funcSymbol.range.end) + } else { + vscode.window.showWarningMessage(`未找到函数 ${targetFunc}`) + } + } + } catch (error) { + vscode.window.showErrorMessage(`无法打开文件: ${error}`) + } + } + // 打开工作区的文件 + async openWorkspaceFile(relativePath: string) { + // 1. 获取工作区根路径 + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri; + if (!workspaceRoot) { + vscode.window.showErrorMessage(" 未检测到工作区"); + return; + } + + // 2. 拼接完整路径(自动处理跨平台路径) + const targetUri = vscode.Uri.joinPath(workspaceRoot, relativePath); + + // 3. 文件存在性检测 + try { + await vscode.workspace.fs.stat(targetUri); + } catch { + const createNew = await vscode.window.showInformationMessage( + "文件不存在,是否创建?", + "创建", "取消" + ); + if (createNew === "创建") { + await vscode.workspace.fs.writeFile(targetUri, Buffer.from("")); + } else { + return; + } + } + this._logger(targetUri + ':::targetUri') + // 4. 打开文件编辑器 + const document = await vscode.workspace.openTextDocument(targetUri); + const editor = await vscode.window.showTextDocument(document); + return { document, editor } + } + ``` +若需要改为标题或列表, 修改 renderer 的对应方法以及触发跳转 + +https://marked.nodejs.cn/using_pro#hooks +![alt text](image-1.png) diff --git a/src/extension.ts b/src/extension.ts index cd3360a..a89fc6a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,7 +9,7 @@ function log(message: string) { if (!outputChannel) { outputChannel = vscode.window.createOutputChannel('AI Chat Plugin'); } - + const timestamp = new Date().toISOString(); outputChannel.appendLine(`[${timestamp}] ${message}`); } @@ -18,7 +18,7 @@ export function activate(context: vscode.ExtensionContext) { // 创建输出通道 outputChannel = vscode.window.createOutputChannel('AI Chat Plugin'); outputChannel.show(true); - + log('VSCode AI Chat Plugin 已激活'); // 注册Webview视图提供程序 @@ -37,7 +37,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.executeCommand('workbench.view.extension.aiChatView'); }) ); - + // 监听配置变更 context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(e => { @@ -50,8 +50,8 @@ export function activate(context: vscode.ExtensionContext) { export function deactivate() { log('VSCode AI Chat Plugin 已停用'); - + if (outputChannel) { outputChannel.dispose(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/extension/ChatViewProvider.ts b/src/extension/ChatViewProvider.ts index e176d05..9eea7d4 100644 --- a/src/extension/ChatViewProvider.ts +++ b/src/extension/ChatViewProvider.ts @@ -486,34 +486,38 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } findFunction(symbols: vscode.DocumentSymbol[], name: string): vscode.DocumentSymbol | false { for (const symbol of symbols) { - if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) { - return symbol; - } - if (symbol.children) { - const found = this.findFunction(symbol.children, name); - if (found) return found; - } + if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) { + return symbol; + } + if (symbol.children) { + const found = this.findFunction(symbol.children, name); + if (found) return found; + } } return false; } async openFileAndGoToFunction(message: ListenerParam): Promise { if (message.type !== 'openFileAndGoToFunction') { return false }; - const filePath = message.filePath; // 确保路径正确,可以是绝对路径或相对于工作区 + const rootUri = vscode.workspace.workspaceFolders?.[0]?.uri; + const filePath = message.filePath; // 确保路径正确,相对于工作区 + this._logger(rootUri + '#####' + filePath) const targetFunc = message.functionName - if (!filePath || !targetFunc){ + if (!filePath || !targetFunc) { vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`) return } try { // 打开文件 - const uri = vscode.Uri.file(filePath) - const doc = await vscode.workspace.openTextDocument(uri) - const editor = await vscode.window.showTextDocument(doc) - + const docInfo = await this.openWorkspaceFile(filePath); + if (!docInfo) { + vscode.window.showWarningMessage(`无法打开或创建文件: ${filePath}`) + return + } + const { editor, document } = docInfo // 获取文档符号 const symbols = await vscode.commands.executeCommand( 'vscode.executeDocumentSymbolProvider', - doc.uri + document.uri ) if (symbols) { @@ -546,4 +550,36 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } webview.postMessage({ type: 'mdContent', data: '未正确传入md文件路径' }) } -} + // 打开工作区的文件 + async openWorkspaceFile(relativePath: string) { + // 1. 获取工作区根路径 + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri; + if (!workspaceRoot) { + vscode.window.showErrorMessage(" 未检测到工作区"); + return; + } + + // 2. 拼接完整路径(自动处理跨平台路径) + const targetUri = vscode.Uri.joinPath(workspaceRoot, relativePath); + + // 3. 文件存在性检测 + try { + await vscode.workspace.fs.stat(targetUri); + } catch { + const createNew = await vscode.window.showInformationMessage( + "文件不存在,是否创建?", + "创建", "取消" + ); + if (createNew === "创建") { + await vscode.workspace.fs.writeFile(targetUri, Buffer.from("")); + } else { + return; + } + } + this._logger(targetUri + ':::targetUri') + // 4. 打开文件编辑器 + const document = await vscode.workspace.openTextDocument(targetUri); + const editor = await vscode.window.showTextDocument(document); + return { document, editor } + } +} \ No newline at end of file diff --git a/webview/public/ruanjian.md b/webview/public/ruanjian.md index 21396de..f40da02 100644 --- a/webview/public/ruanjian.md +++ b/webview/public/ruanjian.md @@ -37,4 +37,9 @@ - **快速主题与滤镜** - 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。 -![image-20250420143847485](./assets/image-20250420143847485.png) \ No newline at end of file +![image](https://img-s.msn.cn/tenant/amp/entityid/AA1DBjm3.img?w=768&h=512&m=6) + +示例: +[打开vscode中的src/test.js:testFunction](__workspace/src/test.js?functionName=resolveTripleslashReference) +格式: +[显示文字](__workspace/[文件的相对路径]?functionName=[函数名]) \ No newline at end of file diff --git a/webview/src/components/MarkdownViewer.vue b/webview/src/components/MarkdownViewer.vue index 059e9f7..fe70335 100644 --- a/webview/src/components/MarkdownViewer.vue +++ b/webview/src/components/MarkdownViewer.vue @@ -7,12 +7,12 @@

目录

- +
-
+
加载中...
{{ error }}
@@ -74,9 +74,9 @@ renderer.heading = ({ text, depth }) => { toc.value.push({ id: escapedText, level: depth, - title: text, - anchor: escapedText - }) + title: text, + anchor: escapedText + }) const node = { key: escapedText, title: text @@ -106,11 +106,61 @@ renderer.heading = ({ text, depth }) => { ${text} `; }; -let renderList = renderer.listitem - -renderer.listitem = (src)=>{ +// 渲染列表 +const renderList = renderer.listitem +// 若需改为列表跳转, 可改此函数 +renderer.listitem = (src) => { return renderList.call(renderer, src); } +const handleDelegateClick = (event) => { + + // 通过事件冒泡捕获目标元素 + const target = event.target.closest('.openFileAndGoToFunction') + if (target) { + const { filepath, functionname } = target.dataset + console.log(filepath, functionname, '_**=== filepath, functionname'); + console.log(target.dataset, '_**=== target.dataset'); + if (window.acquireVsCodeApi && window.vscodeApi) { + window.vscodeApi.postMessage({ + type: 'openFileAndGoToFunction', + filePath: filepath, + functionName: functionname + }); + } + } +} + +const renderLink = renderer.link +renderer.link = function (src) { + /* + { + "type": "link", + "raw": "[打开vscode中的文件1](__workspace/)", + "href": "__workspace/", + "title": null, + "text": "打开vscode中的文件1", + "tokens": [ + { + "type": "text", + "raw": "打开vscode中的文件1", + "text": "打开vscode中的文件1", + "escaped": false + } + ] + } + */ + // 工作区跳转 + if (src.href.startsWith("__workspace/")) { + const { href, title, tokens } = src + const text = renderer.parser.parseInline(tokens); + // const cleanHref = cleanUrl(href); + const titleTip = href.slice("__workspace/".length); + const [filePath, functionName] = titleTip.split('?functionName=') + console.log(filePath, functionName, '_**=== filePath, functionName'); + return `${text}` + } + return renderLink.call(renderer, src); +} marked.setOptions({ renderer, highlight: (code, lang) => { @@ -135,9 +185,8 @@ const loadMarkdownFile = async () => { loading.value = true; error.value = null; let fetchMdPms - - if(filePath.startsWith('__localWebViewPath/')){ - if(!window.vscodeApi){ + if (filePath.startsWith('__localWebViewPath/')) { + if (!window.vscodeApi) { console.error('获取__localWebViewPath的md文档时, 无法获取VSCode API') return } @@ -158,7 +207,7 @@ const loadMarkdownFile = async () => { markdownText.value = event.data.data } }); - }else{ + } else { fetchMdPms = fetch(props.filePath); const response = await fetchMdPms if (!response.ok) throw new Error('无法加载Markdown文件'); @@ -178,8 +227,6 @@ const loadMarkdownFile = async () => { // 渲染Markdown const renderMarkdown = () => { - console.log(markdownText.value); - toc.value = []; // 清空目录 renderedContent.value = marked(markdownText.value); // 等待DOM更新后初始化高亮 @@ -187,6 +234,7 @@ const renderMarkdown = () => { document.querySelectorAll('pre code').forEach(block => { hljs.highlightElement(block); }); + // 懒加载, todo 兼容跳转 // setupHeadingObservers(); }); }; @@ -244,7 +292,7 @@ const handleResize = throttle(() => { }, props.debounceTime); // 生命周期钩子 -onBeforeMount(()=>{ +onBeforeMount(() => { loadMarkdownFile(); }) onMounted(() => { @@ -309,9 +357,11 @@ onUnmounted(() => { margin: 0; font-size: 1.2rem; } + .toc-content { height: calc(100vh - 200px); } + .toc-content ul { list-style: none; padding: 0;