From 24bee8eef27e87e068ca7da4e54e56eef19e3809 Mon Sep 17 00:00:00 2001 From: Muu Date: Sun, 8 Nov 2026 22:00:32 +0800 Subject: [PATCH] feat(vcs): clearer branch sync indicators and diff line-ending badges (#1667) Branch sync markers were drawn three different ways in the title bar, the branches popup and the Git Log tree. Diff panels also said "Contents are identical" for files that differ only in line endings. The markers now share one `BranchSync` component. The diff labels each side with its line ending when they differ, and shows "Contents differ only in line separators" when nothing else does. Siblings covered: long branch names truncate in the Git Log tree without clipping the markers, and static sides with mixed line endings are compared as they are. A live working file with mixed line endings still reads as identical to its normalized form, because open documents are normalized. Co-authored-by: muutot <92625353+muutot@users.noreply.github.com> --- src/editor/api.ts | 3 ++ src/editor/core/model/attachment.ts | 1 + src/features/todo/PreviewEditor.test.tsx | 1 + src/features/vcs/BranchSync.module.css | 25 ++++++++++ src/features/vcs/BranchSync.tsx | 41 ++++++++++++++++ src/features/vcs/BranchWidget.module.css | 21 --------- src/features/vcs/BranchWidget.tsx | 22 ++------- src/features/vcs/DiffEditor.tsx | 38 +++++++++++---- src/features/vcs/GitLog.module.css | 9 ++++ src/features/vcs/GitLog.tsx | 12 +++-- src/features/vcs/__tests__/diffFind.test.tsx | 1 + .../vcs/__tests__/diffToolbar.test.tsx | 47 +++++++++++++++++++ .../vcs/__tests__/logBranches.test.tsx | 17 +++++++ src/features/vcs/diffInput.ts | 39 ++++++++++++++- src/platform/locales/text/en.json | 1 + src/platform/locales/text/es.json | 1 - src/platform/locales/text/fr.json | 1 - src/platform/locales/text/ko.json | 1 - src/platform/locales/text/pl.json | 1 - src/platform/locales/text/pt.json | 1 - src/platform/locales/text/zh.json | 2 +- src/ui/Tree.module.css | 1 + src/ui/Tree.tsx | 8 ++-- 23 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 src/features/vcs/BranchSync.module.css create mode 100644 src/features/vcs/BranchSync.tsx diff --git a/src/editor/api.ts b/src/editor/api.ts index 9804e1361..5206a786b 100644 --- a/src/editor/api.ts +++ b/src/editor/api.ts @@ -30,6 +30,7 @@ import { } from "@codemirror/state"; import { GutterMarker, ViewPlugin, type EditorView } from "@codemirror/view"; import type { LanguageDescription } from "@codemirror/language"; +import type { LineEnding } from "@/platform/ipc"; import { create } from "zustand"; import { maybeEditorService } from "@/workbench/contributions"; @@ -636,6 +637,8 @@ export interface DocumentAttachment { doc: number; /** Current document text; build the view's state from it. */ text(): string; + /** The document's line ending (detected on load, as the status bar shows). */ + lineEnding(): LineEnding; /** The document's language mode name (backend-assigned, as the editor tabs use). */ uiMode(): string | null; readOnly: boolean; diff --git a/src/editor/core/model/attachment.ts b/src/editor/core/model/attachment.ts index 0720754cc..65f496e32 100644 --- a/src/editor/core/model/attachment.ts +++ b/src/editor/core/model/attachment.ts @@ -126,6 +126,7 @@ export async function attachDocument( path, doc: model.doc, text: () => model.sync.localText.toString(), + lineEnding: () => model.getMeta().lineEnding, uiMode: () => model.getMeta().uiMode, get readOnly() { return model.getMeta().readOnly || model.virtual; diff --git a/src/features/todo/PreviewEditor.test.tsx b/src/features/todo/PreviewEditor.test.tsx index 4a8c8f378..0e176044d 100644 --- a/src/features/todo/PreviewEditor.test.tsx +++ b/src/features/todo/PreviewEditor.test.tsx @@ -16,6 +16,7 @@ function stubAttacher(docs: Map docs.get(path)!.text, + lineEnding: () => "lf", dirty: () => !!docs.get(path)!.dirty, uiMode: () => null, readOnly: !!docs.get(path)!.readOnly, diff --git a/src/features/vcs/BranchSync.module.css b/src/features/vcs/BranchSync.module.css new file mode 100644 index 000000000..81d876266 --- /dev/null +++ b/src/features/vcs/BranchSync.module.css @@ -0,0 +1,25 @@ +.sync { + flex: none; + display: inline-flex; + align-items: center; + gap: 3px; + font-family: var(--font-mono); + font-size: var(--fs-small); + line-height: 1; + vertical-align: middle; +} + +.item { + flex: none; + display: inline-flex; + align-items: center; + gap: 1px; +} + +.incoming { + color: var(--accent); +} + +.outgoing { + color: var(--success); +} diff --git a/src/features/vcs/BranchSync.tsx b/src/features/vcs/BranchSync.tsx new file mode 100644 index 000000000..62c011bcc --- /dev/null +++ b/src/features/vcs/BranchSync.tsx @@ -0,0 +1,41 @@ +import clsx from "clsx"; +import { Icon } from "@/ui"; +import { tx } from "@/platform/i18n"; +import styles from "./BranchSync.module.css"; + +interface BranchSyncProps { + ahead: number; + behind: number; + /** Include the commit counts; the title-bar widget passes false and keeps them in the tooltip. */ + showCounts?: boolean; + className?: string; +} + +/** Incoming / outgoing branch markers shared by the branch widget, popup and Git Log tree. */ +export function BranchSync({ ahead, behind, showCounts = true, className }: BranchSyncProps) { + if (ahead <= 0 && behind <= 0) return null; + return ( + + {behind > 0 && } + {ahead > 0 && } + + ); +} + +function BranchSyncItem({ + count, + direction, + showCount, +}: { + count: number; + direction: "incoming" | "outgoing"; + showCount: boolean; +}) { + const label = direction === "incoming" ? tx`${count} incoming commits` : tx`${count} outgoing commits`; + return ( + + + {showCount && {count}} + + ); +} diff --git a/src/features/vcs/BranchWidget.module.css b/src/features/vcs/BranchWidget.module.css index a274be507..2e89df2d1 100644 --- a/src/features/vcs/BranchWidget.module.css +++ b/src/features/vcs/BranchWidget.module.css @@ -35,9 +35,6 @@ text-transform: uppercase; letter-spacing: 0.04em; } -.sync { - flex: none; -} .popup { width: 380px; display: flex; @@ -112,24 +109,6 @@ font-size: var(--fs-tiny); line-height: 15px; } -.syncSmall { - flex: none; - display: inline-flex; - align-items: center; - gap: 1px; - color: var(--fg-muted); - font-family: var(--font-mono); - font-size: var(--fs-tiny); -} -.syncSmall + .syncSmall { - margin-left: -4px; -} -.incoming { - color: var(--accent); -} -.outgoing { - color: var(--success); -} .upstream { flex: 0 4 auto; margin-left: auto; diff --git a/src/features/vcs/BranchWidget.tsx b/src/features/vcs/BranchWidget.tsx index 3ce432b2b..9a09f4547 100644 --- a/src/features/vcs/BranchWidget.tsx +++ b/src/features/vcs/BranchWidget.tsx @@ -37,6 +37,7 @@ import { type RepoRow, } from "./repoBranches"; import { currentRepo, focusRepo, loadBranches, repoName, useVcs } from "./store"; +import { BranchSync } from "./BranchSync"; import styles from "./BranchWidget.module.css"; import { tx } from "@/platform/i18n"; @@ -116,12 +117,7 @@ export function BranchWidget() { {branch.operation} )} {/* Like IDEA's widget: icons only; counts are in the tooltip. */} - {branch.behind > 0 && ( - - )} - {branch.ahead > 0 && ( - - )} + {open && useBranchesPopup.setState({ open: false })} />} @@ -898,19 +894,7 @@ function BranchRowView({ row }: { row: BranchRow | Extractcurrent )} - {/* Like IDEA: incoming (↙) before outgoing (↗). */} - {b.behind > 0 && ( - - - {b.behind} - - )} - {b.ahead > 0 && ( - - - {b.ahead} - - )} + {!b.remote && b.upstream && {b.upstream}} diff --git a/src/features/vcs/DiffEditor.tsx b/src/features/vcs/DiffEditor.tsx index 2ac253588..61b966e6e 100644 --- a/src/features/vcs/DiffEditor.tsx +++ b/src/features/vcs/DiffEditor.tsx @@ -51,6 +51,9 @@ import { import { chainNeighbour, diffTitle, + detectLineEnding, + liveSideText, + lineEndingBadge, isIndex, isWorktree, loadSide, @@ -327,7 +330,7 @@ export function DiffEditor({ }; }, [path]); - const updateChunkInfo = useCallback(() => { + const updateChunkInfo = useCallback((docChanged = false) => { const v = viewsRef.current?.b; if (!v) return; const chunks = getChunks(v.state)?.chunks ?? []; @@ -335,6 +338,7 @@ export function DiffEditor({ const current = chunks.findIndex((c) => c.fromB <= head && head <= Math.max(c.fromB, c.endB)); const { moves, whitespaceOnly } = getDiffInsights(chunks); setChunkInfo((p) => + !docChanged && p.count === chunks.length && p.current === current && p.moves === moves.length && @@ -377,7 +381,7 @@ export function DiffEditor({ prev?.destroy(); host.textContent = ""; const listener = EditorView.updateListener.of((u) => { - if (u.selectionSet || u.docChanged) updateChunkInfo(); + if (u.selectionSet || u.docChanged) updateChunkInfo(u.docChanged); }); const findWatch = (s: Side) => EditorView.updateListener.of((u) => findWatchers.current[s].forEach((fn) => fn(u))); const focusTracker = (side: "a" | "b") => @@ -555,7 +559,7 @@ export function DiffEditor({ viewerRef.current = built; setViewer(built); setViewsGen((g) => g + 1); - requestAnimationFrame(updateChunkInfo); + requestAnimationFrame(() => updateChunkInfo()); }, [ contribs, attached, @@ -1006,8 +1010,18 @@ export function DiffEditor({ [], ); - const identical = - ready && chunkInfo.count === 0 && (attached?.a?.text() ?? statics?.a) === (attached?.b?.text() ?? statics?.b); + const textA = attached?.a?.text() ?? statics?.a ?? ""; + const textB = attached?.b?.text() ?? statics?.b ?? ""; + const eolA = attached?.a?.lineEnding() ?? detectLineEnding(textA); + const eolB = attached?.b?.lineEnding() ?? detectLineEnding(textB); + const rawA = attached?.a ? liveSideText(textA, eolA) : textA; + const rawB = attached?.b ? liveSideText(textB, eolB) : textB; + const badgeA = lineEndingBadge(rawA); + const badgeB = lineEndingBadge(rawB); + const sameText = rawA.replace(/\r\n?/g, "\n") === rawB.replace(/\r\n?/g, "\n"); + const eolOnly = ready && !binary && rawA !== rawB && sameText; + const eolDiffers = ready && !binary && (badgeA !== badgeB || eolOnly); + const identical = ready && chunkInfo.count === 0 && rawA === rawB; // Staging applies to local changes against HEAD / the index only. const canStage = editableB && isWorktree(right) && left.type === "rev" && !!useVcs.getState().root; const hasSource = !!source; @@ -1127,9 +1141,11 @@ export function DiffEditor({ : chunkInfo.count === 0 ? identical ? tx("Contents are identical") - : chunkInfo.whitespaceOnly - ? tx("Whitespace-only changes") - : tx("No differences") + : eolOnly + ? tx("Contents differ only in line separators") + : chunkInfo.whitespaceOnly + ? tx("Whitespace-only changes") + : tx("No differences") : chunkInfo.current >= 0 ? tx`${chunkInfo.current + 1} of ${chunkInfo.count} differences` : chunkInfo.count === 1 @@ -1156,6 +1172,7 @@ export function DiffEditor({
{sideLabel(left)} + {eolDiffers && eolA && {badgeA}} {editableA && ( editable @@ -1164,6 +1181,7 @@ export function DiffEditor({ {sideLabel(right)} + {eolDiffers && eolB && {badgeB}} {editableB && ( {left.type === "rev" ? tx("working copy") : tx("editable")} )} @@ -1172,7 +1190,9 @@ export function DiffEditor({ ) : (
- {sideLabel(left)} → {sideLabel(right)} + {sideLabel(left)} + {eolDiffers && eolA && {badgeA}} → {sideLabel(right)} + {eolDiffers && eolB && {badgeB}}
)} diff --git a/src/features/vcs/GitLog.module.css b/src/features/vcs/GitLog.module.css index 2917f7794..1c735e1c0 100644 --- a/src/features/vcs/GitLog.module.css +++ b/src/features/vcs/GitLog.module.css @@ -60,9 +60,18 @@ min-width: 60px; } .branchTree { + --tree-detail-flex-shrink: 0; flex: 1; min-height: 0; } +.branchDetail { + min-width: 0; + display: inline-flex; + align-items: center; + gap: 6px; + line-height: 1; + vertical-align: middle; +} /* filters */ .filters { diff --git a/src/features/vcs/GitLog.tsx b/src/features/vcs/GitLog.tsx index b1d5e846c..f33e4ec67 100644 --- a/src/features/vcs/GitLog.tsx +++ b/src/features/vcs/GitLog.tsx @@ -88,6 +88,7 @@ import { clearConsole, useVcsConsole } from "./ops"; import { currentRepo, currentRepoRoot, loadBranches, repoName, useVcs } from "./store"; import { canDeleteBranch, loadFavoriteBranches, useFavoriteBranches } from "./branchOps"; import { branchMenu, inRepo, tagMenu } from "./branchMenu"; +import { BranchSync } from "./BranchSync"; import { branchFolderKeys, branchGroupKeys, @@ -348,11 +349,16 @@ export function LogView() { // --- branches --------------------------------------------------------------- -function branchDetail(n: BranchTreeNode): string | undefined { +function branchDetail(n: BranchTreeNode): ReactNode { const b = n.branch; if (!b) return undefined; - const sync = [b.behind > 0 ? `↓${b.behind}` : "", b.ahead > 0 ? `↑${b.ahead}` : ""].filter(Boolean).join(" "); - return [b.current ? tx("current") : "", sync].filter(Boolean).join(" ") || undefined; + if (!b.current && b.behind <= 0 && b.ahead <= 0) return undefined; + return ( + + {b.current && {tx("current")}} + + + ); } export function BranchesPanel() { diff --git a/src/features/vcs/__tests__/diffFind.test.tsx b/src/features/vcs/__tests__/diffFind.test.tsx index 8931ed3d2..3da384e28 100644 --- a/src/features/vcs/__tests__/diffFind.test.tsx +++ b/src/features/vcs/__tests__/diffFind.test.tsx @@ -33,6 +33,7 @@ vi.mock("@/editor/api", async (importOriginal) => ({ Promise.resolve({ path, text: () => "foo\nqux\nfoo\n", + lineEnding: () => "lf", readOnly: false, extension: [], save: () => Promise.resolve(), diff --git a/src/features/vcs/__tests__/diffToolbar.test.tsx b/src/features/vcs/__tests__/diffToolbar.test.tsx index bdac8d2e6..be9fe6a9d 100644 --- a/src/features/vcs/__tests__/diffToolbar.test.tsx +++ b/src/features/vcs/__tests__/diffToolbar.test.tsx @@ -11,6 +11,7 @@ const openFile = vi.fn(); const openInput = vi.fn(); const texts: Record = { HEAD: "a\n", abc123: "a\n" }; let working = "a\n"; +let workingEol = "lf"; vi.mock("@/platform/ipc", async (importOriginal) => { const real = await importOriginal(); @@ -36,6 +37,7 @@ vi.mock("@/editor/api", async (importOriginal) => ({ Promise.resolve({ path, text: () => working, + lineEnding: () => workingEol, readOnly: false, extension: [], save: () => Promise.resolve(), @@ -63,6 +65,7 @@ const input = { const paths = (html: string) => [...html.matchAll(/ d="([^"]+)"/g)].map((m) => m[1]); beforeEach(() => { + workingEol = "lf"; probe.mockReset(); openFile.mockReset(); openInput.mockReset(); @@ -94,6 +97,50 @@ describe("Diff toolbar", () => { await waitFor(() => expect(screen.queryByText("Binary files cannot be shown as text")).toBeNull()); }); + it.each([ + ["CRLF", "a\r\n", "a\n"], + ["CR", "a\r", "a\n"], + ["mixed with the same dominant ending", "a\r\nb\nc\n", "a\nb\nc\n"], + ["mixed with a different dominant ending", "a\r\nb\r\nc\n", "a\nb\nc\n"], + ])("reports %s line-separator-only differences", async (_name, a, b) => { + texts.HEAD = a; + texts.abc123 = b; + render(); + expect(await screen.findByText("Contents differ only in line separators")).toBeInTheDocument(); + expect(screen.queryByText("No differences")).toBeNull(); + expect(screen.queryByText("Contents are identical")).toBeNull(); + }); + + it.each(["a\n", "a\r\n", "a\r\nb\n", ""])("recognizes identical contents %j", async (text) => { + texts.HEAD = texts.abc123 = text; + render(); + expect(await screen.findByText("Contents are identical")).toBeInTheDocument(); + expect(screen.queryByText("Contents differ only in line separators")).toBeNull(); + }); + + it("does not describe ignored whitespace as only line separators", async () => { + localStorage.setItem("openidea.vcs.diffOptions", JSON.stringify({ ignore: "whitespace" })); + texts.HEAD = "a \r\n"; + texts.abc123 = "a\n"; + const mounted = render(); + await waitFor(() => expect(mounted.container.querySelectorAll(".cm-editor")).toHaveLength(2)); + expect(screen.queryByText("Contents differ only in line separators")).toBeNull(); + mounted.unmount(); + localStorage.removeItem("openidea.vcs.diffOptions"); + }); + + it("compares a normalized live CRLF document with a raw CRLF revision", async () => { + texts.HEAD = "a\r\n"; + workingEol = "crLf"; + render( + , + ); + expect(await screen.findByText("Contents are identical")).toBeInTheDocument(); + expect(screen.queryByText("Contents differ only in line separators")).toBeNull(); + }); + it("puts Jump to Source right after Next Difference, in the navigation group", async () => { render(); await screen.findByRole("button", { name: "Jump to Source" }); diff --git a/src/features/vcs/__tests__/logBranches.test.tsx b/src/features/vcs/__tests__/logBranches.test.tsx index 1136d8097..29f3d2195 100644 --- a/src/features/vcs/__tests__/logBranches.test.tsx +++ b/src/features/vcs/__tests__/logBranches.test.tsx @@ -23,6 +23,7 @@ vi.mock("@/platform/actions", async (importOriginal) => ({ import { ContextMenuHost } from "@/ui"; import { BranchesPanel } from "../GitLog"; +import syncStyles from "../BranchSync.module.css"; import { emptyFilters, useGitLog } from "../logStore"; import { useVcs } from "../store"; @@ -88,6 +89,22 @@ describe("Git Log branches pane", () => { expect(screen.getByText("feature").closest("[data-row]")).toHaveAttribute("aria-selected", "true"); }); + it.each([ + [2, 0], + [0, 3], + [2, 3], + [0, 0], + ])("renders shared branch markers for ahead %i / behind %i", (ahead, behind) => { + useVcs.setState({ branches: [b("main", { current: true, ahead, behind })] }); + render(); + const incoming = screen.queryByLabelText("3 incoming commits"); + const outgoing = screen.queryByLabelText("2 outgoing commits"); + if (behind) expect(incoming).toHaveClass(syncStyles.incoming!); + else expect(incoming).toBeNull(); + if (ahead) expect(outgoing).toHaveClass(syncStyles.outgoing!); + else expect(outgoing).toBeNull(); + }); + it("double click switches the log to that branch", () => { render(); fireEvent.doubleClick(screen.getByText("feature")); diff --git a/src/features/vcs/diffInput.ts b/src/features/vcs/diffInput.ts index cea3a56da..05567bd55 100644 --- a/src/features/vcs/diffInput.ts +++ b/src/features/vcs/diffInput.ts @@ -1,6 +1,6 @@ /** Diff editor inputs and helpers to open them. */ import type { ChangedFile, RevisionChange } from "@/bindings"; -import { call, commands } from "@/platform/ipc"; +import { call, commands, type LineEnding } from "@/platform/ipc"; import { editorService, type EditorInput } from "@/workbench/contributions"; import { baseName, effectiveKind } from "./model"; import { readWorkingText } from "./ops"; @@ -219,3 +219,40 @@ export const isWorktree = (s: DiffSide) => s.type === "worktree"; export const sideFile = (s: DiffSide, path: string): string | null => s.type === "worktree" ? path : s.type === "file" ? s.path : null; export const isIndex = (s: DiffSide) => s.type === "rev" && (s.rev === ":0" || s.rev === ":"); + +/** The dominant line ending of `text` (`lf` for ties and no breaks), like the backend's detection. */ +export function detectLineEnding(text: string): LineEnding { + let lf = 0; + let crlf = 0; + let cr = 0; + for (let i = 0; i < text.length; i++) { + const ch = text.charCodeAt(i); + if (ch === 10) lf++; + else if (ch === 13) { + if (text.charCodeAt(i + 1) === 10) { + crlf++; + i++; + } else cr++; + } + } + return lf >= crlf && lf >= cr ? "lf" : crlf >= cr ? "crLf" : "cr"; +} + +const EOL_BADGE: Record = { lf: "LF", crLf: "CRLF", cr: "CR" }; + +const EOL_SEPARATOR: Record = { lf: "\n", crLf: "\r\n", cr: "\r" }; + +/** Reconstruct a normalized document using its configured save separator. */ +export function liveSideText(text: string, ending: LineEnding): string { + return text.replace(/\n/g, EOL_SEPARATOR[ending]); +} + +export function lineEndingBadge(text: string): string { + const separators = new Set(text.match(/\r\n|\r|\n/g)); + return ( + Object.entries(EOL_SEPARATOR) + .filter(([, separator]) => separators.has(separator)) + .map(([ending]) => EOL_BADGE[ending as LineEnding]) + .join(" / ") || "LF" + ); +} diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 0b106bc20..ef5b162e4 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -1,4 +1,5 @@ { + "Contents differ only in line separators": "Contents differ only in line separators", "Cannot remove a worktree containing the open project.": "Cannot remove a worktree containing the open project.", "Delete Remote Branches": "Delete Remote Branches", "Delete the {length} branches '{names}' on the remote? The remote-tracking branches are removed as well.": "Delete the {length} branches '{names}' on the remote? The remote-tracking branches are removed as well.", diff --git a/src/platform/locales/text/es.json b/src/platform/locales/text/es.json index beb81df79..2c6d802dd 100644 --- a/src/platform/locales/text/es.json +++ b/src/platform/locales/text/es.json @@ -1044,7 +1044,6 @@ "Include": "Incluir", "Include non-project items": "Incluir elementos fuera del proyecto", "Include untracked files": "Incluir archivos sin seguimiento", - "Incoming commits": "Confirmaciones entrantes", "Indent": "Aplicar sangría", "Indent Block": "Aplicar sangría al bloque", "Indent Each Line": "Aplicar sangría a cada línea", diff --git a/src/platform/locales/text/fr.json b/src/platform/locales/text/fr.json index 08f620429..10c2b2414 100644 --- a/src/platform/locales/text/fr.json +++ b/src/platform/locales/text/fr.json @@ -975,7 +975,6 @@ "Include": "Inclure", "Include non-project items": "Inclure les éléments hors projet", "Include untracked files": "Inclure les fichiers non suivis", - "Incoming commits": "Commits entrants", "Indent": "Indenter", "Indent Block": "Indenter le bloc", "Indent Each Line": "Indenter chaque ligne", diff --git a/src/platform/locales/text/ko.json b/src/platform/locales/text/ko.json index e4b6206d7..8b0fcf329 100644 --- a/src/platform/locales/text/ko.json +++ b/src/platform/locales/text/ko.json @@ -1085,7 +1085,6 @@ "Include": "포함", "Include non-project items": "프로젝트 외 항목 포함", "Include untracked files": "추적되지 않는 파일 포함", - "Incoming commits": "수신 커밋", "Indent": "들여쓰기", "Indent Block": "블록 들여쓰기", "Indent Each Line": "각 줄 들여쓰기", diff --git a/src/platform/locales/text/pl.json b/src/platform/locales/text/pl.json index 315a58a0c..6cf17dfe2 100644 --- a/src/platform/locales/text/pl.json +++ b/src/platform/locales/text/pl.json @@ -1036,7 +1036,6 @@ "Include": "Dołącz", "Include non-project items": "Dołącz elementy spoza projektu", "Include untracked files": "Dołącz niesformatowane/nieśledzone pliki", - "Incoming commits": "Przychodzące zatwierdzenia", "Indent": "Wcięcie", "Indent Block": "Zwiększ wcięcie bloku", "Indent Each Line": "Zwiększ wcięcie każdego wiersza", diff --git a/src/platform/locales/text/pt.json b/src/platform/locales/text/pt.json index 05280f0da..e31a05e8f 100644 --- a/src/platform/locales/text/pt.json +++ b/src/platform/locales/text/pt.json @@ -1036,7 +1036,6 @@ "Include": "Incluir", "Include non-project items": "Incluir itens fora do projeto", "Include untracked files": "Incluir arquivos não rastreados", - "Incoming commits": "Commits recebidos", "Indent": "Indentar", "Indent Block": "Indentar bloco", "Indent Each Line": "Indentar cada linha", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index b1a884c49..d81acbc2c 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -1138,7 +1138,6 @@ "Include": "包括", "Include non-project items": "包括非项目项", "Include untracked files": "包括未跟踪的文件", - "Incoming commits": "传入的提交", "Indent": "缩进", "Indent Block": "缩进块", "Indent Each Line": "缩进每一行", @@ -4928,6 +4927,7 @@ "Upgrading": "正在升级", "Uninstalling": "正在卸载", "Contents are identical": "内容完全一致", + "Contents differ only in line separators": "内容仅在行分隔符上有差异", "working copy": "工作副本", "No commits match the filters": "没有与筛选条件匹配的提交", "No commits yet": "还没有提交", diff --git a/src/ui/Tree.module.css b/src/ui/Tree.module.css index e94759b2e..99dac0308 100644 --- a/src/ui/Tree.module.css +++ b/src/ui/Tree.module.css @@ -129,6 +129,7 @@ text-overflow: ellipsis; } .detail { + flex-shrink: var(--tree-detail-flex-shrink, 1); min-width: 0; margin-left: 4px; overflow: hidden; diff --git a/src/ui/Tree.tsx b/src/ui/Tree.tsx index f8ab23703..fd6b427e8 100644 --- a/src/ui/Tree.tsx +++ b/src/ui/Tree.tsx @@ -86,8 +86,8 @@ export interface TreeProps { /** Custom row content (after the chevron). Defaults to icon + highlighted label + `getDetail`. */ renderNode?: (node: T, state: TreeRowState) => ReactNode; getIcon?: (node: T, expanded: boolean) => IconName | (string & {}) | undefined; - /** Muted trailing text (e.g. a path next to the project root). */ - getDetail?: (node: T) => string | undefined; + /** Muted trailing detail (e.g. a path next to the project root); may include styled content. */ + getDetail?: (node: T) => ReactNode; /** Label colour, e.g. VCS status. */ getColor?: (node: T) => string | undefined; /** Filesystem directory for actions on the focused selection; groups return null. */ @@ -750,7 +750,9 @@ export function Tree(props: TreeProps) { indices={matches} tone="accent" /> - {detail && {detail}} + {detail != null && detail !== false && detail !== "" && ( + {detail} + )} )}