Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/pi-fff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ All fields are optional:
| `warnOnHomeDirScan` | boolean | `true` |
| `followSymlinks` | boolean | `true` |

Starting a session in a directory the config opts out of indexing (`$HOME` with `enableHomeDirScanning: false`, `/` with `enableFsRootScanning: false`) disables FFF workspace search for that session: the extension reports it once as a warning and keeps the `ffgrep`/`fffind` names even in `override` mode, so pi's built-in `grep`/`find` stay reachable.

CLI flags take precedence over environment variables, which take precedence over this file. A missing file is ignored. Malformed JSON, unknown fields, and invalid values stop the extension from loading and report the file path and error. `/fff-mode` changes the current session; it does not edit this file.

The file is global only. Project-level config cannot safely control tool names because pi decides which tools an extension registers before project configuration can be trusted.
Expand Down
32 changes: 31 additions & 1 deletion packages/pi-fff/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Type, type TSchema } from "@sinclair/typebox";
import { AuxFinderPool, routePathConstraint } from "./aux-finders";
import { type FffMode, loadConfig, VALID_MODES } from "./config";
import { FilePickerFactory } from "./file-picker";
import { isHomeDir, resolveDbPaths } from "./paths";
import { isFsRoot, isHomeDir, resolveDbPaths } from "./paths";
import { buildQuery } from "./query";

export { SCAN_TIMEOUT_MS } from "./sdk";
Expand Down Expand Up @@ -460,8 +460,22 @@ export default function fffExtension(pi: ExtensionAPI) {
});
}

// The native layer refuses a picker rooted at $HOME / the fs root unless the
// matching opt-in is set (crates/fff-core/src/file_picker.rs). Detect that here
// so the opt-out reads as "search off" instead of an init failure (issue #857).
function scanOptOutReason(cwd: string): string | null {
if (!enableHomeDirScanning && isHomeDir(cwd))
return `(fff): cwd is $HOME and "enableHomeDirScanning" is false, so FFF search is disabled for this session. Start pi from a project directory, or set "enableHomeDirScanning": true / --fff-enable-home-scan=true to index $HOME.`;
if (!enableFsRootScanning && isFsRoot(cwd))
return `(fff): cwd is the filesystem root and "enableFsRootScanning" is false, so FFF search is disabled for this session. Start pi from a project directory, or set "enableFsRootScanning": true / --fff-enable-root-scan=true to index it.`;
return null;
}

// in case cwd changes we need to figure this out
function ensureFinder(cwd: string): Promise<FileFinderApi> {
const optOut = scanOptOutReason(cwd);
if (optOut) return Promise.reject(new Error(optOut));

if (mainFinder && !mainFinder.isDestroyed && finderCwd === cwd)
return Promise.resolve(mainFinder);

Expand Down Expand Up @@ -741,13 +755,29 @@ export default function fffExtension(pi: ExtensionAPI) {
}

initializeFinderFactories();

// `override` replaces pi's built-in grep/find. With the cwd opted out of
// indexing that would leave the session without any working workspace
// search, so keep the FFF names and let the built-ins stand (issue #857).
if (currentMode === "override" && scanOptOutReason(activeCwd))
toolNames = FFF_TOOL_NAMES;

registerPendingTools();
}

pi.on("session_start", async (_event, ctx) => {
try {
prepareSession(ctx);
registerAutocompleteProvider(ctx);

// The user opted out of indexing this cwd, so skip the picker entirely
// instead of letting the native refusal surface as an error (issue #857).
const optOut = scanOptOutReason(activeCwd);
if (optOut) {
ctx.ui.notify(optOut, "warning");
return;
}

await ensureFinder(activeCwd);

// Warn when launched from $HOME with home scanning on: indexing a large
Expand Down
6 changes: 6 additions & 0 deletions packages/pi-fff/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export function isHomeDir(dir: string): boolean {
return path.resolve(dir) === HOME_DIR;
}

// Mirrors the native `path.parent().is_none()` check: a path that is its own parent.
export function isFsRoot(dir: string): boolean {
const resolved = path.resolve(dir);
return path.dirname(resolved) === resolved;
}

// Resolution order: explicit override > existing fff.nvim db > pi-local data dir.
// Reusing the nvim db lets pi rank files by the frecency the user built in their editor.
export function resolveDbPaths(overrides: {
Expand Down
54 changes: 50 additions & 4 deletions packages/pi-fff/test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,27 @@ function createMockFinder(): MockFinder {
};
}

const NATIVE_ROOT_REFUSAL =
"Failed to init file picker: Can not run certain FFF features in a file system root or home directories. Consider smaller per-project directories.";

// Mirrors the native guard in crates/fff-core/src/file_picker.rs: a picker rooted
// at $HOME or `/` is refused unless the matching opt-in is set.
function nativeRefusal(options: {
basePath: string;
enableHomeDirScanning?: boolean;
enableFsRootScanning?: boolean;
}): boolean {
const base = path.resolve(options.basePath);
if (options.enableHomeDirScanning === false && base === path.resolve(os.homedir()))
return true;
return options.enableFsRootScanning === false && path.dirname(base) === base;
}

const finderModule = {
FileFinder: {
create: mock((options: unknown) => {
create: mock((options: any) => {
createCalls.push(options);
if (nativeRefusal(options)) return { ok: false, error: NATIVE_ROOT_REFUSAL };
const finder = createMockFinder();
finders.push(finder);
return { ok: true, value: finder };
Expand Down Expand Up @@ -452,12 +469,41 @@ describe("pi-fff $HOME scan warning", () => {
expect(setup.ctx.ui.setStatus).toHaveBeenLastCalledWith("fff", undefined);
});

test("no warning when home scanning is disabled", async () => {
// #857: the opt-out must skip the picker, not surface the native refusal as an
// init error, and must not shadow pi's built-in search tools in override mode.
test("skips the picker and stays out of the way when home scanning is off", async () => {
process.env.FFF_ENABLE_HOME_SCAN = "0";
const setup = await start(undefined, os.homedir());
const setup = await start("override", os.homedir());

expect(setup.ctx.ui.notify).not.toHaveBeenCalled();
expect(createCalls).toHaveLength(0);
expect(setup.ctx.ui.setStatus).not.toHaveBeenCalled();

const [message, level] = setup.ctx.ui.notify.mock.calls[0];
expect(setup.ctx.ui.notify).toHaveBeenCalledTimes(1);
expect(level).toBe("warning");
expect(message).not.toContain("FFF init failed");
expect(message).toContain("enableHomeDirScanning");

const toolNames = setup.pi.registerTool.mock.calls.map(([tool]) => tool.name);
expect(toolNames).toEqual(["ffgrep", "fffind"]);
await shutdown(setup);
});

test("skips the picker at the filesystem root without root scanning", async () => {
const setup = await start(undefined, path.parse(process.cwd()).root);

expect(createCalls).toHaveLength(0);
const [message, level] = setup.ctx.ui.notify.mock.calls[0];
expect(level).toBe("warning");
expect(message).toContain("enableFsRootScanning");
await shutdown(setup);
});

test("still indexes $HOME when the opt-out is not set", async () => {
const setup = await start(undefined, os.homedir());

expect(createCalls).toHaveLength(1);
expect((createCalls[0] as { basePath: string }).basePath).toBe(os.homedir());
await shutdown(setup);
});

Expand Down
Loading