1b279d4d10
e2e Tests / e2e (macos-latest) (push) Waiting to run
e2e Tests / e2e (windows-latest) (push) Waiting to run
Nix CI / check (push) Waiting to run
Python CI / Python bindings (macos-latest) (push) Waiting to run
Python CI / Python bindings (windows-latest) (push) Waiting to run
Build & Publish / Build Neovim aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Neovim x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build C FFI x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build MCP x86_64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Waiting to run
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Waiting to run
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Waiting to run
Build & Publish / Release (push) Blocked by required conditions
Build & Publish / Publish Python wheels to PyPI (push) Blocked by required conditions
Build & Publish / Publish Rust crates (push) Blocked by required conditions
Build & Publish / Publish npm packages (push) Blocked by required conditions
Rust CI / Test (macos-latest) (push) Waiting to run
Rust CI / Test (windows-latest) (push) Waiting to run
Rust CI / Fuzz Tests (macos-latest) (push) Waiting to run
Rust CI / Fuzz Tests (windows-latest) (push) Waiting to run
Build & Publish / Build MCP aarch64-apple-darwin (push) Waiting to run
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Waiting to run
Lua CI / lua-language-server type check (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Python CI / Python bindings (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 3s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 3s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 2s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 1s
Rust CI / cargo fmt (push) Failing after 1s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 4s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 1s
Rust CI / cargo clippy (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 1s
Stylua / Check lua files using Stylua (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 4s
e2e Tests / e2e (alpine-musl) (push) Successful in 58m54s
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
import path from "node:path";
|
|
|
|
export function normalizePathConstraint(
|
|
pathConstraint: string,
|
|
cwd = process.cwd(),
|
|
): string | null {
|
|
let trimmed = pathConstraint.trim();
|
|
if (!trimmed) return trimmed;
|
|
|
|
if (path.isAbsolute(trimmed)) {
|
|
const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
|
|
if (relative === "") return null;
|
|
if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
|
|
throw new Error(
|
|
`Path constraint must be relative to the workspace: ${pathConstraint}`,
|
|
);
|
|
}
|
|
trimmed = relative;
|
|
}
|
|
|
|
if (trimmed === "." || trimmed === "./") return null;
|
|
// Strip a leading `./` so `./**/*.rs` and `**/*.rs` behave identically.
|
|
if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
|
|
|
|
// FFF's glob matcher can treat a hidden directory root glob such as
|
|
// `.agents/**` as empty, while the tool contract says this means "inside
|
|
// this directory". Collapse simple trailing recursive directory globs to the
|
|
// directory-prefix constraint understood by the parser. Keep real file globs
|
|
// such as `src/**/*.ts` unchanged.
|
|
const recursiveDir = trimmed.match(/^(.*)\/\*\*(?:\/\*)?$/);
|
|
if (recursiveDir) {
|
|
const dir = recursiveDir[1];
|
|
if (dir && !/[*?[{]/.test(dir)) return `${dir}/`;
|
|
}
|
|
|
|
// Already signals path-constraint syntax to the parser.
|
|
if (trimmed.startsWith("/") || trimmed.endsWith("/")) return trimmed;
|
|
// Globs (`*.ts`, `src/**/*.cc`, `{src,lib}`) are handled by the parser.
|
|
if (/[*?[{]/.test(trimmed)) return trimmed;
|
|
// Filename with extension (`main.rs`, `config.json`) → FilePath constraint.
|
|
const lastSegment = trimmed.split("/").pop() ?? "";
|
|
if (/\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSegment)) return trimmed;
|
|
// Bare directory prefix → append `/` so the parser sees a PathSegment.
|
|
return `${trimmed}/`;
|
|
}
|
|
|
|
// Exclusions are emitted as `!<constraint>` tokens, which the Rust parser
|
|
// understands (crates/fff-query-parser/src/parser.rs). We normalize each one
|
|
// the same way as the include path so bare dirs become PathSegment excludes.
|
|
// Tolerate callers passing already-negated forms like `!src/` by stripping
|
|
// the leading `!` before normalizing so we never double-negate (`!!src/`).
|
|
export function normalizeExcludes(
|
|
exclude: string | string[] | undefined,
|
|
cwd = process.cwd(),
|
|
): string[] {
|
|
if (!exclude) return [];
|
|
const list = Array.isArray(exclude) ? exclude : [exclude];
|
|
const out: string[] = [];
|
|
for (const raw of list) {
|
|
const parts = raw
|
|
.split(/[,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
for (const p of parts) {
|
|
const stripped = p.startsWith("!") ? p.slice(1) : p;
|
|
const normalized = normalizePathConstraint(stripped, cwd);
|
|
if (normalized) out.push(`!${normalized}`);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function buildQuery(
|
|
path: string | undefined,
|
|
pattern: string,
|
|
exclude?: string | string[],
|
|
cwd = process.cwd(),
|
|
): string {
|
|
const parts: string[] = [];
|
|
if (path) {
|
|
const pathConstraint = normalizePathConstraint(path, cwd);
|
|
if (pathConstraint) parts.push(pathConstraint);
|
|
}
|
|
parts.push(...normalizeExcludes(exclude, cwd));
|
|
parts.push(pattern);
|
|
return parts.join(" ");
|
|
}
|