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: diff --git a/src/App.vue b/src/App.vue index f02d49f9..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/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..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)", @@ -694,6 +695,15 @@ "sendToTerminal": "Send selection to terminal", "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", + "foldAll": "Fold all", + "unfoldAll": "Unfold all", + "goToMatchingBracket": "Go to matching bracket", + "groupCode": "Code", + "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 +748,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", @@ -1118,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", @@ -1327,6 +1339,8 @@ "pathCopied": "Path copied", "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 54ec2b32..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)", @@ -694,6 +695,15 @@ "sendToTerminal": "发送选区到终端", "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", + "foldAll": "折叠全部代码", + "unfoldAll": "展开全部代码", + "goToMatchingBracket": "转到匹配括号", + "groupCode": "代码", + "toggleBookmark": "切换书签", + "nextBookmark": "下一处书签", + "prevBookmark": "上一处书签", + "clearBookmarks": "清空本文件书签", + "groupBookmark": "书签", "copyPermalink": "复制远程永久链接(当前行)", "openPermalink": "在浏览器打开远程链接(当前行)", "sortLinesAsc": "排序行(升序)", @@ -738,7 +748,8 @@ "reopenClosed": "重新打开已关闭标签", "toggleSidebar": "切换侧栏", "toggleTerminal": "切换终端", - "toggleWordWrap": "切换自动换行" + "toggleWordWrap": "切换自动换行", + "toggleBookmark": "切换书签" }, "view": { "clear": "清空", @@ -1118,7 +1129,8 @@ "title": { "explain": "AI 解释代码", "refactor": "AI 重构代码", - "test": "AI 生成测试" + "test": "AI 生成测试", + "fix": "AI 修复诊断" }, "thinking": "AI 思考中…", "replace": "替换选区", @@ -1327,6 +1339,8 @@ "pathCopied": "已复制路径", "copiedMarkdown": "已复制为 Markdown 代码块", "indentConverted": "已转换缩进", + "noMatchingBracket": "光标处没有可匹配的括号", + "noDiagnostics": "当前文件没有诊断问题", "clipboardEmpty": "剪贴板为空", "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ",