diff --git a/src/ui/components/panes/FileListItem.tsx b/src/ui/components/panes/FileListItem.tsx index 9f5fe528..36276e93 100644 --- a/src/ui/components/panes/FileListItem.tsx +++ b/src/ui/components/panes/FileListItem.tsx @@ -1,7 +1,7 @@ -import type { FileGroupEntry, FileListEntry } from "../../lib/files"; +import { fileRowId } from "../../lib/ids"; +import { sidebarEntryStats, type FileGroupEntry, type FileListEntry } from "../../lib/files"; import { fitText, padText } from "../../lib/text"; import type { AppTheme } from "../../themes"; -import { fileRowId } from "../../lib/ids"; /** Get icon and color for file state using standard git status codes. */ function getFileStateIcon(entry: FileListEntry, theme: AppTheme): { icon: string; color: string } { @@ -50,27 +50,26 @@ export function FileGroupHeader({ /** Render one file row in the navigation sidebar. */ export function FileListItem({ - additionsWidth, - deletionsWidth, entry, selected, + statsWidth, textWidth, theme, onSelect, }: { - additionsWidth: number; - deletionsWidth: number; entry: FileListEntry; selected: boolean; + statsWidth: number; textWidth: number; theme: AppTheme; onSelect: () => void; }) { const rowBackground = selected ? theme.panelAlt : theme.panel; - const statsWidth = additionsWidth + 1 + deletionsWidth; + const stats = sidebarEntryStats(entry); const { icon, color } = getFileStateIcon(entry, theme); const iconWidth = icon ? 2 : 0; // icon + space - const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsWidth - 1); + const statsSectionWidth = statsWidth > 0 ? statsWidth + 1 : 0; + const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsSectionWidth); return ( {icon && {icon} } {padText(fitText(entry.name, nameWidth), nameWidth)} - {entry.additionsText.padStart(additionsWidth, " ")} - - {entry.deletionsText.padStart(deletionsWidth, " ")} + {statsSectionWidth > 0 && ( + + {stats.map((stat, index) => ( + + {index > 0 && } + + {stat.text} + + + ))} + + )} ); diff --git a/src/ui/components/panes/SidebarPane.tsx b/src/ui/components/panes/SidebarPane.tsx index ef3abb78..0391c25a 100644 --- a/src/ui/components/panes/SidebarPane.tsx +++ b/src/ui/components/panes/SidebarPane.tsx @@ -1,6 +1,6 @@ import type { ScrollBoxRenderable } from "@opentui/core"; import type { RefObject } from "react"; -import type { SidebarEntry } from "../../lib/files"; +import { sidebarEntryStatsWidth, type SidebarEntry } from "../../lib/files"; import type { AppTheme } from "../../themes"; import { FileGroupHeader, FileListItem } from "./FileListItem"; @@ -23,8 +23,7 @@ export function SidebarPane({ onSelectFile: (fileId: string) => void; }) { const fileEntries = entries.filter((entry) => entry.kind === "file"); - const additionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.additionsText.length)); - const deletionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.deletionsText.length)); + const statsWidth = Math.max(0, ...fileEntries.map((entry) => sidebarEntryStatsWidth(entry))); return ( onSelectFile(entry.id)} diff --git a/src/ui/components/ui-components.test.tsx b/src/ui/components/ui-components.test.tsx index ed865f66..fd42044e 100644 --- a/src/ui/components/ui-components.test.tsx +++ b/src/ui/components/ui-components.test.tsx @@ -345,15 +345,37 @@ describe("UI components", () => { createTestDiffFile( "menu", "src/ui/MenuDropdown.tsx", + lines( + "export const menu = 1;", + "export const remove1 = true;", + "export const remove2 = true;", + "export const remove3 = true;", + ), "export const menu = 1;\n", - "export const menu = 2;\n", ), createTestDiffFile( "watch", "src/core/watch.ts", "export const watch = 1;\n", - "export const watch = 2;\nexport const enabled = true;\n", + lines( + "export const watch = 1;", + "export const add1 = true;", + "export const add2 = true;", + "export const add3 = true;", + "export const add4 = true;", + "export const add5 = true;", + ), ), + { + ...createTestDiffFile( + "rename", + "src/ui/Renamed.tsx", + "export const renamed = true;\n", + "export const renamed = true;\n", + ), + previousPath: "src/ui/Legacy.tsx", + stats: { additions: 0, deletions: 0 }, + }, ]; const frame = await captureFrame( { expect(frame).toContain(" MenuDropdown.tsx"); expect(frame).toContain(" watch.ts"); expect(frame).toContain("+2 -1"); - expect(frame).toContain("+1 -1"); + expect(frame).toContain("+5"); + expect(frame).toContain("-3"); + expect(frame).not.toContain("+0"); + expect(frame).not.toContain("-0"); expect(frame).not.toContain("M +2 -1 AI"); }); diff --git a/src/ui/lib/files.test.ts b/src/ui/lib/files.test.ts new file mode 100644 index 00000000..00a62762 --- /dev/null +++ b/src/ui/lib/files.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test"; +import { createTestDiffFile, lines } from "../../../test/helpers/diff-helpers"; +import { buildSidebarEntries } from "./files"; + +describe("files helpers", () => { + test("buildSidebarEntries hides zero-value sidebar stats", () => { + const onlyAdd = createTestDiffFile({ + id: "only-add", + path: "src/ui/only-add.ts", + before: lines("export const stable = true;"), + after: lines( + "export const stable = true;", + "export const add1 = 1;", + "export const add2 = 2;", + "export const add3 = 3;", + "export const add4 = 4;", + "export const add5 = 5;", + ), + }); + const onlyRemove = createTestDiffFile({ + id: "only-remove", + path: "src/ui/only-remove.ts", + before: lines( + "export const stable = true;", + "export const remove1 = 1;", + "export const remove2 = 2;", + "export const remove3 = 3;", + ), + after: lines("export const stable = true;"), + }); + const renamedWithoutContentChanges = { + ...createTestDiffFile({ + id: "rename-only", + path: "src/ui/Renamed.tsx", + previousPath: "src/ui/Legacy.tsx", + before: lines("export const stable = true;"), + after: lines("export const stable = true;"), + }), + stats: { additions: 0, deletions: 0 }, + }; + + const entries = buildSidebarEntries([onlyAdd, onlyRemove, renamedWithoutContentChanges]).filter( + (entry) => entry.kind === "file", + ); + + expect(entries).toHaveLength(3); + expect(entries[0]).toMatchObject({ + name: "only-add.ts", + additionsText: "+5", + deletionsText: null, + }); + expect(entries[1]).toMatchObject({ + name: "only-remove.ts", + additionsText: null, + deletionsText: "-3", + }); + expect(entries[2]).toMatchObject({ + name: "Legacy.tsx -> Renamed.tsx", + additionsText: null, + deletionsText: null, + }); + }); +}); diff --git a/src/ui/lib/files.ts b/src/ui/lib/files.ts index d877d837..86def67f 100644 --- a/src/ui/lib/files.ts +++ b/src/ui/lib/files.ts @@ -6,8 +6,8 @@ export interface FileListEntry { kind: "file"; id: string; name: string; - additionsText: string; - deletionsText: string; + additionsText: string | null; + deletionsText: string | null; changeType: FileDiffMetadata["type"]; isUntracked: boolean; } @@ -31,6 +31,36 @@ function sidebarFileName(file: DiffFile) { return previousName === nextName ? nextName : `${previousName} -> ${nextName}`; } +/** Hide zero-value file stats so the sidebar only shows real line deltas. */ +function formatSidebarStat(prefix: "+" | "-", value: number) { + return value > 0 ? `${prefix}${value}` : null; +} + +/** Build the visible stats badges for one sidebar row. */ +export function sidebarEntryStats(entry: Pick) { + const stats: Array<{ kind: "addition" | "deletion"; text: string }> = []; + + if (entry.additionsText) { + stats.push({ kind: "addition", text: entry.additionsText }); + } + + if (entry.deletionsText) { + stats.push({ kind: "deletion", text: entry.deletionsText }); + } + + return stats; +} + +/** Measure the rendered sidebar stats width, including the space between badges. */ +export function sidebarEntryStatsWidth( + entry: Pick, +) { + return sidebarEntryStats(entry).reduce( + (width, stat, index) => width + stat.text.length + (index > 0 ? 1 : 0), + 0, + ); +} + /** Merge one file-id keyed annotation map into the review stream file list. */ export function mergeFileAnnotationsByFileId( files: DiffFile[], @@ -93,8 +123,8 @@ export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] { kind: "file", id: file.id, name: sidebarFileName(file), - additionsText: `+${file.stats.additions}`, - deletionsText: `-${file.stats.deletions}`, + additionsText: formatSidebarStat("+", file.stats.additions), + deletionsText: formatSidebarStat("-", file.stats.deletions), changeType: file.metadata.type, isUntracked: file.isUntracked ?? false, });