From f5e90c504dc869b9f5d9fd2f144a9e63e77e1531 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 18:41:26 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(ci):=20docs=20workflow=20=E9=99=90?= =?UTF-8?q?=E5=AE=9A=E5=88=86=E6=94=AF=E8=A7=A6=E5=8F=91=E5=B9=B6=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发布打的 tag(26.3.0) 触发了 docs 部署,但 github-pages 环境保护规则不允许从 tag 部署 → deploy 被拒。push 触发加 branches:['**'] 只在分支生效,标签不再触发部署。 --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 47eabfcc..9f02a43c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,9 @@ name: Deploy docs to GitHub Pages on: push: + # 限定分支触发,排除标签:发布打的 tag 会触发部署,但 github-pages + # 环境保护规则不允许从 tag 部署,导致 deploy 被拒。 + branches: [ '**' ] paths: [ 'docs/**', '.github/workflows/docs.yml' ] workflow_dispatch: From ced7fe7e2c207adebd704528529562a6a1e0b3a1 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 18:49:45 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(editor):=20=E6=96=B0=E5=A2=9E=E8=A1=8C?= =?UTF-8?q?=E4=B9=A6=E7=AD=BE=EF=BC=88=E5=88=87=E6=8D=A2/=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E5=A4=84/=E4=B8=8B=E4=B8=80=E5=A4=84/=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=EF=BC=8C=E6=8C=89=E6=96=87=E4=BB=B6=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeMirror 行装饰(左侧强调条+淡底)标记书签,数据按文件路径记忆并在切换文件时同步; 命令面板「书签」分组 + 快捷键 Mod+Alt+K 切换,可在书签间上下跳转。 --- src/App.vue | 13 ++++- src/composables/useBookmarks.ts | 75 ++++++++++++++++++++++++++ src/composables/useCodeMirrorEditor.ts | 4 ++ src/composables/useShortcuts.ts | 3 +- src/editor/bookmark.ts | 35 ++++++++++++ src/i18n/locales/en.json | 8 ++- src/i18n/locales/zh-CN.json | 8 ++- 7 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/composables/useBookmarks.ts create mode 100644 src/editor/bookmark.ts diff --git a/src/App.vue b/src/App.vue index f02d49f9..b545f1d6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -510,7 +510,7 @@ import {debounce} from 'lodash-es' import {formatDocument, formatSelection, renameSymbol} from 'codemirror-languageserver' import {runGotoDefinition, lspSupportsLanguage, triggerCodeActions, applyCodeAction, formatDocumentAsync} from './editor/lspExtension' import {dapSupportsLanguage} from './debug/dapClient' -import {ArrowDownAZ, ArrowUpAZ, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FolderOpen, GitBranch, GitCompare, History, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, WrapText, X} from 'lucide-vue-next' +import {ArrowDownAZ, ArrowUpAZ, Bookmark, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FolderOpen, GitBranch, GitCompare, History, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, WrapText, X} from 'lucide-vue-next' import {ExecutionResult, LayoutMode, SplitDirection} from './types/app.ts' import AppHeader from './components/AppHeader.vue' import CodeEditor from './components/CodeEditor.vue' @@ -541,6 +541,7 @@ import {useFileManager} from './composables/useFileManager' import {useLanguageRegistry} from './composables/useLanguageRegistry' import {useWorkspace} from './composables/useWorkspace' import {useTextCommands} from './composables/useTextCommands' +import {useBookmarks} from './composables/useBookmarks' import {useGitPermalink} from './composables/useGitPermalink' import {useRevealInTree} from './composables/useRevealInTree' import {useWorkspaceRoots} from './composables/useWorkspaceRoots' @@ -1117,6 +1118,9 @@ const editorView = shallowRef(null) // ===== 文本变换命令(排序行/大小写/去重/去行尾空白)===== const {transformSelectionOrLine, sortLines, removeDuplicateLines, trimTrailingWhitespace} = useTextCommands(editorView) +// ===== 行书签(切换/上一处/下一处/清空,按文件记忆)===== +const {toggleBookmark, nextBookmark, prevBookmark, clearBookmarks} = useBookmarks(editorView, currentFilePath) + // 复制为 Markdown 代码块(选区或全文,带语言围栏) const copyAsMarkdown = async () => { const view = editorView.value @@ -2154,7 +2158,8 @@ const shortcutDispatch: Record void> = { reopenClosed: () => handleReopenClosed(), toggleSidebar: () => toggleSidebar(), toggleTerminal: () => toggleTerminal(), - toggleWordWrap: () => toggleWordWrap() + toggleWordWrap: () => toggleWordWrap(), + toggleBookmark: () => toggleBookmark() } // 切换自动换行(即时生效并随编辑器配置持久化) @@ -2227,6 +2232,10 @@ const paletteCommands = computed(() => [ {id: 'copyAsMarkdown', label: t('command.copyAsMarkdown'), group: t('command.groupText'), icon: Code2, run: () => copyAsMarkdown()}, {id: 'indentToSpaces', label: t('command.indentToSpaces'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(false)}, {id: 'indentToTabs', label: t('command.indentToTabs'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(true)}, + {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()}, + {id: 'clearBookmarks', label: t('command.clearBookmarks'), group: t('command.groupBookmark'), icon: Bookmark, run: () => clearBookmarks()}, {id: 'toggleAutoReveal', label: t('command.toggleAutoReveal'), icon: FolderOpen, run: () => toggleAutoReveal()}, {id: 'toggleSidebar', label: t('command.toggleSidebar'), icon: PanelLeft, hint: hintOf('toggleSidebar'), run: () => toggleSidebar()}, {id: 'toggleZen', label: t('command.toggleZen'), icon: Minimize2, run: () => toggleZen()}, diff --git a/src/composables/useBookmarks.ts b/src/composables/useBookmarks.ts new file mode 100644 index 00000000..4423a202 --- /dev/null +++ b/src/composables/useBookmarks.ts @@ -0,0 +1,75 @@ +// 行书签:按文件记忆书签行,提供切换/上一处/下一处/清空,并在切换文件时同步到编辑器。 +import {ref, watch, type Ref} from 'vue' +import {setBookmarks} from '../editor/bookmark' + +export function useBookmarks(editorView: Ref, currentFilePath: Ref) { + // 按文件路径记忆书签行号(1-based);未命名文件用空串作 key + const map = ref>({}) + const keyOf = () => currentFilePath.value ?? '' + const current = () => map.value[keyOf()] ?? [] + + // 把当前文件的书签派发到编辑器 + const apply = () => { + editorView.value?.dispatch({effects: setBookmarks.of(current())}) + } + + const cursorLine = (view: any): number => view.state.doc.lineAt(view.state.selection.main.head).number + const gotoLine = (view: any, ln: number) => { + const pos = view.state.doc.line(ln).from + view.dispatch({selection: {anchor: pos}, scrollIntoView: true}) + view.focus() + } + + const toggleBookmark = () => { + const view = editorView.value + if (!view) { + return + } + const ln = cursorLine(view) + const key = keyOf() + const arr = map.value[key] ? [...map.value[key]] : [] + const i = arr.indexOf(ln) + if (i >= 0) { + arr.splice(i, 1) + } + else { + arr.push(ln) + } + map.value = {...map.value, [key]: arr} + apply() + } + + const nextBookmark = () => { + const view = editorView.value + const arr = [...current()].sort((a, b) => a - b) + if (!view || !arr.length) { + return + } + const ln = cursorLine(view) + gotoLine(view, arr.find(l => l > ln) ?? arr[0]) + } + + const prevBookmark = () => { + const view = editorView.value + const arr = [...current()].sort((a, b) => a - b) + if (!view || !arr.length) { + return + } + const ln = cursorLine(view) + gotoLine(view, [...arr].reverse().find(l => l < ln) ?? arr[arr.length - 1]) + } + + const clearBookmarks = () => { + const key = keyOf() + if (map.value[key]?.length) { + map.value = {...map.value, [key]: []} + apply() + } + } + + // 切换文件 / 编辑器重挂时同步当前文件的书签 + watch(currentFilePath, () => apply()) + watch(editorView, () => apply()) + + return {toggleBookmark, nextBookmark, prevBookmark, clearBookmarks} +} diff --git a/src/composables/useCodeMirrorEditor.ts b/src/composables/useCodeMirrorEditor.ts index 8a30822c..f3334a62 100644 --- a/src/composables/useCodeMirrorEditor.ts +++ b/src/composables/useCodeMirrorEditor.ts @@ -91,6 +91,7 @@ import {useCodeMirrorSpaceOmission} from './useCodeMirrorSpaceOmission.ts' import {EditorView, keymap} from "@codemirror/view"; import {showMinimap} from "@replit/codemirror-minimap"; import {indentationMarkers} from "@replit/codemirror-indentation-markers"; +import {bookmarkExtension} from "../editor/bookmark"; import {stickyScroll} from "../editor/stickyScroll"; import {breakpointExtension} from "../editor/breakpointGutter"; import {debugHover} from "../editor/debugHover"; @@ -703,6 +704,9 @@ export function useCodeMirrorEditor(props: Props) result.push(indentationMarkers({hideFirstIndent: true, highlightActiveBlock: true})) } + // 书签:行高亮,数据由 App 按当前文件 dispatch + result.push(bookmarkExtension) + // 断点 gutter + 调试悬停求值(仅可调试语言) if (dapSupportsLanguage(props.language)) { result.push(breakpointExtension((line) => debug.toggleBreakpoint(props.filePath ?? null, line))) diff --git a/src/composables/useShortcuts.ts b/src/composables/useShortcuts.ts index c8b6a2cf..1bba7363 100644 --- a/src/composables/useShortcuts.ts +++ b/src/composables/useShortcuts.ts @@ -28,7 +28,8 @@ const SHORTCUT_DEFS: { id: string; default: string }[] = [ {id: 'reopenClosed', default: 'Mod+Shift+T'}, {id: 'toggleSidebar', default: 'Mod+B'}, {id: 'toggleTerminal', default: 'Mod+`'}, - {id: 'toggleWordWrap', default: 'Alt+Z'} + {id: 'toggleWordWrap', default: 'Alt+Z'}, + {id: 'toggleBookmark', default: 'Mod+Alt+K'} ] const STORAGE_KEY = 'shortcuts' diff --git a/src/editor/bookmark.ts b/src/editor/bookmark.ts new file mode 100644 index 00000000..f2eba9b6 --- /dev/null +++ b/src/editor/bookmark.ts @@ -0,0 +1,35 @@ +// 书签:按行高亮(左侧强调条 + 淡底色)。书签数据由外部(App)按当前文件 dispatch 填充。 +import {Decoration, EditorView, type DecorationSet} from '@codemirror/view' +import {StateEffect, StateField} from '@codemirror/state' + +// 由外部派发:设置当前文件的书签行号(1-based) +export const setBookmarks = StateEffect.define() + +const bookmarkLine = Decoration.line({class: 'cm-bookmark-line'}) + +export const bookmarkField = StateField.define({ + create: () => Decoration.none, + update(deco, tr) { + for (const e of tr.effects) { + if (e.is(setBookmarks)) { + const ranges = e.value + .filter(ln => ln >= 1 && ln <= tr.state.doc.lines) + .sort((a, b) => a - b) + .map(ln => bookmarkLine.range(tr.state.doc.line(ln).from)) + return Decoration.set(ranges, true) + } + } + // 文档变化时让标记跟随行偏移 + return deco.map(tr.changes) + }, + provide: f => EditorView.decorations.from(f) +}) + +const bookmarkTheme = EditorView.baseTheme({ + '.cm-bookmark-line': { + backgroundColor: 'rgba(99, 102, 241, 0.10)', + boxShadow: 'inset 3px 0 0 #6366f1' + } +}) + +export const bookmarkExtension = [bookmarkField, bookmarkTheme] diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 62fa370e..2021d972 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -694,6 +694,11 @@ "sendToTerminal": "Send selection to terminal", "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", + "toggleBookmark": "Toggle bookmark", + "nextBookmark": "Next bookmark", + "prevBookmark": "Previous bookmark", + "clearBookmarks": "Clear bookmarks in file", + "groupBookmark": "Bookmarks", "copyPermalink": "Copy remote permalink (current line)", "openPermalink": "Open remote link in browser (current line)", "sortLinesAsc": "Sort lines ascending", @@ -738,7 +743,8 @@ "reopenClosed": "Reopen closed tab", "toggleSidebar": "Toggle sidebar", "toggleTerminal": "Toggle terminal", - "toggleWordWrap": "Toggle word wrap" + "toggleWordWrap": "Toggle word wrap", + "toggleBookmark": "Toggle bookmark" }, "view": { "clear": "Clear", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 54ec2b32..a09c797f 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -694,6 +694,11 @@ "sendToTerminal": "发送选区到终端", "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", + "toggleBookmark": "切换书签", + "nextBookmark": "下一处书签", + "prevBookmark": "上一处书签", + "clearBookmarks": "清空本文件书签", + "groupBookmark": "书签", "copyPermalink": "复制远程永久链接(当前行)", "openPermalink": "在浏览器打开远程链接(当前行)", "sortLinesAsc": "排序行(升序)", @@ -738,7 +743,8 @@ "reopenClosed": "重新打开已关闭标签", "toggleSidebar": "切换侧栏", "toggleTerminal": "切换终端", - "toggleWordWrap": "切换自动换行" + "toggleWordWrap": "切换自动换行", + "toggleBookmark": "切换书签" }, "view": { "clear": "清空", From 609151fcd609110ad244b397aeb394131d671c8c Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 22:36:00 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(editor):=20=E5=91=BD=E4=BB=A4=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=96=B0=E5=A2=9E=E3=80=8C=E6=8A=98=E5=8F=A0=E5=85=A8?= =?UTF-8?q?=E9=83=A8/=E5=B1=95=E5=BC=80=E5=85=A8=E9=83=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 @codemirror/language 的 foldAll/unfoldAll 暴露为命令面板「代码」分组项, 让基础折叠键位之外也能一键折叠/展开整篇。 --- src/App.vue | 5 ++++- src/i18n/locales/en.json | 3 +++ src/i18n/locales/zh-CN.json | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index b545f1d6..c2cb4101 100644 --- a/src/App.vue +++ b/src/App.vue @@ -510,7 +510,7 @@ import {debounce} from 'lodash-es' import {formatDocument, formatSelection, renameSymbol} from 'codemirror-languageserver' import {runGotoDefinition, lspSupportsLanguage, triggerCodeActions, applyCodeAction, formatDocumentAsync} from './editor/lspExtension' import {dapSupportsLanguage} from './debug/dapClient' -import {ArrowDownAZ, ArrowUpAZ, Bookmark, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FolderOpen, GitBranch, GitCompare, History, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, WrapText, X} from 'lucide-vue-next' +import {ArrowDownAZ, ArrowUpAZ, Bookmark, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FoldVertical, FolderOpen, GitBranch, GitCompare, History, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, UnfoldVertical, WrapText, X} from 'lucide-vue-next' import {ExecutionResult, LayoutMode, SplitDirection} from './types/app.ts' import AppHeader from './components/AppHeader.vue' import CodeEditor from './components/CodeEditor.vue' @@ -542,6 +542,7 @@ import {useLanguageRegistry} from './composables/useLanguageRegistry' import {useWorkspace} from './composables/useWorkspace' import {useTextCommands} from './composables/useTextCommands' import {useBookmarks} from './composables/useBookmarks' +import {foldAll, unfoldAll} from '@codemirror/language' import {useGitPermalink} from './composables/useGitPermalink' import {useRevealInTree} from './composables/useRevealInTree' import {useWorkspaceRoots} from './composables/useWorkspaceRoots' @@ -2232,6 +2233,8 @@ const paletteCommands = computed(() => [ {id: 'copyAsMarkdown', label: t('command.copyAsMarkdown'), group: t('command.groupText'), icon: Code2, run: () => copyAsMarkdown()}, {id: 'indentToSpaces', label: t('command.indentToSpaces'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(false)}, {id: 'indentToTabs', label: t('command.indentToTabs'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(true)}, + {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: '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/i18n/locales/en.json b/src/i18n/locales/en.json index 2021d972..fe9c423e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -694,6 +694,9 @@ "sendToTerminal": "Send selection to terminal", "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", + "foldAll": "Fold all", + "unfoldAll": "Unfold all", + "groupCode": "Code", "toggleBookmark": "Toggle bookmark", "nextBookmark": "Next bookmark", "prevBookmark": "Previous bookmark", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index a09c797f..5e50133f 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -694,6 +694,9 @@ "sendToTerminal": "发送选区到终端", "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", + "foldAll": "折叠全部代码", + "unfoldAll": "展开全部代码", + "groupCode": "代码", "toggleBookmark": "切换书签", "nextBookmark": "下一处书签", "prevBookmark": "上一处书签", From 4d356a13d7b67bd1bcfc0dc78ffc31b2228d3ed9 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 22:57:37 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(editor):=20=E5=91=BD=E4=BB=A4=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=96=B0=E5=A2=9E=E3=80=8C=E8=BD=AC=E5=88=B0=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E6=8B=AC=E5=8F=B7=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 @codemirror/language 的 matchBrackets 取光标前后括号的配对并跳转, 无匹配时提示;归入「代码」命令分组。 --- src/App.vue | 20 +++++++++++++++++++- src/i18n/locales/en.json | 2 ++ src/i18n/locales/zh-CN.json | 2 ++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index c2cb4101..b9205664 100644 --- a/src/App.vue +++ b/src/App.vue @@ -542,7 +542,7 @@ import {useLanguageRegistry} from './composables/useLanguageRegistry' import {useWorkspace} from './composables/useWorkspace' import {useTextCommands} from './composables/useTextCommands' import {useBookmarks} from './composables/useBookmarks' -import {foldAll, unfoldAll} from '@codemirror/language' +import {foldAll, unfoldAll, matchBrackets} from '@codemirror/language' import {useGitPermalink} from './composables/useGitPermalink' import {useRevealInTree} from './composables/useRevealInTree' import {useWorkspaceRoots} from './composables/useWorkspaceRoots' @@ -1173,6 +1173,23 @@ const convertIndentation = (toTabs: boolean) => { toast.success(t('app.indentConverted')) } +// 转到匹配括号:取光标前后的括号,跳到其配对处 +const goToMatchingBracket = () => { + const view = editorView.value + if (!view) { + return + } + const pos = view.state.selection.main.head + const m = matchBrackets(view.state, pos, -1) || matchBrackets(view.state, pos, 1) + if (m && m.matched && m.end) { + view.dispatch({selection: {anchor: m.end.from}, scrollIntoView: true}) + view.focus() + } + else { + toast.info(t('app.noMatchingBracket')) + } +} + // AI 自然语言生成 / 选区改写 const showGenerate = ref(false) const generateSelection = ref('') @@ -2235,6 +2252,7 @@ const paletteCommands = computed(() => [ {id: 'indentToTabs', label: t('command.indentToTabs'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(true)}, {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: '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/i18n/locales/en.json b/src/i18n/locales/en.json index fe9c423e..558370e2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -696,6 +696,7 @@ "revealInTree": "Reveal in file tree", "foldAll": "Fold all", "unfoldAll": "Unfold all", + "goToMatchingBracket": "Go to matching bracket", "groupCode": "Code", "toggleBookmark": "Toggle bookmark", "nextBookmark": "Next bookmark", @@ -1336,6 +1337,7 @@ "pathCopied": "Path copied", "copiedMarkdown": "Copied as Markdown code block", "indentConverted": "Indentation converted", + "noMatchingBracket": "No matching bracket at cursor", "clipboardEmpty": "Clipboard is empty", "clipboardReadFailed": "Failed to read clipboard: ", "copyFailed": "Copy failed: ", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 5e50133f..40f8e931 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -696,6 +696,7 @@ "revealInTree": "在文件树中定位", "foldAll": "折叠全部代码", "unfoldAll": "展开全部代码", + "goToMatchingBracket": "转到匹配括号", "groupCode": "代码", "toggleBookmark": "切换书签", "nextBookmark": "下一处书签", @@ -1336,6 +1337,7 @@ "pathCopied": "已复制路径", "copiedMarkdown": "已复制为 Markdown 代码块", "indentConverted": "已转换缩进", + "noMatchingBracket": "光标处没有可匹配的括号", "clipboardEmpty": "剪贴板为空", "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ", From 754b22e76308a0dd04b45c7ecbf07d673a6c5149 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 23:31:39 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(ai):=20=E6=96=B0=E5=A2=9E=E3=80=8CAI?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E8=AF=8A=E6=96=AD=E3=80=8D=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=8A=8A=E5=BD=93=E5=89=8D=E6=96=87=E4=BB=B6=20LSP=20?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E4=BA=A4=E7=BB=99=20AI=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AiCodeAction 增加 fix 模式并接收诊断文本;App 收集当前文件诊断 + 整篇代码送入, 结果经替换应用;编辑器右键(支持 LSP/Git 时)与命令面板均可触发。 --- src/App.vue | 21 +++++++++++++++++++-- src/components/AiCodeAction.vue | 14 +++++++++++--- src/i18n/locales/en.json | 5 ++++- src/i18n/locales/zh-CN.json | 5 ++++- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/App.vue b/src/App.vue index b9205664..930d9bf4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -399,7 +399,7 @@ - @@ -463,6 +463,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' }>() +const props = defineProps<{ language: string; code: string; action: 'explain' | 'refactor' | 'test' | 'fix'; diagnostics?: string }>() const emit = defineEmits<{ replace: [code: string]; insert: [code: string]; close: [] }>() const toast = useToast() @@ -62,9 +62,17 @@ const systemFor = (): string => { return `你是代码助手。重构给定的 ${lang} 代码以提升可读性与质量,保持行为不变。只输出重构后的完整代码,不要解释,不要使用 Markdown 代码块标记。` case 'test': return `你是测试工程师。为给定的 ${lang} 代码生成单元测试。只输出测试代码,不要解释,不要使用 Markdown 代码块标记。` + case 'fix': + return `你是代码助手。修复给定 ${lang} 代码中的错误与警告(用户消息附带诊断信息),保持其余行为不变。只输出修复后的完整代码,不要解释,不要使用 Markdown 代码块标记。` } } +// 'fix' 把诊断附在用户消息里 +const userContent = (): string => + props.action === 'fix' && props.diagnostics + ? `${props.code}\n\n--- 待修复的诊断 ---\n${props.diagnostics}` + : props.code + const run = async () => { reload() if (!active.value.apiKey) { @@ -80,7 +88,7 @@ const run = async () => { apiKey: active.value.apiKey, model: active.value.model, system: systemFor(), - messages: [{role: 'user', content: props.code}] + messages: [{role: 'user', content: userContent()}] }) result.value = props.action === 'explain' ? reply.trim() : stripFences(reply) } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 558370e2..f9018e1f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -683,6 +683,7 @@ "explainCode": "AI explain code (selection or all)", "generateTests": "AI generate tests (selection or all)", "formatWithAi": "AI format code", + "aiFixDiagnostics": "AI fix diagnostics", "history": "Run history", "diff": "Diff (current vs saved)", "preview": "Live preview (Markdown / HTML)", @@ -1128,7 +1129,8 @@ "title": { "explain": "AI: Explain code", "refactor": "AI: Refactor code", - "test": "AI: Generate tests" + "test": "AI: Generate tests", + "fix": "AI: Fix diagnostics" }, "thinking": "AI is thinking…", "replace": "Replace selection", @@ -1338,6 +1340,7 @@ "copiedMarkdown": "Copied as Markdown code block", "indentConverted": "Indentation converted", "noMatchingBracket": "No matching bracket at cursor", + "noDiagnostics": "No diagnostics in the current file", "clipboardEmpty": "Clipboard is empty", "clipboardReadFailed": "Failed to read clipboard: ", "copyFailed": "Copy failed: ", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 40f8e931..17a19a49 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -683,6 +683,7 @@ "explainCode": "AI 解释代码(选中或全文)", "generateTests": "AI 生成测试(选中或全文)", "formatWithAi": "AI 格式化代码", + "aiFixDiagnostics": "AI 修复诊断", "history": "执行历史", "diff": "差异对比(当前 vs 已保存)", "preview": "实时预览(Markdown / HTML)", @@ -1128,7 +1129,8 @@ "title": { "explain": "AI 解释代码", "refactor": "AI 重构代码", - "test": "AI 生成测试" + "test": "AI 生成测试", + "fix": "AI 修复诊断" }, "thinking": "AI 思考中…", "replace": "替换选区", @@ -1338,6 +1340,7 @@ "copiedMarkdown": "已复制为 Markdown 代码块", "indentConverted": "已转换缩进", "noMatchingBracket": "光标处没有可匹配的括号", + "noDiagnostics": "当前文件没有诊断问题", "clipboardEmpty": "剪贴板为空", "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ",