From 184ca267c3077aa11390ea2df67e0b89203b0b38 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 1 Jul 2026 13:44:40 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat(docs):=20=E6=B7=BB=E5=8A=A0=20404=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...in-architecture-for-30-language-runner.md} | 0 docs/src/i18n.ts | 18 ++++- docs/src/pages/NotFound.vue | 66 +++++++++++++++++++ docs/src/router.ts | 4 +- 4 files changed, 85 insertions(+), 3 deletions(-) rename docs/src/content/blog/{welcome.md => tauri2-plugin-architecture-for-30-language-runner.md} (100%) create mode 100644 docs/src/pages/NotFound.vue diff --git a/docs/src/content/blog/welcome.md b/docs/src/content/blog/tauri2-plugin-architecture-for-30-language-runner.md similarity index 100% rename from docs/src/content/blog/welcome.md rename to docs/src/content/blog/tauri2-plugin-architecture-for-30-language-runner.md diff --git a/docs/src/i18n.ts b/docs/src/i18n.ts index 52a31a4..5fec73f 100644 --- a/docs/src/i18n.ts +++ b/docs/src/i18n.ts @@ -44,7 +44,14 @@ const messages = { open: '100%', openLabel: '开源免费' }, releaseList: {title: '发布日志', intro: '每个版本的更新内容如下,点击查看详情。'}, - footer: {download: '下载', blog: '博客', releases: '发布日志'} + footer: {download: '下载', blog: '博客', releases: '发布日志'}, + notFound: { + title: '页面未找到', + description: '抱歉,您访问的页面不存在或已被移动', + goHome: '返回首页', + goBack: '返回上页', + quickLinks: '您可能想访问:' + } }, en: { nav: {download: 'Download', blog: 'Blog', releases: 'Releases'}, @@ -86,7 +93,14 @@ const messages = { open: '100%', openLabel: 'Open source' }, releaseList: {title: 'Releases', intro: 'Update notes for each version. Click to view details.'}, - footer: {download: 'Download', blog: 'Blog', releases: 'Releases'} + footer: {download: 'Download', blog: 'Blog', releases: 'Releases'}, + notFound: { + title: 'Page Not Found', + description: 'Sorry, the page you are looking for does not exist or has been moved', + goHome: 'Go Home', + goBack: 'Go Back', + quickLinks: 'You might want to visit:' + } } } diff --git a/docs/src/pages/NotFound.vue b/docs/src/pages/NotFound.vue new file mode 100644 index 0000000..9b278bd --- /dev/null +++ b/docs/src/pages/NotFound.vue @@ -0,0 +1,66 @@ + + + diff --git a/docs/src/router.ts b/docs/src/router.ts index baf1019..535bef4 100644 --- a/docs/src/router.ts +++ b/docs/src/router.ts @@ -3,6 +3,7 @@ import Home from './pages/Home.vue' import Download from './pages/Download.vue' import ReleaseList from './pages/ReleaseList.vue' import BlogList from './pages/BlogList.vue' +import NotFound from './pages/NotFound.vue' import {releaseRoutes} from './content/releases' import {blogRoutes} from './content/blogs' @@ -12,5 +13,6 @@ export const routes: RouteRecordRaw[] = [ {path: '/release', component: ReleaseList, meta: {title: '发布日志'}}, {path: '/blog', component: BlogList, meta: {title: '技术博客'}}, ...releaseRoutes.map(r => ({...r, meta: {doc: true, docType: 'release'}})), - ...blogRoutes.map(r => ({...r, meta: {doc: true, docType: 'blog'}})) + ...blogRoutes.map(r => ({...r, meta: {doc: true, docType: 'blog'}})), + {path: '/:pathMatch(.*)*', component: NotFound, meta: {title: '页面未找到'}} ] From 508e485b92455789442ab4a6af6a5adf997d0b51 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 1 Jul 2026 17:04:43 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(git):=20=E6=9C=AA=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E7=9A=84=E5=88=86=E6=94=AF=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E6=94=AF=E6=8C=81=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git_branch_delete 增加 force 参数(-D);前端删除分支若因未完全合并被拒, 弹出「强制删除」确认(提示独有提交可能丢失)而非直接报错。 --- src-tauri/src/filesystem.rs | 10 ++++++---- src/components/GitPanel.vue | 24 ++++++++++++++++++++++-- src/i18n/locales/en.json | 3 +++ src/i18n/locales/zh-CN.json | 3 +++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index eb3d230..d165366 100644 --- a/src-tauri/src/filesystem.rs +++ b/src-tauri/src/filesystem.rs @@ -1304,10 +1304,12 @@ pub async fn git_branch_create(root: String, name: String) -> Result Result { - tokio::task::spawn_blocking(move || run_git(&root, &["branch", "-d", &name])) - .await - .map_err(|e| format!("git 任务失败: {}", e))? +pub async fn git_branch_delete(root: String, name: String, force: bool) -> Result { + tokio::task::spawn_blocking(move || { + run_git(&root, &["branch", if force { "-D" } else { "-d" }, &name]) + }) + .await + .map_err(|e| format!("git 任务失败: {}", e))? } /// 重命名分支。 diff --git a/src/components/GitPanel.vue b/src/components/GitPanel.vue index 4c2d53c..2224e34 100644 --- a/src/components/GitPanel.vue +++ b/src/components/GitPanel.vue @@ -295,6 +295,17 @@ + + +
+

{{ t('git.forceDeleteBranchConfirm', { name: forceDelete.name }) }}

+
+ + +
+
+
+
@@ -768,13 +779,22 @@ const createBranch = async () => { } } -const deleteBranch = async (name: string) => { +// 分支未完全合并时的强制删除确认 +const forceDelete = ref<{ name: string } | null>(null) +const deleteBranch = async (name: string, force = false) => { try { - await invoke('git_branch_delete', {root: props.rootDir, name}) + await invoke('git_branch_delete', {root: props.rootDir, name, force}) toast.success(t('git.branchDeleted')) + forceDelete.value = null await refresh() } catch (error) { + const msg = String(error) + // 未完全合并(git -d 拒绝)→ 弹出强制删除确认,而非直接报错 + if (!force && (msg.includes('not fully merged') || msg.includes('没有完全合并'))) { + forceDelete.value = {name} + return + } toast.error(t('git.branchOpFailed') + ': ' + error) } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f9018e1..6212efa 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -445,6 +445,9 @@ "tracked": "Tracking branch checked out", "branchCreated": "Branch created and switched", "branchDeleted": "Branch deleted", + "forceDeleteBranchTitle": "Force delete branch", + "forceDeleteBranchConfirm": "Branch \"{name}\" is not fully merged; commits unique to it may be lost. Force delete?", + "forceDelete": "Force delete", "merged": "Merged", "branchOpFailed": "Branch operation failed", "amend": "Amend last commit", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 17a19a4..ccf989a 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -445,6 +445,9 @@ "tracked": "已检出跟踪分支", "branchCreated": "已创建并切换分支", "branchDeleted": "已删除分支", + "forceDeleteBranchTitle": "强制删除分支", + "forceDeleteBranchConfirm": "分支「{name}」尚未完全合并,删除后其独有的提交可能丢失。确定强制删除?", + "forceDelete": "强制删除", "merged": "已合并", "branchOpFailed": "分支操作失败", "amend": "修正上次提交", From cdf4fa096a52cf95d1960ceb0bfcee80a844bf00 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 1 Jul 2026 17:10:05 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat(editor):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B1=95=E5=BC=80/=E6=94=B6=E7=BC=A9=E9=80=89=E5=8C=BA?= =?UTF-8?q?=EF=BC=88=E6=8C=89=E8=AF=AD=E6=B3=95=E8=8A=82=E7=82=B9=E9=80=90?= =?UTF-8?q?=E7=BA=A7=E6=89=A9=E9=80=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 @codemirror/commands 的 selectParentSyntax 逐级扩展选区,收缩用选区栈回退; 命令面板「代码」分组 + 快捷键 Alt+Shift+←/→(可自定义)。 --- package.json | 1 + src/App.vue | 42 ++++++++++++++++++++++++++++++++- src/composables/useShortcuts.ts | 4 +++- src/i18n/locales/en.json | 6 ++++- src/i18n/locales/zh-CN.json | 6 ++++- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0cc6a3f..91482af 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@babel/runtime": "^7.28.2", "@codemirror/autocomplete": "6.20.3", + "@codemirror/commands": "^6.10.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", diff --git a/src/App.vue b/src/App.vue index 930d9bf..d71abe9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -544,6 +544,7 @@ import {useWorkspace} from './composables/useWorkspace' import {useTextCommands} from './composables/useTextCommands' import {useBookmarks} from './composables/useBookmarks' import {foldAll, unfoldAll, matchBrackets} from '@codemirror/language' +import {selectParentSyntax} from '@codemirror/commands' import {diagnostics} from './editor/lspDiagnostics' import {useGitPermalink} from './composables/useGitPermalink' import {useRevealInTree} from './composables/useRevealInTree' @@ -1175,6 +1176,41 @@ const convertIndentation = (toTabs: boolean) => { toast.success(t('app.indentConverted')) } +// 展开/收缩选区:按语法节点逐级扩选,收缩用选区栈回退 +let expandStack: { anchor: number, head: number }[] = [] +let expandLastKey = '' +const selKey = (view: any) => { + const s = view.state.selection.main + return s.anchor + ':' + s.head +} +const expandSelection = () => { + const view = editorView.value + if (!view) { + return + } + // 用户手动改过选区则重置栈 + if (selKey(view) !== expandLastKey) { + expandStack = [] + } + const s = view.state.selection.main + expandStack.push({anchor: s.anchor, head: s.head}) + selectParentSyntax(view) + view.focus() + expandLastKey = selKey(view) +} +const shrinkSelection = () => { + const view = editorView.value + if (!view) { + return + } + const prev = expandStack.pop() + if (prev) { + view.dispatch({selection: {anchor: prev.anchor, head: prev.head}}) + view.focus() + expandLastKey = selKey(view) + } +} + // 转到匹配括号:取光标前后的括号,跳到其配对处 const goToMatchingBracket = () => { const view = editorView.value @@ -2193,7 +2229,9 @@ const shortcutDispatch: Record void> = { toggleSidebar: () => toggleSidebar(), toggleTerminal: () => toggleTerminal(), toggleWordWrap: () => toggleWordWrap(), - toggleBookmark: () => toggleBookmark() + toggleBookmark: () => toggleBookmark(), + expandSelection: () => expandSelection(), + shrinkSelection: () => shrinkSelection() } // 切换自动换行(即时生效并随编辑器配置持久化) @@ -2270,6 +2308,8 @@ const paletteCommands = computed(() => [ {id: 'foldAll', label: t('command.foldAll'), group: t('command.groupCode'), icon: FoldVertical, run: () => { if (editorView.value) foldAll(editorView.value) }}, {id: 'unfoldAll', label: t('command.unfoldAll'), group: t('command.groupCode'), icon: UnfoldVertical, run: () => { if (editorView.value) unfoldAll(editorView.value) }}, {id: 'goToMatchingBracket', label: t('command.goToMatchingBracket'), group: t('command.groupCode'), icon: Code2, run: () => goToMatchingBracket()}, + {id: 'expandSelection', label: t('command.expandSelection'), group: t('command.groupCode'), icon: Code2, hint: hintOf('expandSelection'), run: () => expandSelection()}, + {id: 'shrinkSelection', label: t('command.shrinkSelection'), group: t('command.groupCode'), icon: Code2, hint: hintOf('shrinkSelection'), run: () => shrinkSelection()}, {id: 'toggleBookmark', label: t('command.toggleBookmark'), group: t('command.groupBookmark'), icon: Bookmark, hint: hintOf('toggleBookmark'), run: () => toggleBookmark()}, {id: 'nextBookmark', label: t('command.nextBookmark'), group: t('command.groupBookmark'), icon: Bookmark, run: () => nextBookmark()}, {id: 'prevBookmark', label: t('command.prevBookmark'), group: t('command.groupBookmark'), icon: Bookmark, run: () => prevBookmark()}, diff --git a/src/composables/useShortcuts.ts b/src/composables/useShortcuts.ts index 1bba736..6efcee9 100644 --- a/src/composables/useShortcuts.ts +++ b/src/composables/useShortcuts.ts @@ -29,7 +29,9 @@ const SHORTCUT_DEFS: { id: string; default: string }[] = [ {id: 'toggleSidebar', default: 'Mod+B'}, {id: 'toggleTerminal', default: 'Mod+`'}, {id: 'toggleWordWrap', default: 'Alt+Z'}, - {id: 'toggleBookmark', default: 'Mod+Alt+K'} + {id: 'toggleBookmark', default: 'Mod+Alt+K'}, + {id: 'expandSelection', default: 'Alt+Shift+ArrowRight'}, + {id: 'shrinkSelection', default: 'Alt+Shift+ArrowLeft'} ] const STORAGE_KEY = 'shortcuts' diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6212efa..c0942b9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -701,6 +701,8 @@ "foldAll": "Fold all", "unfoldAll": "Unfold all", "goToMatchingBracket": "Go to matching bracket", + "expandSelection": "Expand selection", + "shrinkSelection": "Shrink selection", "groupCode": "Code", "toggleBookmark": "Toggle bookmark", "nextBookmark": "Next bookmark", @@ -752,7 +754,9 @@ "toggleSidebar": "Toggle sidebar", "toggleTerminal": "Toggle terminal", "toggleWordWrap": "Toggle word wrap", - "toggleBookmark": "Toggle bookmark" + "toggleBookmark": "Toggle bookmark", + "expandSelection": "Expand selection", + "shrinkSelection": "Shrink selection" }, "view": { "clear": "Clear", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index ccf989a..c57eac6 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -701,6 +701,8 @@ "foldAll": "折叠全部代码", "unfoldAll": "展开全部代码", "goToMatchingBracket": "转到匹配括号", + "expandSelection": "展开选区", + "shrinkSelection": "收缩选区", "groupCode": "代码", "toggleBookmark": "切换书签", "nextBookmark": "下一处书签", @@ -752,7 +754,9 @@ "toggleSidebar": "切换侧栏", "toggleTerminal": "切换终端", "toggleWordWrap": "切换自动换行", - "toggleBookmark": "切换书签" + "toggleBookmark": "切换书签", + "expandSelection": "展开选区", + "shrinkSelection": "收缩选区" }, "view": { "clear": "清空", From 0b0a9966b42e6b87bb11717fc97c2bd0cf9484be Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 1 Jul 2026 17:14:12 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(editor):=20=E7=94=A8=20pnpm=20overrid?= =?UTF-8?q?es=20=E9=92=89=E4=BD=8F=20@codemirror/state=20=E5=8D=95?= =?UTF-8?q?=E4=B8=80=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 @codemirror/commands 依赖拉进了第二个 @codemirror/state(6.7.0 vs 6.6.0), 双实例破坏 Extension 的 instanceof 检查导致运行时报错;overrides 统一到 6.7.0。 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 91482af..160dd9b 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ }, "pnpm": { "overrides": { - "@codemirror/autocomplete": "6.20.3" + "@codemirror/autocomplete": "6.20.3", + "@codemirror/state": "6.7.0" } } } From 8a06ad0f2b0ef9673558b099fb095a95203e1321 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 2 Jul 2026 09:45:27 +0800 Subject: [PATCH 05/10] =?UTF-8?q?feat(palette):=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E6=9C=80=E8=BF=91=E4=BD=BF=E7=94=A8=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=BD=AE=E9=A1=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 空查询时按最近执行顺序把命令置顶(KV 持久化、去重、上限 8), 最近项带时钟标记;有查询时仍走原有匹配排序。 --- src/components/CommandPalette.vue | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/CommandPalette.vue b/src/components/CommandPalette.vue index 341773f..388e301 100644 --- a/src/components/CommandPalette.vue +++ b/src/components/CommandPalette.vue @@ -28,6 +28,7 @@ {{ cmd.label }} {{ cmd.group }} {{ cmd.hint }} +
@@ -37,7 +38,8 @@ diff --git a/src/composables/useNamedWorkspaces.ts b/src/composables/useNamedWorkspaces.ts new file mode 100644 index 0000000..6f6b21f --- /dev/null +++ b/src/composables/useNamedWorkspaces.ts @@ -0,0 +1,41 @@ +// 命名工作区:把一组根(主根 + 额外挂载根)存为命名条目,便于切换。 +import {ref} from 'vue' +import {kvGetJSON, kvSetJSON} from './useKvStore' + +export interface NamedWorkspace { + name: string + rootDir: string + extraRoots: string[] +} + +const KEY = 'named-workspaces' + +export function useNamedWorkspaces() { + const workspaces = ref(kvGetJSON(KEY, [])) + + const persist = () => kvSetJSON(KEY, workspaces.value) + + // 保存/覆盖同名条目 + const save = (name: string, rootDir: string, extraRoots: string[]) => { + const n = name.trim() + if (!n || !rootDir) { + return + } + const entry: NamedWorkspace = {name: n, rootDir, extraRoots: [...extraRoots]} + const idx = workspaces.value.findIndex(w => w.name === n) + if (idx >= 0) { + workspaces.value = workspaces.value.map((w, i) => (i === idx ? entry : w)) + } + else { + workspaces.value = [...workspaces.value, entry] + } + persist() + } + + const remove = (name: string) => { + workspaces.value = workspaces.value.filter(w => w.name !== name) + persist() + } + + return {workspaces, save, remove} +} diff --git a/src/composables/useWorkspaceRoots.ts b/src/composables/useWorkspaceRoots.ts index 251b63a..4ef7fb3 100644 --- a/src/composables/useWorkspaceRoots.ts +++ b/src/composables/useWorkspaceRoots.ts @@ -29,5 +29,11 @@ export function useWorkspaceRoots(rootDir: Ref) { persist() } - return {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots} + // 整体设置额外根(打开命名工作区时用) + const setExtraRoots = (paths: string[]) => { + extraRoots.value = [...paths] + persist() + } + + return {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots, setExtraRoots} } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 25147fb..e5c75e7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -671,6 +671,7 @@ "formatOnSaveOff": "Disable format on save", "open": "Open file", "openFolder": "Open folder", + "workspaces": "Named workspaces", "save": "Save file", "saveAs": "Save as", "newTab": "New tab", @@ -822,6 +823,13 @@ "noOutput": "No output", "noOutputHint": "Try running some code" }, + "workspaces": { + "title": "Named workspaces", + "namePlaceholder": "Name this workspace…", + "saveCurrent": "Save current", + "empty": "No saved workspaces yet", + "delete": "Delete" + }, "sidebar": { "explorer": "Explorer", "openFolder": "Open folder", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 6892c9a..f3f46a2 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -671,6 +671,7 @@ "formatOnSaveOff": "关闭保存时格式化", "open": "打开文件", "openFolder": "打开文件夹", + "workspaces": "命名工作区", "save": "保存文件", "saveAs": "另存为", "newTab": "新建标签", @@ -822,6 +823,13 @@ "noOutput": "没有输出", "noOutputHint": "可以尝试运行一些代码" }, + "workspaces": { + "title": "命名工作区", + "namePlaceholder": "为当前工作区命名…", + "saveCurrent": "保存当前", + "empty": "还没有保存的工作区", + "delete": "删除" + }, "sidebar": { "explorer": "资源管理器", "openFolder": "打开文件夹", From 662511a654f1bc8615b435b7fb288ee9151660cc Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 2 Jul 2026 15:05:16 +0800 Subject: [PATCH 09/10] =?UTF-8?q?feat(ai):=20=E6=96=B0=E5=A2=9E=E3=80=8CAI?= =?UTF-8?q?=20=E7=94=9F=E6=88=90=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AiCodeAction 增加 doc 模式:为选中代码生成规范文档注释;应用时插入到目标行上方并对齐缩进。 编辑器右键与命令面板均可触发。 --- src/App.vue | 18 +++++++++++++++--- src/components/AiCodeAction.vue | 6 ++++-- src/i18n/locales/en.json | 4 +++- src/i18n/locales/zh-CN.json | 4 +++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/App.vue b/src/App.vue index 8fc77d1..4727c4e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -465,6 +465,7 @@ +
- + @@ -37,7 +37,7 @@ import {useI18n} from 'vue-i18n' import {useAiConfig} from '../composables/useAiConfig' import {useToast} from '../plugins/toast' -const props = defineProps<{ language: string; code: string; action: 'explain' | 'refactor' | 'test' | 'fix'; diagnostics?: string }>() +const props = defineProps<{ language: string; code: string; action: 'explain' | 'refactor' | 'test' | 'fix' | 'doc'; diagnostics?: string }>() const emit = defineEmits<{ replace: [code: string]; insert: [code: string]; close: [] }>() const toast = useToast() @@ -64,6 +64,8 @@ const systemFor = (): string => { return `你是测试工程师。为给定的 ${lang} 代码生成单元测试。只输出测试代码,不要解释,不要使用 Markdown 代码块标记。` case 'fix': return `你是代码助手。修复给定 ${lang} 代码中的错误与警告(用户消息附带诊断信息),保持其余行为不变。只输出修复后的完整代码,不要解释,不要使用 Markdown 代码块标记。` + case 'doc': + return `你是代码助手。为给定的 ${lang} 代码生成规范的文档注释(如 JSDoc/docstring/rustdoc 等,与语言习惯一致)。只输出注释块本身,不要重复原代码,不要解释,不要使用 Markdown 代码块标记。` } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e5c75e7..d32ed8f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -688,6 +688,7 @@ "generateTests": "AI generate tests (selection or all)", "formatWithAi": "AI format code", "aiFixDiagnostics": "AI fix diagnostics", + "aiGenDoc": "AI generate doc comment", "history": "Run history", "diff": "Diff (current vs saved)", "preview": "Live preview (Markdown / HTML)", @@ -1147,7 +1148,8 @@ "explain": "AI: Explain code", "refactor": "AI: Refactor code", "test": "AI: Generate tests", - "fix": "AI: Fix diagnostics" + "fix": "AI: Fix diagnostics", + "doc": "AI: Generate doc comment" }, "thinking": "AI is thinking…", "replace": "Replace selection", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index f3f46a2..f20ed59 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -688,6 +688,7 @@ "generateTests": "AI 生成测试(选中或全文)", "formatWithAi": "AI 格式化代码", "aiFixDiagnostics": "AI 修复诊断", + "aiGenDoc": "AI 生成文档注释", "history": "执行历史", "diff": "差异对比(当前 vs 已保存)", "preview": "实时预览(Markdown / HTML)", @@ -1147,7 +1148,8 @@ "explain": "AI 解释代码", "refactor": "AI 重构代码", "test": "AI 生成测试", - "fix": "AI 修复诊断" + "fix": "AI 修复诊断", + "doc": "AI 生成文档注释" }, "thinking": "AI 思考中…", "replace": "替换选区", From d136a0039cff096dabdecdce4fe2a68204e0c0b3 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 2 Jul 2026 15:10:36 +0800 Subject: [PATCH 10/10] =?UTF-8?q?feat(outline):=20=E5=A4=A7=E7=BA=B2?= =?UTF-8?q?=E4=BC=98=E5=85=88=E7=94=A8=20LSP=20documentSymbol=EF=BC=88?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E6=AD=A3=E5=88=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lspExtension 新增 fetchDocumentSymbols(复用 plugin.client.request),兼容分层/扁平结果; 大纲打开时尝试 LSP 精确符号(含真实层级与 SymbolKind),不可用则回退正则提取。 --- src/App.vue | 1 + src/components/Outline.vue | 50 +++++++++++++++++++++++-------- src/editor/lspExtension.ts | 61 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/App.vue b/src/App.vue index 4727c4e..7b821ee 100644 --- a/src/App.vue +++ b/src/App.vue @@ -356,6 +356,7 @@ :code="code" :language="currentLanguage" :current-line="cursorInfo.line" + :view="editorView" @go="gotoLine" @close="showOutline = false"/> diff --git a/src/components/Outline.vue b/src/components/Outline.vue index 73384a8..76ac295 100644 --- a/src/components/Outline.vue +++ b/src/components/Outline.vue @@ -39,14 +39,18 @@ import {computed, nextTick, onMounted, ref, watch} from 'vue' import {useI18n} from 'vue-i18n' import {ListTree} from 'lucide-vue-next' +import {fetchDocumentSymbols, lspSymbolKindLabel} from '../editor/lspExtension' const {t} = useI18n() interface Symbol { name: string; kind: string; line: number; depth: number } -const props = defineProps<{ code: string; language?: string; currentLine?: number }>() +const props = defineProps<{ code: string; language?: string; currentLine?: number; view?: any }>() const emit = defineEmits<{ go: [line: number]; close: [] }>() +// LSP 精确符号(可用时优先于正则) +const lspSymbols = ref(null) + const query = ref('') const activeIndex = ref(0) const inputRef = ref(null) @@ -56,19 +60,36 @@ const setItemRef = (el: any, i: number) => { if (el) itemRefs[i] = el } -onMounted(() => { +// 预选:定位到光标所在(或其上方最近)的符号,打开即落在当前函数 +const preselectAtCursor = () => { + if (!props.currentLine || !symbols.value.length) { + return + } + let idx = -1 + for (let i = 0; i < symbols.value.length; i++) { + if (symbols.value[i].line <= props.currentLine) idx = i + else break + } + if (idx >= 0) { + activeIndex.value = idx + nextTick(() => itemRefs[idx]?.scrollIntoView({block: 'center'})) + } +} + +onMounted(async () => { inputRef.value?.focus() - // 预选:定位到光标所在(或其上方最近)的符号,打开即落在当前函数 - if (props.currentLine && symbols.value.length) { - let idx = -1 - for (let i = 0; i < symbols.value.length; i++) { - if (symbols.value[i].line <= props.currentLine) idx = i - else break - } - if (idx >= 0) { - activeIndex.value = idx - nextTick(() => itemRefs[idx]?.scrollIntoView({block: 'center'})) + preselectAtCursor() + // 尝试用 LSP documentSymbol 精确符号替换正则结果 + if (props.view) { + try { + const syms = await fetchDocumentSymbols(props.view) + if (syms && syms.length) { + lspSymbols.value = syms.map(s => ({name: s.name, kind: lspSymbolKindLabel(s.kind), line: s.line, depth: s.depth})) + activeIndex.value = 0 + preselectAtCursor() + } } + catch { /* 回退到正则 */ } } }) @@ -148,7 +169,10 @@ const rulesFor = (fam: string): [RegExp, string, number][] => { } } -const symbols = computed(() => { +// LSP 符号可用则优先,否则回退到正则提取 +const symbols = computed(() => lspSymbols.value ?? regexSymbols.value) + +const regexSymbols = computed(() => { const lines = (props.code || '').split('\n') const rules = rulesFor(family.value) const raw: { name: string; kind: string; line: number; indent: number }[] = [] diff --git a/src/editor/lspExtension.ts b/src/editor/lspExtension.ts index dce3935..2176157 100644 --- a/src/editor/lspExtension.ts +++ b/src/editor/lspExtension.ts @@ -217,6 +217,67 @@ export const formatDocumentAsync = async ( } } +export interface LspSymbol { + name: string + kind: number // LSP SymbolKind (1..26) + line: number // 1-based + depth: number +} + +// LSP SymbolKind → 大纲显示的短标签 +const SYMBOL_KIND_LABEL: Record = { + 2: 'mod', 3: 'ns', 4: 'pkg', 5: 'class', 6: 'method', 7: 'prop', 8: 'field', + 9: 'ctor', 10: 'enum', 11: 'iface', 12: 'fn', 13: 'var', 14: 'const', + 22: 'member', 23: 'struct', 24: 'event', 26: 'type' +} +export const lspSymbolKindLabel = (kind: number): string => SYMBOL_KIND_LABEL[kind] || 'sym' + +/** + * 请求 textDocument/documentSymbol,规范化为带层级的扁平列表。 + * 兼容分层的 DocumentSymbol[] 与扁平的 SymbolInformation[];不支持则返回 null。 + */ +export const fetchDocumentSymbols = async (view: EditorView): Promise => { + const plugin: any = view.plugin(languageServerPlugin as any) + const client = plugin?.client + if (!client?.ready || !client.capabilities?.documentSymbolProvider) { + return null + } + try { + const res: any = await client.request( + 'textDocument/documentSymbol', + {textDocument: {uri: plugin.documentUri}}, + 10000 + ) + if (!Array.isArray(res) || res.length === 0) { + return null + } + const out: LspSymbol[] = [] + if (res[0].range !== undefined) { + // 分层 DocumentSymbol[] + const walk = (nodes: any[], depth: number) => { + for (const n of nodes) { + const startLine = (n.selectionRange?.start?.line ?? n.range.start.line) + 1 + out.push({name: n.name, kind: n.kind, line: startLine, depth}) + if (n.children?.length) { + walk(n.children, depth + 1) + } + } + } + walk(res, 0) + } + else { + // 扁平 SymbolInformation[] + for (const s of res) { + out.push({name: s.name, kind: s.kind, line: s.location.range.start.line + 1, depth: 0}) + } + } + return out + } + catch { + return null + } +} + /** * 触发代码操作:请求后派发 lsp:code-actions(携带动作与锚点坐标)由 App 弹菜单。 * 供 Cmd+. 键位与右键菜单共用。