| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- // Paths branch softly through the screen,
- // A quiet tree of changed designs;
- // Each leaf remembers what has been,
- // And waits where careful light aligns.
- export type FileTreeItem = {
- readonly file: string
- readonly status?: "added" | "deleted" | "modified"
- }
- export type FileTreeNode = {
- readonly id: number
- readonly name: string
- readonly parent: number | undefined
- readonly children: number[]
- readonly depth: number
- readonly kind: "directory" | "file"
- readonly fileIndex?: number
- }
- export type FileTree = {
- readonly roots: number[]
- readonly nodes: FileTreeNode[]
- }
- export type FileTreeRow = {
- readonly id: number
- readonly depth: number
- readonly kind: "directory" | "file"
- readonly name: string
- readonly fileIndex?: number
- }
- export function buildFileTree(files: readonly FileTreeItem[]): FileTree {
- const roots: number[] = []
- const nodes: FileTreeNode[] = []
- const directoryByPath = new Map<string, number>()
- files.forEach((file, fileIndex) => {
- const segments = file.file.split("/").filter(Boolean)
- if (segments.length === 0) return
- const parent = segments.slice(0, -1).reduce(
- (state, segment) => {
- const directoryPath = state.path ? `${state.path}/${segment}` : segment
- const existing = directoryByPath.get(directoryPath)
- if (existing !== undefined) return { id: existing, path: directoryPath, depth: state.depth + 1 }
- const id = addFileTreeNode(nodes, roots, {
- name: segment,
- parent: state.id,
- depth: state.depth,
- kind: "directory",
- })
- directoryByPath.set(directoryPath, id)
- return { id, path: directoryPath, depth: state.depth + 1 }
- },
- { id: undefined as number | undefined, path: "", depth: 0 },
- )
- addFileTreeNode(nodes, roots, {
- name: segments[segments.length - 1]!,
- parent: parent.id,
- depth: parent.depth,
- kind: "file",
- fileIndex,
- })
- })
- const tree = { roots, nodes }
- tree.roots.sort((left, right) => compareFileTreeNodes(tree, left, right))
- tree.nodes.forEach((node) => node.children.sort((left, right) => compareFileTreeNodes(tree, left, right)))
- return tree
- }
- export function flattenFileTree(tree: FileTree, expanded?: ReadonlySet<number>): FileTreeRow[] {
- const rows: FileTreeRow[] = []
- const visit = (id: number, depth: number) => {
- const node = tree.nodes[id]!
- if (node.kind === "file") {
- rows.push({
- id: node.id,
- depth,
- kind: node.kind,
- name: node.name,
- fileIndex: node.fileIndex,
- })
- return
- }
- const chain = collapsedFileTreeDirectoryChain(tree, node.id)
- const last = chain[chain.length - 1]!
- rows.push({
- id: node.id,
- depth,
- kind: node.kind,
- name: chain.map((item) => item.name).join("/"),
- fileIndex: node.fileIndex,
- })
- if (!expanded || expanded.has(node.id)) last.children.forEach((child) => visit(child, depth + 1))
- }
- tree.roots.forEach((root) => visit(root, 0))
- return rows
- }
- function collapsedFileTreeDirectoryChain(tree: FileTree, id: number): FileTreeNode[] {
- const node = tree.nodes[id]!
- const child = node.children.length === 1 ? tree.nodes[node.children[0]!] : undefined
- if (child?.kind !== "directory") return [node]
- return [node, ...collapsedFileTreeDirectoryChain(tree, child.id)]
- }
- export function compareFileTreeNodes(tree: FileTree, left: number, right: number) {
- const leftNode = tree.nodes[left]!
- const rightNode = tree.nodes[right]!
- if (leftNode.kind !== rightNode.kind) return leftNode.kind === "directory" ? -1 : 1
- if (leftNode.name < rightNode.name) return -1
- if (leftNode.name > rightNode.name) return 1
- return left - right
- }
- export function moveFileTreeSelection(rows: readonly FileTreeRow[], selected: number | undefined, offset: number) {
- if (rows.length === 0) return undefined
- const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
- if (index === -1) return rows[0]!.id
- return rows[Math.max(0, Math.min(rows.length - 1, index + offset))]!.id
- }
- export function moveFileTreeSelectionToFirstChild(rows: readonly FileTreeRow[], selected: number | undefined) {
- const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
- const row = index === -1 ? undefined : rows[index]
- if (row?.kind !== "directory") return selected
- const child = rows[index + 1]
- return child && child.depth > row.depth ? child.id : selected
- }
- export function moveFileTreeSelectionToParent(rows: readonly FileTreeRow[], selected: number | undefined) {
- const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
- const row = index === -1 ? undefined : rows[index]
- if (!row || row.depth === 0) return selected
- return rows.findLast((item, itemIndex) => itemIndex < index && item.depth < row.depth)?.id ?? selected
- }
- export function moveFileTreeSelectionToFile(
- rows: readonly FileTreeRow[],
- selected: number | undefined,
- offset: number,
- ) {
- const fileRows = rows.filter((row) => row.fileIndex !== undefined)
- if (fileRows.length === 0) return undefined
- const selectedIndex = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
- if (selectedIndex === -1) return offset < 0 ? fileRows[fileRows.length - 1]!.id : fileRows[0]!.id
- const next =
- offset < 0
- ? fileRows.findLast((row) => rows.findIndex((item) => item.id === row.id) < selectedIndex)
- : fileRows.find((row) => rows.findIndex((item) => item.id === row.id) > selectedIndex)
- return next?.id ?? (offset < 0 ? fileRows[0]!.id : fileRows[fileRows.length - 1]!.id)
- }
- export function fileTreeFileSelection(tree: FileTree, fileIndex: number) {
- const node = tree.nodes.find((item) => item.kind === "file" && item.fileIndex === fileIndex)
- if (!node) return undefined
- return {
- highlightedNode: node.id,
- expandedNodes: fileTreeParentDirectories(tree, node.id),
- }
- }
- export function singlePatchFileIndex(
- selected: number | undefined,
- active: number | undefined,
- current: number | undefined,
- first: number | undefined,
- ) {
- return selected ?? active ?? current ?? first
- }
- export function orderedPatchFileIndexes(rows: readonly FileTreeRow[]) {
- return rows.flatMap((row) => (row.fileIndex === undefined ? [] : [row.fileIndex]))
- }
- export function showDiffViewerFileTree(showFileTree: boolean, fileCount: number) {
- return showFileTree && fileCount > 0
- }
- export function movePatchFileIndex(fileIndexes: readonly number[], current: number | undefined, offset: number) {
- if (fileIndexes.length === 0) return undefined
- const index = current === undefined ? -1 : fileIndexes.indexOf(current)
- if (index === -1) return fileIndexes[0]
- return fileIndexes[Math.max(0, Math.min(fileIndexes.length - 1, index + offset))]
- }
- export function allExpandedFileTreeDirectories(tree: FileTree) {
- return new Set(tree.nodes.filter((node) => node.kind === "directory").map((node) => node.id))
- }
- export function toggleFileTreeDirectory(tree: FileTree, expanded: ReadonlySet<number>, selected: number | undefined) {
- if (selected === undefined || tree.nodes[selected]?.kind !== "directory") return expanded
- const next = new Set(expanded)
- if (next.has(selected)) next.delete(selected)
- else next.add(selected)
- return next
- }
- export function setFileTreeDirectoryExpanded(
- tree: FileTree,
- expanded: ReadonlySet<number>,
- selected: number | undefined,
- value: boolean,
- ) {
- if (selected === undefined || tree.nodes[selected]?.kind !== "directory") return expanded
- const next = new Set(expanded)
- if (value) next.add(selected)
- else next.delete(selected)
- return next
- }
- function addFileTreeNode(nodes: FileTreeNode[], roots: number[], input: Omit<FileTreeNode, "id" | "children">) {
- const id = nodes.length
- nodes.push({ ...input, id, children: [] })
- if (input.parent === undefined) roots.push(id)
- else nodes[input.parent]!.children.push(id)
- return id
- }
- function fileTreeParentDirectories(tree: FileTree, id: number) {
- const result = new Set<number>()
- for (let parent = tree.nodes[id]?.parent; parent !== undefined; parent = tree.nodes[parent]?.parent) {
- result.add(parent)
- }
- return result
- }
|