Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439export function treeEntries(parent: string, nodes: ReadonlyArray<{ name: string; type: "file" | "directory" }>) { const prefix = parent.replace(/^\/+|\/+$/g, "") return nodes.map((node) => { const path = prefix ? `${prefix}/${node.name}` : node.name return node.type === "directory" ? path + "/" : path })}
export function pickerTreeEntries( parent: string, nodes: ReadonlyArray<{ name: string; type: "file" | "directory" }>, mode: "directory" | "file",) { return treeEntries(parent, mode === "directory" ? nodes.filter((node) => node.type === "directory") : nodes)}
export function pickerSearchEntries<T extends { type: "file" | "directory" }>( nodes: readonly T[], mode: "directory" | "file",) { return mode === "directory" ? nodes.filter((node) => node.type === "directory") : [...nodes]}
export function pickerMode(mode: "directory" | "file", base?: string) { if (mode === "file") { return { includeFiles: true, action: "file" as const, entries(parent: string, nodes: ReadonlyArray<{ name: string; type: "file" | "directory" }>) { return treeEntries(parent, nodes) }, navigation(path: string) { return treePathWithin(base, path) ? path : undefined }, result(root: string, selected: string) { return selected || undefined }, selection(root: string, path: string) { if (!treePathWithin(base, root)) return return selectedTreePath(root, path, "file", base) }, } } return { includeFiles: false, action: "directory" as const, entries(parent: string, nodes: ReadonlyArray<{ name: string; type: "file" | "directory" }>) { return treeEntries( parent, nodes.filter((node) => node.type === "directory"), ) }, navigation(path: string) { return path }, result(root: string, selected: string, valid = true) { if (!valid) return return selected || (root ? nativePickerPath(root) : undefined) }, selection(root: string, path: string) { return selectedTreePath(root, path, "directory") }, }}
export function pickerFileSearchQuery(root: string, input: string, home: string) { const value = input .replace(/\\/g, "/") .replace(/^~(?=\/|$)/, home) .replace(/\/+$/, "") const base = root.replace(/\\/g, "/").replace(/\/+$/, "") if (value === base) return "" if (value.startsWith(base + "/")) return value.slice(base.length + 1) return value}
export function pickerAbsoluteInput(input: string, home: string, current: string) { const value = normalizePickerDrive(input).replace(/^~(?=\/|$)/, normalizePickerDrive(home)) return pickerAbsolutePath(value, current)}
export function pickerAbsolutePath(input: string, current: string) { const value = normalizePickerDrive(input) const absolute = pickerRoot(value) ? value : joinPickerPath(current, value) return canonicalPickerPath(absolute)}
export function treePathWithin(base: string | undefined, path: string) { return pickerRelativePath(base, path) !== undefined}
export function canonicalPickerPath(path: string) { const value = normalizePickerDrive(path) const root = pickerRoot(value) const parts = value.slice(root.length).split("/") const resolved = parts.reduce<string[]>((output, part) => { if (!part || part === ".") return output if (part === "..") { output.pop() return output } output.push(part) return output }, []) return joinPickerPath(root, resolved.join("/"))}
export function pickerRelativePath(base: string | undefined, path: string) { if (!base) return const rootPath = canonicalPickerPath(base) const targetPath = canonicalPickerPath(path) const insensitive = /^[A-Za-z]:\//.test(rootPath) || rootPath.startsWith("//") const root = insensitive ? rootPath.toLowerCase() : rootPath const target = insensitive ? targetPath.toLowerCase() : targetPath if (target === root) return "" const prefix = root.endsWith("/") ? root : root + "/" if (!target.startsWith(prefix)) return return targetPath.slice(prefix.length)}
export function currentPickerSuggestions<T>(result: { query: string; items: readonly T[] } | undefined, query: string) { if (result?.query !== query) return [] return result.items}
export function preloadTreeDirectories( parent: string, nodes: ReadonlyArray<{ name: string; type: "file" | "directory" }>,) { return treeEntries( parent, nodes.filter((node) => node.type === "directory"), )}
export function advanceTreePreload(advanced: Set<string>, path: string) { if (advanced.has(path)) return false advanced.add(path) return true}
export function activeTreeNavigation(request: number, current: number) { return request === current}
export function createPriorityTaskQueue<T>(concurrency: number) { type Job = { key: string priority: "user" | "background" promise: Promise<T> run: () => void }
const jobs = new Map<string, Job>() const user: Job[] = [] const background: Job[] = [] let active = 0
const drain = () => { while (active < concurrency) { const job = user.pop() ?? background.shift() if (!job) return active++ job.run() } }
const schedule = (key: string, priority: Job["priority"], task: () => Promise<T>) => { const existing = jobs.get(key) if (existing) { if (priority === "user") promote(key) return existing.promise }
const deferred = Promise.withResolvers<T>() const job: Job = { key, priority, promise: deferred.promise, run: () => { const complete = () => { active-- jobs.delete(key) drain() } Promise.resolve() .then(task) .then( (value) => { complete() deferred.resolve(value) }, (error) => { complete() deferred.reject(error) }, ) }, } jobs.set(key, job) ;(priority === "user" ? user : background).push(job) drain() return job.promise }
const promote = (key: string) => { const job = jobs.get(key) if (!job || job.priority === "user") return const index = background.indexOf(job) if (index === -1) return background.splice(index, 1) job.priority = "user" user.push(job) }
return { schedule, promote }}
export function nextTreeScrollTop(current: number, delta: number, scrollHeight: number, clientHeight: number) { return Math.min(Math.max(0, scrollHeight - clientHeight), Math.max(0, current + delta))}
export function nextSuggestionIndex(current: number, delta: -1 | 1, count: number) { if (count === 0) return -1 return (current + delta + count) % count}
export function absoluteTreePath(root: string, path: string) { const base = trimPickerPath(root) const relative = path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "") if (!relative) return base || "/" if (!base || base === "/") return "/" + relative if (base.endsWith("/")) return base + relative return `${base}/${relative}`}
export function selectedTreePath(root: string, path: string, mode: "directory" | "file", base?: string) { const directory = path.endsWith("/") if (mode === "file") { if (directory) return if (!base) return path const absolute = absoluteTreePath(root, path) return pickerRelativePath(base, absolute) } return directory ? nativePickerPath(absoluteTreePath(root, path)) : undefined}
export function nativePickerPath(path: string) { const value = trimPickerPath(path) if (/^[A-Za-z]:\//.test(value) || value.startsWith("//")) return value.replaceAll("/", "\\") return value}import { getFilename } from "@opencode/util/path"import fuzzysort from "fuzzysort"import { ServerSDK } from "@/runtime/server/client"
export function cleanPickerInput(value: string) { const first = (value ?? "").split(/\r?\n/)[0] ?? "" return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()}
export function normalizePickerPath(input: string) { const value = input.replaceAll("\\", "/") if (value.startsWith("//") && !value.startsWith("///")) return "//" + value.slice(2).replace(/\/+/g, "/") return value.replace(/\/+/g, "/")}
export function normalizePickerDrive(input: string) { const value = normalizePickerPath(input) if (/^[A-Za-z]:$/.test(value)) return value + "/" return value}
export function trimPickerPath(input: string) { const value = normalizePickerDrive(input) if (value === "/" || value === "//" || /^[A-Za-z]:\/$/.test(value)) return value return value.replace(/\/+$/, "")}
export function joinPickerPath(base: string | undefined, relative: string) { const root = trimPickerPath(base ?? "") const path = trimPickerPath(relative).replace(/^\/+/, "") if (!root) return path if (!path) return root if (root.endsWith("/")) return root + path return root + "/" + path}
export function pickerRoot(input: string) { const value = normalizePickerDrive(input) if (value.startsWith("//")) { const [server, share] = value.slice(2).split("/") if (server && share) return `//${server}/${share}` return "//" } if (value.startsWith("/")) return "/" if (/^[A-Za-z]:\//.test(value)) return value.slice(0, 3) return ""}
export function pickerParent(input: string) { const value = trimPickerPath(input) const root = pickerRoot(value) if (value === root) return value if (value === "/" || value === "//" || /^[A-Za-z]:\/$/.test(value)) return value const index = value.lastIndexOf("/") if (index < root.length) return root if (index <= 0) return "/" if (index === 2 && /^[A-Za-z]:/.test(value)) return value.slice(0, 3) return value.slice(0, index)}
function pickerTilde(absolute: string, home: string) { const path = trimPickerPath(absolute) if (!home) return "" const root = trimPickerPath(home) if (/^[A-Za-z]:\//.test(root)) return "" if (path === root) return "~" if (path.startsWith(root + "/")) return "~" + path.slice(root.length) return ""}
export function displayPickerPath(path: string, input: string, home: string) { const value = trimPickerPath(path) if (/^[A-Za-z]:\//.test(trimPickerPath(home)) || /^[A-Za-z]:\//.test(value)) return value.replaceAll("/", "\\") return pickerTilde(value, home) || value}
export async function listPickerDirectory( sdk: ServerSDK, location: { directory: string; workspace?: string }, directory: string,) { const result = await sdk.api.file.list({ location, path: directory }) return result.data.map((entry) => { const absolute = pickerAbsolutePath(entry.path, result.location.directory) return { name: getFilename(absolute), type: entry.type, absolute } })}
export function createDirectorySearch(args: { sdk: ServerSDK location: () => { directory: string; workspace?: string } | undefined base: () => string | undefined home: () => string}) { const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>() let current = 0
const scoped = (value: string) => { const raw = normalizePickerDrive(value) const root = pickerRoot(raw) if (root) return { directory: trimPickerPath(root), path: raw.slice(root.length) } const base = args.base() if (!base) return if (!raw) return { directory: trimPickerPath(base), path: "" } const home = args.home() if (raw === "~") return { directory: trimPickerPath(home || base), path: "" } if (raw.startsWith("~/")) return { directory: trimPickerPath(home || base), path: raw.slice(2) } return { directory: trimPickerPath(base), path: raw } }
const directories = async (directory: string) => { const location = args.location() if (!location) return [] const key = JSON.stringify([location, trimPickerPath(directory)]) const existing = cache.get(key) if (existing) return existing const request = listPickerDirectory(args.sdk, location, directory) .catch(() => []) .then((nodes) => nodes.filter((node) => node.type === "directory")) cache.set(key, request) return request }
const match = async (directory: string, query: string, limit: number) => { const items = await directories(directory) if (!query) return items.slice(0, limit).map((item) => item.absolute) return fuzzysort.go(query, items, { key: "name", limit }).map((item) => item.obj.absolute) }
return async (filter: string) => { const token = ++current const active = () => token === current const value = cleanPickerInput(filter) const input = scoped(value) const location = args.location() if (!input || !location) return [] as string[] const raw = normalizePickerDrive(value) const pathInput = raw.startsWith("~") || !!pickerRoot(raw) || raw.includes("/") const query = normalizePickerDrive(input.path) if (!pathInput) { const relative = pickerRelativePath(location.directory, input.directory) const results = relative === undefined ? [] : await args.sdk.api.file .find({ location, query: joinPickerPath(relative, query), type: "directory", limit: 50 }) .then((result) => result.data .map((entry) => pickerAbsolutePath(entry.path, result.location.directory)) .filter((path) => treePathWithin(input.directory, path)), ) .catch(() => []) if (!active()) return [] if (results.length) { return results.slice(0, 50) } const fallback = query ? await match(input.directory, query, 50) : (await directories(input.directory)).map((item) => item.absolute) if (!active()) return [] return fallback } const segments = query.replace(/^\/+/, "").split("/") const head = segments.slice(0, -1).filter((part) => part && part !== ".") const tail = segments.at(-1) ?? "" let paths = [input.directory] for (const part of head) { if (!active()) return [] if (part === "..") { paths = paths.map(pickerParent) continue } paths = Array.from(new Set((await Promise.all(paths.map((path) => match(path, part, 4)))).flat())).slice(0, 12) if (!active() || paths.length === 0) return [] } const matches = Array.from(new Set((await Promise.all(paths.map((path) => match(path, tail, 50)))).flat())) if (!active()) return [] const base = raw.startsWith("~") ? trimPickerPath(input.directory) : "" if (raw.endsWith("/") || !tail) return Array.from(new Set([base, ...matches].filter(Boolean))).slice(0, 50) const target = matches.find((path) => getFilename(path).toLowerCase() === tail.toLowerCase()) if (!target) return matches.slice(0, 50) const children = await match(target, "", 30) if (!active()) return [] return Array.from(new Set([base, ...matches, ...children].filter(Boolean))).slice(0, 50) }}