From 5e048b897637c6955a38aa829ae8824d6945a507 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Jun 2026 15:48:35 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat(workspace):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=97=B6=E8=87=AA=E5=8A=A8=E5=9C=A8=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=91=E4=B8=AD=E5=AE=9A=E4=BD=8D=EF=BC=88=E5=8F=AF?= =?UTF-8?q?=E5=BC=80=E5=85=B3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用 reveal 能力,开启后切换标签/打开文件即在文件树展开并滚动到当前文件; 仅当侧栏已打开时触发,避免频繁强开侧栏打扰;偏好持久化到 KV,命令面板可切换。 --- src/App.vue | 17 +++++++++++++++++ src/i18n/locales/en.json | 3 +++ src/i18n/locales/zh-CN.json | 3 +++ 3 files changed, 23 insertions(+) diff --git a/src/App.vue b/src/App.vue index 022edf22..b91d6465 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1202,6 +1202,22 @@ const revealInTree = (path?: string) => { revealRequest.value = {path: target, n: ++revealSeq} } +// 切换文件时自动在文件树中定位(仅当侧栏已打开,避免频繁强开侧栏打扰) +const autoRevealTree = ref(kvGet('auto-reveal-tree') === 'true') +const toggleAutoReveal = () => { + autoRevealTree.value = !autoRevealTree.value + kvSet('auto-reveal-tree', String(autoRevealTree.value)) + toast.info(autoRevealTree.value ? t('app.autoRevealOn') : t('app.autoRevealOff')) + if (autoRevealTree.value && sidebarVisible.value && currentFilePath.value) { + revealRequest.value = {path: currentFilePath.value, n: ++revealSeq} + } +} +watch(currentFilePath, (p) => { + if (autoRevealTree.value && sidebarVisible.value && p) { + revealRequest.value = {path: p, n: ++revealSeq} + } +}) + // 代码片段管理 const showSnippets = ref(false) @@ -2297,6 +2313,7 @@ const paletteCommands = computed(() => [ {id: 'startDebug', label: t('command.startDebug'), icon: Play, run: () => startDebug()}, {id: 'sendToTerminal', label: t('command.sendToTerminal'), icon: TerminalIcon, run: () => sendToTerminal()}, {id: 'revealInTree', label: t('command.revealInTree'), icon: FolderOpen, run: () => revealInTree()}, + {id: 'toggleAutoReveal', label: t('command.toggleAutoReveal'), icon: FolderOpen, run: () => toggleAutoReveal()}, {id: 'toggleSidebar', label: t('command.toggleSidebar'), icon: PanelLeft, hint: hintOf('toggleSidebar'), run: () => toggleSidebar()}, {id: 'toggleWordWrap', label: t('command.toggleWordWrap'), icon: WrapText, hint: hintOf('toggleWordWrap'), run: () => toggleWordWrap()}, {id: 'layoutHorizontal', label: t('command.layoutHorizontal'), group: t('command.groupLayout'), icon: PanelRight, run: () => handleLayoutChange('horizontal')}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 16b6ffcc..9042c73c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -690,6 +690,7 @@ "sendToTerminal": "Send selection to terminal", "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", + "toggleAutoReveal": "Toggle: auto-reveal active file", "toggleSidebar": "Toggle sidebar", "toggleWordWrap": "Toggle word wrap", "layoutHorizontal": "Layout: side-by-side", @@ -1313,6 +1314,8 @@ "wordWrapOff": "Word wrap disabled", "noClosedTab": "No closed tab to reopen", "noFileToReveal": "No file to reveal", + "autoRevealOn": "Auto-reveal active file enabled", + "autoRevealOff": "Auto-reveal active file disabled", "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 54e0e532..8ff2a5d8 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -690,6 +690,7 @@ "sendToTerminal": "发送选区到终端", "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", + "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", "toggleWordWrap": "切换自动换行", "layoutHorizontal": "布局:左右", @@ -1313,6 +1314,8 @@ "wordWrapOff": "已关闭自动换行", "noClosedTab": "没有可重新打开的标签", "noFileToReveal": "当前没有可定位的文件", + "autoRevealOn": "已开启:切换文件自动定位", + "autoRevealOff": "已关闭:切换文件自动定位", "aiPredictOff": "AI 代码预测已关闭", "aiPredictOnNoKey": "AI 代码预测已开启,但尚未配置 API Key,请在「设置 → AI」中填写", "aiPredictOn": "AI 代码预测已开启,停顿打字即可看到灰色补全,Tab 接受", From 428e11415e007a8f51e98d496bf7b5718d618e1c Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Jun 2026 15:53:25 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat(tabs):=20=E6=94=AF=E6=8C=81=E7=BD=AE?= =?UTF-8?q?=E9=A1=B6=E6=A0=87=E7=AD=BE=20(Pin=20Tab)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 右键标签可置顶/取消置顶,置顶标签稳定排到最左、显示图钉图标(hover 让位关闭按钮); 关闭其他/关闭右侧均保留置顶标签;置顶状态随会话持久化恢复。 --- src/App.vue | 19 +++++++++----- src/components/EditorTabs.vue | 19 +++++++++++--- src/composables/useWorkspace.ts | 45 ++++++++++++++++++++++++++++----- src/i18n/locales/en.json | 2 ++ src/i18n/locales/zh-CN.json | 2 ++ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/App.vue b/src/App.vue index b91d6465..97784a2c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -83,7 +83,7 @@
+ @copy-relative="handleCopyRelativePath" @reveal-tree="revealInTree" @reveal-finder="revealInFinder" @toggle-pin="togglePin"/>
@@ -224,7 +224,7 @@
+ @copy-relative="handleCopyRelativePath" @reveal-tree="revealInTree" @reveal-finder="revealInFinder" @toggle-pin="togglePin"/>
@@ -623,6 +623,8 @@ const { closeOthers, closeToRight, reopenClosed, + togglePin, + restorePinned, moveTab, updateTabPath, detachTabPath, @@ -755,19 +757,20 @@ const SESSION_TABS_KEY = 'session-tabs' const persistSession = () => { const paths = editorTabs.value.map(t => t.filePath).filter((p): p is string => !!p) + const pinned = editorTabs.value.filter(t => t.pinned && t.filePath).map(t => t.filePath as string) const activePath = editorTabs.value.find(t => t.id === activeTabId.value)?.filePath || null - kvSetJSON(SESSION_TABS_KEY, {paths, activePath}) + kvSetJSON(SESSION_TABS_KEY, {paths, pinned, activePath}) } -// 标签集合/文件/激活项变化时持久化(不含正文编辑,避免频繁写入) +// 标签集合/文件/激活项/置顶变化时持久化(不含正文编辑,避免频繁写入) watch( - () => editorTabs.value.map(t => t.filePath || '').join('|') + '#' + activeTabId.value, + () => editorTabs.value.map(t => (t.pinned ? '*' : '') + (t.filePath || '')).join('|') + '#' + activeTabId.value, () => persistSession() ) // 启动时恢复上次打开的文件标签(仅已保存且可读的文本文件) const restoreSession = async () => { - const saved = kvGetJSON<{ paths: string[], activePath: string | null } | null>(SESSION_TABS_KEY, null) + const saved = kvGetJSON<{ paths: string[], pinned?: string[], activePath: string | null } | null>(SESSION_TABS_KEY, null) if (!saved || !saved.paths?.length) { return } @@ -785,6 +788,10 @@ const restoreSession = async () => { } } + if (saved.pinned?.length) { + restorePinned(saved.pinned) + } + if (saved.activePath) { const t = editorTabs.value.find(tab => tab.filePath === saved.activePath) if (t) { diff --git a/src/components/EditorTabs.vue b/src/components/EditorTabs.vue index 488501cf..5b21e040 100644 --- a/src/components/EditorTabs.vue +++ b/src/components/EditorTabs.vue @@ -20,9 +20,16 @@ {{ title(tab) }} - - + +
@@ -58,7 +67,7 @@ diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 95bee4c3..4c2d21ab 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1264,6 +1264,13 @@ "mapChina": "First dimension as province name (e.g. \"Beijing\"), first metric as value", "mapWorld": "First dimension as country name (English), first metric as value" } }, + "statusbar": { + "indentTitle": "Indentation settings", + "indentType": "Indent type", + "tabWidth": "Tab width", + "spaces": "Spaces", + "tabs": "Tabs" + }, "app": { "runInputToggle": "Run input (args / stdin / env vars)", "runArgs": "Run arguments", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 5cae8277..a297a991 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1264,6 +1264,13 @@ "mapChina": "首个维度作省份名(如「北京市」)、首个指标作值", "mapWorld": "首个维度作国家名(英文)、首个指标作值" } }, + "statusbar": { + "indentTitle": "缩进设置", + "indentType": "缩进类型", + "tabWidth": "Tab 宽度", + "spaces": "空格", + "tabs": "制表符" + }, "app": { "runInputToggle": "运行输入(参数 / stdin / 环境变量)", "runArgs": "运行参数", From f78ebb09293ecc5c9bb75fa8a8dd6e2d6151c79c Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Jun 2026 16:11:22 +0800 Subject: [PATCH 04/17] =?UTF-8?q?feat(git):=20AI=20=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=A1=E6=81=AF=E6=94=B9=E7=94=A8=E6=9A=82?= =?UTF-8?q?=E5=AD=98=20diff=EF=BC=8C=E6=9B=B4=E8=B4=B4=E5=90=88=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E6=8F=90=E4=BA=A4=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增后端 git_staged_diff(git diff --cached);有暂存改动且非「提交全部」时, AI 提交信息基于将要提交的暂存 diff 生成,避免描述到未纳入本次提交的改动。 --- src-tauri/src/filesystem.rs | 23 +++++++++++++++++++++++ src-tauri/src/main.rs | 2 ++ src/components/GitPanel.vue | 4 +++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index 452e9920..e6e0da56 100644 --- a/src-tauri/src/filesystem.rs +++ b/src-tauri/src/filesystem.rs @@ -469,6 +469,29 @@ pub async fn git_diff(root: String) -> Result { .map_err(|e| format!("git 任务失败: {}", e))? } +/// 已暂存改动的 diff(git diff --cached),用于 AI 生成提交信息 +#[tauri::command] +pub async fn git_staged_diff(root: String) -> Result { + tokio::task::spawn_blocking(move || { + let output = std::process::Command::new("git") + .args(["-C", &root, "diff", "--cached"]) + .output() + .map_err(|e| format!("执行 git 失败: {}", e))?; + if !output.status.success() { + let err = String::from_utf8_lossy(&output.stderr); + return Err(format!("git diff --cached 失败:{}", err.trim())); + } + let mut diff = String::from_utf8_lossy(&output.stdout).to_string(); + if diff.len() > MAX_DIFF_LEN { + diff.truncate(MAX_DIFF_LEN); + diff.push_str("\n…(diff 过长已截断)"); + } + Ok(diff) + }) + .await + .map_err(|e| format!("git 任务失败: {}", e))? +} + // ===== Git 源代码管理 ===== /// 克隆远程仓库到 dir 下,返回克隆出的仓库目录路径。 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b5e3a37c..65b29d24 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -69,6 +69,7 @@ use crate::filesystem::{ git_branches, git_checkout, git_checkout_track, git_cherry_pick, git_clean, git_clean_preview, git_clone, git_commit, git_compare, git_delete_remote_branch, git_diff, git_discard, git_fetch, git_file_diff, git_file_head, git_get_identity, git_get_signing, git_graph, git_hook_delete, + git_staged_diff, git_hook_read, git_hook_save, git_hooks, git_ignore_add, git_ignore_append_block, git_init, git_log, git_log_file, git_merge, git_op_abort, git_op_continue, git_op_skip, git_op_state, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags, git_rebase_interactive, @@ -232,6 +233,7 @@ fn main() { search_in_files, replace_in_files, git_diff, + git_staged_diff, git_file_diff, git_apply_patch, git_status, diff --git a/src/components/GitPanel.vue b/src/components/GitPanel.vue index 7e1d8150..4c2d53ca 100644 --- a/src/components/GitPanel.vue +++ b/src/components/GitPanel.vue @@ -882,7 +882,9 @@ const genMessage = async () => { } generating.value = true try { - const diff = await invoke('git_diff', {root: props.rootDir}) + // 有暂存改动且非「提交全部」时,仅依据将要提交的暂存 diff,避免描述未纳入提交的改动 + const useStaged = staged.value.length > 0 && !commitAll.value + const diff = await invoke(useStaged ? 'git_staged_diff' : 'git_diff', {root: props.rootDir}) if (!diff.trim()) { toast.info(t('git.noDiff')) return From 15cd416957241bb8d7ed07eda2a3f669e07dd0ce Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Jun 2026 16:29:41 +0800 Subject: [PATCH 05/17] =?UTF-8?q?feat(git):=20=E5=A4=8D=E5=88=B6=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E8=A1=8C=E7=9A=84=E8=BF=9C=E7=A8=8B=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增后端 git_permalink:基于 origin 远程与当前 HEAD 提交生成永久链接, 支持 GitHub/Gitee(/blob/) 与 GitLab(/-/blob/)、SSH 与 HTTPS 远程; 命令面板「复制远程永久链接(当前行)」一键复制,便于分享到 PR/IM。 --- src-tauri/src/filesystem.rs | 49 +++++++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 3 ++- src/App.vue | 24 ++++++++++++++++++ src/i18n/locales/en.json | 4 +++ src/i18n/locales/zh-CN.json | 4 +++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index e6e0da56..3f6be1e2 100644 --- a/src-tauri/src/filesystem.rs +++ b/src-tauri/src/filesystem.rs @@ -492,6 +492,55 @@ pub async fn git_staged_diff(root: String) -> Result { .map_err(|e| format!("git 任务失败: {}", e))? } +/// 生成当前文件指定行的远程仓库永久链接(基于 origin 远程与当前 HEAD 提交)。 +/// 支持 GitHub / Gitee(/blob/)与 GitLab(/-/blob/),SSH 与 HTTPS 远程均可解析。 +#[tauri::command] +pub async fn git_permalink(root: String, rel_path: String, line: u32) -> Result { + tokio::task::spawn_blocking(move || { + let git = |args: &[&str]| -> Result { + let mut a: Vec<&str> = vec!["-C", &root]; + a.extend_from_slice(args); + let out = std::process::Command::new("git") + .args(&a) + .output() + .map_err(|e| format!("执行 git 失败: {}", e))?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).trim().to_string()); + } + Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) + }; + + let remote = git(&["remote", "get-url", "origin"]).map_err(|_| "未找到 origin 远程".to_string())?; + let sha = git(&["rev-parse", "HEAD"]).map_err(|_| "无法获取当前提交".to_string())?; + + // 解析远程地址 → (host, owner/repo) + let raw = remote.trim(); + let raw = raw.strip_suffix(".git").unwrap_or(raw); + let (host, path) = if let Some(rest) = raw.strip_prefix("git@") { + // scp 形式:git@host:owner/repo + rest.split_once(':') + .map(|(h, p)| (h.to_string(), p.trim_start_matches('/').to_string())) + .ok_or_else(|| "无法解析远程地址".to_string())? + } else { + let rest = raw + .strip_prefix("https://") + .or_else(|| raw.strip_prefix("http://")) + .or_else(|| raw.strip_prefix("ssh://")) + .ok_or_else(|| "无法解析远程地址".to_string())?; + let rest = rest.strip_prefix("git@").unwrap_or(rest); + rest.split_once('/') + .map(|(h, p)| (h.to_string(), p.trim_start_matches('/').to_string())) + .ok_or_else(|| "无法解析远程地址".to_string())? + }; + + let rel = rel_path.trim_start_matches(['/', '\\']).replace('\\', "/"); + let blob = if host.contains("gitlab") { "/-/blob/" } else { "/blob/" }; + Ok(format!("https://{}/{}{}{}/{}#L{}", host, path, blob, sha, rel, line)) + }) + .await + .map_err(|e| format!("git 任务失败: {}", e))? +} + // ===== Git 源代码管理 ===== /// 克隆远程仓库到 dir 下,返回克隆出的仓库目录路径。 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 65b29d24..19af653d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -69,7 +69,7 @@ use crate::filesystem::{ git_branches, git_checkout, git_checkout_track, git_cherry_pick, git_clean, git_clean_preview, git_clone, git_commit, git_compare, git_delete_remote_branch, git_diff, git_discard, git_fetch, git_file_diff, git_file_head, git_get_identity, git_get_signing, git_graph, git_hook_delete, - git_staged_diff, + git_staged_diff, git_permalink, git_hook_read, git_hook_save, git_hooks, git_ignore_add, git_ignore_append_block, git_init, git_log, git_log_file, git_merge, git_op_abort, git_op_continue, git_op_skip, git_op_state, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags, git_rebase_interactive, @@ -234,6 +234,7 @@ fn main() { replace_in_files, git_diff, git_staged_diff, + git_permalink, git_file_diff, git_apply_patch, git_status, diff --git a/src/App.vue b/src/App.vue index 73959e88..974b6954 100644 --- a/src/App.vue +++ b/src/App.vue @@ -696,6 +696,29 @@ const handleCopyPath = async (path: string) => { toast.error(t('app.copyFailed') + error) } } +// 复制当前文件指定行的远程仓库永久链接 +const copyPermalink = async () => { + if (!rootDir.value || !currentFilePath.value) { + toast.info(t('app.noFileToReveal')) + return + } + const root = rootDir.value + const p = currentFilePath.value + if (!(p === root || p.startsWith(root + '/') || p.startsWith(root + '\\'))) { + toast.info(t('app.permalinkOutside')) + return + } + const rel = p.slice(root.length).replace(/^[\\/]/, '') + try { + const url = await invoke('git_permalink', {root, relPath: rel, line: cursorInfo.value.line}) + await navigator.clipboard.writeText(url) + toast.success(t('app.permalinkCopied')) + } + catch (error) { + toast.error(t('app.permalinkFailed') + ': ' + error) + } +} + const handleCopyRelativePath = (path: string) => { const root = rootDir.value const rel = root && (path === root || path.startsWith(root + '/') || path.startsWith(root + '\\')) @@ -2323,6 +2346,7 @@ const paletteCommands = computed(() => [ {id: 'startDebug', label: t('command.startDebug'), icon: Play, run: () => startDebug()}, {id: 'sendToTerminal', label: t('command.sendToTerminal'), icon: TerminalIcon, run: () => sendToTerminal()}, {id: 'revealInTree', label: t('command.revealInTree'), icon: FolderOpen, run: () => revealInTree()}, + {id: 'copyPermalink', label: t('command.copyPermalink'), icon: GitBranch, run: () => copyPermalink()}, {id: 'toggleAutoReveal', label: t('command.toggleAutoReveal'), icon: FolderOpen, run: () => toggleAutoReveal()}, {id: 'toggleSidebar', label: t('command.toggleSidebar'), icon: PanelLeft, hint: hintOf('toggleSidebar'), run: () => toggleSidebar()}, {id: 'toggleWordWrap', label: t('command.toggleWordWrap'), icon: WrapText, hint: hintOf('toggleWordWrap'), run: () => toggleWordWrap()}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4c2d21ab..dd05408a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -692,6 +692,7 @@ "sendToTerminal": "Send selection to terminal", "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", + "copyPermalink": "Copy remote permalink (current line)", "toggleAutoReveal": "Toggle: auto-reveal active file", "toggleSidebar": "Toggle sidebar", "toggleWordWrap": "Toggle word wrap", @@ -1325,6 +1326,9 @@ "noFileToReveal": "No file to reveal", "autoRevealOn": "Auto-reveal active file enabled", "autoRevealOff": "Auto-reveal active file disabled", + "permalinkCopied": "Remote permalink copied", + "permalinkFailed": "Failed to build permalink", + "permalinkOutside": "Current file is not inside the workspace repository", "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 a297a991..2e70d387 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -692,6 +692,7 @@ "sendToTerminal": "发送选区到终端", "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", + "copyPermalink": "复制远程永久链接(当前行)", "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", "toggleWordWrap": "切换自动换行", @@ -1325,6 +1326,9 @@ "noFileToReveal": "当前没有可定位的文件", "autoRevealOn": "已开启:切换文件自动定位", "autoRevealOff": "已关闭:切换文件自动定位", + "permalinkCopied": "已复制远程永久链接", + "permalinkFailed": "生成永久链接失败", + "permalinkOutside": "当前文件不在工作区仓库内", "aiPredictOff": "AI 代码预测已关闭", "aiPredictOnNoKey": "AI 代码预测已开启,但尚未配置 API Key,请在「设置 → AI」中填写", "aiPredictOn": "AI 代码预测已开启,停顿打字即可看到灰色补全,Tab 接受", From 0495a2f2662dd77d52641403959071b6c2942d6b Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 28 Jun 2026 00:13:04 +0800 Subject: [PATCH 06/17] =?UTF-8?q?fix(editor):=20=E5=8F=B3=E9=94=AE?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8C=89=E7=9C=9F=E5=AE=9E=E5=B0=BA=E5=AF=B8?= =?UTF-8?q?=E5=A4=B9=E5=8F=96=E5=88=B0=E8=A7=86=E5=8F=A3=E5=86=85=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B4=B4=E5=BA=95/=E8=B4=B4=E5=8F=B3?= =?UTF-8?q?=E8=A3=81=E5=88=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原先用固定 190px 估算夹取,菜单项较多时底部仍溢出;改为渲染后测量 getBoundingClientRect 再夹取,保证完整菜单始终落在视口内(留 8px 边距)。 --- src/App.vue | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/App.vue b/src/App.vue index 974b6954..00a56222 100644 --- a/src/App.vue +++ b/src/App.vue @@ -404,7 +404,8 @@
-
@@ -557,6 +563,7 @@ import Modal from './ui/Modal.vue' import Button from './ui/Button.vue' import ExecutionHistory from './components/ExecutionHistory.vue' import {open as openDialog} from '@tauri-apps/plugin-dialog' +import {open as openExternalUrl} from '@tauri-apps/plugin-shell' import {invoke} from '@tauri-apps/api/core' import {useEventManager} from './composables/useEventManager' import {useShortcuts} from './composables/useShortcuts' @@ -697,26 +704,38 @@ const handleCopyPath = async (path: string) => { toast.error(t('app.copyFailed') + error) } } -// 复制当前文件指定行的远程仓库永久链接 -const copyPermalink = async () => { +// 生成当前文件指定行的远程仓库永久链接 +const buildPermalink = async (): Promise => { if (!rootDir.value || !currentFilePath.value) { toast.info(t('app.noFileToReveal')) - return + return null } const root = rootDir.value const p = currentFilePath.value if (!(p === root || p.startsWith(root + '/') || p.startsWith(root + '\\'))) { toast.info(t('app.permalinkOutside')) - return + return null } const rel = p.slice(root.length).replace(/^[\\/]/, '') try { - const url = await invoke('git_permalink', {root, relPath: rel, line: cursorInfo.value.line}) - await navigator.clipboard.writeText(url) - toast.success(t('app.permalinkCopied')) + return await invoke('git_permalink', {root, relPath: rel, line: cursorInfo.value.line}) } catch (error) { toast.error(t('app.permalinkFailed') + ': ' + error) + return null + } +} +const copyPermalink = async () => { + const url = await buildPermalink() + if (url) { + await navigator.clipboard.writeText(url) + toast.success(t('app.permalinkCopied')) + } +} +const openPermalink = async () => { + const url = await buildPermalink() + if (url) { + await openExternalUrl(url).catch((e) => toast.error(t('app.permalinkFailed') + ': ' + e)) } } @@ -2361,6 +2380,7 @@ const paletteCommands = computed(() => [ {id: 'sendToTerminal', label: t('command.sendToTerminal'), icon: TerminalIcon, run: () => sendToTerminal()}, {id: 'revealInTree', label: t('command.revealInTree'), icon: FolderOpen, run: () => revealInTree()}, {id: 'copyPermalink', label: t('command.copyPermalink'), icon: GitBranch, run: () => copyPermalink()}, + {id: 'openPermalink', label: t('command.openPermalink'), icon: GitBranch, run: () => openPermalink()}, {id: 'toggleAutoReveal', label: t('command.toggleAutoReveal'), icon: FolderOpen, run: () => toggleAutoReveal()}, {id: 'toggleSidebar', label: t('command.toggleSidebar'), icon: PanelLeft, hint: hintOf('toggleSidebar'), run: () => toggleSidebar()}, {id: 'toggleWordWrap', label: t('command.toggleWordWrap'), icon: WrapText, hint: hintOf('toggleWordWrap'), run: () => toggleWordWrap()}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e6235a86..c1f43447 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -694,6 +694,7 @@ "reopenClosed": "Reopen closed tab", "revealInTree": "Reveal in file tree", "copyPermalink": "Copy remote permalink (current line)", + "openPermalink": "Open remote link in browser (current line)", "toggleAutoReveal": "Toggle: auto-reveal active file", "toggleSidebar": "Toggle sidebar", "toggleWordWrap": "Toggle word wrap", @@ -1327,6 +1328,8 @@ "noFileToReveal": "No file to reveal", "autoRevealOn": "Auto-reveal active file enabled", "autoRevealOff": "Auto-reveal active file disabled", + "copyPermalinkShort": "Copy remote permalink", + "openOnRemote": "Open in browser", "permalinkCopied": "Remote permalink copied", "permalinkFailed": "Failed to build permalink", "permalinkOutside": "Current file is not inside the workspace repository", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index ebfb5fcf..6f210ef6 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -694,6 +694,7 @@ "reopenClosed": "重新打开已关闭标签", "revealInTree": "在文件树中定位", "copyPermalink": "复制远程永久链接(当前行)", + "openPermalink": "在浏览器打开远程链接(当前行)", "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", "toggleWordWrap": "切换自动换行", @@ -1327,6 +1328,8 @@ "noFileToReveal": "当前没有可定位的文件", "autoRevealOn": "已开启:切换文件自动定位", "autoRevealOff": "已关闭:切换文件自动定位", + "copyPermalinkShort": "复制远程永久链接", + "openOnRemote": "在浏览器打开", "permalinkCopied": "已复制远程永久链接", "permalinkFailed": "生成永久链接失败", "permalinkOutside": "当前文件不在工作区仓库内", From 8ba791739ee7304a33fa54ffc70a8151b5013128 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 28 Jun 2026 00:41:26 +0800 Subject: [PATCH 09/17] =?UTF-8?q?feat(git):=20=E7=8A=B6=E6=80=81=E6=A0=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=BD=93=E5=89=8D=E5=88=86=E6=94=AF=EF=BC=8C?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=89=93=E5=BC=80=20Git=20=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git_status 已返回的 branch 透传到状态栏,仓库内显示分支名(GitBranch 图标), 点击触发打开 Git 面板,便于快速进入分支/提交操作。 --- src/App.vue | 13 ++++++++----- src/components/StatusBar.vue | 14 +++++++++++++- src/i18n/locales/en.json | 3 ++- src/i18n/locales/zh-CN.json | 3 ++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/App.vue b/src/App.vue index 33ddd51d..d5254f82 100644 --- a/src/App.vue +++ b/src/App.vue @@ -283,7 +283,7 @@ @close="showTerminal = false; terminalMounted = false"/> - + @@ -1686,10 +1686,11 @@ const openGit = () => { // 文件树徽标用:绝对路径 → 状态字母(M/A/D/U) const gitStatus = ref>({}) const gitRepo = ref(false) +const gitBranch = ref('') // 计算单个根的 Git 状态(路径用绝对路径作 key,便于多根合并到同一张表) -const gitStatusFor = async (root: string): Promise<{ isRepo: boolean, map: Record }> => { +const gitStatusFor = async (root: string): Promise<{ isRepo: boolean, branch: string, map: Record }> => { try { - const s = await invoke<{ is_repo: boolean, files: { path: string, index: string, worktree: string }[] }>( + const s = await invoke<{ is_repo: boolean, branch: string, files: { path: string, index: string, worktree: string }[] }>( 'git_status', {root} ) const map: Record = {} @@ -1701,20 +1702,22 @@ const gitStatusFor = async (root: string): Promise<{ isRepo: boolean, map: Recor map[`${root}/${f.path}`] = code } } - return {isRepo: s.is_repo, map} + return {isRepo: s.is_repo, branch: s.branch || '', map} } catch { - return {isRepo: false, map: {}} + return {isRepo: false, branch: '', map: {}} } } const refreshGitStatus = async () => { if (!rootDir.value) { gitStatus.value = {} gitRepo.value = false + gitBranch.value = '' return } const primary = await gitStatusFor(rootDir.value) gitRepo.value = primary.isRepo + gitBranch.value = primary.branch const map: Record = {...primary.map} // 额外挂载的根各自可为独立仓库,合并它们的状态徽标 if (extraRoots.value.length) { diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 2197fd57..9a027df7 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -22,6 +22,15 @@
+ + +
@@ -58,7 +67,7 @@