|
|
@@ -129,4 +129,104 @@ export namespace Ripgrep {
|
|
|
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
|
|
return result.split("\n").filter(Boolean)
|
|
|
}
|
|
|
+
|
|
|
+ export async function tree(input: { cwd: string; limit?: number }) {
|
|
|
+ const files = await Ripgrep.files({ cwd: input.cwd })
|
|
|
+ interface Node {
|
|
|
+ path: string[]
|
|
|
+ children: Node[]
|
|
|
+ }
|
|
|
+
|
|
|
+ function getPath(node: Node, parts: string[], create: boolean) {
|
|
|
+ if (parts.length === 0) return node
|
|
|
+ let current = node
|
|
|
+ for (const part of parts) {
|
|
|
+ let existing = current.children.find((x) => x.path.at(-1) === part)
|
|
|
+ if (!existing) {
|
|
|
+ if (!create) return
|
|
|
+ existing = {
|
|
|
+ path: current.path.concat(part),
|
|
|
+ children: [],
|
|
|
+ }
|
|
|
+ current.children.push(existing)
|
|
|
+ }
|
|
|
+ current = existing
|
|
|
+ }
|
|
|
+ return current
|
|
|
+ }
|
|
|
+
|
|
|
+ const root: Node = {
|
|
|
+ path: [],
|
|
|
+ children: [],
|
|
|
+ }
|
|
|
+ for (const file of files) {
|
|
|
+ const parts = file.split(path.sep)
|
|
|
+ getPath(root, parts, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ function sort(node: Node) {
|
|
|
+ node.children.sort((a, b) => {
|
|
|
+ if (!a.children.length && b.children.length) return 1
|
|
|
+ if (!b.children.length && a.children.length) return -1
|
|
|
+ return a.path.at(-1)!.localeCompare(b.path.at(-1)!)
|
|
|
+ })
|
|
|
+ for (const child of node.children) {
|
|
|
+ sort(child)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sort(root)
|
|
|
+
|
|
|
+ let current = [root]
|
|
|
+ const result: Node = {
|
|
|
+ path: [],
|
|
|
+ children: [],
|
|
|
+ }
|
|
|
+
|
|
|
+ let processed = 0
|
|
|
+ const limit = input.limit ?? 50
|
|
|
+ while (current.length > 0) {
|
|
|
+ const next = []
|
|
|
+ for (const node of current) {
|
|
|
+ if (node.children.length) next.push(...node.children)
|
|
|
+ }
|
|
|
+ const max = Math.max(...current.map((x) => x.children.length))
|
|
|
+ for (let i = 0; i < max && processed < limit; i++) {
|
|
|
+ for (const node of current) {
|
|
|
+ const child = node.children[i]
|
|
|
+ if (!child) continue
|
|
|
+ getPath(result, child.path, true)
|
|
|
+ processed++
|
|
|
+ if (processed >= limit) break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (processed >= limit) {
|
|
|
+ for (const node of [...current, ...next]) {
|
|
|
+ const compare = getPath(result, node.path, false)
|
|
|
+ if (!compare) continue
|
|
|
+ if (compare?.children.length !== node.children.length) {
|
|
|
+ const diff = node.children.length - compare.children.length
|
|
|
+ compare.children.push({
|
|
|
+ path: compare.path.concat(`[${diff} truncated]`),
|
|
|
+ children: [],
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break
|
|
|
+ }
|
|
|
+ current = next
|
|
|
+ }
|
|
|
+
|
|
|
+ const lines: string[] = []
|
|
|
+
|
|
|
+ function render(node: Node, depth: number) {
|
|
|
+ const indent = "\t".repeat(depth)
|
|
|
+ lines.push(indent + node.path.at(-1) + (node.children.length ? "/" : ""))
|
|
|
+ for (const child of node.children) {
|
|
|
+ render(child, depth + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ result.children.map((x) => render(x, 0))
|
|
|
+
|
|
|
+ return lines.join("\n")
|
|
|
+ }
|
|
|
}
|