Skip to content

Commit d603d00

Browse files
committed
Add hover open-editor button to changed files
1 parent 9a1a76e commit d603d00

2 files changed

Lines changed: 90 additions & 5 deletions

File tree

src/components/ChangedFilesList.tsx

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IPC } from '../../electron/ipc/channels';
44
import { theme } from '../lib/theme';
55
import { sf } from '../lib/fontScale';
66
import { getStatusColor } from '../lib/status-colors';
7+
import { openFileInEditor } from '../lib/shell';
78
import { buildFileTree, flattenVisibleTree } from '../lib/file-tree';
89
import {
910
type CommitSelection,
@@ -147,9 +148,42 @@ function FileCoverageBadge(props: {
147148
);
148149
}
149150

151+
function OpenInEditorButton(props: { worktreePath: string; filePath: string }) {
152+
return (
153+
<button
154+
class="changed-files-open-editor-btn"
155+
onClick={(e) => {
156+
e.stopPropagation();
157+
void openFileInEditor(props.worktreePath, props.filePath);
158+
}}
159+
onKeyDown={(e) => e.stopPropagation()}
160+
tabIndex={-1}
161+
disabled={!props.worktreePath}
162+
style={{
163+
background: `color-mix(in srgb, ${theme.bgElevated} 92%, transparent)`,
164+
border: 'none',
165+
color: theme.fgMuted,
166+
cursor: props.worktreePath ? 'pointer' : 'default',
167+
padding: '4px',
168+
display: 'flex',
169+
'align-items': 'center',
170+
'justify-content': 'center',
171+
'border-radius': '4px',
172+
}}
173+
title="Open in editor"
174+
aria-label={`Open ${props.filePath} in editor`}
175+
>
176+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
177+
<path d="M3.5 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-3a.75.75 0 0 1 1.5 0v3A3 3 0 0 1 12.5 16h-9A3 3 0 0 1 0 12.5v-9A3 3 0 0 1 3.5 0h3a.75.75 0 0 1 0 1.5h-3ZM10 .75a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V2.56L8.53 8.53a.75.75 0 0 1-1.06-1.06L13.44 1.5H10.75A.75.75 0 0 1 10 .75Z" />
178+
</svg>
179+
</button>
180+
);
181+
}
182+
150183
export function ChangedFilesList(props: ChangedFilesListProps) {
151184
const [files, setFiles] = createSignal<ChangedFile[]>([]);
152185
const [coverage, setCoverage] = createSignal<CoverageSummary | null>(null);
186+
const [canOpenFilesInEditor, setCanOpenFilesInEditor] = createSignal(false);
153187
const [selectedIndex, setSelectedIndex] = createSignal(-1);
154188
const [collapsed, setCollapsed] = createSignal<Set<string>>(new Set());
155189
const rowRefs: HTMLDivElement[] = [];
@@ -301,6 +335,7 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
301335
let cancelled = false;
302336
let inFlight = false;
303337
let usingBranchFallback = false;
338+
setCanOpenFilesInEditor(false);
304339

305340
async function refresh() {
306341
if (inFlight) return;
@@ -313,9 +348,15 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
313348
worktreePath: path,
314349
commitHash: singleCommitHash,
315350
});
316-
if (!cancelled) setFiles(result);
351+
if (!cancelled) {
352+
setFiles(result);
353+
setCanOpenFilesInEditor(true);
354+
}
317355
} catch {
318-
if (!cancelled) setFiles([]);
356+
if (!cancelled) {
357+
setFiles([]);
358+
setCanOpenFilesInEditor(false);
359+
}
319360
}
320361
return;
321362
}
@@ -325,9 +366,15 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
325366
const result = await invoke<ChangedFile[]>(IPC.GetUncommittedChangedFiles, {
326367
worktreePath: path,
327368
});
328-
if (!cancelled) setFiles(result);
369+
if (!cancelled) {
370+
setFiles(result);
371+
setCanOpenFilesInEditor(true);
372+
}
329373
} catch {
330-
if (!cancelled) setFiles([]);
374+
if (!cancelled) {
375+
setFiles([]);
376+
setCanOpenFilesInEditor(false);
377+
}
331378
}
332379
return;
333380
}
@@ -339,9 +386,13 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
339386
worktreePath: path,
340387
baseBranch,
341388
});
342-
if (!cancelled) setFiles(result);
389+
if (!cancelled) {
390+
setFiles(result);
391+
setCanOpenFilesInEditor(true);
392+
}
343393
return;
344394
} catch {
395+
if (!cancelled) setCanOpenFilesInEditor(false);
345396
// Worktree may not exist — try branch fallback below
346397
}
347398
}
@@ -357,8 +408,10 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
357408
});
358409
if (!cancelled) {
359410
setFiles(uncommittedOnly ? result.filter((f) => !f.committed) : result);
411+
setCanOpenFilesInEditor(false);
360412
}
361413
} catch {
414+
if (!cancelled) setCanOpenFilesInEditor(false);
362415
// Branch may no longer exist
363416
}
364417
}
@@ -445,6 +498,7 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
445498
ref={(el) => (rowRefs[i] = el)}
446499
class="file-row"
447500
style={{
501+
position: 'relative',
448502
display: 'flex',
449503
'align-items': 'center',
450504
gap: '6px',
@@ -566,6 +620,12 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
566620
-{row().node.file?.lines_removed}
567621
</span>
568622
</Show>
623+
<Show when={canOpenFilesInEditor()}>
624+
<OpenInEditorButton
625+
worktreePath={props.worktreePath}
626+
filePath={row().node.file?.path ?? row().node.path}
627+
/>
628+
</Show>
569629
</>
570630
)}
571631
</div>

src/styles.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,31 @@ textarea::placeholder {
13121312
background: color-mix(in srgb, var(--accent) 8%, var(--bg-hover));
13131313
}
13141314

1315+
.changed-files-open-editor-btn {
1316+
position: absolute;
1317+
right: 5px;
1318+
top: 50%;
1319+
transform: translateY(-50%);
1320+
opacity: 0;
1321+
pointer-events: none;
1322+
transition:
1323+
opacity 0.12s ease,
1324+
color 0.12s ease,
1325+
background 0.12s ease;
1326+
}
1327+
1328+
.file-row:hover .changed-files-open-editor-btn:not(:disabled),
1329+
.changed-files-open-editor-btn:focus-visible {
1330+
opacity: 1;
1331+
pointer-events: auto;
1332+
}
1333+
1334+
.changed-files-open-editor-btn:hover:not(:disabled),
1335+
.changed-files-open-editor-btn:focus-visible {
1336+
color: var(--fg) !important;
1337+
background: color-mix(in srgb, var(--accent) 12%, var(--bg-elevated)) !important;
1338+
}
1339+
13151340
@keyframes statusPulse {
13161341
0%,
13171342
100% {

0 commit comments

Comments
 (0)