Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
235 lines
8.9 KiB
TypeScript
235 lines
8.9 KiB
TypeScript
export * as FileSystemSearch from "./search"
|
|
|
|
import { makeLocationNode } from "../effect/app-node"
|
|
import path from "path"
|
|
import { Context, Effect, Layer, Scope } from "effect"
|
|
import { Fff } from "#fff"
|
|
import fuzzysort from "fuzzysort"
|
|
import { FileSystem } from "../filesystem"
|
|
import { FSUtil } from "../fs-util"
|
|
import { Location } from "../location"
|
|
import { Ripgrep } from "../ripgrep"
|
|
import { RelativePath } from "../schema"
|
|
import { Flag } from "../flag/flag"
|
|
|
|
export interface Interface {
|
|
readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
|
|
readonly glob: (input: FileSystem.GlobInput) => Effect.Effect<readonly FileSystem.Entry[]>
|
|
readonly grep: (input: FileSystem.GrepInput) => Effect.Effect<readonly FileSystem.Match[]>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {}
|
|
|
|
export const ripgrepLayer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const fs = yield* FSUtil.Service
|
|
const location = yield* Location.Service
|
|
const ripgrep = yield* Ripgrep.Service
|
|
const scope = yield* Scope.Scope
|
|
const state = {
|
|
files: [] as string[],
|
|
directories: [] as string[],
|
|
}
|
|
const directories = new Set<string>()
|
|
yield* ripgrep
|
|
.find({
|
|
cwd: location.directory,
|
|
pattern: "*",
|
|
limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000,
|
|
onEntry: (entry) =>
|
|
Effect.sync(() => {
|
|
state.files.push(entry.path)
|
|
const parts = entry.path.split("/")
|
|
parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep))
|
|
state.directories = Array.from(directories)
|
|
}),
|
|
})
|
|
.pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope))
|
|
return Service.of({
|
|
glob: (input) =>
|
|
Effect.gen(function* () {
|
|
const target = path.resolve(location.directory, input.path ?? ".")
|
|
const info = yield* fs.stat(target).pipe(Effect.orDie)
|
|
const cwd = info.type === "File" ? path.dirname(target) : target
|
|
return yield* ripgrep
|
|
.glob({
|
|
cwd,
|
|
pattern: input.pattern,
|
|
limit: input.limit ?? Number.MAX_SAFE_INTEGER,
|
|
})
|
|
.pipe(
|
|
Effect.map((result) =>
|
|
result.map((entry) =>
|
|
FileSystem.Entry.make({
|
|
...entry,
|
|
path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))),
|
|
}),
|
|
),
|
|
),
|
|
Effect.orDie,
|
|
)
|
|
}),
|
|
grep: (input) =>
|
|
Effect.gen(function* () {
|
|
const target = path.resolve(location.directory, input.path ?? ".")
|
|
const info = yield* fs.stat(target).pipe(Effect.orDie)
|
|
const cwd = info.type === "File" ? path.dirname(target) : target
|
|
return yield* ripgrep
|
|
.grep({
|
|
cwd,
|
|
pattern: input.pattern,
|
|
file: info.type === "File" ? path.basename(target) : undefined,
|
|
include: input.include,
|
|
limit: input.limit ?? Number.MAX_SAFE_INTEGER,
|
|
})
|
|
.pipe(
|
|
Effect.map((result) =>
|
|
result.map((match) =>
|
|
FileSystem.Match.make({
|
|
...match,
|
|
entry: FileSystem.Entry.make({
|
|
...match.entry,
|
|
path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))),
|
|
}),
|
|
}),
|
|
),
|
|
),
|
|
Effect.orDie,
|
|
)
|
|
}),
|
|
find: (input) =>
|
|
Effect.gen(function* () {
|
|
const items =
|
|
input.type === "file"
|
|
? state.files
|
|
: input.type === "directory"
|
|
? state.directories
|
|
: [...state.files, ...state.directories]
|
|
return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => {
|
|
const relative = item.target
|
|
const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const)
|
|
return FileSystem.Entry.make({
|
|
path: RelativePath.make(relative),
|
|
type,
|
|
})
|
|
})
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const fffLayer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const location = yield* Location.Service
|
|
const result = yield* Effect.try({
|
|
try: () =>
|
|
Fff.create({
|
|
basePath: location.directory,
|
|
aiMode: true,
|
|
}),
|
|
catch: (cause) => cause,
|
|
}).pipe(
|
|
Effect.catch((error) => Effect.logWarning("failed to initialize fff", { error }).pipe(Effect.as(undefined))),
|
|
)
|
|
if (!result?.ok) {
|
|
if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error })
|
|
return Service.of({
|
|
find: () => Effect.succeed([]),
|
|
glob: () => Effect.succeed([]),
|
|
grep: () => Effect.succeed([]),
|
|
})
|
|
}
|
|
yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore))
|
|
return Service.of({
|
|
glob: (input) =>
|
|
Effect.sync(() => {
|
|
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
|
const found = result.value.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, {
|
|
pageIndex: 0,
|
|
pageSize: input.limit,
|
|
})
|
|
if (!found.ok) throw found.error
|
|
return found.value.items.map((item) =>
|
|
FileSystem.Entry.make({
|
|
path: RelativePath.make(item.relativePath.replaceAll("\\", "/")),
|
|
type: "file",
|
|
}),
|
|
)
|
|
}),
|
|
grep: (input) =>
|
|
Effect.sync(() => {
|
|
const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "")
|
|
const found = result.value.grep(
|
|
[prefix ? `${prefix}/**` : undefined, input.include, input.pattern]
|
|
.filter((value) => value !== undefined)
|
|
.join(" "),
|
|
{ mode: "regex", pageSize: input.limit, timeBudgetMs: 1_500 },
|
|
)
|
|
if (!found.ok) throw found.error
|
|
return found.value.items.map((match) => {
|
|
const bytes = Buffer.from(match.lineContent)
|
|
return FileSystem.Match.make({
|
|
entry: FileSystem.Entry.make({
|
|
path: RelativePath.make(match.relativePath.replaceAll("\\", "/")),
|
|
type: "file",
|
|
}),
|
|
line: match.lineNumber,
|
|
offset: match.byteOffset,
|
|
text: match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent,
|
|
submatches: match.matchRanges.map(([start, end]) => ({
|
|
text: bytes.subarray(start, end).toString("utf8"),
|
|
start,
|
|
end,
|
|
})),
|
|
})
|
|
})
|
|
}),
|
|
find: (input) =>
|
|
Effect.sync(() => {
|
|
const options = { pageIndex: 0, pageSize: input.limit ?? 50 }
|
|
const items = (() => {
|
|
if (input.type === "file") {
|
|
const found = result.value.fileSearch(input.query.trim(), options)
|
|
if (!found.ok) throw found.error
|
|
return found.value.items.map((item, index) => ({
|
|
path: item.relativePath,
|
|
type: "file" as const,
|
|
score: found.value.scores[index]?.total ?? 0,
|
|
}))
|
|
}
|
|
if (input.type === "directory") {
|
|
const found = result.value.directorySearch(input.query.trim(), options)
|
|
if (!found.ok) throw found.error
|
|
return found.value.items.map((item, index) => ({
|
|
path: item.relativePath,
|
|
type: "directory" as const,
|
|
score: found.value.scores[index]?.total ?? 0,
|
|
}))
|
|
}
|
|
const found = result.value.mixedSearch(input.query.trim(), options)
|
|
if (!found.ok) throw found.error
|
|
return found.value.items.map((item, index) => ({
|
|
path: item.item.relativePath,
|
|
type: item.type,
|
|
score: found.value.scores[index]?.total ?? 0,
|
|
}))
|
|
})()
|
|
return items
|
|
.sort((a, b) => b.score - a.score || a.path.length - b.path.length)
|
|
.map((item) => {
|
|
const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "")
|
|
return FileSystem.Entry.make({
|
|
path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")),
|
|
type: item.type,
|
|
})
|
|
})
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
const layer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)))
|
|
|
|
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })
|