Compare commits

..

3 Commits
main ... main

8 changed files with 308 additions and 42 deletions

BIN
doc/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
doc/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

154
doc/文档tab页.md Normal file
View File

@ -0,0 +1,154 @@
# 文档页面开发记录
## 文档页面实现功能
1. 文档分页获取文件为markdown格式
代码示例
```html
<!-- webview\src\components\DocCodePanel.vue -->
<template>
<MarkdownViewer :file-path="filePath"/>
</template>
<script>
// 文档路径
// const filePath = ref('__localWebViewPath/ruanjian.md')
const filePath = ref('__localWorkspacePath/ruanjian.md')
</script>
```
- 文件支持的路径及修改方式
1. 支持插件自身目录
filePath的值以 `__localWebViewPath/` 开头
2. 支持工作区目录
filePath的值以 `__localWorkspacePath/` 开头
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 `<a href="javascript:void 0" class="openFileAndGoToFunction" title="点击打开关联文件函数: ${titleTip}" data-href="${href}" data-filepath="${filePath}" data-functionname="${functionName}">${text}</a>`
}
return renderLink.call(renderer, src);
}
```
```typescript
// src\extension\ChatViewProvider.ts
async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> {
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.DocumentSymbol[]>(
'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 }
}
```
1. 插件信息与修改: 若需要改为标题或列表, 修改 renderer 的对应方法以及触发跳转, 渲染的元素添加类名
[marked使用文档](https://marked.nodejs.cn/using_pro#hooks)
![alt text](image-1.png)

View File

@ -9,7 +9,7 @@ function log(message: string) {
if (!outputChannel) { if (!outputChannel) {
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin'); outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
} }
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
outputChannel.appendLine(`[${timestamp}] ${message}`); outputChannel.appendLine(`[${timestamp}] ${message}`);
} }
@ -18,7 +18,7 @@ export function activate(context: vscode.ExtensionContext) {
// 创建输出通道 // 创建输出通道
outputChannel = vscode.window.createOutputChannel('AI Chat Plugin'); outputChannel = vscode.window.createOutputChannel('AI Chat Plugin');
outputChannel.show(true); outputChannel.show(true);
log('VSCode AI Chat Plugin 已激活'); log('VSCode AI Chat Plugin 已激活');
// 注册Webview视图提供程序 // 注册Webview视图提供程序
@ -37,7 +37,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.executeCommand('workbench.view.extension.aiChatView'); vscode.commands.executeCommand('workbench.view.extension.aiChatView');
}) })
); );
// 监听配置变更 // 监听配置变更
context.subscriptions.push( context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => { vscode.workspace.onDidChangeConfiguration(e => {
@ -50,8 +50,8 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() { export function deactivate() {
log('VSCode AI Chat Plugin 已停用'); log('VSCode AI Chat Plugin 已停用');
if (outputChannel) { if (outputChannel) {
outputChannel.dispose(); outputChannel.dispose();
} }
} }

View File

@ -486,34 +486,38 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
} }
findFunction(symbols: vscode.DocumentSymbol[], name: string): vscode.DocumentSymbol | false { findFunction(symbols: vscode.DocumentSymbol[], name: string): vscode.DocumentSymbol | false {
for (const symbol of symbols) { for (const symbol of symbols) {
if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) { if (symbol.kind === vscode.SymbolKind.Function && symbol.name === name) {
return symbol; return symbol;
} }
if (symbol.children) { if (symbol.children) {
const found = this.findFunction(symbol.children, name); const found = this.findFunction(symbol.children, name);
if (found) return found; if (found) return found;
} }
} }
return false; return false;
} }
async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> { async openFileAndGoToFunction(message: ListenerParam): Promise<void | false> {
if (message.type !== 'openFileAndGoToFunction') { return false }; 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 const targetFunc = message.functionName
if (!filePath || !targetFunc){ if (!filePath || !targetFunc) {
vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`) vscode.window.showWarningMessage(`未提供文件: ${filePath} 或函数: ${targetFunc}`)
return return
} }
try { try {
// 打开文件 // 打开文件
const uri = vscode.Uri.file(filePath) const docInfo = await this.openWorkspaceFile(filePath);
const doc = await vscode.workspace.openTextDocument(uri) if (!docInfo) {
const editor = await vscode.window.showTextDocument(doc) vscode.window.showWarningMessage(`无法打开或创建文件: ${filePath}`)
return
}
const { editor, document } = docInfo
// 获取文档符号 // 获取文档符号
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>( const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider', 'vscode.executeDocumentSymbolProvider',
doc.uri document.uri
) )
if (symbols) { if (symbols) {
@ -534,7 +538,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
fetchMdFile(message: ListenerParam, webview: WebviewViewIns): false | void { fetchMdFile(message: ListenerParam, webview: WebviewViewIns): false | void {
if (message.type !== 'loadMd') { return false } if (message.type !== 'loadMd') { return false }
if (message.mdPath) { if (message.mdPath) {
const filePath = path.join(this._extensionUri.fsPath, 'webview', 'dist', message.mdPath!.replace('__localWebViewPath/', '')) let filePath
const isFromWorkspace = message.mdPath.startsWith('__localWorkspacePath/')
if (isFromWorkspace) {
// 1. 获取工作区根路径
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!workspaceRoot) {
vscode.window.showErrorMessage(" 未检测到工作区");
return;
}
// 2. 拼接完整路径(自动处理跨平台路径)
filePath = vscode.Uri.joinPath(workspaceRoot, message.mdPath.replace('__localWorkspacePath/', '')).fsPath;
// filePath = path.join(workspaceRoot.toString(), message.mdPath.replace('__localWorkspacePath/', ''))
} else {
filePath = path.join(this._extensionUri.fsPath, 'webview', 'dist', message.mdPath.replace('__localWebViewPath/', ''))
}
this._logger(filePath + '::: 当前md路径')
try { try {
const content = fs.readFileSync(filePath, 'utf-8') const content = fs.readFileSync(filePath, 'utf-8')
webview.postMessage({ type: 'mdContent', data: content }) webview.postMessage({ type: 'mdContent', data: content })
@ -546,4 +566,36 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
} }
webview.postMessage({ type: 'mdContent', data: '未正确传入md文件路径' }) 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 }
}
}

View File

@ -37,4 +37,9 @@
- **快速主题与滤镜** - **快速主题与滤镜**
- 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。 - 全局主题与滤镜:支持全局主题设置,用户可以一键切换不同的主题风格,包括颜色、字体、背景等元素,快速调整数据可视化的整体视觉效果。同时,提供滤镜功能,用户可以对图表进行美化处理,如添加阴影、渐变色等效果,提升数据可视化的美观度。
![image-20250420143847485](./assets/image-20250420143847485.png) ![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=[函数名])

View File

@ -11,11 +11,14 @@
import { ref } from 'vue'; import { ref } from 'vue';
import Header from './Header.vue'; import Header from './Header.vue';
import MarkdownViewer from './MarkdownViewer.vue'; import MarkdownViewer from './MarkdownViewer.vue';
// //
const filePath = ref('__localWebViewPath/ruanjian.md') // const filePath = ref('__localWebViewPath/ruanjian.md')
if(!window.acquireVsCodeApi){ const filePath = ref('__localWorkspacePath/ruanjian.md')
filePath.value = '/ruanjian.md' // if(!window.acquireVsCodeApi){
} // filePath.value = '/ruanjian.md'
// }
/*
//#region wsdoc
if(window.vscodeApi){ if(window.vscodeApi){
window.vscodeApi.postMessage({ window.vscodeApi.postMessage({
type: 'ws:connect', type: 'ws:connect',
@ -49,6 +52,8 @@ if(window.vscodeApi){
} }
}) })
} }
//#endregion
*/
</script> </script>
<style module lang="scss"> <style module lang="scss">

View File

@ -7,12 +7,12 @@
<h2>目录</h2> <h2>目录</h2>
</div> </div>
<div class="toc-content"> <div class="toc-content">
<MDOutlineTree :data="tocTreeData"/> <MDOutlineTree :data="tocTreeData" />
</div> </div>
</aside> </aside>
<!-- 主内容区域 --> <!-- 主内容区域 -->
<main class="content" ref="content"> <main class="content" ref="content" @click="handleDelegateClick">
<div v-if="loading" class="loading-indicator">加载中...</div> <div v-if="loading" class="loading-indicator">加载中...</div>
<div v-else-if="error" class="error-message">{{ error }}</div> <div v-else-if="error" class="error-message">{{ error }}</div>
<div v-else v-html="renderedContent" class="markdown-content"></div> <div v-else v-html="renderedContent" class="markdown-content"></div>
@ -74,9 +74,9 @@ renderer.heading = ({ text, depth }) => {
toc.value.push({ toc.value.push({
id: escapedText, id: escapedText,
level: depth, level: depth,
title: text, title: text,
anchor: escapedText anchor: escapedText
}) })
const node = { const node = {
key: escapedText, key: escapedText,
title: text title: text
@ -106,11 +106,61 @@ renderer.heading = ({ text, depth }) => {
${text} ${text}
</h${depth}>`; </h${depth}>`;
}; };
let renderList = renderer.listitem //
const renderList = renderer.listitem
renderer.listitem = (src)=>{ // ,
renderer.listitem = (src) => {
return renderList.call(renderer, 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 `<a href="javascript:void 0" class="openFileAndGoToFunction" title="点击打开关联文件函数: ${titleTip}" data-href="${href}" data-filepath="${filePath}" data-functionname="${functionName}">${text}</a>`
}
return renderLink.call(renderer, src);
}
marked.setOptions({ marked.setOptions({
renderer, renderer,
highlight: (code, lang) => { highlight: (code, lang) => {
@ -135,9 +185,8 @@ const loadMarkdownFile = async () => {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
let fetchMdPms let fetchMdPms
if (filePath.startsWith('__localWebViewPath/') || filePath.startsWith('__localWorkspacePath/')) {
if(filePath.startsWith('__localWebViewPath/')){ if (!window.vscodeApi) {
if(!window.vscodeApi){
console.error('获取__localWebViewPath的md文档时, 无法获取VSCode API') console.error('获取__localWebViewPath的md文档时, 无法获取VSCode API')
return return
} }
@ -158,7 +207,7 @@ const loadMarkdownFile = async () => {
markdownText.value = event.data.data markdownText.value = event.data.data
} }
}); });
}else{ } else {
fetchMdPms = fetch(props.filePath); fetchMdPms = fetch(props.filePath);
const response = await fetchMdPms const response = await fetchMdPms
if (!response.ok) throw new Error('无法加载Markdown文件'); if (!response.ok) throw new Error('无法加载Markdown文件');
@ -178,8 +227,6 @@ const loadMarkdownFile = async () => {
// Markdown // Markdown
const renderMarkdown = () => { const renderMarkdown = () => {
console.log(markdownText.value);
toc.value = []; // toc.value = []; //
renderedContent.value = marked(markdownText.value); renderedContent.value = marked(markdownText.value);
// DOM // DOM
@ -187,6 +234,7 @@ const renderMarkdown = () => {
document.querySelectorAll('pre code').forEach(block => { document.querySelectorAll('pre code').forEach(block => {
hljs.highlightElement(block); hljs.highlightElement(block);
}); });
// , todo
// setupHeadingObservers(); // setupHeadingObservers();
}); });
}; };
@ -244,7 +292,7 @@ const handleResize = throttle(() => {
}, props.debounceTime); }, props.debounceTime);
// //
onBeforeMount(()=>{ onBeforeMount(() => {
loadMarkdownFile(); loadMarkdownFile();
}) })
onMounted(() => { onMounted(() => {
@ -309,9 +357,11 @@ onUnmounted(() => {
margin: 0; margin: 0;
font-size: 1.2rem; font-size: 1.2rem;
} }
.toc-content { .toc-content {
height: calc(100vh - 200px); height: calc(100vh - 200px);
} }
.toc-content ul { .toc-content ul {
list-style: none; list-style: none;
padding: 0; padding: 0;