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 <path>` via the shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -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 <path>`).
|
||||
/// `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,
|
||||
|
||||
@ -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;
|
||||
|
||||
10
src/App.tsx
10
src/App.tsx
@ -39,6 +39,7 @@ const I = {
|
||||
revert: <svg viewBox="0 0 24 24" fill="none"><path d="M4 12a8 8 0 0 1 14-5.3L21 9M21 4v5h-5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
clock: <svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8.5" stroke="currentColor" strokeWidth="1.6"/><path d="M12 8v4l2.5 2.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
log: <svg viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor" strokeWidth="1.6"/><path d="M8 8h8M8 12h8M8 16h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
vscode: <svg viewBox="0 0 24 24" fill="none"><path d="m9 8-5 4 5 4M15 8l5 4-5 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
ue: <svg viewBox="0 0 1280 1280"><path fillRule="evenodd" clipRule="evenodd" fill="currentColor" d="M640,1280c353.46,0,640-286.54,640-640S993.46,0,640,0S0,286.54,0,640S286.54,1280,640,1280z M803.7,995.81c156.5-73.92,205.56-210.43,216.6-263.61c-57.22,58.6-120.53,118-163.11,76.88c0,0-2.33-219.45-2.33-309.43c0-121,114.75-211.18,114.75-211.18c-63.11,11.24-138.89,33.71-219.33,112.65c-7.26,7.2-14.14,14.76-20.62,22.67c-34.47-26.39-79.14-18.48-79.14-18.48c24.14,13.26,48.23,51.88,48.23,83.85v314.26c0,0-52.63,46.3-93.19,46.3c-9.14,0.07-18.17-2.05-26.33-6.18c-8.16-4.13-15.21-10.15-20.56-17.56c-3.21-4.19-5.87-8.78-7.91-13.65V424.07c-11.99,9.89-52.51,18.04-52.51-49.22c0-41.79,30.11-91.6,83.73-122.15c-73.63,11.23-142.59,43.04-198.92,91.76c-42.8,36.98-77.03,82.85-100.31,134.4c-23.28,51.55-35.06,107.55-34.51,164.12c0,0,39.21-122.51,88.32-133.83c7.15-1.88,14.65-2.07,21.89-0.54c7.24,1.53,14.02,4.72,19.81,9.34c5.79,4.61,10.41,10.51,13.51,17.23c3.1,6.72,4.59,14.07,4.34,21.46V844.3c0,29.16-18.8,35.53-36.17,35.22c-11.77-0.83-23.4-3.02-34.66-6.53c35.86,48.53,82.46,88.12,136.15,115.66c53.69,27.54,113.03,42.29,173.37,43.1l106.05-106.6L803.7,995.81z"/></svg>,
|
||||
};
|
||||
|
||||
@ -729,7 +730,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
</div>
|
||||
|
||||
{tab === "changes"
|
||||
? <Preview file={selFile} onRevert={doRevert} />
|
||||
? <Preview file={selFile} onRevert={doRevert} onFlash={flash} />
|
||||
: <ChangeDetail detail={detail} busy={detailBusy} />}
|
||||
</div>
|
||||
|
||||
@ -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 <div className="right"><div className="nofile">{I.cube}<span>{t("Select a file on the left")}</span></div></div>;
|
||||
const dp = file.depotFile || "";
|
||||
const { name, dir } = splitPath(dp);
|
||||
@ -1136,6 +1137,11 @@ function Preview({ file, onRevert }: { file?: OpenedFile; onRevert: (f: OpenedFi
|
||||
<span className="m">{file.action} · <span className="rev">{rev}</span> · {file.type}</span>
|
||||
</span>
|
||||
<span className="tools">
|
||||
{kind === "code" && (
|
||||
<span className="gbtn vsc" onClick={async () => { try { await p4.openInVscode(dp); onFlash(t("Opening in VS Code…")); } catch (e) { onFlash(String(e), true); } }}>
|
||||
{I.vscode}{t("Edit in VS Code")}
|
||||
</span>
|
||||
)}
|
||||
<span className="gbtn" onClick={() => onRevert(file)}>{I.revert}{t("Revert")}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -126,6 +126,8 @@ const D: Record<string, Tr> = {
|
||||
"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}" },
|
||||
|
||||
@ -74,6 +74,7 @@ export const p4 = {
|
||||
openInExplorer: (target: string) => invoke<void>("open_in_explorer", { target }),
|
||||
findUproject: (scope: string) => invoke<string>("find_uproject", { scope }),
|
||||
launchFile: (path: string) => invoke<void>("launch_file", { path }),
|
||||
openInVscode: (depot: string) => invoke<void>("open_in_vscode", { depot }),
|
||||
readDepot: async (depot: string): Promise<ArrayBuffer> => {
|
||||
const r: unknown = await invoke("read_depot", { depot });
|
||||
if (r instanceof ArrayBuffer) return r;
|
||||
|
||||
Reference in New Issue
Block a user