Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/src/components/dialog-select-file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export function DialogSelectFile(props: {
items={items}
key={(item) => item.id}
filterKeys={["title", "description", "category"]}
skipFilter={(item) => item.type === "file"}
groupBy={grouped() ? (item) => item.category : () => ""}
onMove={handleMove}
onSelect={handleSelect}
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
},
key: atKey,
filterKeys: ["display"],
skipFilter: (item) => item.type === "file" && !item.recent,
groupBy: (item) => {
if (item.type === "agent") return "agent"
if (item.recent) return "recent"
Expand Down
34 changes: 15 additions & 19 deletions packages/core/src/filesystem/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,12 @@ function item(hit: Fff.Hit): Item {
}

function collectPaths<T>(
out: { items: T[]; scores: Array<{ total: number }> },
items: T[],
toPath: (item: T) => string,
opts?: { includeZeroScore?: boolean },
): string[] {
return Array.from(
new Set(
out.items.flatMap((item, idx): string[] => {
const score = out.scores[idx]
if (!score || (!opts?.includeZeroScore && score.total <= 0)) return []
items.flatMap((item): string[] => {
const text = toPath(item)
if (!text) return []
return [text]
Expand All @@ -162,22 +159,22 @@ function searchFff(
if (!out.ok) return out
return {
ok: true,
value: collectPaths(out.value, (entry) => normalize(entry.relativePath), { includeZeroScore: !query }),
value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
}
}
if (kind === "all") {
const out = pick.mixedSearch(query, opts)
if (!out.ok) return out
return {
ok: true,
value: collectPaths(out.value, (entry) => normalize(entry.item.relativePath), { includeZeroScore: !query }),
value: collectPaths(out.value.items, (entry) => normalize(entry.item.relativePath)),
}
}
const out = pick.fileSearch(query, opts)
if (!out.ok) return out
return {
ok: true,
value: collectPaths(out.value, (entry) => normalize(entry.relativePath), { includeZeroScore: !query }),
value: collectPaths(out.value.items, (entry) => normalize(entry.relativePath)),
}
}

Expand Down Expand Up @@ -241,6 +238,13 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
// remove before process exit, so use ripgrep instead of native FFF there.
if (process.env.OPENCODE_TEST_HOME) return undefined

const dir = FSUtil.resolve(cwd)
const existing = state.pick.get(dir)
if (existing) return existing

const pending = state.wait.get(dir)
if (pending) return yield* Deferred.await(pending)

const available = yield* fffSync("check availability", () => Fff.available()).pipe(
Effect.catch((error) => {
log.warn("fff availability check failed", { error })
Expand All @@ -249,13 +253,6 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
)
if (!available) return undefined

const dir = FSUtil.resolve(cwd)
const existing = state.pick.get(dir)
if (existing) return existing

const pending = state.wait.get(dir)
if (pending) return yield* Deferred.await(pending)

const gate = yield* Deferred.make<Picker, Error>()
state.wait.set(dir, gate)
return yield* Effect.gen(function* () {
Expand Down Expand Up @@ -353,13 +350,12 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Ripgrep.Service
const query = input.query.trim()
const kind = input.kind ?? "file"

const pick = yield* picker(input.cwd)
if (!pick) return undefined

const entry = yield* acquire(input.cwd).pipe(Effect.catch(() => Effect.succeed<Picker | undefined>(undefined)))
if (!entry) return undefined
const dir = FSUtil.resolve(input.cwd)
const limit = input.limit ?? 100
const fffResult = yield* fffSync(`${kind} search`, () =>
searchFff(pick, kind, query, {
searchFff(entry.pick, kind, query, {
pageIndex: 0,
currentFile: input.current, // supports both relative and absolute (relative preferred)
pageSize: limit,
Expand Down
28 changes: 14 additions & 14 deletions packages/opencode/src/cli/cmd/run/footer.prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,19 +368,6 @@ export function createPromptState(input: PromptInput): PromptState {
const next = extractLineRange(value)
const list = await input.findFiles(next.base)
return list
.sort((a, b) => {
const dir = Number(b.endsWith("/")) - Number(a.endsWith("/"))
if (dir !== 0) {
return dir
}

const depth = a.split("/").length - b.split("/").length
if (depth !== 0) {
return depth
}

return a.localeCompare(b)
})
.map((item): Auto => {
const url = pathToFileURL(path.resolve(input.directory, item))
let filename = item
Expand Down Expand Up @@ -472,8 +459,21 @@ export function createPromptState(input: PromptInput): PromptState {
return mixed
}

const next = removeLineRange(query())
if (mode() === "mention") {
return [
...fuzzysort
.go(next, agents(), { keys: ["value", "display", "description"] })
.map((item) => item.obj),
...files(),
...fuzzysort
.go(next, resources(), { keys: ["value", "display", "description"] })
.map((item) => item.obj),
]
}

return fuzzysort
.go(removeLineRange(query()), mixed, {
.go(next, mixed, {
keys: [(item) => (item.kind === "mention" ? item.value : item.name).trimEnd(), "display", "description"],
})
.map((item) => item.obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Log } from "@opencode-ai/core/util/log"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import path from "path"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"

const log = Log.create({ service: "server.file" })

export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () {
const ripgrep = yield* Ripgrep.Service
Expand All @@ -34,11 +37,23 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const directory = (yield* InstanceState.context).directory
const limit = ctx.query.limit ?? 10
const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
const started = performance.now()
// Prefer fff (frecency + fuzzy ranking) and trust its ordering. Fall back
// to the ripgrep-backed FileSystem.find when fff is unavailable.
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
if (fff !== undefined) return fff
return (yield* filesystem(
if (fff !== undefined) {
log.info("find file", {
engine: "fff",
query: ctx.query.query,
kind,
directory,
limit,
results: fff.length,
duration: Math.round(performance.now() - started),
})
return fff
}
const fallback = (yield* filesystem(
FileSystem.Service.use((fs) =>
fs.find({
query: ctx.query.query,
Expand All @@ -47,6 +62,16 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
}),
),
)).map((item) => item.path)
log.info("find file", {
engine: "ripgrep",
query: ctx.query.query,
kind,
directory,
limit,
results: fallback.length,
duration: Math.round(performance.now() - started),
})
return fallback
})

const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/line-comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
},
key: (item) => item.path,
filterKeys: ["path"],
skipFilter: () => true,
onSelect: selectMention,
})

Expand Down
13 changes: 9 additions & 4 deletions packages/ui/src/hooks/use-filtered-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface FilteredListProps<T> {
groupBy?: (x: T) => string
sortBy?: (a: T, b: T) => number
sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
skipFilter?: (item: T) => boolean
onSelect?: (value: T | undefined, index: number) => void
noInitialSelection?: boolean
}
Expand All @@ -35,10 +36,14 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
all,
(x) => {
if (!needle) return x
if (!props.filterKeys && Array.isArray(x) && x.every((e) => typeof e === "string")) {
return fuzzysort.go(needle, x).map((x) => x.target) as T[]
}
return fuzzysort.go(needle, x, { keys: props.filterKeys! }).map((x) => x.obj)
const skipFilter = props.skipFilter
const filterable = skipFilter ? x.filter((item) => !skipFilter(item)) : x
const skipped = skipFilter ? x.filter(skipFilter) : []
const filtered =
!props.filterKeys && Array.isArray(filterable) && filterable.every((e) => typeof e === "string")
? (fuzzysort.go(needle, filterable).map((x) => x.target) as T[])
: fuzzysort.go(needle, filterable, { keys: props.filterKeys! }).map((x) => x.obj)
return skipped.length ? [...filtered, ...skipped] : filtered
},
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
entries(),
Expand Down
Loading