From e6d9e60b8f145ea4b48a4e20a8caeded51b530ff Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Thu, 9 Jul 2026 08:38:33 +0200 Subject: [PATCH] feat(status-panel): enhance file staging and un-staging functionality This update improves the file staging and un-staging process by allowing multiple files to be selected and acted upon simultaneously. The user interface has been enhanced to provide clear feedback on the number of selected files and their respective statuses. - Added support for staging and un-staging multiple files at once - Introduced visual indicators for selected files in the UI - Improved event handling for file selection and actions --- src/App.svelte | 24 ++++-- src/app.css | 8 ++ src/lib/components/StatusPanel.svelte | 118 ++++++++++++++++++++++++-- 3 files changed, 134 insertions(+), 16 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index 257cf4b..4ba1889 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2521,22 +2521,30 @@ // ── File staging / restore ───────────────────────────────────────────────── - async function stageFile(file: GitFileStatus) { - await runOperation(`Staging ${file.path}`, async () => { - applyStatus(await stageFiles(activeRepoPath, [file.path])); + async function stageFile(files: GitFileStatus[]) { + const targets = files.filter((file) => file.unstaged !== null); + if (targets.length === 0) return; + const paths = targets.map((file) => file.path); + await runOperation(targets.length === 1 ? `Staging ${targets[0].path}` : `Staging ${targets.length} files`, async () => { + applyStatus(await stageFiles(activeRepoPath, paths)); await refreshExplorerFiles(activeRepoPath); trackEvent("file_staged", { - status: file.unstaged ?? file.staged ?? "unknown", + files: targets.length, + status: targets.length === 1 ? (targets[0].unstaged ?? targets[0].staged ?? "unknown") : "multiple", }); }); } - async function unstageFile(file: GitFileStatus) { - await runOperation(`Unstaging ${file.path}`, async () => { - applyStatus(await unstageFiles(activeRepoPath, [file.path])); + async function unstageFile(files: GitFileStatus[]) { + const targets = files.filter((file) => file.staged !== null); + if (targets.length === 0) return; + const paths = targets.map((file) => file.path); + await runOperation(targets.length === 1 ? `Unstaging ${targets[0].path}` : `Unstaging ${targets.length} files`, async () => { + applyStatus(await unstageFiles(activeRepoPath, paths)); await refreshExplorerFiles(activeRepoPath); trackEvent("file_unstaged", { - status: file.staged ?? file.unstaged ?? "unknown", + files: targets.length, + status: targets.length === 1 ? (targets[0].staged ?? targets[0].unstaged ?? "unknown") : "multiple", }); }); } diff --git a/src/app.css b/src/app.css index 1b31446..d610f01 100644 --- a/src/app.css +++ b/src/app.css @@ -1320,6 +1320,7 @@ .file-row { display: grid; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); } .file-row + .file-row { margin-top: 6px; } .file-row.selected { border-color: rgba(90,140,248,0.36); background: rgba(90,140,248,0.1); } + .file-row.active { box-shadow: inset 3px 0 0 rgba(65,209,255,0.7); } .file-title-button { justify-content: flex-start; @@ -1370,6 +1371,13 @@ border-bottom: 1px solid var(--color-border-subtle); background: var(--color-surface-dim); } + .status-selection-count { + padding: 0 7px; + color: var(--color-ink-dim); + font-size: 11.5px; + font-weight: 800; + white-space: nowrap; + } /* --- Stash panel --- */ diff --git a/src/lib/components/StatusPanel.svelte b/src/lib/components/StatusPanel.svelte index d692be5..bfaa781 100644 --- a/src/lib/components/StatusPanel.svelte +++ b/src/lib/components/StatusPanel.svelte @@ -11,8 +11,8 @@ status: GitStatus | null; selectedFilePath: string; onSelectFile: (file: GitFileStatus) => void; - onStage: (file: GitFileStatus) => void; - onUnstage: (file: GitFileStatus) => void; + onStage: (files: GitFileStatus[]) => void; + onUnstage: (files: GitFileStatus[]) => void; onDiscard: (file: GitFileStatus, staged: boolean) => void; onPatch: (file: GitFileStatus, staged: boolean) => void; onStageAll: () => void; @@ -56,8 +56,84 @@ return kind === "modified"; } + function fileKey(file: GitFileStatus): string { + return `${file.old_path ?? ""}:${file.path}`; + } + + let selectedStatusPaths = $state>(new Set()); + let selectionAnchorKey = $state(""); + + function isStatusSelected(file: GitFileStatus): boolean { + return selectedStatusPaths.has(fileKey(file)); + } + + function selectedFiles(): GitFileStatus[] { + return changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file))); + } + + function selectedStageTargets(file: GitFileStatus): GitFileStatus[] { + const files = isStatusSelected(file) ? selectedFiles() : [file]; + return files.filter((item) => item.unstaged !== null); + } + + function selectedUnstageTargets(file: GitFileStatus): GitFileStatus[] { + const files = isStatusSelected(file) ? selectedFiles() : [file]; + return files.filter((item) => item.staged !== null); + } + + function handleFileSelect(event: MouseEvent, file: GitFileStatus) { + const key = fileKey(file); + const allKeys = changedFiles.map(fileKey); + const next = new Set(selectedStatusPaths); + + if (event.shiftKey && selectionAnchorKey) { + const anchorIndex = allKeys.indexOf(selectionAnchorKey); + const currentIndex = allKeys.indexOf(key); + if (anchorIndex >= 0 && currentIndex >= 0) { + const start = Math.min(anchorIndex, currentIndex); + const end = Math.max(anchorIndex, currentIndex); + for (const itemKey of allKeys.slice(start, end + 1)) next.add(itemKey); + } else { + next.add(key); + } + } else if (event.ctrlKey || event.metaKey) { + if (next.has(key)) next.delete(key); + else next.add(key); + selectionAnchorKey = key; + } else { + next.clear(); + next.add(key); + selectionAnchorKey = key; + } + + selectedStatusPaths = next; + onSelectFile(file); + } + + function stageFromFile(file: GitFileStatus) { + const targets = selectedStageTargets(file); + if (targets.length === 0) return; + onStage(targets); + } + + function unstageFromFile(file: GitFileStatus) { + const targets = selectedUnstageTargets(file); + if (targets.length === 0) return; + onUnstage(targets); + } + let hasUnstaged = $derived(changedFiles.some((f) => f.unstaged !== null)); let hasStaged = $derived(changedFiles.some((f) => f.staged !== null)); + let selectedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f))).length); + let selectedUnstagedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f)) && f.unstaged !== null).length); + let selectedStagedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f)) && f.staged !== null).length); + + $effect(() => { + const validKeys = new Set(changedFiles.map(fileKey)); + const next = new Set([...selectedStatusPaths].filter((key) => validKeys.has(key))); + if (next.size !== selectedStatusPaths.size) selectedStatusPaths = next; + if (selectionAnchorKey && !validKeys.has(selectionAnchorKey)) selectionAnchorKey = ""; + });
@@ -94,6 +170,29 @@