Browse Source

fix(app): allow adding projects from any depth

Adam 1 month ago
parent
commit
972cb01d5c
1 changed files with 75 additions and 36 deletions
  1. 75 36
      packages/app/src/components/dialog-select-directory.tsx

+ 75 - 36
packages/app/src/components/dialog-select-directory.tsx

@@ -25,6 +25,8 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
 
   const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
 
+  const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
+
   function normalize(input: string) {
     const v = input.replaceAll("\\", "/")
     if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
@@ -40,6 +42,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   function trimTrailing(input: string) {
     const v = normalizeDriveRoot(input)
     if (v === "/") return v
+    if (v === "//") return v
     if (/^[A-Za-z]:\/$/.test(v)) return v
     return v.replace(/\/+$/, "")
   }
@@ -61,12 +64,6 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
     return ""
   }
 
-  function isRoot(input: string) {
-    const v = trimTrailing(input)
-    if (v === "/") return true
-    return /^[A-Za-z]:\/$/.test(v)
-  }
-
   function display(path: string) {
     const full = trimTrailing(path)
     const h = home()
@@ -80,53 +77,95 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
     return full
   }
 
-  function parse(filter: string) {
+  function scoped(filter: string) {
     const base = start()
     if (!base) return
 
     const raw = normalizeDriveRoot(filter.trim())
-    if (!raw) return { directory: trimTrailing(base), query: "" }
+    if (!raw) return { directory: trimTrailing(base), path: "" }
 
     const h = home()
-    const expanded = raw === "~" ? h : raw.startsWith("~/") ? (h ? join(h, raw.slice(2)) : raw.slice(2)) : raw
-    const absolute = rootOf(expanded) ? expanded : join(base, expanded)
-    const abs = normalizeDriveRoot(absolute)
+    if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" }
+    if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) }
 
-    if (abs.endsWith("/")) return { directory: trimTrailing(abs), query: "" }
-    const i = abs.lastIndexOf("/")
-    if (i === -1) return { directory: trimTrailing(base), query: abs }
-
-    const dir = i === 0 ? "/" : /^[A-Za-z]:$/.test(abs.slice(0, i)) ? abs.slice(0, i) + "/" : abs.slice(0, i)
-    return { directory: trimTrailing(dir), query: abs.slice(i + 1) }
+    const root = rootOf(raw)
+    if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
+    return { directory: trimTrailing(base), path: raw }
   }
 
-  async function fetchDirs(input: { directory: string; query: string }) {
-    if (isRoot(input.directory)) {
-      const nodes = await sdk.client.file
-        .list({ directory: input.directory, path: "" })
-        .then((x) => x.data ?? [])
-        .catch(() => [])
+  async function dirs(dir: string) {
+    const key = trimTrailing(dir)
+    const existing = cache.get(key)
+    if (existing) return existing
 
-      const dirs = nodes.filter((n) => n.type === "directory").map((n) => n.name)
-      const sorted = input.query
-        ? fuzzysort.go(input.query, dirs, { limit: 50 }).map((x) => x.target)
-        : dirs.slice().sort((a, b) => a.localeCompare(b))
-
-      return sorted.slice(0, 50).map((name) => join(input.directory, name))
-    }
-
-    const results = await sdk.client.find
-      .files({ directory: input.directory, query: input.query, type: "directory", limit: 50 })
+    const request = sdk.client.file
+      .list({ directory: key, path: "" })
       .then((x) => x.data ?? [])
       .catch(() => [])
+      .then((nodes) =>
+        nodes
+          .filter((n) => n.type === "directory")
+          .map((n) => ({
+            name: n.name,
+            absolute: trimTrailing(normalizeDriveRoot(n.absolute)),
+          })),
+      )
+
+    cache.set(key, request)
+    return request
+  }
 
-    return results.map((rel) => join(input.directory, rel))
+  async function match(dir: string, query: string, limit: number) {
+    const items = await dirs(dir)
+    if (!query) return items.slice(0, limit).map((x) => x.absolute)
+    return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
   }
 
   const directories = async (filter: string) => {
-    const input = parse(filter)
+    const input = scoped(filter)
     if (!input) return [] as string[]
-    return fetchDirs(input)
+
+    const raw = normalizeDriveRoot(filter.trim())
+    const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
+
+    const query = normalizeDriveRoot(input.path)
+
+    if (!isPath) {
+      const results = await sdk.client.find
+        .files({ directory: input.directory, query, type: "directory", limit: 50 })
+        .then((x) => x.data ?? [])
+        .catch(() => [])
+
+      return results.map((rel) => join(input.directory, rel)).slice(0, 50)
+    }
+
+    const segments = query.replace(/^\/+/, "").split("/")
+    const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".")
+    const tail = segments[segments.length - 1] ?? ""
+
+    const cap = 12
+    const branch = 4
+    let paths = [input.directory]
+    for (const part of head) {
+      if (part === "..") {
+        paths = paths.map((p) => {
+          const v = trimTrailing(p)
+          if (v === "/") return v
+          if (/^[A-Za-z]:\/$/.test(v)) return v
+          const i = v.lastIndexOf("/")
+          if (i <= 0) return "/"
+          return v.slice(0, i)
+        })
+        continue
+      }
+
+      const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
+      paths = Array.from(new Set(next)).slice(0, cap)
+      if (paths.length === 0) return [] as string[]
+    }
+
+    const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
+    return Array.from(new Set(out)).slice(0, 50)
   }
 
   function resolve(absolute: string) {