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 52a31a4a..5fec73ff 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 00000000..9b278bd2 --- /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 baf10192..535bef4a 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: '页面未找到'}} ] diff --git a/package.json b/package.json index 0cc6a3fa..f9e0ed62 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", @@ -53,6 +54,7 @@ "lodash-es": "^4.17.21", "lucide-vue-next": "^0.539.0", "markdown-it": "^14.2.0", + "sql-formatter": "^15.8.2", "vscode-languageserver-protocol": "^3.18.0", "vue": "^3.5.13", "vue-codemirror": "^6.1.1", @@ -77,7 +79,8 @@ }, "pnpm": { "overrides": { - "@codemirror/autocomplete": "6.20.3" + "@codemirror/autocomplete": "6.20.3", + "@codemirror/state": "6.7.0" } } } diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index eb3d230a..d165366c 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/App.vue b/src/App.vue index 930d9bf4..7b821eef 100644 --- a/src/App.vue +++ b/src/App.vue @@ -331,6 +331,8 @@ + + @@ -463,6 +466,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/components/CommandPalette.vue b/src/components/CommandPalette.vue index 341773f3..388e3017 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/useGitPermalink.ts b/src/composables/useGitPermalink.ts index f7c7c61c..27cc7f3d 100644 --- a/src/composables/useGitPermalink.ts +++ b/src/composables/useGitPermalink.ts @@ -54,5 +54,38 @@ export function useGitPermalink( } } - return {copyPermalink, openPermalink} + // 把 git 远程地址规范化为网页地址(git@host:owner/repo(.git) 或 https://... → https://host/owner/repo) + const remoteToWebUrl = (raw: string): string | null => { + let u = raw.trim().replace(/\.git$/, '') + const scp = u.match(/^git@([^:]+):(.+)$/) + if (scp) { + return `https://${scp[1]}/${scp[2]}` + } + u = u.replace(/^ssh:\/\/(git@)?/, 'https://').replace(/^git:\/\//, 'https://') + if (u.startsWith('http://') || u.startsWith('https://')) { + return u.replace(/^http:\/\//, 'https://') + } + return null + } + + const openRepoOnWeb = async () => { + if (!rootDir.value) { + return + } + try { + const remotes = await invoke<{ name: string, url: string }[]>('git_remotes', {root: rootDir.value}) + const origin = remotes.find(r => r.name === 'origin') || remotes[0] + const web = origin && remoteToWebUrl(origin.url) + if (!web) { + toast.info(t('app.noRemoteWebUrl')) + return + } + await openExternalUrl(web) + } + catch (error) { + toast.error(t('app.permalinkFailed') + ': ' + error) + } + } + + return {copyPermalink, openPermalink, openRepoOnWeb} } diff --git a/src/composables/useNamedWorkspaces.ts b/src/composables/useNamedWorkspaces.ts new file mode 100644 index 00000000..6f6b21f2 --- /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/useShortcuts.ts b/src/composables/useShortcuts.ts index 1bba7363..6efcee98 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/composables/useWorkspaceRoots.ts b/src/composables/useWorkspaceRoots.ts index 251b63a9..4ef7fb38 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/editor/lspExtension.ts b/src/editor/lspExtension.ts index dce39353..21761572 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+. 键位与右键菜单共用。 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f9018e1f..d32ed8f8 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", @@ -668,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", @@ -684,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)", @@ -698,6 +703,9 @@ "foldAll": "Fold all", "unfoldAll": "Unfold all", "goToMatchingBracket": "Go to matching bracket", + "formatSql": "Format SQL", + "expandSelection": "Expand selection", + "shrinkSelection": "Shrink selection", "groupCode": "Code", "toggleBookmark": "Toggle bookmark", "nextBookmark": "Next bookmark", @@ -706,6 +714,7 @@ "groupBookmark": "Bookmarks", "copyPermalink": "Copy remote permalink (current line)", "openPermalink": "Open remote link in browser (current line)", + "openRepoOnWeb": "Open repository in browser", "sortLinesAsc": "Sort lines ascending", "sortLinesDesc": "Sort lines descending", "toUpperCase": "Transform to uppercase", @@ -749,7 +758,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", @@ -813,6 +824,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", @@ -1130,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", @@ -1341,6 +1360,9 @@ "indentConverted": "Indentation converted", "noMatchingBracket": "No matching bracket at cursor", "noDiagnostics": "No diagnostics in the current file", + "sqlFormatted": "SQL formatted", + "sqlFormatFailed": "SQL format failed: ", + "sqlFormatOnlySql": "Only available when editing SQL", "clipboardEmpty": "Clipboard is empty", "clipboardReadFailed": "Failed to read clipboard: ", "copyFailed": "Copy failed: ", @@ -1366,6 +1388,7 @@ "permalinkCopied": "Remote permalink copied", "permalinkFailed": "Failed to build permalink", "permalinkOutside": "Current file is not inside the workspace repository", + "noRemoteWebUrl": "No remote repository URL to open", "aiPredictOff": "AI code prediction disabled", "aiPredictOnNoKey": "AI code prediction enabled, but no API Key is configured. Fill it in \"Settings → AI\"", "aiPredictOn": "AI code prediction enabled; pause typing to see the gray completion, press Tab to accept", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 17a19a49..f20ed598 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": "修正上次提交", @@ -668,6 +671,7 @@ "formatOnSaveOff": "关闭保存时格式化", "open": "打开文件", "openFolder": "打开文件夹", + "workspaces": "命名工作区", "save": "保存文件", "saveAs": "另存为", "newTab": "新建标签", @@ -684,6 +688,7 @@ "generateTests": "AI 生成测试(选中或全文)", "formatWithAi": "AI 格式化代码", "aiFixDiagnostics": "AI 修复诊断", + "aiGenDoc": "AI 生成文档注释", "history": "执行历史", "diff": "差异对比(当前 vs 已保存)", "preview": "实时预览(Markdown / HTML)", @@ -698,6 +703,9 @@ "foldAll": "折叠全部代码", "unfoldAll": "展开全部代码", "goToMatchingBracket": "转到匹配括号", + "formatSql": "格式化 SQL", + "expandSelection": "展开选区", + "shrinkSelection": "收缩选区", "groupCode": "代码", "toggleBookmark": "切换书签", "nextBookmark": "下一处书签", @@ -706,6 +714,7 @@ "groupBookmark": "书签", "copyPermalink": "复制远程永久链接(当前行)", "openPermalink": "在浏览器打开远程链接(当前行)", + "openRepoOnWeb": "在浏览器打开仓库主页", "sortLinesAsc": "排序行(升序)", "sortLinesDesc": "排序行(降序)", "toUpperCase": "转为大写", @@ -749,7 +758,9 @@ "toggleSidebar": "切换侧栏", "toggleTerminal": "切换终端", "toggleWordWrap": "切换自动换行", - "toggleBookmark": "切换书签" + "toggleBookmark": "切换书签", + "expandSelection": "展开选区", + "shrinkSelection": "收缩选区" }, "view": { "clear": "清空", @@ -813,6 +824,13 @@ "noOutput": "没有输出", "noOutputHint": "可以尝试运行一些代码" }, + "workspaces": { + "title": "命名工作区", + "namePlaceholder": "为当前工作区命名…", + "saveCurrent": "保存当前", + "empty": "还没有保存的工作区", + "delete": "删除" + }, "sidebar": { "explorer": "资源管理器", "openFolder": "打开文件夹", @@ -1130,7 +1148,8 @@ "explain": "AI 解释代码", "refactor": "AI 重构代码", "test": "AI 生成测试", - "fix": "AI 修复诊断" + "fix": "AI 修复诊断", + "doc": "AI 生成文档注释" }, "thinking": "AI 思考中…", "replace": "替换选区", @@ -1341,6 +1360,9 @@ "indentConverted": "已转换缩进", "noMatchingBracket": "光标处没有可匹配的括号", "noDiagnostics": "当前文件没有诊断问题", + "sqlFormatted": "SQL 已格式化", + "sqlFormatFailed": "SQL 格式化失败: ", + "sqlFormatOnlySql": "仅在 SQL 编辑时可用", "clipboardEmpty": "剪贴板为空", "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ", @@ -1366,6 +1388,7 @@ "permalinkCopied": "已复制远程永久链接", "permalinkFailed": "生成永久链接失败", "permalinkOutside": "当前文件不在工作区仓库内", + "noRemoteWebUrl": "未找到可打开的远程仓库地址", "aiPredictOff": "AI 代码预测已关闭", "aiPredictOnNoKey": "AI 代码预测已开启,但尚未配置 API Key,请在「设置 → AI」中填写", "aiPredictOn": "AI 代码预测已开启,停顿打字即可看到灰色补全,Tab 接受",