fix(core): expose partial filesystem scan results
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
export * as FileSystemSearch from "./search"
|
export * as FileSystemSearch from "./search"
|
||||||
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Context, Effect, Fiber, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import { Fff } from "#fff"
|
import { Fff } from "#fff"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { FileSystem } from "../filesystem"
|
import { FileSystem } from "../filesystem"
|
||||||
@@ -28,22 +28,20 @@ export const ripgrepLayer = Layer.effect(
|
|||||||
const state = {
|
const state = {
|
||||||
files: [] as string[],
|
files: [] as string[],
|
||||||
directories: [] as string[],
|
directories: [] as string[],
|
||||||
scan: undefined as Fiber.Fiber<void, never> | undefined,
|
|
||||||
}
|
}
|
||||||
state.scan = yield* ripgrep.find({ cwd: location.directory, pattern: "*", limit: 100_000 }).pipe(
|
const directories = new Set<string>()
|
||||||
Effect.tap((result) =>
|
yield* ripgrep.find({
|
||||||
|
cwd: location.directory,
|
||||||
|
pattern: "*",
|
||||||
|
limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000,
|
||||||
|
onEntry: (entry) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
state.files = result.map((item) => item.path)
|
state.files.push(entry.path)
|
||||||
state.directories = Array.from(
|
const parts = entry.path.split("/")
|
||||||
new Set(
|
parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep))
|
||||||
state.files.flatMap((file) => {
|
state.directories = Array.from(directories)
|
||||||
const parts = file.split("/")
|
|
||||||
return parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join("/") + path.sep)
|
|
||||||
}),
|
}),
|
||||||
),
|
}).pipe(
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.asVoid,
|
Effect.asVoid,
|
||||||
Effect.forkIn(scope),
|
Effect.forkIn(scope),
|
||||||
@@ -104,7 +102,6 @@ export const ripgrepLayer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
find: (input) =>
|
find: (input) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (input.query) yield* Fiber.join(state.scan!)
|
|
||||||
const items =
|
const items =
|
||||||
input.type === "file"
|
input.type === "file"
|
||||||
? state.files
|
? state.files
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface FindInput {
|
|||||||
readonly hidden?: boolean
|
readonly hidden?: boolean
|
||||||
readonly follow?: boolean
|
readonly follow?: boolean
|
||||||
readonly signal?: AbortSignal
|
readonly signal?: AbortSignal
|
||||||
|
readonly onEntry?: (entry: Entry) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobInput {
|
export interface GlobInput {
|
||||||
@@ -102,6 +103,7 @@ export const layer = Layer.effect(
|
|||||||
readonly signal?: AbortSignal
|
readonly signal?: AbortSignal
|
||||||
readonly parse: (line: string) => Effect.Effect<A | undefined, Error>
|
readonly parse: (line: string) => Effect.Effect<A | undefined, Error>
|
||||||
readonly pattern?: string
|
readonly pattern?: string
|
||||||
|
readonly onItem?: (item: A) => Effect.Effect<void>
|
||||||
}) => {
|
}) => {
|
||||||
const program = Effect.scoped(
|
const program = Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -112,11 +114,16 @@ export const layer = Layer.effect(
|
|||||||
Effect.map((output) => output.buffer.toString("utf8")),
|
Effect.map((output) => output.buffer.toString("utf8")),
|
||||||
Effect.forkScoped,
|
Effect.forkScoped,
|
||||||
)
|
)
|
||||||
|
let observed = 0
|
||||||
const rows = yield* Stream.decodeText(handle.stdout).pipe(
|
const rows = yield* Stream.decodeText(handle.stdout).pipe(
|
||||||
Stream.splitLines,
|
Stream.splitLines,
|
||||||
Stream.filter((line) => line.length > 0),
|
Stream.filter((line) => line.length > 0),
|
||||||
Stream.mapEffect(input.parse),
|
Stream.mapEffect(input.parse),
|
||||||
Stream.filter((row): row is A => row !== undefined),
|
Stream.filter((row): row is A => row !== undefined),
|
||||||
|
Stream.tap((row) => {
|
||||||
|
if (!input.onItem || observed++ >= input.limit) return Effect.void
|
||||||
|
return input.onItem(row)
|
||||||
|
}),
|
||||||
Stream.take(input.limit + 1),
|
Stream.take(input.limit + 1),
|
||||||
Stream.runCollect,
|
Stream.runCollect,
|
||||||
Effect.map((chunk) => [...chunk]),
|
Effect.map((chunk) => [...chunk]),
|
||||||
@@ -181,7 +188,7 @@ export const layer = Layer.effect(
|
|||||||
Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
|
Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
|
||||||
),
|
),
|
||||||
find: (input) =>
|
find: (input) =>
|
||||||
run<string>({
|
run<Entry>({
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
signal: input.signal,
|
signal: input.signal,
|
||||||
@@ -194,24 +201,22 @@ export const layer = Layer.effect(
|
|||||||
`--glob=${input.pattern}`,
|
`--glob=${input.pattern}`,
|
||||||
".",
|
".",
|
||||||
],
|
],
|
||||||
parse: (line) =>
|
parse: (line) => {
|
||||||
Effect.succeed(
|
const relative = line
|
||||||
line
|
|
||||||
.replace(/^(?:\.[\\/])+/u, "")
|
.replace(/^(?:\.[\\/])+/u, "")
|
||||||
.replace(/^[\\/]+/u, "")
|
.replace(/^[\\/]+/u, "")
|
||||||
.replaceAll("\\", "/"),
|
.replaceAll("\\", "/")
|
||||||
),
|
return Effect.succeed(
|
||||||
}).pipe(
|
new Entry({
|
||||||
Effect.map((result) =>
|
|
||||||
result.items.map((relative) => {
|
|
||||||
const absolute = path.resolve(input.cwd, relative)
|
|
||||||
return new Entry({
|
|
||||||
path: RelativePath.make(relative),
|
path: RelativePath.make(relative),
|
||||||
type: "file",
|
type: "file",
|
||||||
mime: FSUtil.mimeType(absolute),
|
mime: FSUtil.mimeType(path.resolve(input.cwd, relative)),
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
},
|
||||||
|
onItem: input.onEntry,
|
||||||
|
}).pipe(
|
||||||
|
Effect.map((result) => result.items),
|
||||||
Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
|
Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
|
||||||
),
|
),
|
||||||
grep: (input) =>
|
grep: (input) =>
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ describe("Ripgrep", () => {
|
|||||||
expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
|
expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
|
||||||
expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config"))
|
expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config"))
|
||||||
|
|
||||||
|
const observed: string[] = []
|
||||||
|
const limited = yield* ripgrep.find({
|
||||||
|
cwd: tmp.path,
|
||||||
|
pattern: "**/*",
|
||||||
|
limit: 1,
|
||||||
|
onEntry: (entry) => Effect.sync(() => observed.push(entry.path)),
|
||||||
|
})
|
||||||
|
expect(observed).toEqual(limited.map((item) => item.path))
|
||||||
|
|
||||||
const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
|
const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
|
||||||
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
|
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
|
||||||
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config"))
|
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config"))
|
||||||
|
|||||||
Reference in New Issue
Block a user