Просмотр исходного кода

fix(app): stack overflow in filetree (#13667)

Co-authored-by: adamelmore <[email protected]>
Adam 2 месяцев назад
Родитель
Сommit
460a87f359
1 измененных файлов с 66 добавлено и 28 удалено
  1. 66 28
      packages/app/src/components/file-tree.tsx

+ 66 - 28
packages/app/src/components/file-tree.tsx

@@ -21,6 +21,8 @@ import {
 import { Dynamic } from "solid-js/web"
 import type { FileNode } from "@opencode-ai/sdk/v2"
 
+const MAX_DEPTH = 128
+
 function pathToFileUrl(filepath: string): string {
   return `file://${encodeFilePath(filepath)}`
 }
@@ -260,12 +262,20 @@ export default function FileTree(props: {
   _marks?: Set<string>
   _deeps?: Map<string, number>
   _kinds?: ReadonlyMap<string, Kind>
+  _chain?: readonly string[]
 }) {
   const file = useFile()
   const level = props.level ?? 0
   const draggable = () => props.draggable ?? true
   const tooltip = () => props.tooltip ?? true
 
+  const key = (p: string) =>
+    file
+      .normalize(p)
+      .replace(/[\\/]+$/, "")
+      .replaceAll("\\", "/")
+  const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)]
+
   const filter = createMemo(() => {
     if (props._filter) return props._filter
 
@@ -307,23 +317,45 @@ export default function FileTree(props: {
 
     const out = new Map<string, number>()
 
-    const visit = (dir: string, lvl: number): number => {
-      const expanded = file.tree.state(dir)?.expanded ?? false
-      if (!expanded) return -1
+    const root = props.path
+    if (!(file.tree.state(root)?.expanded ?? false)) return out
+
+    const seen = new Set<string>()
+    const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = []
+
+    const push = (dir: string, lvl: number) => {
+      const id = key(dir)
+      if (seen.has(id)) return
+      seen.add(id)
 
-      const nodes = file.tree.children(dir)
-      const max = nodes.reduce((max, node) => {
-        if (node.type !== "directory") return max
-        const open = file.tree.state(node.path)?.expanded ?? false
-        if (!open) return max
-        return Math.max(max, visit(node.path, lvl + 1))
-      }, lvl)
+      const kids = file.tree
+        .children(dir)
+        .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false))
+        .map((node) => node.path)
 
-      out.set(dir, max)
-      return max
+      stack.push({ dir, lvl, i: 0, kids, max: lvl })
+    }
+
+    push(root, level - 1)
+
+    while (stack.length > 0) {
+      const top = stack[stack.length - 1]!
+
+      if (top.i < top.kids.length) {
+        const next = top.kids[top.i]!
+        top.i++
+        push(next, top.lvl + 1)
+        continue
+      }
+
+      out.set(top.dir, top.max)
+      stack.pop()
+
+      const parent = stack[stack.length - 1]
+      if (!parent) continue
+      parent.max = Math.max(parent.max, top.max)
     }
 
-    visit(props.path, level - 1)
     return out
   })
 
@@ -459,21 +491,27 @@ export default function FileTree(props: {
                       }}
                       style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
                     />
-                    <FileTree
-                      path={node.path}
-                      level={level + 1}
-                      allowed={props.allowed}
-                      modified={props.modified}
-                      kinds={props.kinds}
-                      active={props.active}
-                      draggable={props.draggable}
-                      tooltip={props.tooltip}
-                      onFileClick={props.onFileClick}
-                      _filter={filter()}
-                      _marks={marks()}
-                      _deeps={deeps()}
-                      _kinds={kinds()}
-                    />
+                    <Show
+                      when={level < MAX_DEPTH && !chain.includes(key(node.path))}
+                      fallback={<div class="px-2 py-1 text-12-regular text-text-weak">...</div>}
+                    >
+                      <FileTree
+                        path={node.path}
+                        level={level + 1}
+                        allowed={props.allowed}
+                        modified={props.modified}
+                        kinds={props.kinds}
+                        active={props.active}
+                        draggable={props.draggable}
+                        tooltip={props.tooltip}
+                        onFileClick={props.onFileClick}
+                        _filter={filter()}
+                        _marks={marks()}
+                        _deeps={deeps()}
+                        _kinds={kinds()}
+                        _chain={chain}
+                      />
+                    </Show>
                   </Collapsible.Content>
                 </Collapsible>
               </Match>