import { describe, expect, test } from "bun:test" import { allExpandedFileTreeDirectories, buildFileTree, fileTreeFileSelection, flattenFileTree, moveFileTreeSelection, moveFileTreeSelectionToFirstChild, moveFileTreeSelectionToFile, moveFileTreeSelectionToParent, movePatchFileIndex, orderedPatchFileIndexes, setFileTreeDirectoryExpanded, showDiffViewerFileTree, singlePatchFileIndex, toggleFileTreeDirectory, } from "../../src/feature-plugins/system/diff-viewer-file-tree-utils" describe("diff viewer file tree utilities", () => { test("builds a nested tree with deduplicated directories and file indexes", () => { const tree = buildFileTree([ { file: "src/config/tui.ts" }, { file: "src/config/keybind.ts" }, { file: "src/session/index.ts" }, ]) expect(tree.nodes.filter((node) => node.kind === "directory" && node.name === "src")).toHaveLength(1) expect(tree.nodes.filter((node) => node.kind === "directory" && node.name === "config")).toHaveLength(1) expect(tree.nodes.filter((node) => node.kind === "directory" && node.name === "session")).toHaveLength(1) expect( tree.nodes .filter((node) => node.kind === "file") .map((node) => ({ name: node.name, fileIndex: node.fileIndex, depth: node.depth })), ).toEqual([ { name: "tui.ts", fileIndex: 0, depth: 2 }, { name: "keybind.ts", fileIndex: 1, depth: 2 }, { name: "index.ts", fileIndex: 2, depth: 2 }, ]) }) test("sorts directories before files and alphabetically within each group", () => { const rows = flattenFileTree( buildFileTree([ { file: "z-file.ts" }, { file: "b/file.ts" }, { file: "a/zeta.ts" }, { file: "b/alpha.ts" }, { file: "a/alpha.ts" }, ]), ) expect(rows.map((row) => `${" ".repeat(row.depth)}${row.kind}:${row.name}`)).toEqual([ "directory:a", " file:alpha.ts", " file:zeta.ts", "directory:b", " file:alpha.ts", " file:file.ts", "file:z-file.ts", ]) }) test("sorts root-level files without creating directories", () => { const tree = buildFileTree([{ file: "zeta.ts" }, { file: "alpha.ts" }, { file: "beta.ts" }]) expect(tree.nodes.every((node) => node.kind === "file")).toBe(true) expect(flattenFileTree(tree).map((row) => row.name)).toEqual(["alpha.ts", "beta.ts", "zeta.ts"]) }) test("collapses unary directory chains while flattening", () => { const rows = flattenFileTree( buildFileTree([{ file: "packages/opencode/src/cli/app.ts" }, { file: "packages/opencode/src/server/server.ts" }]), ) expect(rows.map((row) => `${" ".repeat(row.depth)}${row.kind}:${row.name}`)).toEqual([ "directory:packages/opencode/src", " directory:cli", " file:app.ts", " directory:server", " file:server.ts", ]) }) test("does not collapse a directory into a file row", () => { const rows = flattenFileTree(buildFileTree([{ file: "packages/opencode/src/app.ts" }])) expect(rows.map((row) => `${" ".repeat(row.depth)}${row.kind}:${row.name}`)).toEqual([ "directory:packages/opencode/src", " file:app.ts", ]) }) test("stops collapsing at branches", () => { const rows = flattenFileTree( buildFileTree([ { file: "packages/opencode/src/cli/app.ts" }, { file: "packages/opencode/src/server/server.ts" }, { file: "packages/readme.md" }, ]), ) expect(rows.map((row) => `${" ".repeat(row.depth)}${row.kind}:${row.name}`)).toEqual([ "directory:packages", " directory:opencode/src", " directory:cli", " file:app.ts", " directory:server", " file:server.ts", " file:readme.md", ]) }) test("keeps same directory names under different parents separate", () => { const rows = flattenFileTree( buildFileTree([{ file: "components/button.ts" }, { file: "docs/components/usage.md" }]), ) expect(rows.map((row) => `${" ".repeat(row.depth)}${row.kind}:${row.name}`)).toEqual([ "directory:components", " file:button.ts", "directory:docs/components", " file:usage.md", ]) }) test("flattens all-expanded rows depth-first with depths and file references", () => { const rows = flattenFileTree( buildFileTree([{ file: "src/config/tui.ts" }, { file: "src/config/keybind.ts" }, { file: "README.md" }]), ) expect(rows.map((row) => ({ name: row.name, kind: row.kind, depth: row.depth, fileIndex: row.fileIndex }))).toEqual( [ { name: "src/config", kind: "directory", depth: 0, fileIndex: undefined }, { name: "keybind.ts", kind: "file", depth: 1, fileIndex: 1 }, { name: "tui.ts", kind: "file", depth: 1, fileIndex: 0 }, { name: "README.md", kind: "file", depth: 0, fileIndex: 2 }, ], ) }) test("collapses expanded unary children under the first visible directory id", () => { const tree = buildFileTree([ { file: "packages/opencode/src/cli/app.ts" }, { file: "packages/opencode/src/server/server.ts" }, ]) const packages = tree.nodes.find((node) => node.kind === "directory" && node.name === "packages")! expect(flattenFileTree(tree, new Set()).map((row) => row.name)).toEqual(["packages/opencode/src"]) expect(flattenFileTree(tree, new Set([packages.id])).map((row) => row.name)).toEqual([ "packages/opencode/src", "cli", "server", ]) }) test("flattens only expanded directory descendants when expansion is provided", () => { const tree = buildFileTree([{ file: "src/config/tui.ts" }, { file: "src/session/index.ts" }, { file: "README.md" }]) const src = tree.nodes.find((node) => node.kind === "directory" && node.name === "src")! const config = tree.nodes.find((node) => node.kind === "directory" && node.name === "config")! expect(flattenFileTree(tree, new Set()).map((row) => row.name)).toEqual(["src", "README.md"]) expect(flattenFileTree(tree, new Set([src.id])).map((row) => row.name)).toEqual([ "src", "config", "session", "README.md", ]) expect(flattenFileTree(tree, new Set([src.id, config.id])).map((row) => row.name)).toEqual([ "src", "config", "tui.ts", "session", "README.md", ]) }) test("moves selection across visible rows and clamps to bounds", () => { const rows = flattenFileTree(buildFileTree([{ file: "src/config/tui.ts" }, { file: "README.md" }])) expect(moveFileTreeSelection(rows, undefined, 1)).toBe(rows[0]!.id) expect(moveFileTreeSelection(rows, rows[0]!.id, 1)).toBe(rows[1]!.id) expect(moveFileTreeSelection(rows, rows[1]!.id, 99)).toBe(rows[rows.length - 1]!.id) expect(moveFileTreeSelection(rows, rows[1]!.id, -99)).toBe(rows[0]!.id) expect(moveFileTreeSelection([], undefined, 1)).toBeUndefined() }) test("moves directory selection to first visible child", () => { const rows = flattenFileTree(buildFileTree([{ file: "src/config/tui.ts" }, { file: "src/session/index.ts" }])) const src = rows.find((row) => row.kind === "directory" && row.name === "src")! const config = rows.find((row) => row.kind === "directory" && row.name === "config")! const tui = rows.find((row) => row.name === "tui.ts")! expect(moveFileTreeSelectionToFirstChild(rows, src.id)).toBe(config.id) expect(moveFileTreeSelectionToFirstChild(rows, tui.id)).toBe(tui.id) expect(moveFileTreeSelectionToFirstChild(rows, undefined)).toBeUndefined() }) test("moves collapsed chain selection to first visible child", () => { const rows = flattenFileTree( buildFileTree([{ file: "packages/opencode/src/cli/app.ts" }, { file: "packages/opencode/src/server/server.ts" }]), ) const packages = rows.find((row) => row.kind === "directory" && row.name === "packages/opencode/src")! const cli = rows.find((row) => row.kind === "directory" && row.name === "cli")! expect(moveFileTreeSelectionToFirstChild(rows, packages.id)).toBe(cli.id) }) test("moves file and collapsed directory selection to visible parent", () => { const rows = flattenFileTree( buildFileTree([{ file: "packages/opencode/src/cli/app.ts" }, { file: "packages/opencode/src/server/server.ts" }]), ) const root = rows.find((row) => row.kind === "directory" && row.name === "packages/opencode/src")! const cli = rows.find((row) => row.kind === "directory" && row.name === "cli")! const app = rows.find((row) => row.name === "app.ts")! expect(moveFileTreeSelectionToParent(rows, app.id)).toBe(cli.id) expect(moveFileTreeSelectionToParent(rows, cli.id)).toBe(root.id) expect(moveFileTreeSelectionToParent(rows, root.id)).toBe(root.id) expect(moveFileTreeSelectionToParent(rows, undefined)).toBeUndefined() }) test("moves file selection relative to the highlighted row", () => { const rows = flattenFileTree( buildFileTree([{ file: "src/config/tui.ts" }, { file: "src/session/index.ts" }, { file: "README.md" }]), ) const config = rows.find((row) => row.kind === "directory" && row.name === "config")! const session = rows.find((row) => row.kind === "directory" && row.name === "session")! const tui = rows.find((row) => row.name === "tui.ts")! const index = rows.find((row) => row.name === "index.ts")! const readme = rows.find((row) => row.name === "README.md")! expect(moveFileTreeSelectionToFile(rows, undefined, 1)).toBe(tui.id) expect(moveFileTreeSelectionToFile(rows, undefined, -1)).toBe(readme.id) expect(moveFileTreeSelectionToFile(rows, config.id, 1)).toBe(tui.id) expect(moveFileTreeSelectionToFile(rows, session.id, -1)).toBe(tui.id) expect(moveFileTreeSelectionToFile(rows, tui.id, 1)).toBe(index.id) expect(moveFileTreeSelectionToFile(rows, index.id, -1)).toBe(tui.id) expect(moveFileTreeSelectionToFile(rows, readme.id, 1)).toBe(readme.id) }) test("selects a file tree node and expands its parents for a patch file", () => { const tree = buildFileTree([{ file: "src/config/tui.ts" }, { file: "src/session/index.ts" }, { file: "README.md" }]) const selection = fileTreeFileSelection(tree, 1) expect(selection?.highlightedNode).toBe( tree.nodes.find((node) => node.kind === "file" && node.name === "index.ts")?.id, ) expect([...selection!.expandedNodes].map((id) => tree.nodes[id]!.name)).toEqual(["session", "src"]) expect(fileTreeFileSelection(tree, 99)).toBeUndefined() }) test("prefers the selected file when choosing the single patch file", () => { expect(singlePatchFileIndex(2, 1, 0, 3)).toBe(2) expect(singlePatchFileIndex(undefined, 1, 0, 3)).toBe(1) expect(singlePatchFileIndex(undefined, undefined, 0, 3)).toBe(0) expect(singlePatchFileIndex(undefined, undefined, undefined, 3)).toBe(3) }) test("orders patches by the flattened file tree order", () => { const rows = flattenFileTree( buildFileTree([ { file: "src/dir-8/juniper-4.ts" }, { file: "src/dir-8/harbor-94.ts" }, { file: "src/dir-8/cedar-16.ts" }, ]), ) expect(orderedPatchFileIndexes(rows)).toEqual([2, 1, 0]) }) test("shows the diff viewer file tree only when enabled and files exist", () => { expect(showDiffViewerFileTree(true, 1)).toBe(true) expect(showDiffViewerFileTree(true, 0)).toBe(false) expect(showDiffViewerFileTree(false, 1)).toBe(false) expect(showDiffViewerFileTree(false, 0)).toBe(false) }) test("moves patch selection through the ordered patch file indexes", () => { const fileIndexes = [2, 1, 0] expect(movePatchFileIndex(fileIndexes, undefined, 1)).toBe(2) expect(movePatchFileIndex(fileIndexes, undefined, -1)).toBe(2) expect(movePatchFileIndex(fileIndexes, 2, 1)).toBe(1) expect(movePatchFileIndex(fileIndexes, 1, -1)).toBe(2) expect(movePatchFileIndex(fileIndexes, 0, 1)).toBe(0) expect(movePatchFileIndex(fileIndexes, 99, 1)).toBe(2) expect(movePatchFileIndex(fileIndexes, 99, -1)).toBe(2) expect(movePatchFileIndex([], undefined, 1)).toBeUndefined() }) test("toggles only selected directory expansion", () => { const tree = buildFileTree([{ file: "src/config/tui.ts" }, { file: "README.md" }]) const src = tree.nodes.find((node) => node.kind === "directory" && node.name === "src")! const readme = tree.nodes.find((node) => node.kind === "file" && node.name === "README.md")! const expanded = allExpandedFileTreeDirectories(tree) const collapsed = toggleFileTreeDirectory(tree, expanded, src.id) expect(collapsed.has(src.id)).toBe(false) expect(flattenFileTree(tree, collapsed).map((row) => row.name)).toEqual(["src/config", "README.md"]) const reopened = toggleFileTreeDirectory(tree, collapsed, src.id) expect(reopened.has(src.id)).toBe(true) expect(toggleFileTreeDirectory(tree, reopened, readme.id)).toBe(reopened) expect(toggleFileTreeDirectory(tree, reopened, undefined)).toBe(reopened) }) test("sets only selected directory expansion", () => { const tree = buildFileTree([{ file: "src/config/tui.ts" }, { file: "README.md" }]) const src = tree.nodes.find((node) => node.kind === "directory" && node.name === "src")! const readme = tree.nodes.find((node) => node.kind === "file" && node.name === "README.md")! const expanded = allExpandedFileTreeDirectories(tree) const collapsed = setFileTreeDirectoryExpanded(tree, expanded, src.id, false) expect(collapsed.has(src.id)).toBe(false) const reopened = setFileTreeDirectoryExpanded(tree, collapsed, src.id, true) expect(reopened.has(src.id)).toBe(true) expect(setFileTreeDirectoryExpanded(tree, reopened, readme.id, false)).toBe(reopened) expect(setFileTreeDirectoryExpanded(tree, reopened, undefined, false)).toBe(reopened) }) })