| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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)
- })
- })
|