From 72b163f5929bf588e6adaefb8dc76ccc52098718 Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Tue, 7 Jul 2026 15:51:06 +0300 Subject: [PATCH] Add "Edit in VS Code" button to the code preview Code/text files get a VS Code action in the preview header; a backend open_in_vscode command resolves the depot file to its local working copy and launches `code ` via the shell. Co-Authored-By: Claude Opus 4.8 --- src-tauri/src/lib.rs | 23 +++++++++++++++++++++++ src/App.css | 2 ++ src/App.tsx | 10 ++++++++-- src/i18n.ts | 2 ++ src/p4.ts | 1 + 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a492034..453eb8f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -610,6 +610,28 @@ async fn open_in_explorer(state: State<'_, AppState>, target: String) -> Result< Ok(()) } +/// Open a depot file's local working copy in VS Code (`code `). +/// `code` is a .cmd on Windows, so it must be launched through the shell. +#[tauri::command] +async fn open_in_vscode(state: State<'_, AppState>, depot: String) -> Result<(), String> { + let conn = current(&state)?; + let local = run_json(&conn, &["where", &depot])? + .into_iter() + .next() + .and_then(|v| v.get("path").and_then(|p| p.as_str()).map(String::from)) + .ok_or_else(|| "Could not resolve the file path".to_string())?; + let mut c = Command::new("cmd"); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + c.creation_flags(0x0800_0000); + } + c.arg("/C").arg(format!("code \"{local}\"")); + c.spawn() + .map_err(|e| format!("VS Code not found (is `code` in PATH?): {e}"))?; + Ok(()) +} + /// Open a local file with its associated application (e.g. launch Unreal for a /// `.uproject`). Uses the shell `start` so there are no path-scope restrictions. #[tauri::command] @@ -771,6 +793,7 @@ pub fn run() { open_in_explorer, find_uproject, launch_file, + open_in_vscode, p4_describe, read_file, read_depot, diff --git a/src/App.css b/src/App.css index ad19652..d87948e 100644 --- a/src/App.css +++ b/src/App.css @@ -388,6 +388,8 @@ body.resizing{cursor:col-resize!important;user-select:none} .gbtn{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);background:var(--panel); border:1px solid var(--border);border-radius:9px;padding:7px 12px;cursor:pointer;transition:.15s} .gbtn:hover{color:var(--txt);border-color:var(--accent)}.gbtn svg{width:14px;height:14px} +.gbtn.vsc svg{color:#3aa0ff} +.gbtn.vsc:hover{border-color:#3aa0ff;color:#3aa0ff} .asset{flex:1;min-height:0;display:grid;grid-template-rows:1fr auto} .stage{position:relative;min-height:0;overflow:hidden;display:grid;place-items:center; diff --git a/src/App.tsx b/src/App.tsx index 339c1de..a875be1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,7 @@ const I = { revert: , clock: , log: , + vscode: , ue: , }; @@ -729,7 +730,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set {tab === "changes" - ? + ? : } @@ -1119,7 +1120,7 @@ function FileList({ files, sel, checked, onSelect, onToggle }: } /* ---------------- preview panel ---------------- */ -function Preview({ file, onRevert }: { file?: OpenedFile; onRevert: (f: OpenedFile) => void }) { +function Preview({ file, onRevert, onFlash }: { file?: OpenedFile; onRevert: (f: OpenedFile) => void; onFlash: (text: string, err?: boolean) => void }) { if (!file) return
{I.cube}{t("Select a file on the left")}
; const dp = file.depotFile || ""; const { name, dir } = splitPath(dp); @@ -1136,6 +1137,11 @@ function Preview({ file, onRevert }: { file?: OpenedFile; onRevert: (f: OpenedFi {file.action} · {rev} · {file.type} + {kind === "code" && ( + { try { await p4.openInVscode(dp); onFlash(t("Opening in VS Code…")); } catch (e) { onFlash(String(e), true); } }}> + {I.vscode}{t("Edit in VS Code")} + + )} onRevert(file)}>{I.revert}{t("Revert")} diff --git a/src/i18n.ts b/src/i18n.ts index 8e92176..733fd3c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -126,6 +126,8 @@ const D: Record = { "Revert file?": { ru: "Откатить файл?", de: "Datei zurücksetzen?", fr: "Annuler le fichier ?", es: "¿Revertir archivo?" }, "{name}\n\nLocal edits to this file will be lost.": { ru: "{name}\n\nЛокальные правки этого файла будут потеряны.", de: "{name}\n\nLokale Änderungen an dieser Datei gehen verloren.", fr: "{name}\n\nLes modifications locales de ce fichier seront perdues.", es: "{name}\n\nLos cambios locales de este archivo se perderán." }, "Revert": { ru: "Откатить", de: "Zurücksetzen", fr: "Annuler", es: "Revertir" }, + "Edit in VS Code": { ru: "Открыть в VS Code", de: "In VS Code öffnen", fr: "Ouvrir dans VS Code", es: "Abrir en VS Code" }, + "Opening in VS Code…": { ru: "Открываю в VS Code…", de: "Wird in VS Code geöffnet…", fr: "Ouverture dans VS Code…", es: "Abriendo en VS Code…" }, "Revert changelist #{n}?": { ru: "Откатить changelist #{n}?", de: "Changelist #{n} zurücksetzen?", fr: "Annuler le changelist #{n} ?", es: "¿Revertir changelist #{n}?" }, "Perforce will create a NEW changelist that undoes #{n} and submit it to the server. History is kept (like “Revert” in GitHub Desktop).\n\n“{desc}”": { ru: "Perforce создаст НОВЫЙ changelist, отменяющий изменения из #{n}, и засабмитит его на сервер. История сохранится (как «Revert» в GitHub Desktop).\n\n«{desc}»", de: "Perforce erstellt eine NEUE Changelist, die #{n} rückgängig macht, und sendet sie an den Server. Der Verlauf bleibt erhalten (wie „Revert“ in GitHub Desktop).\n\n„{desc}“", fr: "Perforce créera un NOUVEAU changelist qui annule #{n} et l’enverra au serveur. L’historique est conservé (comme « Revert » dans GitHub Desktop).\n\n« {desc} »", es: "Perforce creará un NUEVO changelist que deshace #{n} y lo enviará al servidor. El historial se conserva (como «Revert» en GitHub Desktop).\n\n«{desc}»" }, "Revert #{n}": { ru: "Откатить #{n}", de: "#{n} zurücksetzen", fr: "Annuler #{n}", es: "Revertir #{n}" }, diff --git a/src/p4.ts b/src/p4.ts index 926a69d..5bac700 100644 --- a/src/p4.ts +++ b/src/p4.ts @@ -74,6 +74,7 @@ export const p4 = { openInExplorer: (target: string) => invoke("open_in_explorer", { target }), findUproject: (scope: string) => invoke("find_uproject", { scope }), launchFile: (path: string) => invoke("launch_file", { path }), + openInVscode: (depot: string) => invoke("open_in_vscode", { depot }), readDepot: async (depot: string): Promise => { const r: unknown = await invoke("read_depot", { depot }); if (r instanceof ArrayBuffer) return r;