Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ba8415039 | |||
| b1be35cc5f | |||
| 9ff925e31e | |||
| e64d2e2a55 | |||
| 2b6ace888d |
@@ -92,4 +92,21 @@ jobs:
|
||||
|
||||
- name: Run bun tests
|
||||
shell: bash
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: make test-bun
|
||||
|
||||
- name: Install Node.js
|
||||
if: ${{ matrix.os != 'ubuntu-latest' }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Install node deps
|
||||
if: ${{ matrix.os != 'ubuntu-latest' }}
|
||||
shell: bash
|
||||
run: npm install
|
||||
|
||||
- name: Run node tests
|
||||
if: ${{ matrix.os != 'ubuntu-latest' }}
|
||||
shell: bash
|
||||
run: make test-node
|
||||
|
||||
+70
-107
@@ -2,7 +2,9 @@ name: Prebuild
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, feat/mcp-ai]
|
||||
branches: [main]
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@@ -137,26 +139,26 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
zigbuild_target: x86_64-unknown-linux-gnu.2.17
|
||||
artifact_name: target/x86_64-unknown-linux-gnu/release/libfff_c.so
|
||||
npm_package: fff-bun-linux-x64-gnu
|
||||
npm_package: fff-bin-linux-x64-gnu
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
zigbuild_target: aarch64-unknown-linux-gnu.2.17
|
||||
artifact_name: target/aarch64-unknown-linux-gnu/release/libfff_c.so
|
||||
npm_package: fff-bun-linux-arm64-gnu
|
||||
npm_package: fff-bin-linux-arm64-gnu
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
artifact_name: target/x86_64-unknown-linux-musl/release/libfff_c.so
|
||||
npm_package: fff-bun-linux-x64-musl
|
||||
npm_package: fff-bin-linux-x64-musl
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
artifact_name: target/aarch64-unknown-linux-musl/release/libfff_c.so
|
||||
npm_package: fff-bun-linux-arm64-musl
|
||||
npm_package: fff-bin-linux-arm64-musl
|
||||
lib_filename: libfff_c.so
|
||||
ext: so
|
||||
|
||||
@@ -171,13 +173,13 @@ jobs:
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
artifact_name: target/x86_64-apple-darwin/release/libfff_c.dylib
|
||||
npm_package: fff-bun-darwin-x64
|
||||
npm_package: fff-bin-darwin-x64
|
||||
lib_filename: libfff_c.dylib
|
||||
ext: dylib
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
artifact_name: target/aarch64-apple-darwin/release/libfff_c.dylib
|
||||
npm_package: fff-bun-darwin-arm64
|
||||
npm_package: fff-bin-darwin-arm64
|
||||
lib_filename: libfff_c.dylib
|
||||
ext: dylib
|
||||
|
||||
@@ -185,13 +187,13 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact_name: target/x86_64-pc-windows-msvc/release/fff_c.dll
|
||||
npm_package: fff-bun-win32-x64
|
||||
npm_package: fff-bin-win32-x64
|
||||
lib_filename: fff_c.dll
|
||||
ext: dll
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
artifact_name: target/aarch64-pc-windows-msvc/release/fff_c.dll
|
||||
npm_package: fff-bun-win32-arm64
|
||||
npm_package: fff-bin-win32-arm64
|
||||
lib_filename: fff_c.dll
|
||||
ext: dll
|
||||
|
||||
@@ -428,25 +430,22 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Prepare tag
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
sha="$(git rev-parse --short HEAD)"
|
||||
echo "tag=$sha" >> $GITHUB_OUTPUT
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: ./scripts/determine-version.sh
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "${{ steps.vars.outputs.tag }}"
|
||||
tag_name: "${{ steps.vars.outputs.tag }}"
|
||||
name: "${{ steps.version.outputs.version }}"
|
||||
tag_name: "${{ steps.version.outputs.is_release == 'true' && format('v{0}', steps.version.outputs.version) || steps.version.outputs.version }}"
|
||||
token: ${{ github.token }}
|
||||
files: ./binaries/*
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: false
|
||||
prerelease: ${{ steps.version.outputs.is_release != 'true' }}
|
||||
generate_release_notes: ${{ steps.version.outputs.is_release == 'true' }}
|
||||
body: |
|
||||
Nightly release from commit: ${{ github.sha }}
|
||||
${{ steps.version.outputs.is_release == 'true' && format('Release {0}', steps.version.outputs.version) || format('Nightly release from commit: {0}', github.sha) }}
|
||||
|
||||
## Neovim Plugin
|
||||
- `{target}.so` / `.dylib` / `.dll` - Lua module for Neovim
|
||||
@@ -462,13 +461,38 @@ jobs:
|
||||
curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.sh | bash
|
||||
```
|
||||
|
||||
crates-publish:
|
||||
name: Publish Rust crates
|
||||
needs: [build-nvim, build-c, build-mcp]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/node' || startsWith(github.ref, 'refs/tags/v')))
|
||||
|| (github.event_name == 'pull_request' && (github.head_ref == 'main' || github.head_ref == 'node'))
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-edit
|
||||
run: cargo install cargo-edit
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: ./scripts/determine-version.sh
|
||||
|
||||
- name: Publish crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: make publish-crates V="${{ steps.version.outputs.version }}"
|
||||
|
||||
npm-publish:
|
||||
name: Publish npm packages
|
||||
needs: [build-c]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/interchangable-ffi'))
|
||||
|| (github.event_name == 'pull_request' && (github.head_ref == 'main' || github.head_ref == 'feat/interchangable-ffi'))
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/node' || startsWith(github.ref, 'refs/tags/v')))
|
||||
|| (github.event_name == 'pull_request' && (github.head_ref == 'main' || github.head_ref == 'node'))
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
@@ -477,24 +501,12 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "25"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
# Read the base version from fff-core Cargo.toml (single source of truth)
|
||||
base_version=$(grep '^version' crates/fff-core/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
short_sha=$(git rev-parse --short HEAD)
|
||||
|
||||
# Always publish as nightly prerelease: X.Y.Z-nightly.<short-sha>
|
||||
echo "version=${base_version}-nightly.${short_sha}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "tag=nightly" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
run: ./scripts/determine-version.sh
|
||||
|
||||
- name: Download npm package artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -507,94 +519,45 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ steps.version.outputs.tag }}"
|
||||
|
||||
TAG="${{ steps.version.outputs.npm_tag }}"
|
||||
|
||||
for pkg_dir in ./npm-packages/npm-*/; do
|
||||
if [ -d "$pkg_dir" ]; then
|
||||
pkg_name=$(node -p "require('${pkg_dir}package.json').name")
|
||||
echo "Publishing ${pkg_name}@${VERSION} with tag ${TAG}..."
|
||||
|
||||
# Update version in package.json
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('${pkg_dir}package.json', 'utf8'));
|
||||
pkg.version = '${VERSION}';
|
||||
fs.writeFileSync('${pkg_dir}package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
|
||||
make set-npm-version PKG="$pkg_dir" VERSION="$VERSION"
|
||||
|
||||
cd "$pkg_dir"
|
||||
npm publish --tag "$TAG" --access public || echo "Failed to publish ${pkg_name} (may already exist)"
|
||||
cd -
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Publish main package
|
||||
- name: Publish bun package
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ steps.version.outputs.tag }}"
|
||||
|
||||
TAG="${{ steps.version.outputs.npm_tag }}"
|
||||
|
||||
echo "Publishing @ff-labs/fff-bun@${VERSION} with tag ${TAG}..."
|
||||
|
||||
# Update version and optionalDependencies versions in the main package
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('./packages/fff-bun/package.json', 'utf8'));
|
||||
pkg.version = '${VERSION}';
|
||||
if (pkg.optionalDependencies) {
|
||||
for (const dep of Object.keys(pkg.optionalDependencies)) {
|
||||
pkg.optionalDependencies[dep] = '${VERSION}';
|
||||
}
|
||||
}
|
||||
fs.writeFileSync('./packages/fff-bun/package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
make set-npm-version PKG=packages/fff-bun VERSION="$VERSION"
|
||||
|
||||
cd packages/fff-bun
|
||||
npm publish --tag "$TAG" --access public || echo "Failed to publish @ff-labs/fff-bun (may already exist)"
|
||||
|
||||
comment-on-pr:
|
||||
name: Comment on PR
|
||||
needs: [build-nvim, build-c, build-mcp]
|
||||
runs-on: ubuntu-latest
|
||||
# comments doesn't work on forks
|
||||
if: github.event_name == 'pull_request' && github.repository == 'dmtrKovalenko/fff.nvim' && github.event.pull_request.user.login == 'dmtrKovalenko'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
|
||||
- name: Publish Node.js package
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${{ steps.version.outputs.npm_tag }}"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "<!-- fff-nvim-build-comment -->"
|
||||
echo "Publishing @ff-labs/fff-node@${VERSION} with tag ${TAG}..."
|
||||
make set-npm-version PKG=packages/fff-node VERSION="$VERSION"
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!-- fff-nvim-build-comment -->
|
||||
## Build Artifacts for your PR
|
||||
|
||||
### Neovim Plugin
|
||||
Test with lazy.nvim:
|
||||
```lua
|
||||
{
|
||||
"dmtrKovalenko/fff.nvim",
|
||||
tag = "${{ steps.vars.outputs.short_sha }}",
|
||||
}
|
||||
```
|
||||
|
||||
### Bun/TypeScript Package
|
||||
The `fff` npm package will download binaries from this release automatically.
|
||||
|
||||
---
|
||||
*Built from ${{ github.sha }}*
|
||||
cd packages/fff-node
|
||||
npm install
|
||||
npx tsc
|
||||
npm publish --tag "$TAG" --access public || echo "Failed to publish @ff-labs/fff-node (may already exist)"
|
||||
|
||||
@@ -37,9 +37,7 @@ jobs:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo test --verbose -p fff-core -p fff-query-parser -p fff-c --features zlob
|
||||
cargo test --verbose -p grep-searcher
|
||||
run: cargo test --features zlob --workspace --exclude fff-nvim
|
||||
|
||||
fmt:
|
||||
name: cargo fmt
|
||||
@@ -73,6 +71,4 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cargo clippy -p fff-core -p fff-query-parser -p fff-nvim -p fff-c --features zlob -- -D warnings
|
||||
cargo clippy -p grep-searcher -- -D warnings
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
Generated
+53
-54
@@ -591,10 +591,10 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fff-c"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"fff-core",
|
||||
"fff-query-parser",
|
||||
"fff-search",
|
||||
"git2",
|
||||
"mimalloc",
|
||||
"serde",
|
||||
@@ -603,56 +603,21 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fff-core"
|
||||
version = "0.1.0"
|
||||
name = "fff-grep"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"aho-corasick",
|
||||
"bindet",
|
||||
"blake3",
|
||||
"chrono",
|
||||
"criterion",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"fff-query-parser",
|
||||
"git2",
|
||||
"glidesort",
|
||||
"globset",
|
||||
"bstr",
|
||||
"grep-matcher",
|
||||
"grep-searcher",
|
||||
"heed",
|
||||
"ignore",
|
||||
"memchr",
|
||||
"memmap2",
|
||||
"neo_frizbee",
|
||||
"notify",
|
||||
"notify-debouncer-full 0.7.0",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"rand",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"zlob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fff-mcp"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"fff-core",
|
||||
"fff-query-parser",
|
||||
"fff-search",
|
||||
"git2",
|
||||
"mimalloc",
|
||||
"rmcp",
|
||||
@@ -665,7 +630,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-nvim"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"blake3",
|
||||
@@ -673,8 +638,8 @@ dependencies = [
|
||||
"criterion",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"fff-core",
|
||||
"fff-query-parser",
|
||||
"fff-search",
|
||||
"git2",
|
||||
"glidesort",
|
||||
"heed",
|
||||
@@ -698,10 +663,53 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fff-query-parser"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"zlob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fff-search"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"aho-corasick",
|
||||
"bindet",
|
||||
"blake3",
|
||||
"chrono",
|
||||
"criterion",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"fff-grep",
|
||||
"fff-query-parser",
|
||||
"git2",
|
||||
"glidesort",
|
||||
"globset",
|
||||
"grep-matcher",
|
||||
"heed",
|
||||
"ignore",
|
||||
"memchr",
|
||||
"memmap2",
|
||||
"neo_frizbee",
|
||||
"notify",
|
||||
"notify-debouncer-full 0.7.0",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"rand",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"zlob",
|
||||
]
|
||||
|
||||
@@ -890,15 +898,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-searcher"
|
||||
version = "0.1.16"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"grep-matcher",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
|
||||
+5
-7
@@ -5,11 +5,14 @@ members = [
|
||||
"crates/fff-mcp",
|
||||
"crates/fff-nvim",
|
||||
"crates/fff-query-parser",
|
||||
"crates/fff-searcher",
|
||||
"crates/fff-grep",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
fff-grep = { version = "0.4.0", path = "crates/fff-grep" }
|
||||
fff-query-parser = { version = "0.4.0", path = "crates/fff-query-parser", default-features = false }
|
||||
|
||||
# Shared dependencies
|
||||
ahash = "0.8"
|
||||
bindet = "0.3"
|
||||
@@ -25,7 +28,6 @@ git2 = { version = "0.20.2", default-features = false, features = [
|
||||
glidesort = "0.1"
|
||||
globset = "0.4"
|
||||
grep-matcher = "0.1.8"
|
||||
grep-searcher = { path = "crates/fff-searcher" }
|
||||
heed = "0.22.0"
|
||||
ignore = "0.4.22"
|
||||
memmap2 = "0.9"
|
||||
@@ -41,11 +43,7 @@ parking_lot = "0.12"
|
||||
pathdiff = "0.2.1"
|
||||
rayon = "1.8.0"
|
||||
regex = "1.11"
|
||||
smallvec = { version = "1.13", features = [
|
||||
"const_generics",
|
||||
"union",
|
||||
"may_dangle",
|
||||
] }
|
||||
smallvec = { version = "1.13", features = ["const_generics", "union"] }
|
||||
thiserror = "2.0.10"
|
||||
tracing = "0.1"
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
PLENARY_DIR ?= ../plenary.nvim
|
||||
|
||||
.PHONY: build test test-rust test-lua test-bun test-setup prepare-bun
|
||||
.PHONY: build test test-rust test-lua test-bun test-node prepare-bun prepare-node set-npm-version header
|
||||
|
||||
build:
|
||||
cargo build --release --features zlob
|
||||
|
||||
header:
|
||||
cbindgen --config crates/fff-c/cbindgen.toml --crate fff-c --output crates/fff-c/include/fff.h
|
||||
|
||||
test-setup:
|
||||
@if [ ! -d "$(PLENARY_DIR)" ]; then \
|
||||
echo "Cloning plenary.nvim..."; \
|
||||
@@ -12,7 +15,7 @@ test-setup:
|
||||
fi
|
||||
|
||||
test-rust:
|
||||
cargo test --workspace --features zlob
|
||||
cargo test --workspace --features zlob --exclude fff-nvim
|
||||
|
||||
test-lua: test-setup build
|
||||
nvim --headless -u tests/minimal_init.lua \
|
||||
@@ -24,15 +27,39 @@ prepare-bun: build
|
||||
cp target/release/libfff_c.so packages/fff-bun/bin/ 2>/dev/null; \
|
||||
cp target/release/fff_c.dll packages/fff-bun/bin/ 2>/dev/null; \
|
||||
true
|
||||
@# Re-sign on macOS: cp can invalidate ad-hoc code signatures
|
||||
@if [ "$$(uname)" = "Darwin" ] && command -v codesign >/dev/null 2>&1; then \
|
||||
codesign --sign - packages/fff-bun/bin/libfff_c.dylib 2>/dev/null || true; \
|
||||
fi
|
||||
|
||||
prepare-node: build
|
||||
mkdir -p packages/fff-node/bin
|
||||
cp target/release/libfff_c.dylib packages/fff-node/bin/ 2>/dev/null; \
|
||||
cp target/release/libfff_c.so packages/fff-node/bin/ 2>/dev/null; \
|
||||
cp target/release/fff_c.dll packages/fff-node/bin/ 2>/dev/null; \
|
||||
true
|
||||
|
||||
test-bun: prepare-bun
|
||||
cd packages/fff-bun && bun test src/
|
||||
|
||||
test: test-rust test-lua test-bun
|
||||
test-node: prepare-node
|
||||
cd packages/fff-node && npm run build && node test/e2e.mjs
|
||||
|
||||
test: test-rust test-lua test-bun test-node
|
||||
|
||||
# Update version in a package.json, including optionalDependencies.
|
||||
# Usage: make set-npm-version PKG=packages/fff-bun VERSION=1.0.0-nightly.abc1234
|
||||
set-npm-version:
|
||||
@test -n "$(PKG)" || (echo "PKG is required" && exit 1)
|
||||
@test -n "$(VERSION)" || (echo "VERSION is required" && exit 1)
|
||||
node -e " \
|
||||
const fs = require('fs'); \
|
||||
const pkg = JSON.parse(fs.readFileSync('$(PKG)/package.json', 'utf8')); \
|
||||
pkg.version = '$(VERSION)'; \
|
||||
if (pkg.optionalDependencies) { \
|
||||
for (const dep of Object.keys(pkg.optionalDependencies)) { \
|
||||
pkg.optionalDependencies[dep] = '$(VERSION)'; \
|
||||
} \
|
||||
} \
|
||||
fs.writeFileSync('$(PKG)/package.json', JSON.stringify(pkg, null, 2) + '\n'); \
|
||||
"
|
||||
@echo "Set $(PKG) to $(VERSION)"
|
||||
|
||||
format-rust:
|
||||
cargo fmt --all
|
||||
@@ -53,3 +80,13 @@ lint-ts:
|
||||
lint: lint-rust lint-lua lint-ts
|
||||
|
||||
check: format lint
|
||||
|
||||
CRATES_TO_PUBLISH= fff-grep fff-query-parser fff-search
|
||||
|
||||
publish-crates:
|
||||
@test -n "$(V)" || (echo "V is required. Usage: make publish-crates V=0.2.0" && exit 1)
|
||||
cargo install cargo-edit
|
||||
cargo set-version $(V) || exit 1;
|
||||
@for crate in $(CRATES_TO_PUBLISH); do \
|
||||
cargo publish -p $$crate --allow-dirty $$(if [ -n "$$CI" ]; then echo "--no-verify"; fi) || exit 1; \
|
||||
done
|
||||
|
||||
@@ -33,26 +33,31 @@
|
||||
"bun": ">=1.0.0",
|
||||
},
|
||||
},
|
||||
"packages/fff-mcp": {
|
||||
"name": "@ff-labs/fff-mcp",
|
||||
"version": "0.1.0",
|
||||
"packages/fff-node": {
|
||||
"name": "@ff-labs/fff-node",
|
||||
"version": "0.1.37",
|
||||
"bin": {
|
||||
"fff-mcp": "./src/index.ts",
|
||||
"fff-node": "./dist/scripts/cli.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@ff-labs/fff-bun": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"zod": "^3.24.0",
|
||||
"ffi-rs": "^1.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ff-labs/fff-bun-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bun-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bun-win32-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bun-win32-x64": "0.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ff-labs/fff-bun",
|
||||
],
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
|
||||
|
||||
@@ -74,11 +79,7 @@
|
||||
|
||||
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@workspace:packages/fff-bun"],
|
||||
|
||||
"@ff-labs/fff-mcp": ["@ff-labs/fff-mcp@workspace:packages/fff-mcp"],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
"@ff-labs/fff-node": ["@ff-labs/fff-node@workspace:packages/fff-node"],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PXgg5gqcS/rHwa1hF0JdM1y5TiyejVrMHoBmWY/DjtfYZoFTXie1RCFOkoG0b5diOOmUcuYarMpH7CSNTqwj+w=="],
|
||||
|
||||
@@ -106,192 +107,42 @@
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-V4nmlXdOYZEa7GOxSExVG95SLp8FE0iTq2yKeN54UlfNMr3Sik+1Ff57LcCv7qYcn4TBqnBAt5rT3FAM6T6caQ=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
"@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YlnTMIyzfW3mAULC5ZA774nzQfFlYXM0rrfq/8ZzWt+IMbYk55a++jrI+6JeKV+1EqlDS3TFBEFtjdBNG94KzQ=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
"@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-sI3LpQQ34SX4nyOHc5yxA7FSqs9qPEUMqW/y/wWo9cuyPpaHMFsi/BeOVYsnC0syp3FrY7gzn6RnD6PlXCktXg=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
"@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-1WkcGkJTlwh4ZA59htKI+RXhiL3oKiYwLv7PO8LUf6FuADK73s5GcXp67iakKu243uYu+qGYr4RHco4ySddYhQ=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-J2PwqviycZxaEVA0Bwv38LqGDGSB9A1DPN4iYginYJZSvTvKW8kh7Tis0HbZrX1YDKnY8hi3lt0N0tCTNPDH5Q=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hn1W1hBPssTaqikU1Bqp1XUdDdOgbnYVIOtR++LVx66hhrtjf/xrIUQOhTm+NmOFDG16JUKXe1skfM4gpaqYwg=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kW6e+oCYZPvpH2ppPsffA18e1aLowtmWTRjVlyHtY04g/nQDepQvDUkkcvInh9fW5jLna7PjHvktW1tVgYIj2A=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HTwblAzruUS16nQPrez3ozvEHm1Xxh8J8w7rZYrpmAcNl1hzyOT8z/hY70M9Rt9fOqQ4Ovgor9qVy/U3ZJo0ZA=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-WeZkGl2BP1U4tRhEQH+FXLQS52N8obp74smK5AAGOfzPAT1pHkq6+dVkC1QCSIt7dHJs7SPtlnQw+5DkdZYlWA=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.1", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-rNGgMeCH5mdeHiMiJgt7wWXovZ+FHEfXhU9p4zZBH4n8M1/QnEsRUwlapISPLpILSGpoYS6iBuq9/fUlZY8Mhg=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dr2LcLD2CXo2a7BktlOpV68QhayqiI112KxIJC9tBgQO/Dkdg4CPsdqmvzzLhFo64iC5RLl2BT7M5lJImrfUWw=="],
|
||||
|
||||
"bun": ["bun@1.3.10", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.10", "@oven/bun-darwin-x64": "1.3.10", "@oven/bun-darwin-x64-baseline": "1.3.10", "@oven/bun-linux-aarch64": "1.3.10", "@oven/bun-linux-aarch64-musl": "1.3.10", "@oven/bun-linux-x64": "1.3.10", "@oven/bun-linux-x64-baseline": "1.3.10", "@oven/bun-linux-x64-musl": "1.3.10", "@oven/bun-linux-x64-musl-baseline": "1.3.10", "@oven/bun-windows-aarch64": "1.3.10", "@oven/bun-windows-x64": "1.3.10", "@oven/bun-windows-x64-baseline": "1.3.10" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
"ffi-rs": ["ffi-rs@1.3.1", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.1", "@yuuang/ffi-rs-darwin-arm64": "1.3.1", "@yuuang/ffi-rs-darwin-x64": "1.3.1", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.1", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.1", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.1", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.1", "@yuuang/ffi-rs-linux-x64-musl": "1.3.1", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.1", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.1", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.1" } }, "sha512-ZyNXL9fnclnZV+waQmWB9JrfbIEyxQa1OWtMrHOrAgcC04PgP5hBMG5TdhVN8N4uT/eul8zCFMVnJUukAFFlXA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
"bun-types/@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "fff-c"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
description = "C FFI bindings for fff-core - use from any language with C FFI support"
|
||||
description = "Raw C api of FFF file finder"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
@@ -10,14 +10,14 @@ crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zlob = ["fff-core/zlob"]
|
||||
zlob = ["fff/zlob"]
|
||||
|
||||
[dependencies]
|
||||
mimalloc.workspace = true
|
||||
tracing.workspace = true
|
||||
git2.workspace = true
|
||||
|
||||
fff-core = { path = "../fff-core" }
|
||||
fff-query-parser = { path = "../fff-query-parser" }
|
||||
fff = { package = "fff-search", path = "../fff-core" , version = "0.4.0" }
|
||||
fff-query-parser = { path = "../fff-query-parser" , version = "0.4.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
language = "C"
|
||||
header = "/* Generated by cbindgen — do not edit manually. */"
|
||||
include_guard = "FFF_C_H"
|
||||
include_version = true
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stdbool.h", "stddef.h"]
|
||||
|
||||
[export]
|
||||
include = [
|
||||
"FffResult",
|
||||
"FffSearchResult", "FffFileItem", "FffScore", "FffLocation",
|
||||
"FffGrepResult", "FffGrepMatch", "FffMatchRange",
|
||||
"FffScanProgress",
|
||||
]
|
||||
|
||||
[export.rename]
|
||||
"FffResult" = "FffResult"
|
||||
"FffSearchResult" = "FffSearchResult"
|
||||
"FffFileItem" = "FffFileItem"
|
||||
"FffScore" = "FffScore"
|
||||
"FffLocation" = "FffLocation"
|
||||
"FffGrepResult" = "FffGrepResult"
|
||||
"FffGrepMatch" = "FffGrepMatch"
|
||||
"FffMatchRange" = "FffMatchRange"
|
||||
"FffScanProgress" = "FffScanProgress"
|
||||
|
||||
[fn]
|
||||
sort_by = "None"
|
||||
@@ -0,0 +1,539 @@
|
||||
/* Generated by cbindgen — do not edit manually. */
|
||||
|
||||
#ifndef FFF_C_H
|
||||
#define FFF_C_H
|
||||
|
||||
/* Generated with cbindgen:0.29.2 */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Result envelope returned by all `fff_*` functions.
|
||||
*
|
||||
* Heap-allocated — the caller must free it with `fff_free_result`.
|
||||
*
|
||||
* Depending on the function, the payload is delivered through different fields:
|
||||
*
|
||||
* | Function | Payload field | Type |
|
||||
* |----------------------------|---------------|-------------------------------|
|
||||
* | `fff_create_instance` | `handle` | opaque instance pointer |
|
||||
* | `fff_search` | `handle` | `*mut FffSearchResult` |
|
||||
* | `fff_live_grep` | `handle` | `*mut FffGrepResult` |
|
||||
* | `fff_multi_grep` | `handle` | `*mut FffGrepResult` |
|
||||
* | `fff_get_scan_progress` | `handle` | `*mut FffScanProgress` |
|
||||
* | `fff_health_check` | `handle` | `*mut c_char` (JSON string) |
|
||||
* | `fff_get_historical_query` | `handle` | `*mut c_char` (string or null)|
|
||||
* | `fff_wait_for_scan` | `int_value` | 1 = completed, 0 = timed out |
|
||||
* | `fff_track_query` | `int_value` | 1 = success, 0 = failure |
|
||||
* | `fff_refresh_git_status` | `int_value` | number of files updated |
|
||||
* | `fff_scan_files` | (none) | success flag only |
|
||||
* | `fff_restart_index` | (none) | success flag only |
|
||||
*
|
||||
* On failure, `success` is false and `error` contains the message.
|
||||
*
|
||||
* **Important:** `fff_free_result` frees `error` but does **not** free `handle`.
|
||||
* The caller must free the handle with the appropriate function
|
||||
* (`fff_destroy`, `fff_free_search_result`, `fff_free_grep_result`,
|
||||
* `fff_free_string`, etc.).
|
||||
*/
|
||||
typedef struct FffResult {
|
||||
/**
|
||||
* Whether the operation succeeded.
|
||||
*/
|
||||
bool success;
|
||||
/**
|
||||
* Error message on failure. Null on success.
|
||||
*/
|
||||
char *error;
|
||||
/**
|
||||
* Opaque pointer payload (instance handle, typed result struct, or string). May be null.
|
||||
*/
|
||||
void *handle;
|
||||
/**
|
||||
* Integer payload for simple return values (bool as 0/1, counts, etc.).
|
||||
*/
|
||||
int64_t int_value;
|
||||
} FffResult;
|
||||
|
||||
/**
|
||||
* A file item returned by `fff_search`.
|
||||
*
|
||||
* All string fields are heap-allocated and owned by the parent `FffSearchResult`.
|
||||
* Free the entire result with `fff_free_search_result`.
|
||||
*/
|
||||
typedef struct FffFileItem {
|
||||
char *path;
|
||||
char *relative_path;
|
||||
char *file_name;
|
||||
char *git_status;
|
||||
uint64_t size;
|
||||
uint64_t modified;
|
||||
int64_t access_frecency_score;
|
||||
int64_t modification_frecency_score;
|
||||
int64_t total_frecency_score;
|
||||
bool is_binary;
|
||||
} FffFileItem;
|
||||
|
||||
/**
|
||||
* Score breakdown for a search result.
|
||||
*/
|
||||
typedef struct FffScore {
|
||||
int32_t total;
|
||||
int32_t base_score;
|
||||
int32_t filename_bonus;
|
||||
int32_t special_filename_bonus;
|
||||
int32_t frecency_boost;
|
||||
int32_t distance_penalty;
|
||||
int32_t current_file_penalty;
|
||||
int32_t combo_match_boost;
|
||||
bool exact_match;
|
||||
char *match_type;
|
||||
} FffScore;
|
||||
|
||||
/**
|
||||
* Location parsed from a query string (e.g. `"file.ts:42:10"`).
|
||||
*
|
||||
* `tag` encodes the variant:
|
||||
* 0 = no location,
|
||||
* 1 = line only (`line` is set),
|
||||
* 2 = position (`line` + `col`),
|
||||
* 3 = range (`line`/`col` = start, `end_line`/`end_col` = end).
|
||||
*/
|
||||
typedef struct FffLocation {
|
||||
uint8_t tag;
|
||||
int32_t line;
|
||||
int32_t col;
|
||||
int32_t end_line;
|
||||
int32_t end_col;
|
||||
} FffLocation;
|
||||
|
||||
/**
|
||||
* Search result returned by `fff_search`.
|
||||
*
|
||||
* The caller must free this with `fff_free_search_result`.
|
||||
*/
|
||||
typedef struct FffSearchResult {
|
||||
/**
|
||||
* Pointer to a heap-allocated array of `FffFileItem` (length = `count`).
|
||||
*/
|
||||
struct FffFileItem *items;
|
||||
/**
|
||||
* Pointer to a heap-allocated array of `FffScore` (length = `count`).
|
||||
*/
|
||||
struct FffScore *scores;
|
||||
/**
|
||||
* Number of items/scores in the arrays.
|
||||
*/
|
||||
uint32_t count;
|
||||
/**
|
||||
* Total number of files that matched the query.
|
||||
*/
|
||||
uint32_t total_matched;
|
||||
/**
|
||||
* Total number of indexed files.
|
||||
*/
|
||||
uint32_t total_files;
|
||||
/**
|
||||
* Location parsed from the query string.
|
||||
*/
|
||||
struct FffLocation location;
|
||||
} FffSearchResult;
|
||||
|
||||
/**
|
||||
* A byte range within a matched line, used for highlighting.
|
||||
*/
|
||||
typedef struct FffMatchRange {
|
||||
uint32_t start;
|
||||
uint32_t end;
|
||||
} FffMatchRange;
|
||||
|
||||
/**
|
||||
* A single grep match with file and line information.
|
||||
*
|
||||
* All string fields and arrays are heap-allocated. Free the parent
|
||||
* `FffGrepResult` with `fff_free_grep_result` to release everything.
|
||||
*/
|
||||
typedef struct FffGrepMatch {
|
||||
char *path;
|
||||
char *relative_path;
|
||||
char *file_name;
|
||||
char *git_status;
|
||||
char *line_content;
|
||||
struct FffMatchRange *match_ranges;
|
||||
char **context_before;
|
||||
char **context_after;
|
||||
uint64_t size;
|
||||
uint64_t modified;
|
||||
int64_t total_frecency_score;
|
||||
int64_t access_frecency_score;
|
||||
int64_t modification_frecency_score;
|
||||
uint64_t line_number;
|
||||
uint64_t byte_offset;
|
||||
uint32_t col;
|
||||
uint32_t match_ranges_count;
|
||||
uint32_t context_before_count;
|
||||
uint32_t context_after_count;
|
||||
uint16_t fuzzy_score;
|
||||
bool has_fuzzy_score;
|
||||
bool is_binary;
|
||||
bool is_definition;
|
||||
} FffGrepMatch;
|
||||
|
||||
/**
|
||||
* Grep result returned by `fff_live_grep` and `fff_multi_grep`.
|
||||
*
|
||||
* The caller must free this with `fff_free_grep_result`.
|
||||
*/
|
||||
typedef struct FffGrepResult {
|
||||
/**
|
||||
* Pointer to a heap-allocated array of `FffGrepMatch` (length = `count`).
|
||||
*/
|
||||
struct FffGrepMatch *items;
|
||||
/**
|
||||
* Number of matches in the `items` array.
|
||||
*/
|
||||
uint32_t count;
|
||||
/**
|
||||
* Total number of matches (always equal to `count`).
|
||||
*/
|
||||
uint32_t total_matched;
|
||||
/**
|
||||
* Number of files actually opened and searched in this call.
|
||||
*/
|
||||
uint32_t total_files_searched;
|
||||
/**
|
||||
* Total number of indexed files (before any filtering).
|
||||
*/
|
||||
uint32_t total_files;
|
||||
/**
|
||||
* Number of files eligible for search after filtering.
|
||||
*/
|
||||
uint32_t filtered_file_count;
|
||||
/**
|
||||
* File offset for the next page. 0 if all files have been searched.
|
||||
*/
|
||||
uint32_t next_file_offset;
|
||||
/**
|
||||
* Regex compilation error when falling back to literal matching. Null if none.
|
||||
*/
|
||||
char *regex_fallback_error;
|
||||
} FffGrepResult;
|
||||
|
||||
/**
|
||||
* Scan progress returned by `fff_get_scan_progress`.
|
||||
*
|
||||
* The caller must free this with `fff_free_scan_progress`.
|
||||
*/
|
||||
typedef struct FffScanProgress {
|
||||
uint64_t scanned_files_count;
|
||||
bool is_scanning;
|
||||
} FffScanProgress;
|
||||
|
||||
/**
|
||||
* Create a new file finder instance.
|
||||
*
|
||||
* Returns an opaque pointer that must be passed to all other `fff_*` calls
|
||||
* and eventually freed with `fff_destroy`.
|
||||
*
|
||||
* # Parameters
|
||||
*
|
||||
* * `base_path` – directory to index (required)
|
||||
* * `frecency_db_path` – path to frecency LMDB database (NULL/empty to skip)
|
||||
* * `history_db_path` – path to query history LMDB database (NULL/empty to skip)
|
||||
* * `use_unsafe_no_lock` – use MDB_NOLOCK for LMDB (useful in single-process setups)
|
||||
* * `warmup_mmap_cache` – pre-populate mmap caches after the initial scan
|
||||
* * `ai_mode` – enable AI-agent optimizations (auto-track frecency on modifications)
|
||||
*
|
||||
* ## Safety
|
||||
* String parameters must be valid null-terminated UTF-8 or NULL.
|
||||
*/
|
||||
struct FffResult *fff_create_instance(const char *base_path,
|
||||
const char *frecency_db_path,
|
||||
const char *history_db_path,
|
||||
bool use_unsafe_no_lock,
|
||||
bool warmup_mmap_cache,
|
||||
bool ai_mode);
|
||||
|
||||
/**
|
||||
* Destroy a file finder instance and free all its resources.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid pointer returned by `fff_create_instance`, or null (no-op).
|
||||
*/
|
||||
void fff_destroy(void *fff_handle);
|
||||
|
||||
/**
|
||||
* Perform fuzzy search on indexed files.
|
||||
*
|
||||
* # Parameters
|
||||
*
|
||||
* * `fff_handle` – instance from `fff_create_instance`
|
||||
* * `query` – search query string
|
||||
* * `current_file` – path of the currently open file for deprioritization (NULL/empty to skip)
|
||||
* * `max_threads` – maximum worker threads (0 = auto-detect)
|
||||
* * `page_index` – pagination offset (0 = first page)
|
||||
* * `page_size` – results per page (0 = default 100)
|
||||
* * `combo_boost_multiplier` – score multiplier for combo matches (0 = default 100)
|
||||
* * `min_combo_count` – minimum combo count before boost applies (0 = default 3)
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
* * `query` and `current_file` must be valid null-terminated UTF-8 strings or NULL.
|
||||
*/
|
||||
struct FffResult *fff_search(void *fff_handle,
|
||||
const char *query,
|
||||
const char *current_file,
|
||||
uint32_t max_threads,
|
||||
uint32_t page_index,
|
||||
uint32_t page_size,
|
||||
int32_t combo_boost_multiplier,
|
||||
uint32_t min_combo_count);
|
||||
|
||||
/**
|
||||
* Perform content search (grep) across indexed files.
|
||||
*
|
||||
* # Parameters
|
||||
*
|
||||
* * `fff_handle` – instance from `fff_create_instance`
|
||||
* * `query` – search query (supports constraint syntax like `*.rs pattern`)
|
||||
* * `mode` – 0 = plain text (SIMD), 1 = regex, 2 = fuzzy
|
||||
* * `max_file_size` – skip files larger than this in bytes (0 = default 10 MB)
|
||||
* * `max_matches_per_file` – max matches per file (0 = unlimited)
|
||||
* * `smart_case` – case-insensitive when query is all lowercase
|
||||
* * `file_offset` – file-based pagination offset (0 = start)
|
||||
* * `page_limit` – max matches to return (0 = default 50)
|
||||
* * `time_budget_ms` – wall-clock budget in ms (0 = unlimited)
|
||||
* * `before_context` – context lines before each match
|
||||
* * `after_context` – context lines after each match
|
||||
* * `classify_definitions` – tag matches that are code definitions
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
* * `query` must be a valid null-terminated UTF-8 string.
|
||||
*/
|
||||
struct FffResult *fff_live_grep(void *fff_handle,
|
||||
const char *query,
|
||||
uint8_t mode,
|
||||
uint64_t max_file_size,
|
||||
uint32_t max_matches_per_file,
|
||||
bool smart_case,
|
||||
uint32_t file_offset,
|
||||
uint32_t page_limit,
|
||||
uint64_t time_budget_ms,
|
||||
uint32_t before_context,
|
||||
uint32_t after_context,
|
||||
bool classify_definitions);
|
||||
|
||||
/**
|
||||
* Perform multi-pattern OR search (Aho-Corasick) across indexed files.
|
||||
*
|
||||
* Searches for lines matching ANY of the provided patterns using
|
||||
* SIMD-accelerated multi-needle matching.
|
||||
*
|
||||
* # Parameters
|
||||
*
|
||||
* * `fff_handle` – instance from `fff_create_instance`
|
||||
* * `patterns_joined` – patterns separated by `\n` (e.g. `"foo\nbar\nbaz"`)
|
||||
* * `constraints` – file filter like `"*.rs"` or `"/src/"` (NULL/empty to skip)
|
||||
* * `max_file_size` – skip files larger than this in bytes (0 = default 10 MB)
|
||||
* * `max_matches_per_file` – max matches per file (0 = unlimited)
|
||||
* * `smart_case` – case-insensitive when all patterns are lowercase
|
||||
* * `file_offset` – file-based pagination offset (0 = start)
|
||||
* * `page_limit` – max matches to return (0 = default 50)
|
||||
* * `time_budget_ms` – wall-clock budget in ms (0 = unlimited)
|
||||
* * `before_context` – context lines before each match
|
||||
* * `after_context` – context lines after each match
|
||||
* * `classify_definitions` – tag matches that are code definitions
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
* * `patterns_joined` and `constraints` must be valid null-terminated UTF-8 or NULL.
|
||||
*/
|
||||
struct FffResult *fff_multi_grep(void *fff_handle,
|
||||
const char *patterns_joined,
|
||||
const char *constraints,
|
||||
uint64_t max_file_size,
|
||||
uint32_t max_matches_per_file,
|
||||
bool smart_case,
|
||||
uint32_t file_offset,
|
||||
uint32_t page_limit,
|
||||
uint64_t time_budget_ms,
|
||||
uint32_t before_context,
|
||||
uint32_t after_context,
|
||||
bool classify_definitions);
|
||||
|
||||
/**
|
||||
* Trigger a rescan of the file index.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
struct FffResult *fff_scan_files(void *fff_handle);
|
||||
|
||||
/**
|
||||
* Check if a scan is currently in progress.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
bool fff_is_scanning(void *fff_handle);
|
||||
|
||||
/**
|
||||
* Get scan progress information.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
struct FffResult *fff_get_scan_progress(void *fff_handle);
|
||||
|
||||
/**
|
||||
* Wait for initial scan to complete.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
struct FffResult *fff_wait_for_scan(void *fff_handle, uint64_t timeout_ms);
|
||||
|
||||
/**
|
||||
* Restart indexing in a new directory.
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
* * `new_path` must be a valid null-terminated UTF-8 string.
|
||||
*/
|
||||
struct FffResult *fff_restart_index(void *fff_handle, const char *new_path);
|
||||
|
||||
/**
|
||||
* Refresh git status cache.
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
struct FffResult *fff_refresh_git_status(void *fff_handle);
|
||||
|
||||
/**
|
||||
* Track query completion for smart suggestions.
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
* * `query` and `file_path` must be valid null-terminated UTF-8 strings.
|
||||
*/
|
||||
struct FffResult *fff_track_query(void *fff_handle, const char *query, const char *file_path);
|
||||
|
||||
/**
|
||||
* Get historical query by offset (0 = most recent).
|
||||
*
|
||||
* ## Safety
|
||||
* `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||||
*/
|
||||
struct FffResult *fff_get_historical_query(void *fff_handle, uint64_t offset);
|
||||
|
||||
/**
|
||||
* Get health check information.
|
||||
*
|
||||
* ## Safety
|
||||
* * `fff_handle` must be a valid instance pointer from `fff_create_instance`, or null for
|
||||
* a limited health check (version + git only).
|
||||
* * `test_path` can be null or a valid null-terminated UTF-8 string.
|
||||
*/
|
||||
struct FffResult *fff_health_check(void *fff_handle, const char *test_path);
|
||||
|
||||
/**
|
||||
* Free a search result returned by `fff_search`.
|
||||
*
|
||||
* This frees the `FffSearchResult` struct, its `items` and `scores` arrays,
|
||||
* and all heap-allocated strings within each item and score.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid pointer previously returned via `FffResult.handle`
|
||||
* from `fff_search`, or null (no-op).
|
||||
*/
|
||||
void fff_free_search_result(struct FffSearchResult *result);
|
||||
|
||||
/**
|
||||
* Get a pointer to the `index`-th `FffFileItem` in a search result.
|
||||
*
|
||||
* Returns null if `result` is null or `index >= result->count`.
|
||||
* The returned pointer is valid until the search result is freed.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid `FffSearchResult` pointer from `fff_search`.
|
||||
*/
|
||||
const struct FffFileItem *fff_search_result_get_item(const struct FffSearchResult *result,
|
||||
uint32_t index);
|
||||
|
||||
/**
|
||||
* Get a pointer to the `index`-th `FffScore` in a search result.
|
||||
*
|
||||
* Returns null if `result` is null or `index >= result->count`.
|
||||
* The returned pointer is valid until the search result is freed.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid `FffSearchResult` pointer from `fff_search`.
|
||||
*/
|
||||
const struct FffScore *fff_search_result_get_score(const struct FffSearchResult *result,
|
||||
uint32_t index);
|
||||
|
||||
/**
|
||||
* Free a grep result returned by `fff_live_grep` or `fff_multi_grep`.
|
||||
*
|
||||
* This frees the `FffGrepResult` struct, its `items` array, and all
|
||||
* heap-allocated strings, match ranges, and context arrays within each match.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid pointer previously returned via `FffResult.handle`
|
||||
* from `fff_live_grep` or `fff_multi_grep`, or null (no-op).
|
||||
*/
|
||||
void fff_free_grep_result(struct FffGrepResult *result);
|
||||
|
||||
/**
|
||||
* Get a pointer to the `index`-th `FffGrepMatch` in a grep result.
|
||||
*
|
||||
* Returns null if `result` is null or `index >= result->count`.
|
||||
* The returned pointer is valid until the grep result is freed.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid `FffGrepResult` pointer from `fff_live_grep` or `fff_multi_grep`.
|
||||
*/
|
||||
const struct FffGrepMatch *fff_grep_result_get_match(const struct FffGrepResult *result,
|
||||
uint32_t index);
|
||||
|
||||
/**
|
||||
* Free a scan progress result returned by `fff_get_scan_progress`.
|
||||
*
|
||||
* ## Safety
|
||||
* `result` must be a valid pointer previously returned via `FffResult.handle`
|
||||
* from `fff_get_scan_progress`, or null (no-op).
|
||||
*/
|
||||
void fff_free_scan_progress(struct FffScanProgress *result);
|
||||
|
||||
/**
|
||||
* Offset a pointer by `byte_offset` bytes.
|
||||
*
|
||||
* General-purpose utility for FFI consumers that need pointer arithmetic
|
||||
* (e.g. iterating over arrays). Returns null if `base` is null.
|
||||
*
|
||||
* ## Safety
|
||||
* The resulting pointer must be within the bounds of the original allocation.
|
||||
*/
|
||||
const void *fff_ptr_offset(const void *base, uintptr_t byte_offset);
|
||||
|
||||
/**
|
||||
* Free a result returned by any `fff_*` function.
|
||||
*
|
||||
* ## Safety
|
||||
* `result_ptr` must be a valid pointer returned by a `fff_*` function.
|
||||
*/
|
||||
void fff_free_result(struct FffResult *result_ptr);
|
||||
|
||||
/**
|
||||
* Free a string returned by `fff_*` functions.
|
||||
*
|
||||
* ## Safety
|
||||
* `s` must be a valid C string allocated by this library.
|
||||
*/
|
||||
void fff_free_string(char *s);
|
||||
|
||||
#endif /* FFF_C_H */
|
||||
+430
-305
@@ -1,152 +1,122 @@
|
||||
//! FFI-compatible type definitions
|
||||
//!
|
||||
//! These types use #[repr(C)] for C ABI compatibility and implement
|
||||
//! serde traits for JSON serialization.
|
||||
//! All result types use `#[repr(C)]` structs for direct memory access from any
|
||||
//! language with C FFI support. No JSON serialization is used for search or grep
|
||||
//! results — callers read struct fields directly.
|
||||
|
||||
use std::ffi::{CString, c_char, c_void};
|
||||
use std::ptr;
|
||||
|
||||
use fff_core::git::format_git_status;
|
||||
use fff_core::{FileItem, GrepMatch, GrepResult, Location, Score, SearchResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use fff::git::format_git_status;
|
||||
use fff::{FileItem, GrepMatch, GrepResult, Location, Score, SearchResult};
|
||||
|
||||
/// Result type returned by all FFI functions
|
||||
/// Returned as a heap-allocated pointer that must be freed with fff_free_result
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Allocate a heap CString from a `&str`, returning a raw pointer.
|
||||
fn cstring_new(s: &str) -> *mut c_char {
|
||||
CString::new(s).unwrap_or_default().into_raw()
|
||||
}
|
||||
|
||||
/// Convert a `Vec<T>` into a raw pointer + count, leaking the memory.
|
||||
fn vec_to_raw<T>(v: Vec<T>) -> (*mut T, u32) {
|
||||
if v.is_empty() {
|
||||
return (ptr::null_mut(), 0);
|
||||
}
|
||||
let count = v.len() as u32;
|
||||
let mut boxed = v.into_boxed_slice();
|
||||
let p = boxed.as_mut_ptr();
|
||||
std::mem::forget(boxed);
|
||||
(p, count)
|
||||
}
|
||||
|
||||
/// Convert a `&[String]` into a heap-allocated array of C strings.
|
||||
fn strings_to_raw(v: &[String]) -> (*mut *mut c_char, u32) {
|
||||
if v.is_empty() {
|
||||
return (ptr::null_mut(), 0);
|
||||
}
|
||||
let ptrs: Vec<*mut c_char> = v.iter().map(|s| cstring_new(s)).collect();
|
||||
vec_to_raw(ptrs)
|
||||
}
|
||||
|
||||
/// Free a heap-allocated array of C strings.
|
||||
///
|
||||
/// ## Safety
|
||||
/// `arr` must have been produced by `strings_to_raw`.
|
||||
unsafe fn free_cstring_array(arr: *mut *mut c_char, count: u32) {
|
||||
if arr.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let ptrs = Vec::from_raw_parts(arr, count as usize, count as usize);
|
||||
for p in ptrs {
|
||||
if !p.is_null() {
|
||||
drop(CString::from_raw(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A file item returned by `fff_search`.
|
||||
///
|
||||
/// All string fields are heap-allocated and owned by the parent `FffSearchResult`.
|
||||
/// Free the entire result with `fff_free_search_result`.
|
||||
#[repr(C)]
|
||||
pub struct FffResult {
|
||||
/// Whether the operation succeeded
|
||||
pub success: bool,
|
||||
/// JSON data on success (null-terminated string, caller must free)
|
||||
pub data: *mut c_char,
|
||||
/// Error message on failure (null-terminated string, caller must free)
|
||||
pub error: *mut c_char,
|
||||
/// Opaque handle pointer (used by fff_create to return the instance)
|
||||
pub handle: *mut c_void,
|
||||
}
|
||||
|
||||
impl FffResult {
|
||||
/// Create a successful result with no data, returned as heap pointer
|
||||
pub fn ok_empty() -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
data: ptr::null_mut(),
|
||||
error: ptr::null_mut(),
|
||||
handle: ptr::null_mut(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a successful result with data, returned as heap pointer
|
||||
pub fn ok_data(data: &str) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
data: CString::new(data).unwrap_or_default().into_raw(),
|
||||
error: ptr::null_mut(),
|
||||
handle: ptr::null_mut(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a successful result carrying an opaque instance handle.
|
||||
pub fn ok_handle(handle: *mut c_void) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
data: ptr::null_mut(),
|
||||
error: ptr::null_mut(),
|
||||
handle,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create an error result, returned as heap pointer
|
||||
pub fn err(error: &str) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: false,
|
||||
data: ptr::null_mut(),
|
||||
error: CString::new(error).unwrap_or_default().into_raw(),
|
||||
handle: ptr::null_mut(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialization options (JSON-deserializable)
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InitOptions {
|
||||
/// Base directory to index (required)
|
||||
pub base_path: String,
|
||||
/// Path to frecency database (optional, omit to skip frecency initialization)
|
||||
pub frecency_db_path: Option<String>,
|
||||
/// Path to query history database (optional, omit to skip query tracker initialization)
|
||||
pub history_db_path: Option<String>,
|
||||
/// Use unsafe no-lock mode for databases (optional, defaults to false)
|
||||
#[serde(default)]
|
||||
pub use_unsafe_no_lock: bool,
|
||||
/// Pre-populate mmap caches for all files after initial scan so the first
|
||||
/// grep search is as fast as subsequent ones (optional, defaults to false)
|
||||
#[serde(default)]
|
||||
pub warmup_mmap_cache: bool,
|
||||
/// AI mode: automatically track frecency for all file modifications detected
|
||||
/// by the background watcher (optional, defaults to false)
|
||||
#[serde(default)]
|
||||
pub ai_mode: bool,
|
||||
}
|
||||
|
||||
/// Search options (JSON-deserializable)
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct SearchOptions {
|
||||
/// Maximum threads for parallel search (0 = auto)
|
||||
pub max_threads: Option<usize>,
|
||||
/// Current file path (for deprioritization)
|
||||
pub current_file: Option<String>,
|
||||
/// Combo boost score multiplier
|
||||
pub combo_boost_multiplier: Option<i32>,
|
||||
/// Minimum combo count for boost
|
||||
pub min_combo_count: Option<u32>,
|
||||
/// Page index for pagination
|
||||
pub page_index: Option<usize>,
|
||||
/// Page size for pagination
|
||||
pub page_size: Option<usize>,
|
||||
}
|
||||
|
||||
/// Scan progress (JSON-serializable)
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ScanProgress {
|
||||
pub scanned_files_count: usize,
|
||||
pub is_scanning: bool,
|
||||
}
|
||||
|
||||
/// File item for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FileItemJson {
|
||||
pub path: String,
|
||||
pub relative_path: String,
|
||||
pub file_name: String,
|
||||
pub struct FffFileItem {
|
||||
pub path: *mut c_char,
|
||||
pub relative_path: *mut c_char,
|
||||
pub file_name: *mut c_char,
|
||||
pub git_status: *mut c_char,
|
||||
pub size: u64,
|
||||
pub modified: u64,
|
||||
pub access_frecency_score: i64,
|
||||
pub modification_frecency_score: i64,
|
||||
pub total_frecency_score: i64,
|
||||
pub git_status: String,
|
||||
pub is_binary: bool,
|
||||
}
|
||||
|
||||
impl FileItemJson {
|
||||
pub fn from_file_item(item: &FileItem) -> Self {
|
||||
FileItemJson {
|
||||
path: item.path.to_string_lossy().to_string(),
|
||||
relative_path: item.relative_path.clone(),
|
||||
file_name: item.file_name.clone(),
|
||||
impl From<&FileItem> for FffFileItem {
|
||||
fn from(item: &FileItem) -> Self {
|
||||
FffFileItem {
|
||||
path: cstring_new(&item.path.to_string_lossy()),
|
||||
relative_path: cstring_new(&item.relative_path),
|
||||
file_name: cstring_new(&item.file_name),
|
||||
git_status: cstring_new(format_git_status(item.git_status)),
|
||||
size: item.size,
|
||||
modified: item.modified,
|
||||
access_frecency_score: item.access_frecency_score,
|
||||
modification_frecency_score: item.modification_frecency_score,
|
||||
total_frecency_score: item.total_frecency_score,
|
||||
git_status: format_git_status(item.git_status).to_string(),
|
||||
is_binary: item.is_binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Score for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ScoreJson {
|
||||
impl FffFileItem {
|
||||
/// ## Safety
|
||||
/// All string pointers must have been allocated by `CString::into_raw`.
|
||||
pub unsafe fn free_strings(&mut self) {
|
||||
unsafe {
|
||||
if !self.path.is_null() {
|
||||
drop(CString::from_raw(self.path));
|
||||
}
|
||||
if !self.relative_path.is_null() {
|
||||
drop(CString::from_raw(self.relative_path));
|
||||
}
|
||||
if !self.file_name.is_null() {
|
||||
drop(CString::from_raw(self.file_name));
|
||||
}
|
||||
if !self.git_status.is_null() {
|
||||
drop(CString::from_raw(self.git_status));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Score breakdown for a search result.
|
||||
#[repr(C)]
|
||||
pub struct FffScore {
|
||||
pub total: i32,
|
||||
pub base_score: i32,
|
||||
pub filename_bonus: i32,
|
||||
@@ -156,12 +126,12 @@ pub struct ScoreJson {
|
||||
pub current_file_penalty: i32,
|
||||
pub combo_match_boost: i32,
|
||||
pub exact_match: bool,
|
||||
pub match_type: String,
|
||||
pub match_type: *mut c_char,
|
||||
}
|
||||
|
||||
impl ScoreJson {
|
||||
pub fn from_score(score: &Score) -> Self {
|
||||
ScoreJson {
|
||||
impl From<&Score> for FffScore {
|
||||
fn from(score: &Score) -> Self {
|
||||
FffScore {
|
||||
total: score.total,
|
||||
base_score: score.base_score,
|
||||
filename_bonus: score.filename_bonus,
|
||||
@@ -171,234 +141,389 @@ impl ScoreJson {
|
||||
current_file_penalty: score.current_file_penalty,
|
||||
combo_match_boost: score.combo_match_boost,
|
||||
exact_match: score.exact_match,
|
||||
match_type: score.match_type.to_string(),
|
||||
match_type: cstring_new(score.match_type),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Location for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum LocationJson {
|
||||
#[serde(rename = "line")]
|
||||
Line { line: i32 },
|
||||
#[serde(rename = "position")]
|
||||
Position { line: i32, col: i32 },
|
||||
#[serde(rename = "range")]
|
||||
Range {
|
||||
start: PositionJson,
|
||||
end: PositionJson,
|
||||
},
|
||||
impl FffScore {
|
||||
/// ## Safety
|
||||
/// `match_type` must have been allocated by `CString::into_raw`.
|
||||
pub unsafe fn free_strings(&mut self) {
|
||||
unsafe {
|
||||
if !self.match_type.is_null() {
|
||||
drop(CString::from_raw(self.match_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PositionJson {
|
||||
/// Location parsed from a query string (e.g. `"file.ts:42:10"`).
|
||||
///
|
||||
/// `tag` encodes the variant:
|
||||
/// 0 = no location,
|
||||
/// 1 = line only (`line` is set),
|
||||
/// 2 = position (`line` + `col`),
|
||||
/// 3 = range (`line`/`col` = start, `end_line`/`end_col` = end).
|
||||
#[repr(C)]
|
||||
pub struct FffLocation {
|
||||
pub tag: u8,
|
||||
pub line: i32,
|
||||
pub col: i32,
|
||||
pub end_line: i32,
|
||||
pub end_col: i32,
|
||||
}
|
||||
|
||||
impl LocationJson {
|
||||
pub fn from_location(loc: &Location) -> Self {
|
||||
impl From<Option<&Location>> for FffLocation {
|
||||
fn from(loc: Option<&Location>) -> Self {
|
||||
match loc {
|
||||
Location::Line(line) => LocationJson::Line { line: *line },
|
||||
Location::Position { line, col } => LocationJson::Position {
|
||||
None => FffLocation {
|
||||
tag: 0,
|
||||
line: 0,
|
||||
col: 0,
|
||||
end_line: 0,
|
||||
end_col: 0,
|
||||
},
|
||||
Some(Location::Line(line)) => FffLocation {
|
||||
tag: 1,
|
||||
line: *line,
|
||||
col: 0,
|
||||
end_line: 0,
|
||||
end_col: 0,
|
||||
},
|
||||
Some(Location::Position { line, col }) => FffLocation {
|
||||
tag: 2,
|
||||
line: *line,
|
||||
col: *col,
|
||||
end_line: 0,
|
||||
end_col: 0,
|
||||
},
|
||||
Location::Range { start, end } => LocationJson::Range {
|
||||
start: PositionJson {
|
||||
line: start.0,
|
||||
col: start.1,
|
||||
},
|
||||
end: PositionJson {
|
||||
line: end.0,
|
||||
col: end.1,
|
||||
},
|
||||
Some(Location::Range { start, end }) => FffLocation {
|
||||
tag: 3,
|
||||
line: start.0,
|
||||
col: start.1,
|
||||
end_line: end.0,
|
||||
end_col: end.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search result for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SearchResultJson {
|
||||
pub items: Vec<FileItemJson>,
|
||||
pub scores: Vec<ScoreJson>,
|
||||
pub total_matched: usize,
|
||||
pub total_files: usize,
|
||||
pub location: Option<LocationJson>,
|
||||
/// Search result returned by `fff_search`.
|
||||
///
|
||||
/// The caller must free this with `fff_free_search_result`.
|
||||
#[repr(C)]
|
||||
pub struct FffSearchResult {
|
||||
/// Pointer to a heap-allocated array of `FffFileItem` (length = `count`).
|
||||
pub items: *mut FffFileItem,
|
||||
/// Pointer to a heap-allocated array of `FffScore` (length = `count`).
|
||||
pub scores: *mut FffScore,
|
||||
/// Number of items/scores in the arrays.
|
||||
pub count: u32,
|
||||
/// Total number of files that matched the query.
|
||||
pub total_matched: u32,
|
||||
/// Total number of indexed files.
|
||||
pub total_files: u32,
|
||||
/// Location parsed from the query string.
|
||||
pub location: FffLocation,
|
||||
}
|
||||
|
||||
impl SearchResultJson {
|
||||
pub fn from_search_result(result: &SearchResult) -> Self {
|
||||
SearchResultJson {
|
||||
items: result
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| FileItemJson::from_file_item(item))
|
||||
.collect(),
|
||||
scores: result.scores.iter().map(ScoreJson::from_score).collect(),
|
||||
total_matched: result.total_matched,
|
||||
total_files: result.total_files,
|
||||
location: result.location.as_ref().map(LocationJson::from_location),
|
||||
}
|
||||
impl FffSearchResult {
|
||||
/// Convert a core `SearchResult` into a heap-allocated `FffSearchResult`.
|
||||
pub fn from_core(result: &SearchResult) -> *mut Self {
|
||||
let items: Vec<FffFileItem> = result.items.iter().map(|i| FffFileItem::from(*i)).collect();
|
||||
let scores: Vec<FffScore> = result.scores.iter().map(FffScore::from).collect();
|
||||
let count = items.len() as u32;
|
||||
|
||||
let (items_ptr, _) = vec_to_raw(items);
|
||||
let (scores_ptr, _) = vec_to_raw(scores);
|
||||
|
||||
Box::into_raw(Box::new(FffSearchResult {
|
||||
items: items_ptr,
|
||||
scores: scores_ptr,
|
||||
count,
|
||||
total_matched: result.total_matched as u32,
|
||||
total_files: result.total_files as u32,
|
||||
location: FffLocation::from(result.location.as_ref()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Multi-grep (Aho-Corasick multi-pattern) types
|
||||
// ============================================================================
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grep result types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Multi-grep search options (JSON-deserializable)
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct MultiGrepOptionsJson {
|
||||
/// Patterns to search (OR logic — matches lines containing any pattern)
|
||||
pub patterns: Vec<String>,
|
||||
/// Optional constraint query like "*.rs" or "/src/"
|
||||
pub constraints: Option<String>,
|
||||
/// Maximum file size to search (bytes, default: 10MB)
|
||||
pub max_file_size: Option<u64>,
|
||||
/// Maximum matches per file (default: 0 = unlimited)
|
||||
pub max_matches_per_file: Option<usize>,
|
||||
/// Smart case: case-insensitive if all patterns are lowercase (default: true)
|
||||
pub smart_case: Option<bool>,
|
||||
/// File-based pagination offset (default: 0)
|
||||
pub file_offset: Option<usize>,
|
||||
/// Maximum matches to return per page (default: 50)
|
||||
pub page_limit: Option<usize>,
|
||||
/// Time budget in milliseconds, 0 = unlimited (default: 0)
|
||||
pub time_budget_ms: Option<u64>,
|
||||
/// Number of context lines before each match (default: 0)
|
||||
pub before_context: Option<usize>,
|
||||
/// Number of context lines after each match (default: 0)
|
||||
pub after_context: Option<usize>,
|
||||
/// Whether to classify matches as definition lines (default: false)
|
||||
pub classify_definitions: Option<bool>,
|
||||
/// A byte range within a matched line, used for highlighting.
|
||||
#[repr(C)]
|
||||
pub struct FffMatchRange {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grep (live search) types
|
||||
// ============================================================================
|
||||
|
||||
/// Grep search options (JSON-deserializable)
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct GrepSearchOptionsJson {
|
||||
/// Maximum file size to search (bytes, default: 10MB)
|
||||
pub max_file_size: Option<u64>,
|
||||
/// Maximum matches per file (default: 200)
|
||||
pub max_matches_per_file: Option<usize>,
|
||||
/// Smart case: case-insensitive if query is lowercase (default: true)
|
||||
pub smart_case: Option<bool>,
|
||||
/// File-based pagination offset (default: 0)
|
||||
pub file_offset: Option<usize>,
|
||||
/// Maximum matches to return (default: 50)
|
||||
pub page_limit: Option<usize>,
|
||||
/// Search mode: "plain", "regex", or "fuzzy" (default: "plain")
|
||||
pub mode: Option<String>,
|
||||
/// Time budget in milliseconds, 0 = unlimited (default: 0)
|
||||
pub time_budget_ms: Option<u64>,
|
||||
/// Number of context lines before each match (default: 0)
|
||||
pub before_context: Option<usize>,
|
||||
/// Number of context lines after each match (default: 0)
|
||||
pub after_context: Option<usize>,
|
||||
/// Whether to classify matches as definition lines (default: false)
|
||||
pub classify_definitions: Option<bool>,
|
||||
}
|
||||
|
||||
/// A single grep match for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GrepMatchJson {
|
||||
/// File metadata
|
||||
pub path: String,
|
||||
pub relative_path: String,
|
||||
pub file_name: String,
|
||||
pub git_status: String,
|
||||
/// A single grep match with file and line information.
|
||||
///
|
||||
/// All string fields and arrays are heap-allocated. Free the parent
|
||||
/// `FffGrepResult` with `fff_free_grep_result` to release everything.
|
||||
#[repr(C)]
|
||||
pub struct FffGrepMatch {
|
||||
// -- pointers (8 bytes each) --
|
||||
pub path: *mut c_char,
|
||||
pub relative_path: *mut c_char,
|
||||
pub file_name: *mut c_char,
|
||||
pub git_status: *mut c_char,
|
||||
pub line_content: *mut c_char,
|
||||
pub match_ranges: *mut FffMatchRange,
|
||||
pub context_before: *mut *mut c_char,
|
||||
pub context_after: *mut *mut c_char,
|
||||
// -- 8-byte numeric fields --
|
||||
pub size: u64,
|
||||
pub modified: u64,
|
||||
pub is_binary: bool,
|
||||
pub total_frecency_score: i64,
|
||||
pub access_frecency_score: i64,
|
||||
pub modification_frecency_score: i64,
|
||||
/// Match metadata
|
||||
pub line_number: u64,
|
||||
pub col: usize,
|
||||
pub byte_offset: u64,
|
||||
pub line_content: String,
|
||||
/// Byte offset pairs (start, end) within line_content for highlighting
|
||||
pub match_ranges: Vec<[u32; 2]>,
|
||||
/// Fuzzy match score (only in fuzzy mode)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fuzzy_score: Option<u16>,
|
||||
/// Whether the matched line is a code definition (struct, fn, class, etc.)
|
||||
// -- 4-byte fields --
|
||||
pub col: u32,
|
||||
pub match_ranges_count: u32,
|
||||
pub context_before_count: u32,
|
||||
pub context_after_count: u32,
|
||||
// -- 2-byte fields --
|
||||
pub fuzzy_score: u16,
|
||||
// -- 1-byte fields --
|
||||
pub has_fuzzy_score: bool,
|
||||
pub is_binary: bool,
|
||||
pub is_definition: bool,
|
||||
/// Lines before the match (context)
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub context_before: Vec<String>,
|
||||
/// Lines after the match (context)
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub context_after: Vec<String>,
|
||||
}
|
||||
|
||||
impl GrepMatchJson {
|
||||
pub fn from_grep_match(m: &GrepMatch, file: &FileItem) -> Self {
|
||||
GrepMatchJson {
|
||||
path: file.path.to_string_lossy().to_string(),
|
||||
relative_path: file.relative_path.clone(),
|
||||
file_name: file.file_name.clone(),
|
||||
git_status: format_git_status(file.git_status).to_string(),
|
||||
impl FffGrepMatch {
|
||||
fn from_core_with_file(m: &GrepMatch, file: &FileItem) -> Self {
|
||||
let ranges: Vec<FffMatchRange> = m
|
||||
.match_byte_offsets
|
||||
.iter()
|
||||
.map(|&(start, end)| FffMatchRange { start, end })
|
||||
.collect();
|
||||
let (match_ranges, match_ranges_count) = vec_to_raw(ranges);
|
||||
let (context_before, context_before_count) = strings_to_raw(&m.context_before);
|
||||
let (context_after, context_after_count) = strings_to_raw(&m.context_after);
|
||||
let (has_fuzzy_score, fuzzy_score) = match m.fuzzy_score {
|
||||
Some(s) => (true, s),
|
||||
None => (false, 0),
|
||||
};
|
||||
|
||||
FffGrepMatch {
|
||||
path: cstring_new(&file.path.to_string_lossy()),
|
||||
relative_path: cstring_new(&file.relative_path),
|
||||
file_name: cstring_new(&file.file_name),
|
||||
git_status: cstring_new(format_git_status(file.git_status)),
|
||||
line_content: cstring_new(&m.line_content),
|
||||
match_ranges,
|
||||
context_before,
|
||||
context_after,
|
||||
size: file.size,
|
||||
modified: file.modified,
|
||||
is_binary: file.is_binary,
|
||||
total_frecency_score: file.total_frecency_score,
|
||||
access_frecency_score: file.access_frecency_score,
|
||||
modification_frecency_score: file.modification_frecency_score,
|
||||
line_number: m.line_number,
|
||||
col: m.col,
|
||||
byte_offset: m.byte_offset,
|
||||
line_content: m.line_content.clone(),
|
||||
match_ranges: m
|
||||
.match_byte_offsets
|
||||
.iter()
|
||||
.map(|&(start, end)| [start, end])
|
||||
.collect(),
|
||||
fuzzy_score: m.fuzzy_score,
|
||||
col: m.col as u32,
|
||||
match_ranges_count,
|
||||
context_before_count,
|
||||
context_after_count,
|
||||
fuzzy_score,
|
||||
has_fuzzy_score,
|
||||
is_binary: file.is_binary,
|
||||
is_definition: m.is_definition,
|
||||
context_before: m.context_before.clone(),
|
||||
context_after: m.context_after.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Safety
|
||||
/// All pointers must have been allocated by the corresponding `from_core`.
|
||||
pub unsafe fn free_fields(&mut self) {
|
||||
unsafe {
|
||||
if !self.path.is_null() {
|
||||
drop(CString::from_raw(self.path));
|
||||
}
|
||||
if !self.relative_path.is_null() {
|
||||
drop(CString::from_raw(self.relative_path));
|
||||
}
|
||||
if !self.file_name.is_null() {
|
||||
drop(CString::from_raw(self.file_name));
|
||||
}
|
||||
if !self.git_status.is_null() {
|
||||
drop(CString::from_raw(self.git_status));
|
||||
}
|
||||
if !self.line_content.is_null() {
|
||||
drop(CString::from_raw(self.line_content));
|
||||
}
|
||||
if !self.match_ranges.is_null() {
|
||||
drop(Vec::from_raw_parts(
|
||||
self.match_ranges,
|
||||
self.match_ranges_count as usize,
|
||||
self.match_ranges_count as usize,
|
||||
));
|
||||
}
|
||||
free_cstring_array(self.context_before, self.context_before_count);
|
||||
free_cstring_array(self.context_after, self.context_after_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grep result for JSON serialization
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GrepResultJson {
|
||||
pub items: Vec<GrepMatchJson>,
|
||||
pub total_matched: usize,
|
||||
pub total_files_searched: usize,
|
||||
pub total_files: usize,
|
||||
pub filtered_file_count: usize,
|
||||
pub next_file_offset: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regex_fallback_error: Option<String>,
|
||||
/// Grep result returned by `fff_live_grep` and `fff_multi_grep`.
|
||||
///
|
||||
/// The caller must free this with `fff_free_grep_result`.
|
||||
#[repr(C)]
|
||||
pub struct FffGrepResult {
|
||||
/// Pointer to a heap-allocated array of `FffGrepMatch` (length = `count`).
|
||||
pub items: *mut FffGrepMatch,
|
||||
/// Number of matches in the `items` array.
|
||||
pub count: u32,
|
||||
/// Total number of matches (always equal to `count`).
|
||||
pub total_matched: u32,
|
||||
/// Number of files actually opened and searched in this call.
|
||||
pub total_files_searched: u32,
|
||||
/// Total number of indexed files (before any filtering).
|
||||
pub total_files: u32,
|
||||
/// Number of files eligible for search after filtering.
|
||||
pub filtered_file_count: u32,
|
||||
/// File offset for the next page. 0 if all files have been searched.
|
||||
pub next_file_offset: u32,
|
||||
/// Regex compilation error when falling back to literal matching. Null if none.
|
||||
pub regex_fallback_error: *mut c_char,
|
||||
}
|
||||
|
||||
impl GrepResultJson {
|
||||
pub fn from_grep_result(result: &GrepResult) -> Self {
|
||||
GrepResultJson {
|
||||
items: result
|
||||
.matches
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let file = result.files[m.file_index];
|
||||
GrepMatchJson::from_grep_match(m, file)
|
||||
})
|
||||
.collect(),
|
||||
total_matched: result.matches.len(),
|
||||
total_files_searched: result.total_files_searched,
|
||||
total_files: result.total_files,
|
||||
filtered_file_count: result.filtered_file_count,
|
||||
next_file_offset: result.next_file_offset,
|
||||
regex_fallback_error: result.regex_fallback_error.clone(),
|
||||
}
|
||||
impl FffGrepResult {
|
||||
/// Convert a core `GrepResult` into a heap-allocated `FffGrepResult`.
|
||||
pub fn from_core(result: &GrepResult) -> *mut Self {
|
||||
let items: Vec<FffGrepMatch> = result
|
||||
.matches
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let file = result.files[m.file_index];
|
||||
FffGrepMatch::from_core_with_file(m, file)
|
||||
})
|
||||
.collect();
|
||||
let (items_ptr, count) = vec_to_raw(items);
|
||||
|
||||
Box::into_raw(Box::new(FffGrepResult {
|
||||
items: items_ptr,
|
||||
count,
|
||||
total_matched: result.matches.len() as u32,
|
||||
total_files_searched: result.total_files_searched as u32,
|
||||
total_files: result.total_files as u32,
|
||||
filtered_file_count: result.filtered_file_count as u32,
|
||||
next_file_offset: result.next_file_offset as u32,
|
||||
regex_fallback_error: match &result.regex_fallback_error {
|
||||
Some(e) => cstring_new(e),
|
||||
None => ptr::null_mut(),
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Result envelope returned by all `fff_*` functions.
|
||||
///
|
||||
/// Heap-allocated — the caller must free it with `fff_free_result`.
|
||||
///
|
||||
/// Depending on the function, the payload is delivered through different fields:
|
||||
///
|
||||
/// | Function | Payload field | Type |
|
||||
/// |----------------------------|---------------|-------------------------------|
|
||||
/// | `fff_create_instance` | `handle` | opaque instance pointer |
|
||||
/// | `fff_search` | `handle` | `*mut FffSearchResult` |
|
||||
/// | `fff_live_grep` | `handle` | `*mut FffGrepResult` |
|
||||
/// | `fff_multi_grep` | `handle` | `*mut FffGrepResult` |
|
||||
/// | `fff_get_scan_progress` | `handle` | `*mut FffScanProgress` |
|
||||
/// | `fff_health_check` | `handle` | `*mut c_char` (JSON string) |
|
||||
/// | `fff_get_historical_query` | `handle` | `*mut c_char` (string or null)|
|
||||
/// | `fff_wait_for_scan` | `int_value` | 1 = completed, 0 = timed out |
|
||||
/// | `fff_track_query` | `int_value` | 1 = success, 0 = failure |
|
||||
/// | `fff_refresh_git_status` | `int_value` | number of files updated |
|
||||
/// | `fff_scan_files` | (none) | success flag only |
|
||||
/// | `fff_restart_index` | (none) | success flag only |
|
||||
///
|
||||
/// On failure, `success` is false and `error` contains the message.
|
||||
///
|
||||
/// **Important:** `fff_free_result` frees `error` but does **not** free `handle`.
|
||||
/// The caller must free the handle with the appropriate function
|
||||
/// (`fff_destroy`, `fff_free_search_result`, `fff_free_grep_result`,
|
||||
/// `fff_free_string`, etc.).
|
||||
#[repr(C)]
|
||||
pub struct FffResult {
|
||||
/// Whether the operation succeeded.
|
||||
pub success: bool,
|
||||
/// Error message on failure. Null on success.
|
||||
pub error: *mut c_char,
|
||||
/// Opaque pointer payload (instance handle, typed result struct, or string). May be null.
|
||||
pub handle: *mut c_void,
|
||||
/// Integer payload for simple return values (bool as 0/1, counts, etc.).
|
||||
pub int_value: i64,
|
||||
}
|
||||
|
||||
impl FffResult {
|
||||
/// Create a successful result with no payload, returned as heap pointer.
|
||||
pub fn ok_empty() -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
error: ptr::null_mut(),
|
||||
handle: ptr::null_mut(),
|
||||
int_value: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a successful result with an integer value.
|
||||
pub fn ok_int(value: i64) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
error: ptr::null_mut(),
|
||||
handle: ptr::null_mut(),
|
||||
int_value: value,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a successful result carrying an opaque pointer (handle, typed struct, or string).
|
||||
pub fn ok_handle(handle: *mut c_void) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
error: ptr::null_mut(),
|
||||
handle,
|
||||
int_value: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a successful result carrying a C string in the `handle` field.
|
||||
/// The caller must free it with `fff_free_string`.
|
||||
pub fn ok_string(s: &str) -> *mut Self {
|
||||
let cstr = CString::new(s).unwrap_or_default().into_raw();
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: true,
|
||||
error: ptr::null_mut(),
|
||||
handle: cstr as *mut c_void,
|
||||
int_value: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create an error result, returned as heap pointer.
|
||||
pub fn err(error: &str) -> *mut Self {
|
||||
Box::into_raw(Box::new(FffResult {
|
||||
success: false,
|
||||
error: CString::new(error).unwrap_or_default().into_raw(),
|
||||
handle: ptr::null_mut(),
|
||||
int_value: 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan progress
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Scan progress returned by `fff_get_scan_progress`.
|
||||
///
|
||||
/// The caller must free this with `fff_free_scan_progress`.
|
||||
#[repr(C)]
|
||||
pub struct FffScanProgress {
|
||||
pub scanned_files_count: u64,
|
||||
pub is_scanning: bool,
|
||||
}
|
||||
|
||||
+428
-223
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "fff-core"
|
||||
version = "0.1.0"
|
||||
name = "fff-search"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
description = "High-performance file finder core library"
|
||||
license = "MIT"
|
||||
authors = ["Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>"]
|
||||
description = "Faboulous & Fast File Finder - a fast and extremely correct file finder SDK with typo resistance, SIMD, prefiltering, and more"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
@@ -18,15 +19,12 @@ ffi = []
|
||||
zlob = ["dep:zlob", "fff-query-parser/zlob"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace dependencies
|
||||
ahash = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Local crates
|
||||
fff-query-parser = { path = "../fff-query-parser", default-features = false }
|
||||
fff-query-parser = { workspace = true , version = "0.2.4" }
|
||||
|
||||
# External dependencies
|
||||
bindet = { workspace = true }
|
||||
@@ -37,7 +35,7 @@ git2 = { workspace = true }
|
||||
glidesort = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
grep-matcher = { workspace = true }
|
||||
grep-searcher = { workspace = true }
|
||||
fff-grep = { workspace = true , version = "0.2.4" }
|
||||
aho-corasick = "1"
|
||||
memchr = "2"
|
||||
heed = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# fff
|
||||
|
||||
fff is a file search toolkit. It is faster than ripgrep and fzf and designed for a long running applications like file editors, ai agents, or file exploerers.
|
||||
|
||||
## Features
|
||||
|
||||
- Fuzzy file name search
|
||||
- Typo resistance
|
||||
- Frecency and query history ranking
|
||||
- Native git support via libgit
|
||||
- Advanced ranking
|
||||
- Grep functionality with SIMD optimized plain matcher and regex
|
||||
- Multi grep using aho-corasick algorithm
|
||||
- Efficient memory mapping for file system
|
||||
- Cross platform support (Linux, Windows, MacOS)
|
||||
- Advnaced constraints syntax allowing to prefilter based on git status, glob, extension, size, timing and more
|
||||
|
||||
## Performance
|
||||
|
||||
FFF is designed for high performance and low latency. SIMD optimized where needed, parallelized for multi core systems, efficient sorting and ranking algorithms, memaps and much more.
|
||||
|
||||
On MacOS FFF is about 20-50 times faster than ripgrep for content search and around 10 times faster than fzf for file name search.
|
||||
|
||||
## Documentation
|
||||
|
||||
Refer rust docs https://docs.rs/crate/fff-search/latest
|
||||
@@ -1,8 +1,41 @@
|
||||
//! Core file picker: filesystem indexing, background watching, and fuzzy search.
|
||||
//!
|
||||
//! [`FilePicker`] is the central component of fff-search. It:
|
||||
//!
|
||||
//! 1. **Indexes** a directory tree in a background thread, collecting every
|
||||
//! non-ignored file into a path-sorted `Vec<FileItem>`.
|
||||
//! 2. **Watches** the filesystem via the `notify` crate, applying
|
||||
//! create/modify/delete events to the index in real time.
|
||||
//! 3. **Owns files**: Provides a values for search and provides a good entry point for
|
||||
//! fuzzy search and live grep
|
||||
//!
|
||||
//! # Lifecycle
|
||||
//!
|
||||
//! ```text
|
||||
//! new_with_shared_state()
|
||||
//! │
|
||||
//! ├─> background scan thread ──> populates SharedPicker
|
||||
//! └─> file-system watcher ──> live updates SharedPicker
|
||||
//!
|
||||
//! fuzzy_search() <── static, borrows &[FileItem]
|
||||
//! grep() <── static, borrows &[FileItem] (live content search)
|
||||
//! trigger_rescan() <── synchronous re-index
|
||||
//! cancel() <── shuts down background work
|
||||
//! ```
|
||||
//!
|
||||
//! # Thread Safety
|
||||
//!
|
||||
//! `FilePicker` itself is **not** `Sync`!
|
||||
//! all concurrent access goes through [`SharedPicker`](crate::SharedPicker) .
|
||||
//! The background scanner and watcher acquire write locks only when mutating
|
||||
//! the file index, so read-heavy search workloads rarely contend.
|
||||
|
||||
use crate::background_watcher::BackgroundWatcher;
|
||||
use crate::error::Error;
|
||||
use crate::frecency::FrecencyTracker;
|
||||
use crate::git::GitStatusCache;
|
||||
use crate::query_tracker::QueryMatchEntry;
|
||||
use crate::grep::{GrepResult, GrepSearchOptions, grep_search};
|
||||
use crate::query_tracker::QueryTracker;
|
||||
use crate::score::match_and_score_files;
|
||||
use crate::types::{FileItem, PaginationArgs, ScoringContext, SearchResult};
|
||||
use crate::{SharedFrecency, SharedPicker};
|
||||
@@ -32,31 +65,15 @@ impl FFFMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if a file is binary by checking for NUL bytes in the first 512 bytes.
|
||||
/// This is the same heuristic used by git and grep — simple, fast, and sufficient.
|
||||
#[inline]
|
||||
fn detect_binary(path: &Path, size: u64) -> bool {
|
||||
// Empty files are not binary
|
||||
if size == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(file) = std::fs::File::open(path) else {
|
||||
return false;
|
||||
};
|
||||
let mut reader = std::io::BufReader::with_capacity(1024, file);
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
let n = reader.read(&mut buf).unwrap_or(0);
|
||||
buf[..n].contains(&0)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// Configuration for a single fuzzy search invocation.
|
||||
///
|
||||
/// Passed to [`FilePicker::fuzzy_search`] to control threading, pagination,
|
||||
/// and scoring behavior.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct FuzzySearchOptions<'a> {
|
||||
pub max_threads: usize,
|
||||
pub current_file: Option<&'a str>,
|
||||
pub project_path: Option<&'a Path>,
|
||||
pub last_same_query_match: Option<&'a QueryMatchEntry>,
|
||||
pub combo_boost_score_multiplier: i32,
|
||||
pub min_combo_count: u32,
|
||||
pub pagination: PaginationArgs,
|
||||
@@ -196,6 +213,10 @@ impl FileItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// The main file picker engine storage
|
||||
///
|
||||
/// It maintains an in memory index of all the files that are resent in the file system
|
||||
/// and borrows them to perform the search
|
||||
pub struct FilePicker {
|
||||
base_path: PathBuf,
|
||||
sync_data: FileSync,
|
||||
@@ -310,20 +331,15 @@ impl FilePicker {
|
||||
|
||||
/// Perform fuzzy search on files with a pre-parsed query.
|
||||
///
|
||||
/// The query should be parsed using `QueryParser::parse()` before calling this function.
|
||||
/// This allows the caller to handle location parsing and other preprocessing.
|
||||
/// The query should be parsed using [`FFFQuery`]::parse() before calling
|
||||
/// this function. If a [`QueryTracker`] is provided, the search will
|
||||
/// automatically look up the last selected file for this query and apply
|
||||
/// combo-boost scoring.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `files` - Slice of files to search
|
||||
/// * `query` - The raw query string (used for max_typos calculation and debugging)
|
||||
/// * `parsed` - Pre-parsed query result (can be None for simple single-token queries)
|
||||
/// * `options` - Search options including pagination, threading, and scoring parameters
|
||||
///
|
||||
/// # Returns
|
||||
/// SearchResult containing matched files, scores, and location information
|
||||
pub fn fuzzy_search<'a, 'q>(
|
||||
files: &'a [FileItem],
|
||||
query: &'q FFFQuery<'q>,
|
||||
query_tracker: Option<&QueryTracker>,
|
||||
options: FuzzySearchOptions<'q>,
|
||||
) -> SearchResult<'a> {
|
||||
let max_threads = if options.max_threads == 0 {
|
||||
@@ -333,6 +349,7 @@ impl FilePicker {
|
||||
} else {
|
||||
options.max_threads
|
||||
};
|
||||
|
||||
debug!(
|
||||
raw_query = ?query.raw_query,
|
||||
pagination = ?options.pagination,
|
||||
@@ -342,7 +359,6 @@ impl FilePicker {
|
||||
);
|
||||
|
||||
let total_files = files.len();
|
||||
|
||||
let location = query.location;
|
||||
|
||||
// Get effective query for max_typos calculation (without location suffix)
|
||||
@@ -354,24 +370,37 @@ impl FilePicker {
|
||||
|
||||
// small queries with a large number of results can match absolutely everything
|
||||
let max_typos = (effective_query.len() as u16 / 4).clamp(2, 6);
|
||||
// Look up the last file selected for this query (combo-boost scoring)
|
||||
let last_same_query_entry =
|
||||
query_tracker
|
||||
.zip(options.project_path)
|
||||
.and_then(|(tracker, project_path)| {
|
||||
tracker
|
||||
.get_last_query_entry(
|
||||
query.raw_query,
|
||||
project_path,
|
||||
options.min_combo_count,
|
||||
)
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let context = ScoringContext {
|
||||
query,
|
||||
project_path: options.project_path,
|
||||
max_typos,
|
||||
max_threads,
|
||||
project_path: options.project_path,
|
||||
current_file: options.current_file,
|
||||
last_same_query_match: options.last_same_query_match,
|
||||
last_same_query_match: last_same_query_entry,
|
||||
combo_boost_score_multiplier: options.combo_boost_score_multiplier,
|
||||
min_combo_count: options.min_combo_count,
|
||||
pagination: options.pagination,
|
||||
};
|
||||
|
||||
let time = std::time::Instant::now();
|
||||
|
||||
let (items, scores, total_matched) = match_and_score_files(files, &context);
|
||||
|
||||
debug!(
|
||||
info!(
|
||||
?query,
|
||||
completed_in = ?time.elapsed(),
|
||||
total_matched,
|
||||
@@ -389,6 +418,16 @@ impl FilePicker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a live grep search across indexed files with a pre-parsed query.
|
||||
pub fn grep<'a>(
|
||||
files: &'a [FileItem],
|
||||
query: &FFFQuery<'_>,
|
||||
options: &GrepSearchOptions,
|
||||
) -> GrepResult<'a> {
|
||||
grep_search(files, query, options)
|
||||
}
|
||||
|
||||
// Returns an ongoing or finisshed scan progress
|
||||
pub fn get_scan_progress(&self) -> ScanProgress {
|
||||
let scanned_count = self.scanned_files_count.load(Ordering::Relaxed);
|
||||
let is_scanning = self.is_scanning.load(Ordering::Relaxed);
|
||||
@@ -607,7 +646,7 @@ impl FilePicker {
|
||||
.retain_files(|file| !file.path.starts_with(dir_path))
|
||||
}
|
||||
|
||||
/// We use this to prevent any substantial background threads from acquiring the locks
|
||||
/// Use this to prevent any substantial background threads from acquiring the locks
|
||||
pub fn cancel(&self) {
|
||||
self.cancelled.store(true, Ordering::Release);
|
||||
}
|
||||
@@ -657,6 +696,7 @@ impl FilePicker {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Quick way to check if scan is going without acquiring a lock for [Self::get_scan_progress]
|
||||
pub fn is_scan_active(&self) -> bool {
|
||||
self.is_scanning.load(Ordering::Relaxed)
|
||||
}
|
||||
@@ -666,12 +706,39 @@ impl FilePicker {
|
||||
pub fn scan_signal(&self) -> Arc<AtomicBool> {
|
||||
Arc::clone(&self.is_scanning)
|
||||
}
|
||||
|
||||
/// Block the current thread until the background filesystem scan finishes.
|
||||
///
|
||||
/// Briefly acquires a read lock on the shared picker to obtain the scan
|
||||
/// signal, then drops the lock and polls without holding it — so the
|
||||
/// background thread can still write to the index.
|
||||
///
|
||||
/// Returns immediately if no picker has been initialised yet.
|
||||
pub fn wait_for_scan(shared_picker: &SharedPicker) {
|
||||
let signal = {
|
||||
let guard = shared_picker.read().expect("shared picker lock poisoned");
|
||||
match guard.as_ref() {
|
||||
Some(picker) => picker.scan_signal(),
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
|
||||
while signal.load(Ordering::Acquire) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A point-in-time snapshot of the file-scanning progress.
|
||||
///
|
||||
/// Returned by [`FilePicker::get_scan_progress`]. Useful for displaying
|
||||
/// a progress indicator while the initial scan is running.
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanProgress {
|
||||
/// Number of files indexed so far.
|
||||
pub scanned_files_count: usize,
|
||||
/// `true` while the background scan thread is still running.
|
||||
pub is_scanning: bool,
|
||||
}
|
||||
|
||||
@@ -932,3 +999,22 @@ fn is_git_file(path: &Path) -> bool {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect if a file is binary by checking for NUL bytes in the first 512 bytes.
|
||||
/// This is the same heuristic used by git and grep — simple, fast, and sufficient.
|
||||
#[inline]
|
||||
fn detect_binary(path: &Path, size: u64) -> bool {
|
||||
// Empty files are not binary
|
||||
if size == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(file) = std::fs::File::open(path) else {
|
||||
return false;
|
||||
};
|
||||
let mut reader = std::io::BufReader::with_capacity(1024, file);
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
let n = reader.read(&mut buf).unwrap_or(0);
|
||||
buf[..n].contains(&0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::db_healthcheck::DbHealthChecker;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::file_picker::FFFMode;
|
||||
use crate::{SharedFrecency, error::Error, git::is_modified_status};
|
||||
use crate::{SharedFrecency, git::is_modified_status};
|
||||
use heed::{Database, Env, EnvOpenOptions};
|
||||
use heed::{
|
||||
EnvFlags,
|
||||
@@ -47,7 +48,7 @@ impl DbHealthChecker for FrecencyTracker {
|
||||
&self.env
|
||||
}
|
||||
|
||||
fn count_entries(&self) -> Result<Vec<(&'static str, u64)>, Error> {
|
||||
fn count_entries(&self) -> Result<Vec<(&'static str, u64)>> {
|
||||
let rtxn = self.env.read_txn().map_err(Error::DbStartReadTxn)?;
|
||||
let count = self.db.len(&rtxn).map_err(Error::DbRead)?;
|
||||
|
||||
@@ -56,8 +57,10 @@ impl DbHealthChecker for FrecencyTracker {
|
||||
}
|
||||
|
||||
impl FrecencyTracker {
|
||||
pub fn new(db_path: &str, use_unsafe_no_lock: bool) -> Result<Self, Error> {
|
||||
pub fn new(db_path: impl AsRef<Path>, use_unsafe_no_lock: bool) -> Result<Self> {
|
||||
let db_path = db_path.as_ref();
|
||||
fs::create_dir_all(db_path).map_err(Error::CreateDir)?;
|
||||
|
||||
let env = unsafe {
|
||||
let mut opts = EnvOpenOptions::new();
|
||||
opts.map_size(24 * 1024 * 1024); // 24 MiB
|
||||
@@ -82,16 +85,24 @@ impl FrecencyTracker {
|
||||
}
|
||||
|
||||
/// Spawns a background thread to purge stale frecency entries and compact the database.
|
||||
/// Run it once in a while to purge old pages and keep DB file size reasonable.
|
||||
///
|
||||
/// Phase 1 (read lock): purge stale entries — deletes expired entries and prunes old timestamps.
|
||||
/// Phase 2 (write lock): compact the database by re-writing entries into a fresh LMDB env.
|
||||
/// We can't use LMDB's copy_to_path with NO_LOCK envs (MDB_INCOMPATIBLE),
|
||||
/// so instead we: read all entries → drop env → delete files → reopen → write back.
|
||||
pub fn spawn_gc(shared: SharedFrecency, db_path: String, use_unsafe_no_lock: bool) {
|
||||
std::thread::Builder::new()
|
||||
/// It's okay to not join this thread since it acquires locks for the db access
|
||||
///
|
||||
/// ```
|
||||
/// use fff_search::frecency::FrecencyTracker;
|
||||
/// use fff_search::SharedFrecency;
|
||||
/// let shared_frecency: SharedFrecency = Default::default();
|
||||
/// let _ = FrecencyTracker::spawn_gc(shared_frecency, "/path/to/frecency_db".into(), true).ok();
|
||||
/// ```
|
||||
pub fn spawn_gc(
|
||||
shared: SharedFrecency,
|
||||
db_path: String,
|
||||
use_unsafe_no_lock: bool,
|
||||
) -> Result<std::thread::JoinHandle<()>> {
|
||||
Ok(std::thread::Builder::new()
|
||||
.name("fff-frecency-gc".into())
|
||||
.spawn(move || Self::run_frecency_gc(shared, db_path, use_unsafe_no_lock))
|
||||
.ok();
|
||||
.spawn(move || Self::run_frecency_gc(shared, db_path, use_unsafe_no_lock))?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(shared), fields(db_path = %db_path))]
|
||||
@@ -223,7 +234,7 @@ impl FrecencyTracker {
|
||||
/// Removes entries where all timestamps are older than MAX_HISTORY_DAYS,
|
||||
/// and prunes stale timestamps from entries that still have recent ones.
|
||||
/// Returns (deleted_count, pruned_count).
|
||||
fn purge_stale_entries(&self) -> Result<(usize, usize), Error> {
|
||||
fn purge_stale_entries(&self) -> Result<(usize, usize)> {
|
||||
let now = self.get_now();
|
||||
let cutoff_time = now.saturating_sub((MAX_HISTORY_DAYS * SECONDS_PER_DAY) as u64);
|
||||
|
||||
@@ -275,7 +286,7 @@ impl FrecencyTracker {
|
||||
Ok((to_delete.len(), to_update.len()))
|
||||
}
|
||||
|
||||
fn get_accesses(&self, path: &Path) -> Result<Option<VecDeque<u64>>, Error> {
|
||||
fn get_accesses(&self, path: &Path) -> Result<Option<VecDeque<u64>>> {
|
||||
let rtxn = self.env.read_txn().map_err(Error::DbStartReadTxn)?;
|
||||
|
||||
let key_hash = Self::path_to_hash_bytes(path)?;
|
||||
@@ -289,7 +300,7 @@ impl FrecencyTracker {
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn path_to_hash_bytes(path: &Path) -> Result<[u8; 32], Error> {
|
||||
fn path_to_hash_bytes(path: &Path) -> Result<[u8; 32]> {
|
||||
let Some(key) = path.to_str() else {
|
||||
return Err(Error::InvalidPath(path.to_path_buf()));
|
||||
};
|
||||
@@ -299,13 +310,13 @@ impl FrecencyTracker {
|
||||
|
||||
/// Returns seconds since the most recent tracked access, or `None` if the
|
||||
/// file has never been tracked.
|
||||
pub fn seconds_since_last_access(&self, path: &Path) -> Result<Option<u64>, Error> {
|
||||
pub fn seconds_since_last_access(&self, path: &Path) -> Result<Option<u64>> {
|
||||
let accesses = self.get_accesses(path)?;
|
||||
let last = accesses.and_then(|a| a.back().copied());
|
||||
Ok(last.map(|ts| self.get_now().saturating_sub(ts)))
|
||||
}
|
||||
|
||||
pub fn track_access(&self, path: &Path) -> Result<(), Error> {
|
||||
pub fn track_access(&self, path: &Path) -> Result<()> {
|
||||
let mut wtxn = self.env.write_txn().map_err(Error::DbStartWriteTxn)?;
|
||||
|
||||
let key_hash = Self::path_to_hash_bytes(path)?;
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::constraints::apply_constraints;
|
||||
use crate::sort_buffer::sort_with_buffer;
|
||||
use crate::types::FileItem;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use fff_grep::lines::{self, LineStep};
|
||||
use fff_grep::{Searcher, SearcherBuilder, Sink, SinkMatch};
|
||||
use fff_query_parser::{Constraint, FFFQuery, GrepConfig, QueryParser};
|
||||
use grep_matcher::{Match, Matcher, NoCaptures, NoError};
|
||||
use grep_searcher::lines::{self, LineStep};
|
||||
use grep_searcher::{Searcher, SearcherBuilder, Sink, SinkMatch};
|
||||
use rayon::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -721,7 +721,7 @@ impl Sink for PlainTextSink<'_> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn finish(&mut self, _: &Searcher, _: &grep_searcher::SinkFinish) -> Result<(), Self::Error> {
|
||||
fn finish(&mut self, _: &Searcher, _: &fff_grep::SinkFinish) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -779,7 +779,7 @@ impl Sink for RegexSink<'_> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn finish(&mut self, _: &Searcher, _: &grep_searcher::SinkFinish) -> Result<(), Self::Error> {
|
||||
fn finish(&mut self, _: &Searcher, _: &fff_grep::SinkFinish) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -862,7 +862,7 @@ impl Sink for AhoCorasickSink<'_> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn finish(&mut self, _: &Searcher, _: &grep_searcher::SinkFinish) -> Result<(), Self::Error> {
|
||||
fn finish(&mut self, _: &Searcher, _: &fff_grep::SinkFinish) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+136
-28
@@ -1,46 +1,154 @@
|
||||
//! fff-core - High-performance file finder library
|
||||
//! # FFF Search — High-performance file finder core
|
||||
//!
|
||||
//! This crate provides the core file indexing and fuzzy search functionality.
|
||||
//! This crate provides the core search engine for [FFF (Fast File Finder)](https://github.com/dmtrKovalenko/fff.nvim).
|
||||
//! It includes filesystem indexing with real-time watching, fuzzy matching powered
|
||||
//! by [frizbee](https://docs.rs/neo_frizbee), frecency scoring backed by LMDB,
|
||||
//! and multi-mode grep search.
|
||||
//!
|
||||
//! # State management
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! All state is instance-based. Callers create their own `SharedPicker` /
|
||||
//! `SharedFrecency` / `SharedQueryTracker` and pass them into
|
||||
//! `FilePicker::new_with_shared_state`. Multiple independent instances can
|
||||
//! coexist in the same process.
|
||||
//! - [`file_picker::FilePicker`] — Main entry point. Indexes a directory tree in a
|
||||
//! background thread, maintains a sorted file list, watches the filesystem for
|
||||
//! changes, and performs fuzzy search with frecency-weighted scoring.
|
||||
//! - [`frecency::FrecencyTracker`] — LMDB-backed database that tracks file access
|
||||
//! and modification patterns for intelligent result ranking.
|
||||
//! - [`query_tracker::QueryTracker`] — Tracks search query history and provides
|
||||
//! "combo-boost" scoring for repeatedly matched files.
|
||||
//! - [`grep`] — Live grep search supporting regex, plain-text, and fuzzy modes
|
||||
//! with optional constraint filtering.
|
||||
//! - [`git`] — Git status caching and repository detection.
|
||||
//!
|
||||
//! ## Shared State
|
||||
//!
|
||||
//! [`SharedPicker`], [`SharedFrecency`], and [`SharedQueryTracker`] are
|
||||
//! `Arc<RwLock<Option<T>>>` type aliases for thread-safe shared access. FFF
|
||||
//! is designed for long-running processes that keep the file index in global
|
||||
//! state, so these wrappers let background threads (scanner, watcher) share
|
||||
//! data with the calling code safely.
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```
|
||||
//! use fff_search::file_picker::FilePicker;
|
||||
//! use fff_search::frecency::FrecencyTracker;
|
||||
//! use fff_search::query_tracker::QueryTracker;
|
||||
//! use fff_search::{
|
||||
//! FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser,
|
||||
//! SharedFrecency, SharedPicker, SharedQueryTracker,
|
||||
//! };
|
||||
//!
|
||||
//! let shared_picker: SharedPicker = Default::default();
|
||||
//! let shared_frecency: SharedFrecency = Default::default();
|
||||
//! let shared_query_tracker: SharedQueryTracker = Default::default();
|
||||
//!
|
||||
//! let tmp = std::env::temp_dir().join("fff-doctest");
|
||||
//! std::fs::create_dir_all(&tmp).unwrap();
|
||||
//!
|
||||
//! // 1. Optionally initialize frecency and query tracker databases
|
||||
//! let frecency = FrecencyTracker::new(tmp.join("frecency"), false)?;
|
||||
//! *shared_frecency.write().unwrap() = Some(frecency);
|
||||
//!
|
||||
//! let query_tracker = QueryTracker::new(tmp.join("queries"), false)?;
|
||||
//! *shared_query_tracker.write().unwrap() = Some(query_tracker);
|
||||
//!
|
||||
//! // 2. Init the file picker (spawns background scan + watcher)
|
||||
//! FilePicker::new_with_shared_state(
|
||||
//! ".".into(),
|
||||
//! /* warmup memap caches = */ false,
|
||||
//! FFFMode::Ai, // use AI for ai agents, and Neovim for editors
|
||||
//! shared_picker.clone(),
|
||||
//! shared_frecency.clone(),
|
||||
//! )?;
|
||||
//!
|
||||
//! // 3. Wait for scan (in real app you would like to add some tokio flavor here)
|
||||
//! FilePicker::wait_for_scan(&shared_picker);
|
||||
//!
|
||||
//! // 4. Search: lock the picker and query tracker
|
||||
//! let picker_lock_guard = shared_picker.read().unwrap();
|
||||
//! let picker = picker_lock_guard.as_ref().unwrap();
|
||||
//! let query_tracker_lock_guard = shared_query_tracker.read().unwrap();
|
||||
//!
|
||||
//! // 5. Parse the query and perform fuzzy search with frecency and combo-boost scoring
|
||||
//! let parser = QueryParser::default();
|
||||
//! let query = parser.parse("lib.rs");
|
||||
//!
|
||||
//! let results = FilePicker::fuzzy_search(
|
||||
//! picker.get_files(),
|
||||
//! &query,
|
||||
//! query_tracker_lock_guard.as_ref(),
|
||||
//! FuzzySearchOptions {
|
||||
//! max_threads: 0,
|
||||
//! current_file: None,
|
||||
//! pagination: PaginationArgs { offset: 0, limit: 50 },
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! );
|
||||
//!
|
||||
//! assert!(results.total_matched > 0);
|
||||
//! assert!(results.items.first().unwrap().path.ends_with("lib.rs"));
|
||||
//!
|
||||
//! let _ = std::fs::remove_dir_all(&tmp);
|
||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
mod background_watcher;
|
||||
pub mod constraints;
|
||||
mod constraints;
|
||||
mod db_healthcheck;
|
||||
mod error;
|
||||
pub mod file_picker;
|
||||
pub mod frecency;
|
||||
pub mod git;
|
||||
pub mod grep;
|
||||
pub mod log;
|
||||
pub mod path_utils;
|
||||
pub mod query_tracker;
|
||||
pub mod score;
|
||||
mod score;
|
||||
mod sort_buffer;
|
||||
|
||||
/// Core file picker: filesystem indexing, background watching, and fuzzy search.
|
||||
///
|
||||
/// See [`FilePicker`](file_picker::FilePicker) for the main entry point.
|
||||
pub mod file_picker;
|
||||
|
||||
/// Frecency (frequency + recency) database for file access scoring.
|
||||
///
|
||||
/// Backed by LMDB for persistent, crash-safe storage.
|
||||
pub mod frecency;
|
||||
|
||||
/// Git status caching and repository detection utilities.
|
||||
pub mod git;
|
||||
|
||||
/// Live grep search with regex, plain-text, and fuzzy matching modes.
|
||||
///
|
||||
/// Supports constraint filtering (file extensions, path segments, globs)
|
||||
/// and parallel execution via rayon.
|
||||
pub mod grep;
|
||||
|
||||
/// Tracing/logging initialization and panic hook setup.
|
||||
pub mod log;
|
||||
|
||||
/// Path manipulation utilities: cross platform canonicalization, tilde expansion, and
|
||||
/// directory distance penalties for search scoring.
|
||||
pub mod path_utils;
|
||||
|
||||
/// Search query history tracker for combo-boost scoring.
|
||||
///
|
||||
/// Records which files a user selects for each query, enabling the scorer
|
||||
/// to boost files that were previously chosen for similar searches.
|
||||
pub mod query_tracker;
|
||||
|
||||
/// Core data types shared across the crate.
|
||||
pub mod types;
|
||||
|
||||
use file_picker::FilePicker;
|
||||
use frecency::FrecencyTracker;
|
||||
use query_tracker::QueryTracker;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// Thread-safe shared handle to the [`FilePicker`] instance.
|
||||
pub type SharedPicker = Arc<RwLock<Option<FilePicker>>>;
|
||||
|
||||
/// Thread-safe shared handle to the [`FrecencyTracker`] instance.
|
||||
pub type SharedFrecency = Arc<RwLock<Option<FrecencyTracker>>>;
|
||||
|
||||
/// Thread-safe shared handle to the [`QueryTracker`] instance.
|
||||
pub type SharedQueryTracker = Arc<RwLock<Option<QueryTracker>>>;
|
||||
|
||||
pub use db_healthcheck::{DbHealth, DbHealthChecker};
|
||||
pub use error::{Error, Result};
|
||||
pub use fff_query_parser::{
|
||||
Constraint, FFFQuery, FuzzyQuery, Location, QueryParser, location::parse_location,
|
||||
};
|
||||
pub use file_picker::{FFFMode, FuzzySearchOptions, ScanProgress};
|
||||
pub use grep::{
|
||||
GrepMatch, GrepMode, GrepResult, GrepSearchOptions, has_regex_metacharacters,
|
||||
is_definition_line, is_import_line, multi_grep_search,
|
||||
};
|
||||
pub use types::{FileItem, PaginationArgs, Score, ScoringContext, SearchResult};
|
||||
pub use fff_query_parser::*;
|
||||
pub use file_picker::*;
|
||||
pub use frecency::*;
|
||||
pub use grep::*;
|
||||
pub use query_tracker::*;
|
||||
pub use types::*;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
//! Path utility functions for file picker scoring
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Canonicalize a path, resolving symlinks and producing an absolute path.
|
||||
///
|
||||
/// On Windows, uses `dunce::canonicalize` to avoid the `\\?\` extended-length path prefix
|
||||
/// that `std::fs::canonicalize` produces. Neovim cannot open paths with this prefix.
|
||||
/// On other platforms, delegates directly to `std::fs::canonicalize`.
|
||||
#[cfg(windows)]
|
||||
pub fn canonicalize(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
|
||||
dunce::canonicalize(path)
|
||||
|
||||
@@ -61,8 +61,10 @@ impl DbHealthChecker for QueryTracker {
|
||||
}
|
||||
|
||||
impl QueryTracker {
|
||||
pub fn new(db_path: &str, use_unsafe_no_lock: bool) -> Result<Self, Error> {
|
||||
pub fn new(db_path: impl AsRef<Path>, use_unsafe_no_lock: bool) -> Result<Self, Error> {
|
||||
let db_path = db_path.as_ref();
|
||||
fs::create_dir_all(db_path).map_err(Error::CreateDir)?;
|
||||
|
||||
let env = unsafe {
|
||||
let mut opts = EnvOpenOptions::new();
|
||||
opts.map_size(10 * 1024 * 1024); // 100 MiB
|
||||
|
||||
@@ -281,6 +281,7 @@ pub fn match_and_score_files<'a>(
|
||||
let combo_match_boost = {
|
||||
let last_same_query_match = context
|
||||
.last_same_query_match
|
||||
.as_ref()
|
||||
.filter(|m| m.file_path.as_os_str() == file.path.as_os_str());
|
||||
|
||||
match last_same_query_match {
|
||||
|
||||
@@ -210,6 +210,15 @@ pub struct PaginationArgs {
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
impl Default for PaginationArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for scoring files during search.
|
||||
///
|
||||
/// The `query` field contains the pre-parsed query with constraints,
|
||||
@@ -223,7 +232,7 @@ pub struct ScoringContext<'a> {
|
||||
pub current_file: Option<&'a str>,
|
||||
pub max_typos: u16,
|
||||
pub max_threads: usize,
|
||||
pub last_same_query_match: Option<&'a QueryMatchEntry>,
|
||||
pub last_same_query_match: Option<QueryMatchEntry>,
|
||||
pub combo_boost_score_multiplier: i32,
|
||||
pub min_combo_count: u32,
|
||||
pub pagination: PaginationArgs,
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use fff_core::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use fff_core::types::FileItem;
|
||||
use fff_search::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use fff_search::types::FileItem;
|
||||
|
||||
/// Create a file inside a temp dir and return its `FileItem`.
|
||||
fn create_file(base: &Path, relative: &str, contents: &str) -> FileItem {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
[package]
|
||||
name = "grep-searcher"
|
||||
version = "0.1.16"
|
||||
name = "fff-grep"
|
||||
description = "File grepping logic for fff"
|
||||
license = "MIT"
|
||||
authors = ["Dmitriy Kovalenko <dmtr.kovalenko@outlok.com>"]
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-mcp"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
description = "MCP server for FFF file finder - drop-in replacement for AI code assistant search tools"
|
||||
license = "MIT"
|
||||
@@ -11,11 +11,11 @@ path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["zlob"]
|
||||
zlob = ["fff-core/zlob"]
|
||||
zlob = ["fff/zlob"]
|
||||
|
||||
[dependencies]
|
||||
fff-core = { path = "../fff-core", default-features = false }
|
||||
fff-query-parser = { path = "../fff-query-parser", default-features = false }
|
||||
fff = { package = "fff-search", path = "../fff-core", default-features = false , version = "0.4.0" }
|
||||
fff-query-parser = { path = "../fff-query-parser", default-features = false , version = "0.4.0" }
|
||||
mimalloc = { workspace = true }
|
||||
rmcp = { version = "1.1.0", features = ["server", "transport-io"] }
|
||||
schemars = "1.0"
|
||||
|
||||
@@ -14,9 +14,9 @@ mod update_check;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use clap::Parser;
|
||||
use fff_core::file_picker::FilePicker;
|
||||
use fff_core::frecency::FrecencyTracker;
|
||||
use fff_core::{FFFMode, SharedFrecency, SharedPicker};
|
||||
use fff::file_picker::FilePicker;
|
||||
use fff::frecency::FrecencyTracker;
|
||||
use fff::{FFFMode, SharedFrecency, SharedPicker};
|
||||
use git2::Repository;
|
||||
use mimalloc::MiMalloc;
|
||||
use rmcp::{ServiceExt, transport::stdio};
|
||||
@@ -198,7 +198,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
resolve_defaults(&mut args);
|
||||
|
||||
let log_file = args.log_file.as_deref().unwrap_or("");
|
||||
if let Err(e) = fff_core::log::init_tracing(log_file, args.log_level.as_deref()) {
|
||||
if let Err(e) = fff::log::init_tracing(log_file, args.log_level.as_deref()) {
|
||||
eprintln!("Warning: Failed to init tracing: {}", e);
|
||||
}
|
||||
|
||||
@@ -223,7 +223,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Ok(mut guard) = shared_frecency.write() {
|
||||
*guard = Some(tracker);
|
||||
}
|
||||
FrecencyTracker::spawn_gc(Arc::clone(&shared_frecency), frecency_db_path, false);
|
||||
|
||||
let _ =
|
||||
FrecencyTracker::spawn_gc(Arc::clone(&shared_frecency), frecency_db_path, false);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to init frecency db: {}", e);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
//! Port of `packages/fff-mcp/src/output.ts` — token-efficient formatting
|
||||
//! with definition auto-expansion, frecency/git annotations, and Read suggestions.
|
||||
|
||||
use fff_core::GrepMatch;
|
||||
use fff_core::git::format_git_status_opt;
|
||||
use fff_core::grep::is_import_line;
|
||||
use fff_core::types::FileItem;
|
||||
use fff::GrepMatch;
|
||||
use fff::git::format_git_status_opt;
|
||||
use fff::grep::is_import_line;
|
||||
use fff::types::FileItem;
|
||||
|
||||
use crate::cursor::CursorStore;
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::borrow::Cow;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use fff_core::file_picker::FilePicker;
|
||||
use fff_core::grep::{self, GrepMode, GrepSearchOptions, has_regex_metacharacters};
|
||||
use fff_core::types::{FileItem, PaginationArgs};
|
||||
use fff_core::{FuzzySearchOptions, QueryParser, SharedFrecency, SharedPicker};
|
||||
use fff::file_picker::FilePicker;
|
||||
use fff::grep::{self, GrepMode, GrepSearchOptions, has_regex_metacharacters};
|
||||
use fff::types::{FileItem, PaginationArgs};
|
||||
use fff::{FuzzySearchOptions, QueryParser, SharedFrecency, SharedPicker};
|
||||
use fff_query_parser::AiGrepConfig;
|
||||
use rmcp::handler::server::router::tool::ToolRouter;
|
||||
use rmcp::handler::server::wrapper::Parameters;
|
||||
@@ -334,7 +334,6 @@ impl FffServer {
|
||||
max_threads: 0,
|
||||
current_file: None,
|
||||
project_path: Some(picker.base_path()),
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
@@ -342,7 +341,7 @@ impl FffServer {
|
||||
limit: 1,
|
||||
},
|
||||
};
|
||||
let file_result = FilePicker::fuzzy_search(files, &file_query, file_opts);
|
||||
let file_result = FilePicker::fuzzy_search(files, &file_query, None, file_opts);
|
||||
if let (Some(top), Some(score)) =
|
||||
(file_result.items.first(), file_result.scores.first())
|
||||
{
|
||||
@@ -423,7 +422,6 @@ impl FffServer {
|
||||
max_threads: 0,
|
||||
current_file: None,
|
||||
project_path: Some(base_path),
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
@@ -434,7 +432,7 @@ impl FffServer {
|
||||
|
||||
let parser = QueryParser::default();
|
||||
let fff_query = parser.parse(query);
|
||||
let result = FilePicker::fuzzy_search(files, &fff_query, make_opts(page_offset));
|
||||
let result = FilePicker::fuzzy_search(files, &fff_query, None, make_opts(page_offset));
|
||||
let total_files = result.total_files;
|
||||
|
||||
// Auto-retry with fewer terms if 3+ words return 0 results
|
||||
@@ -445,7 +443,12 @@ impl FffServer {
|
||||
if result.items.is_empty() && words.len() >= 3 && page_offset == 0 {
|
||||
if let Some(shorter) = &shorter {
|
||||
let shorter_query = parser.parse(shorter);
|
||||
let retry = FilePicker::fuzzy_search(files, &shorter_query, make_opts(0));
|
||||
let retry = FilePicker::fuzzy_search(
|
||||
files,
|
||||
&shorter_query,
|
||||
/*query_tracker=*/ None,
|
||||
make_opts(0),
|
||||
);
|
||||
|
||||
(retry.items, retry.scores, retry.total_matched)
|
||||
} else {
|
||||
|
||||
@@ -76,43 +76,3 @@ fn fetch_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_update_check_end_to_end() {
|
||||
// Fetch the actual latest release tag from GitHub
|
||||
let tag = fetch_latest_tag().expect("Failed to fetch latest release tag from GitHub");
|
||||
assert!(!tag.is_empty(), "Release tag should not be empty");
|
||||
|
||||
let notice = compare_versions(BUILD_HASH, &tag);
|
||||
let build_short = &BUILD_HASH[..BUILD_HASH.len().min(7)];
|
||||
|
||||
if BUILD_HASH.starts_with(tag.trim()) || tag.trim().starts_with(BUILD_HASH) {
|
||||
// If by chance we're on the exact release commit
|
||||
assert!(notice.is_empty(), "Should be empty when hashes match");
|
||||
} else {
|
||||
assert!(
|
||||
notice.contains("fff update available"),
|
||||
"Expected update notice for mismatched hashes (build: {}, release: {}), got: '{}'",
|
||||
build_short,
|
||||
tag.trim(),
|
||||
notice
|
||||
);
|
||||
assert!(
|
||||
notice.contains(tag.trim()),
|
||||
"Notice should contain release tag"
|
||||
);
|
||||
assert!(
|
||||
notice.contains(build_short),
|
||||
"Notice should contain our short hash"
|
||||
);
|
||||
assert!(
|
||||
notice.contains("install-mcp.sh"),
|
||||
"Notice should contain install command"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fff-nvim"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
zlob = ["fff-core/zlob"]
|
||||
zlob = ["fff/zlob"]
|
||||
|
||||
[[bin]]
|
||||
name = "test_watcher"
|
||||
@@ -44,8 +44,8 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Local crates
|
||||
fff-core = { path = "../fff-core" }
|
||||
fff-query-parser = { path = "../fff-query-parser" }
|
||||
fff = { package = "fff-search", path = "../fff-core" , version = "0.4.0" }
|
||||
fff-query-parser = { path = "../fff-query-parser" , version = "0.4.0" }
|
||||
|
||||
# External dependencies
|
||||
blake3 = "1.8.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use fff_core::file_picker::{FFFMode, FilePicker};
|
||||
use fff_core::types::{FileItem, PaginationArgs};
|
||||
use fff_core::{FuzzySearchOptions, QueryParser, SharedFrecency, SharedPicker};
|
||||
use fff::file_picker::{FFFMode, FilePicker};
|
||||
use fff::types::{FileItem, PaginationArgs};
|
||||
use fff::{FuzzySearchOptions, QueryParser, SharedFrecency, SharedPicker};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
@@ -129,7 +129,7 @@ fn setup_once() -> Result<(Vec<FileItem>, SharedPicker, SharedFrecency), String>
|
||||
return Err("./big-repo directory does not exist. Run git clone https://github.com/torvalds/linux.git big-repo".to_string());
|
||||
}
|
||||
|
||||
let canonical_path = fff_core::path_utils::canonicalize(&big_repo_path)
|
||||
let canonical_path = fff::path_utils::canonicalize(&big_repo_path)
|
||||
.map_err(|e| format!("Failed to canonicalize path: {}", e))?;
|
||||
eprintln!(" Path: {:?}", canonical_path);
|
||||
|
||||
@@ -165,7 +165,7 @@ fn bench_indexing(c: &mut Criterion) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canonical_path = match fff_core::path_utils::canonicalize(&big_repo_path) {
|
||||
let canonical_path = match fff::path_utils::canonicalize(&big_repo_path) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("⚠ Failed to canonicalize path: {}", e);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use fff_core::query_tracker::QueryTracker;
|
||||
use fff::query_tracker::QueryTracker;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Simple search profiler that directly uses scan_filesystem without background thread overhead
|
||||
use fff_core::file_picker::FilePicker;
|
||||
use fff_core::{FileItem, FuzzySearchOptions, PaginationArgs, QueryParser};
|
||||
use fff::file_picker::FilePicker;
|
||||
use fff::{FileItem, FuzzySearchOptions, PaginationArgs, QueryParser};
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
@@ -14,7 +14,7 @@ fn main() {
|
||||
}
|
||||
|
||||
let canonical_path =
|
||||
fff_core::path_utils::canonicalize(&big_repo_path).expect("Failed to canonicalize path");
|
||||
fff::path_utils::canonicalize(&big_repo_path).expect("Failed to canonicalize path");
|
||||
|
||||
eprintln!("Loading files from: {:?}", canonical_path);
|
||||
|
||||
@@ -88,11 +88,11 @@ fn main() {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
&files,
|
||||
&parsed,
|
||||
None,
|
||||
FuzzySearchOptions {
|
||||
max_threads: 4,
|
||||
current_file: None,
|
||||
project_path: None,
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use fff_core::FileItem;
|
||||
use fff::FileItem;
|
||||
/// Fuzzy grep quality test against ~/dev/lightsource
|
||||
///
|
||||
/// Runs queries through the fuzzy grep pipeline and prints results
|
||||
@@ -7,7 +7,7 @@ use fff_core::FileItem;
|
||||
/// Usage:
|
||||
/// cargo run --release --bin fuzzy_grep_test # runs default test queries
|
||||
/// cargo run --release --bin fuzzy_grep_test -- "query" # runs a single user query
|
||||
use fff_core::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use fff::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
@@ -166,8 +166,7 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let canonical =
|
||||
fff_core::path_utils::canonicalize(&repo_path).expect("Failed to canonicalize path");
|
||||
let canonical = fff::path_utils::canonicalize(&repo_path).expect("Failed to canonicalize path");
|
||||
eprintln!("=== Fuzzy Grep Quality Test ===");
|
||||
eprintln!("Repository: {:?}\n", canonical);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use fff_core::FileItem;
|
||||
use fff::FileItem;
|
||||
/// Live grep benchmark profiler for fff.nvim
|
||||
///
|
||||
/// Benchmarks the full grep pipeline against a large repository (Linux kernel).
|
||||
@@ -10,7 +10,7 @@ use fff_core::FileItem;
|
||||
/// Usage:
|
||||
/// cargo build --release --bin grep_profiler
|
||||
/// ./target/release/grep_profiler [--path /path/to/repo]
|
||||
use fff_core::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use fff::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -222,7 +222,7 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let canonical = fff_core::path_utils::canonicalize(&repo).expect("Failed to canonicalize path");
|
||||
let canonical = fff::path_utils::canonicalize(&repo).expect("Failed to canonicalize path");
|
||||
eprintln!("=== FFF Live Grep Profiler ===");
|
||||
eprintln!("Repository: {:?}", canonical);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fff_core::FFFQuery;
|
||||
use fff_core::FileItem;
|
||||
use fff::FFFQuery;
|
||||
use fff::FileItem;
|
||||
/// FFF vs ripgrep comparison benchmark
|
||||
///
|
||||
/// Demonstrates why a persistent in-process search engine (fff) is fundamentally
|
||||
@@ -21,7 +21,7 @@ use fff_core::FileItem;
|
||||
/// Usage:
|
||||
/// cargo build --release --bin grep_vs_rg
|
||||
/// ./target/release/grep_vs_rg [--path /path/to/repo] [--iters 5]
|
||||
use fff_core::grep::{GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use fff::grep::{GrepSearchOptions, grep_search, parse_grep_query};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -319,7 +319,7 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let canonical = fff_core::path_utils::canonicalize(&repo).expect("Failed to canonicalize path");
|
||||
let canonical = fff::path_utils::canonicalize(&repo).expect("Failed to canonicalize path");
|
||||
|
||||
let rg_version = Command::new("rg")
|
||||
.arg("--version")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fff_core::file_picker::{FFFMode, FilePicker};
|
||||
use fff_core::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
|
||||
use fff::file_picker::{FFFMode, FilePicker};
|
||||
use fff::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
|
||||
use std::env;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
@@ -91,11 +91,11 @@ fn test_search_memory_pattern(
|
||||
let search_result = FilePicker::fuzzy_search(
|
||||
picker.get_files(),
|
||||
&parsed,
|
||||
None,
|
||||
FuzzySearchOptions {
|
||||
max_threads: 1 + (i % 4),
|
||||
current_file: None,
|
||||
project_path: None,
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fff_core::file_picker::{FFFMode, FilePicker};
|
||||
use fff_core::{
|
||||
use fff::file_picker::{FFFMode, FilePicker};
|
||||
use fff::{
|
||||
FileItem, FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
@@ -71,7 +71,7 @@ fn main() {
|
||||
}
|
||||
|
||||
let canonical_path =
|
||||
fff_core::path_utils::canonicalize(&big_repo_path).expect("Failed to canonicalize path");
|
||||
fff::path_utils::canonicalize(&big_repo_path).expect("Failed to canonicalize path");
|
||||
|
||||
// Create shared state
|
||||
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
|
||||
@@ -127,11 +127,11 @@ fn main() {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
&files,
|
||||
&parsed,
|
||||
None,
|
||||
FuzzySearchOptions {
|
||||
max_threads: 4,
|
||||
current_file: None,
|
||||
project_path: None,
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fff_core::file_picker::{FFFMode, FilePicker};
|
||||
use fff_core::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
|
||||
use fff::file_picker::{FFFMode, FilePicker};
|
||||
use fff::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::{Arc, RwLock};
|
||||
@@ -201,11 +201,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let search_result = FilePicker::fuzzy_search(
|
||||
picker.get_files(),
|
||||
&parsed,
|
||||
None,
|
||||
FuzzySearchOptions {
|
||||
max_threads,
|
||||
current_file: None,
|
||||
project_path: None,
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::enum_variant_names)]
|
||||
|
||||
use fff_core::file_picker::FilePicker;
|
||||
use fff_core::git::format_git_status;
|
||||
use fff_core::{
|
||||
FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker,
|
||||
};
|
||||
use fff::file_picker::FilePicker;
|
||||
use fff::git::format_git_status;
|
||||
use fff::{FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -156,11 +154,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let search_results = FilePicker::fuzzy_search(
|
||||
files,
|
||||
&parsed,
|
||||
None,
|
||||
FuzzySearchOptions {
|
||||
max_threads: 2,
|
||||
current_file: None,
|
||||
project_path: None,
|
||||
last_same_query_match: None,
|
||||
combo_boost_score_multiplier: 100,
|
||||
min_combo_count: 3,
|
||||
pagination: PaginationArgs {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Error handling for fff-nvim
|
||||
//!
|
||||
//! This module provides utilities for converting fff_core errors to mlua errors.
|
||||
//! This module provides utilities for converting fff errors to mlua errors.
|
||||
|
||||
use fff_core::Error as CoreError;
|
||||
use fff::Error as CoreError;
|
||||
|
||||
/// Convert a fff_core::Error to mlua::Error
|
||||
/// Convert a fff::Error to mlua::Error
|
||||
///
|
||||
/// This function is used because we can't implement From<CoreError> for mlua::Error
|
||||
/// due to Rust's orphan rules (both types are foreign to this crate).
|
||||
@@ -14,7 +14,7 @@ pub fn to_lua_error(err: CoreError) -> mlua::Error {
|
||||
mlua::Error::RuntimeError(string_value)
|
||||
}
|
||||
|
||||
/// Extension trait for Result<T, fff_core::Error> to convert to LuaResult<T>
|
||||
/// Extension trait for Result<T, fff::Error> to convert to LuaResult<T>
|
||||
pub trait IntoLuaResult<T> {
|
||||
fn into_lua_result(self) -> mlua::Result<T>;
|
||||
}
|
||||
|
||||
+28
-39
@@ -1,12 +1,12 @@
|
||||
use crate::path_shortening::shorten_path_with_cache;
|
||||
use error::{IntoCoreError, IntoLuaResult};
|
||||
use fff_core::file_picker::FilePicker;
|
||||
use fff_core::frecency::FrecencyTracker;
|
||||
use fff_core::path_utils::expand_tilde;
|
||||
use fff_core::query_tracker::QueryTracker;
|
||||
use fff_core::{
|
||||
DbHealthChecker, Error, FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser, Score,
|
||||
SearchResult, SharedFrecency, SharedPicker, SharedQueryTracker,
|
||||
use fff::file_picker::FilePicker;
|
||||
use fff::frecency::FrecencyTracker;
|
||||
use fff::path_utils::expand_tilde;
|
||||
use fff::query_tracker::QueryTracker;
|
||||
use fff::{
|
||||
DbHealthChecker, Error, FFFMode, FileSearchConfig, FuzzySearchOptions, PaginationArgs,
|
||||
QueryParser, Score, SearchResult, SharedFrecency, SharedPicker, SharedQueryTracker,
|
||||
};
|
||||
use mimalloc::MiMalloc;
|
||||
use mlua::prelude::*;
|
||||
@@ -48,7 +48,7 @@ pub fn init_db(
|
||||
drop(frecency);
|
||||
|
||||
// Spawn background GC to purge stale entries without blocking startup
|
||||
FrecencyTracker::spawn_gc(Arc::clone(&FRECENCY), frecency_db_path, use_unsafe_no_lock);
|
||||
let _ = FrecencyTracker::spawn_gc(Arc::clone(&FRECENCY), frecency_db_path, use_unsafe_no_lock);
|
||||
|
||||
let mut query_tracker = QUERY_TRACKER
|
||||
.write()
|
||||
@@ -58,10 +58,10 @@ pub fn init_db(
|
||||
*query_tracker = None;
|
||||
}
|
||||
|
||||
let tracker = QueryTracker::new(&history_db_path, use_unsafe_no_lock).into_lua_result()?;
|
||||
*query_tracker = Some(tracker);
|
||||
tracing::info!("Query tracker database initialized at {}", history_db_path);
|
||||
*query_tracker =
|
||||
Some(QueryTracker::new(&history_db_path, use_unsafe_no_lock).into_lua_result()?);
|
||||
|
||||
tracing::info!("Query tracker database initialized at {}", history_db_path);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ pub fn restart_index_in_path(_: &Lua, new_path: String) -> LuaResult<()> {
|
||||
)));
|
||||
}
|
||||
|
||||
let canonical_path = fff_core::path_utils::canonicalize(&path).map_err(|e| {
|
||||
let canonical_path = fff::path_utils::canonicalize(&path).map_err(|e| {
|
||||
LuaError::RuntimeError(format!("Failed to canonicalize path '{}': {}", new_path, e))
|
||||
})?;
|
||||
|
||||
@@ -218,26 +218,16 @@ pub fn fuzzy_search_files(
|
||||
let base_path = picker.base_path();
|
||||
let min_combo_count = min_combo_count.unwrap_or(3);
|
||||
|
||||
let last_same_query_entry = {
|
||||
let query_tracker = QUERY_TRACKER
|
||||
.read()
|
||||
.with_lock_error(Error::AcquireFrecencyLock)
|
||||
.into_lua_result()?;
|
||||
let query_tracker_guard = QUERY_TRACKER
|
||||
.read()
|
||||
.with_lock_error(Error::AcquireFrecencyLock)
|
||||
.into_lua_result()?;
|
||||
|
||||
if query_tracker.as_ref().is_none() {
|
||||
tracing::warn!("Query tracker not initialized");
|
||||
}
|
||||
|
||||
query_tracker
|
||||
.as_ref()
|
||||
.map(|tracker| tracker.get_last_query_entry(&query, base_path, min_combo_count))
|
||||
.transpose()
|
||||
.into_lua_result()?
|
||||
.flatten()
|
||||
};
|
||||
if query_tracker_guard.as_ref().is_none() {
|
||||
tracing::warn!("Query tracker not initialized");
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
?last_same_query_entry,
|
||||
?base_path,
|
||||
?query,
|
||||
?min_combo_count,
|
||||
@@ -246,19 +236,18 @@ pub fn fuzzy_search_files(
|
||||
"Fuzzy search parameters"
|
||||
);
|
||||
|
||||
// Parse the query once at the API boundary
|
||||
let parser = QueryParser::default();
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let parsed = parser.parse(&query);
|
||||
|
||||
let files = picker.get_files();
|
||||
let results = FilePicker::fuzzy_search(
|
||||
files,
|
||||
&parsed,
|
||||
query_tracker_guard.as_ref(),
|
||||
FuzzySearchOptions {
|
||||
max_threads,
|
||||
current_file: current_file.as_deref(),
|
||||
project_path: Some(picker.base_path()),
|
||||
last_same_query_match: last_same_query_entry.as_ref(),
|
||||
combo_boost_score_multiplier,
|
||||
min_combo_count,
|
||||
pagination: PaginationArgs {
|
||||
@@ -330,15 +319,15 @@ pub fn live_grep(
|
||||
return Err(error::to_lua_error(Error::FilePickerMissing));
|
||||
};
|
||||
|
||||
let parsed = fff_core::grep::parse_grep_query(&query);
|
||||
let parsed = fff::grep::parse_grep_query(&query);
|
||||
|
||||
let mode = match grep_mode.as_deref() {
|
||||
Some("regex") => fff_core::GrepMode::Regex,
|
||||
Some("fuzzy") => fff_core::GrepMode::Fuzzy,
|
||||
_ => fff_core::GrepMode::PlainText, // "plain" or nil or unknown
|
||||
Some("regex") => fff::GrepMode::Regex,
|
||||
Some("fuzzy") => fff::GrepMode::Fuzzy,
|
||||
_ => fff::GrepMode::PlainText, // "plain" or nil or unknown
|
||||
};
|
||||
|
||||
let options = fff_core::GrepSearchOptions {
|
||||
let options = fff::GrepSearchOptions {
|
||||
max_file_size: max_file_size.unwrap_or(10 * 1024 * 1024),
|
||||
max_matches_per_file: max_matches_per_file.unwrap_or(200),
|
||||
smart_case: smart_case.unwrap_or(true),
|
||||
@@ -351,7 +340,7 @@ pub fn live_grep(
|
||||
classify_definitions: false,
|
||||
};
|
||||
|
||||
let result = fff_core::grep::grep_search(picker.get_files(), &parsed, &options);
|
||||
let result = fff::grep::grep_search(picker.get_files(), &parsed, &options);
|
||||
|
||||
lua_types::GrepResultLua::from(result).into_lua(lua)
|
||||
}
|
||||
@@ -561,7 +550,7 @@ pub fn track_query_completion(_: &Lua, (query, file_path): (String, String)) ->
|
||||
};
|
||||
|
||||
// Canonicalize the file path before spawning thread
|
||||
let file_path = match fff_core::path_utils::canonicalize(&file_path) {
|
||||
let file_path = match fff::path_utils::canonicalize(&file_path) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::warn!(?file_path, error = ?e, "Failed to canonicalize file path for tracking");
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
//! Logging setup for fff-nvim — delegates to the shared fff-core::log utilities.
|
||||
|
||||
pub use fff_core::log::{init_tracing, install_panic_hook};
|
||||
pub use fff::log::{init_tracing, install_panic_hook};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use fff_core::git::format_git_status;
|
||||
use fff_core::{FileItem, GrepResult, Location, Score, SearchResult};
|
||||
use fff::git::format_git_status;
|
||||
use fff::{FileItem, GrepResult, Location, Score, SearchResult};
|
||||
use mlua::prelude::*;
|
||||
|
||||
pub struct SearchResultLua<'a> {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
[package]
|
||||
name = "fff-query-parser"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
description = "Query parser for fff file finder - includes specific syntax for various constraints like globs, extensions, regex etc"
|
||||
license = "MIT"
|
||||
authors = ["Dmitriy Kovalenko <dmtr.kovalenko@outlok.com>"]
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
@@ -11,7 +14,6 @@ default = []
|
||||
zlob = ["dep:zlob"]
|
||||
|
||||
[dependencies]
|
||||
smallvec = { workspace = true }
|
||||
zlob = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -98,7 +98,7 @@ fn bench_parse_various_lengths(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_config_comparison(c: &mut Criterion) {
|
||||
let file_picker = QueryParser::new(FilePickerConfig);
|
||||
let file_picker = QueryParser::new(FileSearchConfig);
|
||||
let grep = QueryParser::new(GrepConfig);
|
||||
|
||||
let query = "src name *.rs !test";
|
||||
|
||||
@@ -99,9 +99,9 @@ pub trait ParserConfig {
|
||||
|
||||
/// Default configuration for file picker - all features enabled
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct FilePickerConfig;
|
||||
pub struct FileSearchConfig;
|
||||
|
||||
impl ParserConfig for FilePickerConfig {
|
||||
impl ParserConfig for FileSearchConfig {
|
||||
/// Detect bare filenames (`score.rs`) and path-prefixed filenames (`src/main.rs`)
|
||||
/// as `FilePath` constraints so that multi-token queries like `score.rs file_picker`
|
||||
/// filter by filename first, then fuzzy-match the remaining text against the path.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Constraint types that can be extracted from a query
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Constraint<'a> {
|
||||
@@ -45,5 +43,5 @@ pub enum GitStatusFilter {
|
||||
Unmodified,
|
||||
}
|
||||
|
||||
/// Stack-allocated buffer for text parts (up to 16 parts without heap allocation)
|
||||
pub(crate) type TextPartsBuffer<'a> = SmallVec<[&'a str; 16]>;
|
||||
/// Buffer for text parts during query parsing.
|
||||
pub(crate) type TextPartsBuffer<'a> = Vec<&'a str>;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Fast, zero-allocation query parser for file search
|
||||
//! Fast query parser for file search
|
||||
//!
|
||||
//! This parser takes a search query and extracts structured constraints
|
||||
//! while preserving text for fuzzy matching. Designed for maximum performance:
|
||||
//! - Zero allocations for queries with ≤8 constraints (SmallVec)
|
||||
//! - Single-pass parsing with minimal branching
|
||||
//! - Stack-allocated string buffers
|
||||
//!
|
||||
@@ -46,16 +45,12 @@ pub mod glob_detect;
|
||||
pub mod location;
|
||||
mod parser;
|
||||
|
||||
pub use config::{AiGrepConfig, FilePickerConfig, GrepConfig, ParserConfig};
|
||||
pub use config::{AiGrepConfig, FileSearchConfig, GrepConfig, ParserConfig};
|
||||
pub use constraints::{Constraint, GitStatusFilter};
|
||||
pub use location::Location;
|
||||
pub use parser::{FFFQuery, FuzzyQuery, QueryParser};
|
||||
|
||||
// Re-export SmallVec for convenience
|
||||
pub use smallvec::SmallVec;
|
||||
|
||||
/// Type alias for constraint vector - stack-allocated for ≤8 constraints
|
||||
pub type ConstraintVec<'a> = SmallVec<[Constraint<'a>; 8]>;
|
||||
pub type ConstraintVec<'a> = Vec<Constraint<'a>>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -213,11 +208,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_heap_allocation_for_small_queries() {
|
||||
fn test_small_constraint_count() {
|
||||
let parser = QueryParser::default();
|
||||
let result = parser.parse("*.rs *.toml !test");
|
||||
// SmallVec should not have spilled to heap
|
||||
assert!(!result.constraints.spilled());
|
||||
assert_eq!(result.constraints.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -208,9 +208,9 @@ fn strip_leading_backslash(token: &str) -> &str {
|
||||
token
|
||||
}
|
||||
|
||||
impl Default for QueryParser<crate::FilePickerConfig> {
|
||||
impl Default for QueryParser<crate::FileSearchConfig> {
|
||||
fn default() -> Self {
|
||||
Self::new(crate::FilePickerConfig)
|
||||
Self::new(crate::FileSearchConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +437,7 @@ fn parse_git_status(value: &str) -> Option<Constraint<'_>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{FilePickerConfig, GrepConfig};
|
||||
use crate::{FileSearchConfig, GrepConfig};
|
||||
|
||||
#[test]
|
||||
fn test_parse_extension() {
|
||||
@@ -452,7 +452,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_incomplete_patterns_ignored() {
|
||||
let config = FilePickerConfig;
|
||||
let config = FileSearchConfig;
|
||||
// Incomplete patterns should return None and be treated as noise
|
||||
assert_eq!(parse_token("*", &config), None);
|
||||
assert_eq!(parse_token("*.", &config), None);
|
||||
@@ -496,7 +496,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_trailing_slash_in_query() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("www/ test");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
assert!(matches!(
|
||||
@@ -532,7 +532,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_negation_text() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
// Need two tokens for parsing to return Some
|
||||
let result = parser.parse("!test foo");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
@@ -546,7 +546,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_negation_extension() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("!*.rs foo");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
match &result.constraints[0] {
|
||||
@@ -559,7 +559,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_negation_path_segment() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("!/src/ foo");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
match &result.constraints[0] {
|
||||
@@ -572,7 +572,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_negation_git_status() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("!status:modified foo");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
match &result.constraints[0] {
|
||||
@@ -588,7 +588,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backslash_escape_extension() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("\\*.rs foo");
|
||||
// \*.rs should NOT be parsed as an Extension constraint
|
||||
assert_eq!(result.constraints.len(), 0);
|
||||
@@ -605,7 +605,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backslash_escape_path_segment() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("\\/src/ foo");
|
||||
assert_eq!(result.constraints.len(), 0);
|
||||
match result.fuzzy_query {
|
||||
@@ -619,7 +619,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backslash_escape_negation() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("\\!test foo");
|
||||
assert_eq!(result.constraints.len(), 0);
|
||||
}
|
||||
@@ -1039,7 +1039,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_bare_filename_constraint() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("score.rs file_picker");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
assert!(
|
||||
@@ -1052,7 +1052,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_path_prefixed_filename_constraint() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("libswscale/slice.c lum_convert");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
assert!(
|
||||
@@ -1068,7 +1068,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_single_token_filename_stays_fuzzy() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
// Single-token filename should NOT become a constraint -- it should
|
||||
// return FFFQuery with Text fuzzy query so the caller uses it for fuzzy matching.
|
||||
let result = parser.parse("score.rs");
|
||||
@@ -1078,7 +1078,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_absolute_path_with_location_not_path_segment() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
// Absolute file path with :line should parse as text + location,
|
||||
// NOT as a PathSegment constraint (which would eat the whole token).
|
||||
let result = parser.parse("/Users/neogoose/dev/fframes/src/renderer/concatenator.rs:12");
|
||||
@@ -1096,7 +1096,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_filename_with_multiple_fuzzy_parts() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("main.rs src components");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
assert!(matches!(
|
||||
@@ -1105,13 +1105,13 @@ mod tests {
|
||||
));
|
||||
assert_eq!(
|
||||
result.fuzzy_query,
|
||||
FuzzyQuery::Parts(smallvec::smallvec!["src", "components"])
|
||||
FuzzyQuery::Parts(vec!["src", "components"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_version_number_not_filename() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("v2.0 release");
|
||||
// v2.0 extension starts with digit → not a filename constraint
|
||||
assert!(
|
||||
@@ -1123,7 +1123,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_only_one_filepath_constraint() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("main.rs score.rs");
|
||||
// Only first filename becomes a constraint; second is text
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
@@ -1136,7 +1136,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_filename_with_extension_constraint() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("main.rs *.lua");
|
||||
// main.rs → FilePath, *.lua → Extension
|
||||
assert_eq!(result.constraints.len(), 2);
|
||||
@@ -1152,7 +1152,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_dotfile_is_filename() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse(".gitignore src");
|
||||
assert_eq!(result.constraints.len(), 1);
|
||||
assert!(
|
||||
@@ -1165,7 +1165,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_file_picker_no_extension_not_filename() {
|
||||
let parser = QueryParser::new(FilePickerConfig);
|
||||
let parser = QueryParser::new(FileSearchConfig);
|
||||
let result = parser.parse("Makefile src");
|
||||
// No dot → not a filename constraint
|
||||
assert!(
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 March 17
|
||||
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 March 18
|
||||
|
||||
==============================================================================
|
||||
Table of Contents *fff.nvim-table-of-contents*
|
||||
|
||||
Generated
+9
-9
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1773189535,
|
||||
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
|
||||
"lastModified": 1773857772,
|
||||
"narHash": "sha256-5xsK26KRHf0WytBtsBnQYC/lTWDhQuT57HJ7SzuqZcM=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
|
||||
"rev": "b556d7bbae5ff86e378451511873dfd07e4504cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -35,11 +35,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1773597492,
|
||||
"narHash": "sha256-hQ284SkIeNaeyud+LS0WVLX+WL2rxcVZLFEaK0e03zg=",
|
||||
"lastModified": 1773628058,
|
||||
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a07d4ce6bee67d7c838a8a5796e75dff9caa21ef",
|
||||
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -64,11 +64,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773716879,
|
||||
"narHash": "sha256-vXCTasEzzTTd0ZGEuyle20H2hjRom66JeNr7i2ktHD0=",
|
||||
"lastModified": 1773803479,
|
||||
"narHash": "sha256-GD6i1F2vrSxbsmbS92+8+x3DbHOJ+yrS78Pm4xigW4M=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "1a9ddeb45c5751b800331363703641b84d1f41f0",
|
||||
"rev": "f17186f52e82ec5cf40920b58eac63b78692ac7c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
Generated
+709
@@ -0,0 +1,709 @@
|
||||
{
|
||||
"name": "fff.nvim",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"workspaces": [
|
||||
"packages/fff-bun",
|
||||
"packages/fff-node"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/biome": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.7.tgz",
|
||||
"integrity": "sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"bin": {
|
||||
"biome": "bin/biome"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/biome"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@biomejs/cli-darwin-arm64": "2.4.7",
|
||||
"@biomejs/cli-darwin-x64": "2.4.7",
|
||||
"@biomejs/cli-linux-arm64": "2.4.7",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.4.7",
|
||||
"@biomejs/cli-linux-x64": "2.4.7",
|
||||
"@biomejs/cli-linux-x64-musl": "2.4.7",
|
||||
"@biomejs/cli-win32-arm64": "2.4.7",
|
||||
"@biomejs/cli-win32-x64": "2.4.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.7.tgz",
|
||||
"integrity": "sha512-Oo0cF5mHzmvDmTXw8XSjhCia8K6YrZnk7aCS54+/HxyMdZMruMO3nfpDsrlar/EQWe41r1qrwKiCa2QDYHDzWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-x64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.7.tgz",
|
||||
"integrity": "sha512-I+cOG3sd/7HdFtvDSnF9QQPrWguUH7zrkIMMykM3PtfWU9soTcS2yRb9Myq6MHmzbeCT08D1UmY+BaiMl5CcoQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.7.tgz",
|
||||
"integrity": "sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.7.tgz",
|
||||
"integrity": "sha512-I2NvM9KPb09jWml93O2/5WMfNR7Lee5Latag1JThDRMURVhPX74p9UDnyTw3Ae6cE1DgXfw7sqQgX7rkvpc0vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.7.tgz",
|
||||
"integrity": "sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.7.tgz",
|
||||
"integrity": "sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-arm64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.7.tgz",
|
||||
"integrity": "sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-x64": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.7.tgz",
|
||||
"integrity": "sha512-qEpGjSkPC3qX4ycbMUthXvi9CkRq7kZpkqMY1OyhmYlYLnANnooDQ7hDerM8+0NJ+DZKVnsIc07h30XOpt7LtQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ff-labs/fff-bun": {
|
||||
"resolved": "packages/fff-bun",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ff-labs/fff-node": {
|
||||
"resolved": "packages/fff-node",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-aarch64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.10.tgz",
|
||||
"integrity": "sha512-PXgg5gqcS/rHwa1hF0JdM1y5TiyejVrMHoBmWY/DjtfYZoFTXie1RCFOkoG0b5diOOmUcuYarMpH7CSNTqwj+w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-x64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.10.tgz",
|
||||
"integrity": "sha512-Nhssuh7GBpP5PiDSOl3+qnoIG7PJo+ec2oomDevnl9pRY6x6aD2gRt0JE+uf+A8Om2D6gjeHCxjEdrw5ZHE8mA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-darwin-x64-baseline": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.10.tgz",
|
||||
"integrity": "sha512-w1gaTlqU0IJCmJ1X+PGHkdNU1n8Gemx5YKkjhkJIguvFINXEBB5U1KG82QsT65Tk4KyNMfbLTlmy4giAvUoKfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-aarch64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.10.tgz",
|
||||
"integrity": "sha512-OUgPHfL6+PM2Q+tFZjcaycN3D7gdQdYlWnwMI31DXZKY1r4HINWk9aEz9t/rNaHg65edwNrt7dsv9TF7xK8xIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-aarch64-musl": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.10.tgz",
|
||||
"integrity": "sha512-Ui5pAgM7JE9MzHokF0VglRMkbak3lTisY4Mf1AZutPACXWgKJC5aGrgnHBfkl7QS6fEeYb0juy1q4eRznRHOsw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.10.tgz",
|
||||
"integrity": "sha512-bzUgYj/PIZziB/ZesIP9HUyfvh6Vlf3od+TrbTTyVEuCSMKzDPQVW/yEbRp0tcHO3alwiEXwJDrWrHAguXlgiQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-baseline": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.10.tgz",
|
||||
"integrity": "sha512-oqvMDYpX6dGJO03HgO5bXuccEsH3qbdO3MaAiAlO4CfkBPLUXz3N0DDElg5hz0L6ktdDVKbQVE5lfe+LAUISQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-musl": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.10.tgz",
|
||||
"integrity": "sha512-poVXvOShekbexHq45b4MH/mRjQKwACAC8lHp3Tz/hEDuz0/20oncqScnmKwzhBPEpqJvydXficXfBYuSim8opw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-linux-x64-musl-baseline": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.10.tgz",
|
||||
"integrity": "sha512-/hOZ6S1VsTX6vtbhWVL9aAnOrdpuO54mAGUWpTdMz7dFG5UBZ/VUEiK0pBkq9A1rlBk0GeD/6Y4NBFl8Ha7cRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-windows-aarch64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-aarch64/-/bun-windows-aarch64-1.3.10.tgz",
|
||||
"integrity": "sha512-GXbz2swvN2DLw2dXZFeedMxSJtI64xQ9xp9Eg7Hjejg6mS2E4dP1xoQ2yAo2aZPi/2OBPAVaGzppI2q20XumHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-windows-x64": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.10.tgz",
|
||||
"integrity": "sha512-qaS1In3yfC/Z/IGQriVmF8GWwKuNqiw7feTSJWaQhH5IbL6ENR+4wGNPniZSJFaM/SKUO0e/YCRdoVBvgU4C1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@oven/bun-windows-x64-baseline": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.10.tgz",
|
||||
"integrity": "sha512-gh3UAHbUdDUG6fhLc1Csa4IGdtghue6U8oAIXWnUqawp6lwb3gOCRvp25IUnLF5vUHtgfMxuEUYV7YA2WxVutw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.10.tgz",
|
||||
"integrity": "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bun-types": "1.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-android-arm64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-android-arm64/-/ffi-rs-android-arm64-1.3.1.tgz",
|
||||
"integrity": "sha512-V4nmlXdOYZEa7GOxSExVG95SLp8FE0iTq2yKeN54UlfNMr3Sik+1Ff57LcCv7qYcn4TBqnBAt5rT3FAM6T6caQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-darwin-arm64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-darwin-arm64/-/ffi-rs-darwin-arm64-1.3.1.tgz",
|
||||
"integrity": "sha512-YlnTMIyzfW3mAULC5ZA774nzQfFlYXM0rrfq/8ZzWt+IMbYk55a++jrI+6JeKV+1EqlDS3TFBEFtjdBNG94KzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-darwin-x64": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-darwin-x64/-/ffi-rs-darwin-x64-1.3.1.tgz",
|
||||
"integrity": "sha512-sI3LpQQ34SX4nyOHc5yxA7FSqs9qPEUMqW/y/wWo9cuyPpaHMFsi/BeOVYsnC0syp3FrY7gzn6RnD6PlXCktXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-linux-arm-gnueabihf": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-linux-arm-gnueabihf/-/ffi-rs-linux-arm-gnueabihf-1.3.1.tgz",
|
||||
"integrity": "sha512-1WkcGkJTlwh4ZA59htKI+RXhiL3oKiYwLv7PO8LUf6FuADK73s5GcXp67iakKu243uYu+qGYr4RHco4ySddYhQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-linux-arm64-gnu": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-linux-arm64-gnu/-/ffi-rs-linux-arm64-gnu-1.3.1.tgz",
|
||||
"integrity": "sha512-J2PwqviycZxaEVA0Bwv38LqGDGSB9A1DPN4iYginYJZSvTvKW8kh7Tis0HbZrX1YDKnY8hi3lt0N0tCTNPDH5Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-linux-arm64-musl": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-linux-arm64-musl/-/ffi-rs-linux-arm64-musl-1.3.1.tgz",
|
||||
"integrity": "sha512-Hn1W1hBPssTaqikU1Bqp1XUdDdOgbnYVIOtR++LVx66hhrtjf/xrIUQOhTm+NmOFDG16JUKXe1skfM4gpaqYwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-linux-x64-gnu": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-linux-x64-gnu/-/ffi-rs-linux-x64-gnu-1.3.1.tgz",
|
||||
"integrity": "sha512-kW6e+oCYZPvpH2ppPsffA18e1aLowtmWTRjVlyHtY04g/nQDepQvDUkkcvInh9fW5jLna7PjHvktW1tVgYIj2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-linux-x64-musl": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-linux-x64-musl/-/ffi-rs-linux-x64-musl-1.3.1.tgz",
|
||||
"integrity": "sha512-HTwblAzruUS16nQPrez3ozvEHm1Xxh8J8w7rZYrpmAcNl1hzyOT8z/hY70M9Rt9fOqQ4Ovgor9qVy/U3ZJo0ZA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-win32-arm64-msvc": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-win32-arm64-msvc/-/ffi-rs-win32-arm64-msvc-1.3.1.tgz",
|
||||
"integrity": "sha512-WeZkGl2BP1U4tRhEQH+FXLQS52N8obp74smK5AAGOfzPAT1pHkq6+dVkC1QCSIt7dHJs7SPtlnQw+5DkdZYlWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-win32-ia32-msvc": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-win32-ia32-msvc/-/ffi-rs-win32-ia32-msvc-1.3.1.tgz",
|
||||
"integrity": "sha512-rNGgMeCH5mdeHiMiJgt7wWXovZ+FHEfXhU9p4zZBH4n8M1/QnEsRUwlapISPLpILSGpoYS6iBuq9/fUlZY8Mhg==",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/@yuuang/ffi-rs-win32-x64-msvc": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-win32-x64-msvc/-/ffi-rs-win32-x64-msvc-1.3.1.tgz",
|
||||
"integrity": "sha512-dr2LcLD2CXo2a7BktlOpV68QhayqiI112KxIJC9tBgQO/Dkdg4CPsdqmvzzLhFo64iC5RLl2BT7M5lJImrfUWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/bun": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/bun/-/bun-1.3.10.tgz",
|
||||
"integrity": "sha512-S/CXaXXIyA4CMjdMkYQ4T2YMqnAn4s0ysD3mlsY4bUiOCqGlv28zck4Wd4H4kpvbekx15S9mUeLQ7Uxd0tYTLA==",
|
||||
"cpu": [
|
||||
"arm64",
|
||||
"x64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"bun": "bin/bun.exe",
|
||||
"bunx": "bin/bunx.exe"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oven/bun-darwin-aarch64": "1.3.10",
|
||||
"@oven/bun-darwin-x64": "1.3.10",
|
||||
"@oven/bun-darwin-x64-baseline": "1.3.10",
|
||||
"@oven/bun-linux-aarch64": "1.3.10",
|
||||
"@oven/bun-linux-aarch64-musl": "1.3.10",
|
||||
"@oven/bun-linux-x64": "1.3.10",
|
||||
"@oven/bun-linux-x64-baseline": "1.3.10",
|
||||
"@oven/bun-linux-x64-musl": "1.3.10",
|
||||
"@oven/bun-linux-x64-musl-baseline": "1.3.10",
|
||||
"@oven/bun-windows-aarch64": "1.3.10",
|
||||
"@oven/bun-windows-x64": "1.3.10",
|
||||
"@oven/bun-windows-x64-baseline": "1.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.10.tgz",
|
||||
"integrity": "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ffi-rs": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ffi-rs/-/ffi-rs-1.3.1.tgz",
|
||||
"integrity": "sha512-ZyNXL9fnclnZV+waQmWB9JrfbIEyxQa1OWtMrHOrAgcC04PgP5hBMG5TdhVN8N4uT/eul8zCFMVnJUukAFFlXA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@yuuang/ffi-rs-android-arm64": "1.3.1",
|
||||
"@yuuang/ffi-rs-darwin-arm64": "1.3.1",
|
||||
"@yuuang/ffi-rs-darwin-x64": "1.3.1",
|
||||
"@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.1",
|
||||
"@yuuang/ffi-rs-linux-arm64-gnu": "1.3.1",
|
||||
"@yuuang/ffi-rs-linux-arm64-musl": "1.3.1",
|
||||
"@yuuang/ffi-rs-linux-x64-gnu": "1.3.1",
|
||||
"@yuuang/ffi-rs-linux-x64-musl": "1.3.1",
|
||||
"@yuuang/ffi-rs-win32-arm64-msvc": "1.3.1",
|
||||
"@yuuang/ffi-rs-win32-ia32-msvc": "1.3.1",
|
||||
"@yuuang/ffi-rs-win32-x64-msvc": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/fff-bun": {
|
||||
"name": "@ff-labs/fff-bun",
|
||||
"version": "0.1.37",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"fff-demo": "examples/search.ts",
|
||||
"fff-grep": "examples/grep.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun": ">=1.0.0"
|
||||
}
|
||||
},
|
||||
"packages/fff-node": {
|
||||
"name": "@ff-labs/fff-node",
|
||||
"version": "0.1.37",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"ffi-rs": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"workspaces": ["packages/fff-bun", "packages/fff-mcp"],
|
||||
"packageManager": "bun@1.3.9",
|
||||
"workspaces": ["packages/fff-bun", "packages/fff-node"],
|
||||
|
||||
"scripts": {
|
||||
"format": "biome format --write",
|
||||
"format:check": "biome format",
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-darwin-arm64",
|
||||
"name": "@ff-labs/fff-bin-darwin-arm64",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for macOS ARM64 (Apple Silicon)",
|
||||
"os": ["darwin"],
|
||||
@@ -13,6 +13,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-darwin-arm64"
|
||||
"directory": "packages/fff-bin-darwin-arm64"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-darwin-x64",
|
||||
"name": "@ff-labs/fff-bin-darwin-x64",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for macOS x64 (Intel)",
|
||||
"os": ["darwin"],
|
||||
@@ -13,6 +13,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-darwin-x64"
|
||||
"directory": "packages/fff-bin-darwin-x64"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-linux-arm64-gnu",
|
||||
"name": "@ff-labs/fff-bin-linux-arm64-gnu",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Linux ARM64 (glibc)",
|
||||
"os": ["linux"],
|
||||
@@ -13,7 +13,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-linux-arm64-gnu"
|
||||
"directory": "packages/fff-bin-linux-arm64-gnu"
|
||||
},
|
||||
"libc": ["glibc"]
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-linux-arm64-musl",
|
||||
"name": "@ff-labs/fff-bin-linux-arm64-musl",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Linux ARM64 (musl)",
|
||||
"os": ["linux"],
|
||||
@@ -13,7 +13,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-linux-arm64-musl"
|
||||
"directory": "packages/fff-bin-linux-arm64-musl"
|
||||
},
|
||||
"libc": ["musl"]
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-linux-x64-gnu",
|
||||
"name": "@ff-labs/fff-bin-linux-x64-gnu",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Linux x64 (glibc)",
|
||||
"os": ["linux"],
|
||||
@@ -13,7 +13,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-linux-x64-gnu"
|
||||
"directory": "packages/fff-bin-linux-x64-gnu"
|
||||
},
|
||||
"libc": ["glibc"]
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-linux-x64-musl",
|
||||
"name": "@ff-labs/fff-bin-linux-x64-musl",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Linux x64 (musl)",
|
||||
"os": ["linux"],
|
||||
@@ -13,7 +13,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-linux-x64-musl"
|
||||
"directory": "packages/fff-bin-linux-x64-musl"
|
||||
},
|
||||
"libc": ["musl"]
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-win32-arm64",
|
||||
"name": "@ff-labs/fff-bin-win32-arm64",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Windows ARM64",
|
||||
"os": ["win32"],
|
||||
@@ -13,6 +13,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-win32-arm64"
|
||||
"directory": "packages/fff-bin-win32-arm64"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-bun-win32-x64",
|
||||
"name": "@ff-labs/fff-bin-win32-x64",
|
||||
"version": "0.0.0",
|
||||
"description": "fff native binary for Windows x64",
|
||||
"os": ["win32"],
|
||||
@@ -13,6 +13,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-bun-win32-x64"
|
||||
"directory": "packages/fff-bin-win32-x64"
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -16,20 +16,20 @@ High-performance fuzzy file finder for Bun, powered by Rust. Perfect for LLM age
|
||||
bun add @ff-labs/bun
|
||||
```
|
||||
|
||||
The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bun-darwin-arm64`, `@ff-labs/fff-bun-linux-x64-gnu`). No GitHub downloads are needed.
|
||||
The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bin-darwin-arm64`, `@ff-labs/fff-bin-linux-x64-gnu`). No GitHub downloads are needed.
|
||||
|
||||
### Supported Platforms
|
||||
|
||||
| Platform | Architecture | Package |
|
||||
|----------|-------------|---------|
|
||||
| macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bun-darwin-arm64` |
|
||||
| macOS | x64 (Intel) | `@ff-labs/fff-bun-darwin-x64` |
|
||||
| Linux | x64 (glibc) | `@ff-labs/fff-bun-linux-x64-gnu` |
|
||||
| Linux | ARM64 (glibc) | `@ff-labs/fff-bun-linux-arm64-gnu` |
|
||||
| Linux | x64 (musl) | `@ff-labs/fff-bun-linux-x64-musl` |
|
||||
| Linux | ARM64 (musl) | `@ff-labs/fff-bun-linux-arm64-musl` |
|
||||
| Windows | x64 | `@ff-labs/fff-bun-win32-x64` |
|
||||
| Windows | ARM64 | `@ff-labs/fff-bun-win32-arm64` |
|
||||
| macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bin-darwin-arm64` |
|
||||
| macOS | x64 (Intel) | `@ff-labs/fff-bin-darwin-x64` |
|
||||
| Linux | x64 (glibc) | `@ff-labs/fff-bin-linux-x64-gnu` |
|
||||
| Linux | ARM64 (glibc) | `@ff-labs/fff-bin-linux-arm64-gnu` |
|
||||
| Linux | x64 (musl) | `@ff-labs/fff-bin-linux-x64-musl` |
|
||||
| Linux | ARM64 (musl) | `@ff-labs/fff-bin-linux-arm64-musl` |
|
||||
| Windows | x64 | `@ff-labs/fff-bin-win32-x64` |
|
||||
| Windows | ARM64 | `@ff-labs/fff-bin-win32-arm64` |
|
||||
|
||||
If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback.
|
||||
|
||||
@@ -113,7 +113,7 @@ Track file access for frecency scoring.
|
||||
FileFinder.trackAccess("/path/to/file.ts");
|
||||
```
|
||||
|
||||
### `FileFinder.liveGrep(query, options?)`
|
||||
### `FileFinder.grep(query, options?)`
|
||||
|
||||
Search file contents with SIMD-accelerated matching.
|
||||
|
||||
@@ -129,7 +129,7 @@ interface GrepOptions {
|
||||
}
|
||||
|
||||
// Plain text search
|
||||
const result = FileFinder.liveGrep("TODO", { pageLimit: 20 });
|
||||
const result = FileFinder.grep("TODO", { pageLimit: 20 });
|
||||
if (result.ok) {
|
||||
for (const match of result.value.items) {
|
||||
console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
@@ -137,22 +137,22 @@ if (result.ok) {
|
||||
}
|
||||
|
||||
// Regex search
|
||||
const regexResult = FileFinder.liveGrep("fn\\s+\\w+", { mode: "regex" });
|
||||
const regexResult = FileFinder.grep("fn\\s+\\w+", { mode: "regex" });
|
||||
|
||||
// Fuzzy search
|
||||
const fuzzyResult = FileFinder.liveGrep("imprt recat", { mode: "fuzzy" });
|
||||
const fuzzyResult = FileFinder.grep("imprt recat", { mode: "fuzzy" });
|
||||
|
||||
// Pagination
|
||||
const page1 = FileFinder.liveGrep("error");
|
||||
const page1 = FileFinder.grep("error");
|
||||
if (page1.ok && page1.value.nextCursor) {
|
||||
const page2 = FileFinder.liveGrep("error", {
|
||||
const page2 = FileFinder.grep("error", {
|
||||
cursor: page1.value.nextCursor,
|
||||
});
|
||||
}
|
||||
|
||||
// With file constraints
|
||||
const tsOnly = FileFinder.liveGrep("*.ts useState");
|
||||
const srcOnly = FileFinder.liveGrep("src/ handleClick");
|
||||
const tsOnly = FileFinder.grep("*.ts useState");
|
||||
const srcOnly = FileFinder.grep("src/ handleClick");
|
||||
```
|
||||
|
||||
### `FileFinder.trackQuery(query, selectedFile)`
|
||||
@@ -178,7 +178,7 @@ if (health.ok) {
|
||||
|
||||
### Other Methods
|
||||
|
||||
- `FileFinder.liveGrep(query, options?)` - Search file contents
|
||||
- `FileFinder.grep(query, options?)` - Search file contents
|
||||
- `FileFinder.scanFiles()` - Trigger rescan
|
||||
- `FileFinder.isScanning()` - Check scan status
|
||||
- `FileFinder.getScanProgress()` - Get scan progress
|
||||
|
||||
Binary file not shown.
@@ -190,7 +190,7 @@ async function main() {
|
||||
}
|
||||
|
||||
const searchStart = Date.now();
|
||||
const result = finder.liveGrep(query, {
|
||||
const result = finder.grep(query, {
|
||||
mode: currentMode,
|
||||
pageLimit: 30,
|
||||
timeBudgetMs: 5000,
|
||||
|
||||
@@ -13,19 +13,14 @@
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"fff": "./scripts/cli.ts",
|
||||
"fff-demo": "./examples/search.ts",
|
||||
"fff-grep": "./examples/grep.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"bin",
|
||||
"scripts",
|
||||
"examples"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "bun ./scripts/postinstall.ts",
|
||||
"download": "bun ./scripts/cli.ts download",
|
||||
"test": "bun test src/",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"demo": "bun ./examples/search.ts"
|
||||
@@ -67,14 +62,14 @@
|
||||
},
|
||||
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
||||
"optionalDependencies": {
|
||||
"@ff-labs/fff-bun-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bun-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bun-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bun-win32-x64": "0.0.0",
|
||||
"@ff-labs/fff-bun-win32-arm64": "0.0.0"
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
/**
|
||||
* Binary resolution utilities for fff
|
||||
*
|
||||
* Resolves the native binary from:
|
||||
* 1. Platform-specific npm package (e.g. @ff-labs/fff-bun-darwin-arm64) - primary
|
||||
* 2. Local bin/ directory (legacy or manual download)
|
||||
* 3. Local dev build (target/release or target/debug)
|
||||
* 4. GitHub releases (fallback, requires network)
|
||||
* Resolves the native library from:
|
||||
* 1. Platform-specific npm package (e.g. @ff-labs/fff-bin-darwin-arm64)
|
||||
* 2. Local dev build (target/release or target/debug)
|
||||
*/
|
||||
|
||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
getLibExtension,
|
||||
getLibFilename,
|
||||
getNpmPackageName,
|
||||
getTriple,
|
||||
} from "./platform";
|
||||
|
||||
const GITHUB_REPO = "dmtrKovalenko/fff.nvim";
|
||||
import { getLibFilename, getNpmPackageName } from "./platform";
|
||||
|
||||
/**
|
||||
* Get the current file's directory
|
||||
@@ -47,21 +38,6 @@ function getPackageDir(): string {
|
||||
return dirname(currentDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory where binaries are stored (legacy/fallback)
|
||||
*/
|
||||
export function getBinDir(): string {
|
||||
return join(getPackageDir(), "bin");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path to the native library in bin/ (legacy/fallback)
|
||||
*/
|
||||
export function getBinaryPath(): string {
|
||||
const binDir = getBinDir();
|
||||
return join(binDir, getLibFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the binary exists in any known location
|
||||
*/
|
||||
@@ -72,8 +48,8 @@ export function binaryExists(): boolean {
|
||||
/**
|
||||
* Try to resolve the binary from the platform-specific npm package.
|
||||
*
|
||||
* When users install @ff-labs/bun, npm/bun automatically installs the matching
|
||||
* optionalDependency (e.g. @ff-labs/fff-bun-darwin-arm64). We resolve the binary
|
||||
* When users install @ff-labs/fff-bun, npm/bun automatically installs the matching
|
||||
* optionalDependency (e.g. @ff-labs/fff-bin-darwin-arm64). We resolve the binary
|
||||
* path by requiring that package's package.json and looking for the binary
|
||||
* in the same directory.
|
||||
*/
|
||||
@@ -101,7 +77,7 @@ function resolveFromNpmPackage(): string | null {
|
||||
/**
|
||||
* Get the development binary path (for local development)
|
||||
*/
|
||||
export function getDevBinaryPath(): string | null {
|
||||
function getDevBinaryPath(): string | null {
|
||||
const packageDir = getPackageDir();
|
||||
const workspaceRoot = join(packageDir, "..", "..");
|
||||
|
||||
@@ -125,11 +101,20 @@ function isDevWorkspace(): boolean {
|
||||
return existsSync(join(workspaceRoot, "Cargo.toml"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the native library binary.
|
||||
*
|
||||
* Resolution order:
|
||||
* - Dev workspace: local dev build first, then npm package
|
||||
* - Production: npm package first, then dev build
|
||||
*
|
||||
* @returns Absolute path to the library, or null if not found
|
||||
*/
|
||||
export function findBinary(): string | null {
|
||||
if (isDevWorkspace()) {
|
||||
// 1. Local bin/ directory (populated by `make prepare-bun`)
|
||||
const installedPath = getBinaryPath();
|
||||
if (existsSync(installedPath)) return installedPath;
|
||||
const binPath = join(getPackageDir(), "bin", getLibFilename());
|
||||
if (existsSync(binPath)) return binPath;
|
||||
|
||||
// 2. Local dev build (target/release or target/debug)
|
||||
const devPath = getDevBinaryPath();
|
||||
@@ -143,117 +128,9 @@ export function findBinary(): string | null {
|
||||
}
|
||||
|
||||
// Production: npm package first
|
||||
// 1. Try platform-specific npm package first
|
||||
const npmPath = resolveFromNpmPackage();
|
||||
if (npmPath) return npmPath;
|
||||
|
||||
// 2. Try local bin/ directory (legacy or manual download)
|
||||
const installedPath = getBinaryPath();
|
||||
if (existsSync(installedPath)) return installedPath;
|
||||
|
||||
// 3. Try local dev build
|
||||
// Fallback: local dev build (e.g. user built from source)
|
||||
return getDevBinaryPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the binary from GitHub releases as a fallback.
|
||||
* This is only used when the platform npm package is not available.
|
||||
*
|
||||
* @param tag - The release tag to download (e.g. commit hash), or "latest"
|
||||
*/
|
||||
export async function downloadBinary(tag?: string): Promise<string> {
|
||||
const resolvedTag = tag || "latest";
|
||||
const triple = getTriple();
|
||||
const ext = getLibExtension();
|
||||
|
||||
// Resolve "latest" tag via GitHub API
|
||||
let releaseTag = resolvedTag;
|
||||
if (releaseTag === "latest") {
|
||||
console.log("fff: Fetching latest release tag from GitHub...");
|
||||
releaseTag = await fetchLatestReleaseTag();
|
||||
}
|
||||
|
||||
const binaryName = `c-lib-${triple}.${ext}`;
|
||||
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${releaseTag}`;
|
||||
const binaryUrl = `${baseUrl}/${binaryName}`;
|
||||
|
||||
console.log(`fff: Downloading native library for ${triple}...`);
|
||||
console.log(`fff: Release: ${releaseTag}`);
|
||||
|
||||
const binaryResponse = await fetch(binaryUrl);
|
||||
if (!binaryResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}\nURL: ${binaryUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
const binaryBuffer = Buffer.from(await binaryResponse.arrayBuffer());
|
||||
|
||||
const binDir = getBinDir();
|
||||
if (!existsSync(binDir)) {
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
const binaryPath = getBinaryPath();
|
||||
writeFileSync(binaryPath, binaryBuffer);
|
||||
|
||||
// Make executable on Unix
|
||||
if (process.platform !== "win32") {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
console.log(`fff: Binary downloaded to ${binaryPath}`);
|
||||
return releaseTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest release tag from GitHub
|
||||
*/
|
||||
async function fetchLatestReleaseTag(): Promise<string> {
|
||||
const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "fff-bun-client",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const allReleasesUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
||||
const allResponse = await fetch(allReleasesUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "fff-bun-client",
|
||||
},
|
||||
});
|
||||
|
||||
if (!allResponse.ok) {
|
||||
throw new Error(`Failed to fetch releases: ${allResponse.status}`);
|
||||
}
|
||||
|
||||
const releases = (await allResponse.json()) as Array<{ tag_name: string }>;
|
||||
if (releases.length === 0) {
|
||||
throw new Error("No releases found");
|
||||
}
|
||||
|
||||
return releases[0].tag_name;
|
||||
}
|
||||
|
||||
const release = (await response.json()) as { tag_name: string };
|
||||
return release.tag_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the binary exists, downloading from GitHub if necessary.
|
||||
*/
|
||||
export async function ensureBinary(): Promise<string> {
|
||||
const existingPath = findBinary();
|
||||
if (existingPath) {
|
||||
return existingPath;
|
||||
}
|
||||
|
||||
// Fallback: download from GitHub
|
||||
await downloadBinary();
|
||||
return getBinaryPath();
|
||||
}
|
||||
|
||||
+638
-124
@@ -9,15 +9,45 @@
|
||||
*/
|
||||
|
||||
import { CString, dlopen, FFIType, type Pointer, ptr, read } from "bun:ffi";
|
||||
import { ensureBinary, findBinary } from "./download";
|
||||
import type { Result } from "./types";
|
||||
import { err } from "./types";
|
||||
import { findBinary } from "./download";
|
||||
import type {
|
||||
FileItem,
|
||||
GrepMatch,
|
||||
GrepResult,
|
||||
Location,
|
||||
Result,
|
||||
Score,
|
||||
SearchResult,
|
||||
} from "./types";
|
||||
import { createGrepCursor, err } from "./types";
|
||||
|
||||
/** Grep mode constants matching the C API (u8). */
|
||||
const GREP_MODE_PLAIN = 0;
|
||||
const GREP_MODE_REGEX = 1;
|
||||
const GREP_MODE_FUZZY = 2;
|
||||
|
||||
/** Map string mode to u8 */
|
||||
function grepModeToU8(mode?: string): number {
|
||||
switch (mode) {
|
||||
case "regex":
|
||||
return GREP_MODE_REGEX;
|
||||
case "fuzzy":
|
||||
return GREP_MODE_FUZZY;
|
||||
default:
|
||||
return GREP_MODE_PLAIN;
|
||||
}
|
||||
}
|
||||
|
||||
// Define the FFI symbols
|
||||
const ffiDefinition = {
|
||||
// Lifecycle
|
||||
fff_create: {
|
||||
args: [FFIType.cstring],
|
||||
fff_create_instance: {
|
||||
args: [
|
||||
FFIType.cstring, // base_path
|
||||
FFIType.cstring, // frecency_db_path
|
||||
FFIType.cstring, // history_db_path
|
||||
FFIType.bool, // use_unsafe_no_lock
|
||||
FFIType.bool, // warmup_mmap_cache
|
||||
FFIType.bool, // ai_mode
|
||||
],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
fff_destroy: {
|
||||
@@ -27,19 +57,54 @@ const ffiDefinition = {
|
||||
|
||||
// Search
|
||||
fff_search: {
|
||||
args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
|
||||
args: [
|
||||
FFIType.ptr, // handle
|
||||
FFIType.cstring, // query
|
||||
FFIType.cstring, // current_file
|
||||
FFIType.u32, // max_threads
|
||||
FFIType.u32, // page_index
|
||||
FFIType.u32, // page_size
|
||||
FFIType.i32, // combo_boost_multiplier
|
||||
FFIType.u32, // min_combo_count
|
||||
],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
// Live grep (content search)
|
||||
fff_live_grep: {
|
||||
args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
|
||||
args: [
|
||||
FFIType.ptr, // handle
|
||||
FFIType.cstring, // query
|
||||
FFIType.u8, // mode
|
||||
FFIType.u64, // max_file_size
|
||||
FFIType.u32, // max_matches_per_file
|
||||
FFIType.bool, // smart_case
|
||||
FFIType.u32, // file_offset
|
||||
FFIType.u32, // page_limit
|
||||
FFIType.u64, // time_budget_ms
|
||||
FFIType.u32, // before_context
|
||||
FFIType.u32, // after_context
|
||||
FFIType.bool, // classify_definitions
|
||||
],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
// Multi-pattern grep (Aho-Corasick)
|
||||
fff_multi_grep: {
|
||||
args: [FFIType.ptr, FFIType.cstring],
|
||||
args: [
|
||||
FFIType.ptr, // handle
|
||||
FFIType.cstring, // patterns_joined (\n-separated)
|
||||
FFIType.cstring, // constraints
|
||||
FFIType.u64, // max_file_size
|
||||
FFIType.u32, // max_matches_per_file
|
||||
FFIType.bool, // smart_case
|
||||
FFIType.u32, // file_offset
|
||||
FFIType.u32, // page_limit
|
||||
FFIType.u64, // time_budget_ms
|
||||
FFIType.u32, // before_context
|
||||
FFIType.u32, // after_context
|
||||
FFIType.bool, // classify_definitions
|
||||
],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
@@ -87,6 +152,30 @@ const ffiDefinition = {
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
// Search result accessors / free
|
||||
fff_free_search_result: {
|
||||
args: [FFIType.ptr],
|
||||
returns: FFIType.void,
|
||||
},
|
||||
fff_search_result_get_item: {
|
||||
args: [FFIType.ptr, FFIType.u32],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
fff_search_result_get_score: {
|
||||
args: [FFIType.ptr, FFIType.u32],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
// Grep result accessors / free
|
||||
fff_free_grep_result: {
|
||||
args: [FFIType.ptr],
|
||||
returns: FFIType.void,
|
||||
},
|
||||
fff_grep_result_get_match: {
|
||||
args: [FFIType.ptr, FFIType.u32],
|
||||
returns: FFIType.ptr,
|
||||
},
|
||||
|
||||
// Memory management
|
||||
fff_free_result: {
|
||||
args: [FFIType.ptr],
|
||||
@@ -96,6 +185,10 @@ const ffiDefinition = {
|
||||
args: [FFIType.ptr],
|
||||
returns: FFIType.void,
|
||||
},
|
||||
fff_free_scan_progress: {
|
||||
args: [FFIType.ptr],
|
||||
returns: FFIType.void,
|
||||
},
|
||||
} as const;
|
||||
|
||||
type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
|
||||
@@ -112,7 +205,7 @@ function loadLibrary(): FFFLibrary {
|
||||
const binaryPath = findBinary();
|
||||
if (!binaryPath) {
|
||||
throw new Error(
|
||||
"fff native library not found. Run `bunx fff download` or build from source with `cargo build --release -p fff-c`",
|
||||
"fff native library not found. Build from source with `cargo build --release -p fff-c` or install the platform package.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,59 +241,100 @@ function snakeToCamel(obj: unknown): unknown {
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
||||
letter.toUpperCase(),
|
||||
);
|
||||
result[camelKey] = snakeToCamel(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FffResult byte offsets (must match #[repr(C)] layout on 64-bit)
|
||||
// { success: bool(1+7pad), error: *char(8), handle: *void(8), int_value: i64(8) }
|
||||
// ---------------------------------------------------------------------------
|
||||
const RES_SUCCESS = 0; // bool (1 + 7 padding)
|
||||
const RES_ERROR = 8; // *mut c_char (8)
|
||||
const RES_HANDLE = 16; // *mut c_void (8)
|
||||
const RES_INT_VALUE = 24; // i64 (8)
|
||||
|
||||
/**
|
||||
* Parse a FffResult from the FFI return value.
|
||||
*
|
||||
* The result is a pointer to a struct:
|
||||
* { success: bool, data: *char, error: *char, handle: *void }
|
||||
*
|
||||
* Layout (with alignment padding):
|
||||
* offset 0: success (bool, 1 byte + 7 padding)
|
||||
* offset 8: data pointer (8 bytes)
|
||||
* offset 16: error pointer (8 bytes)
|
||||
* offset 24: handle pointer (8 bytes)
|
||||
* Read the FffResult envelope: check success, extract payload, free envelope.
|
||||
* On error returns a Result<never>. On success returns the raw handle pointer and int_value.
|
||||
*/
|
||||
function parseResult<T>(resultPtr: Pointer | null): Result<T> {
|
||||
function readResultEnvelope(resultPtr: Pointer | null): { success: true; handlePtr: number; intValue: number } | Result<never> {
|
||||
if (resultPtr === null) {
|
||||
return err("FFI returned null pointer");
|
||||
}
|
||||
|
||||
const success = read.u8(resultPtr, 0) !== 0;
|
||||
const dataPtr = read.ptr(resultPtr, 8);
|
||||
const errorPtr = read.ptr(resultPtr, 16);
|
||||
|
||||
const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
|
||||
const library = loadLibrary();
|
||||
|
||||
if (success) {
|
||||
const data = readCString(dataPtr);
|
||||
// Free the result
|
||||
library.symbols.fff_free_result(resultPtr);
|
||||
|
||||
if (data === null || data === "") {
|
||||
return { ok: true, value: undefined as T };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
// Convert snake_case to camelCase for TypeScript consumers
|
||||
const transformed = snakeToCamel(parsed) as T;
|
||||
return { ok: true, value: transformed };
|
||||
} catch {
|
||||
// For simple values like "true" or numbers
|
||||
return { ok: true, value: data as T };
|
||||
}
|
||||
} else {
|
||||
if (!success) {
|
||||
const errorPtr = read.ptr(resultPtr, RES_ERROR);
|
||||
const errorMsg = readCString(errorPtr) || "Unknown error";
|
||||
// Free the result
|
||||
library.symbols.fff_free_result(resultPtr);
|
||||
return err(errorMsg);
|
||||
}
|
||||
|
||||
const handlePtr = read.ptr(resultPtr, RES_HANDLE);
|
||||
const intValue = Number(read.i64(resultPtr, RES_INT_VALUE));
|
||||
library.symbols.fff_free_result(resultPtr);
|
||||
return { success: true, handlePtr, intValue };
|
||||
}
|
||||
|
||||
/** Parse a FffResult that carries a bool in int_value (0 = false, nonzero = true). */
|
||||
function parseBoolResult(resultPtr: Pointer | null): Result<boolean> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
return { ok: true, value: envelope.intValue !== 0 };
|
||||
}
|
||||
|
||||
/** Parse a FffResult that carries an integer in int_value. */
|
||||
function parseIntResult(resultPtr: Pointer | null): Result<number> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
return { ok: true, value: envelope.intValue };
|
||||
}
|
||||
|
||||
/** Parse a FffResult that carries a string in handle (freed with fff_free_string). */
|
||||
function parseStringResult(resultPtr: Pointer | null): Result<string | null> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
|
||||
if (envelope.handlePtr === 0) return { ok: true, value: null };
|
||||
|
||||
const library = loadLibrary();
|
||||
const str = readCString(envelope.handlePtr);
|
||||
library.symbols.fff_free_string(asPtr(envelope.handlePtr));
|
||||
return { ok: true, value: str };
|
||||
}
|
||||
|
||||
/** Parse a FffResult that carries a JSON string in handle. */
|
||||
function parseJsonResult<T>(resultPtr: Pointer | null): Result<T> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
|
||||
if (envelope.handlePtr === 0) return { ok: true, value: undefined as T };
|
||||
|
||||
const library = loadLibrary();
|
||||
const jsonStr = readCString(envelope.handlePtr);
|
||||
library.symbols.fff_free_string(asPtr(envelope.handlePtr));
|
||||
|
||||
if (jsonStr === null || jsonStr === "") return { ok: true, value: undefined as T };
|
||||
|
||||
try {
|
||||
return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) as T };
|
||||
} catch {
|
||||
return { ok: true, value: jsonStr as T };
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse a FffResult with no payload (void, success/error only). */
|
||||
function parseVoidResult(resultPtr: Pointer | null): Result<void> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
return { ok: true, value: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,28 +344,39 @@ export type NativeHandle = Pointer;
|
||||
|
||||
/**
|
||||
* Create a new file finder instance.
|
||||
*
|
||||
* Returns the opaque native handle on success. The handle must be passed to
|
||||
* all subsequent FFI calls and freed with `ffiDestroy`.
|
||||
*/
|
||||
export function ffiCreate(optsJson: string): Result<NativeHandle> {
|
||||
export function ffiCreate(
|
||||
basePath: string,
|
||||
frecencyDbPath: string,
|
||||
historyDbPath: string,
|
||||
useUnsafeNoLock: boolean,
|
||||
warmupMmapCache: boolean,
|
||||
aiMode: boolean,
|
||||
): Result<NativeHandle> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_create(ptr(encodeString(optsJson)));
|
||||
const resultPtr = library.symbols.fff_create_instance(
|
||||
ptr(encodeString(basePath)),
|
||||
ptr(encodeString(frecencyDbPath)),
|
||||
ptr(encodeString(historyDbPath)),
|
||||
useUnsafeNoLock,
|
||||
warmupMmapCache,
|
||||
aiMode,
|
||||
);
|
||||
|
||||
if (resultPtr === null) {
|
||||
return err("FFI returned null pointer");
|
||||
}
|
||||
|
||||
const success = read.u8(resultPtr, 0) !== 0;
|
||||
const errorPtr = read.ptr(resultPtr, 16);
|
||||
const handlePtr = read.ptr(resultPtr, 24);
|
||||
const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
|
||||
const errorPtr = read.ptr(resultPtr, RES_ERROR);
|
||||
const handlePtr = read.ptr(resultPtr, RES_HANDLE);
|
||||
|
||||
if (success) {
|
||||
const handle = handlePtr as unknown as Pointer;
|
||||
library.symbols.fff_free_result(resultPtr);
|
||||
|
||||
if (!handle || handle === (0 as unknown as Pointer)) {
|
||||
return err("fff_create returned null handle");
|
||||
return err("fff_create_instance returned null handle");
|
||||
}
|
||||
|
||||
return { ok: true, value: handle };
|
||||
@@ -250,21 +395,415 @@ export function ffiDestroy(handle: NativeHandle): void {
|
||||
library.symbols.fff_destroy(handle);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Struct byte offsets (must match #[repr(C)] layout on 64-bit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// FffSearchResult { items: *mut, scores: *mut, count: u32, total_matched: u32, total_files: u32, location: FffLocation }
|
||||
const SR_ITEMS = 0; // *mut FffFileItem (8)
|
||||
const SR_SCORES = 8; // *mut FffScore (8)
|
||||
const SR_COUNT = 16; // u32 (4)
|
||||
const SR_MATCHED = 20; // u32 (4)
|
||||
const SR_TOTAL = 24; // u32 (4)
|
||||
// FffLocation is inlined at offset 28
|
||||
const SR_LOC_TAG = 28; // u8 (1 + 3 padding)
|
||||
const SR_LOC_LINE = 32; // i32 (4)
|
||||
const SR_LOC_COL = 36; // i32 (4)
|
||||
const SR_LOC_END_LINE = 40; // i32 (4)
|
||||
const SR_LOC_END_COL = 44; // i32 (4)
|
||||
|
||||
// FffFileItem (80 bytes)
|
||||
const FI_PATH = 0; // *mut c_char (8)
|
||||
const FI_RELPATH = 8; // *mut c_char (8)
|
||||
const FI_FNAME = 16; // *mut c_char (8)
|
||||
const FI_GIT = 24; // *mut c_char (8)
|
||||
const FI_SIZE = 32; // u64 (8)
|
||||
const FI_MODIFIED = 40; // u64 (8)
|
||||
const FI_ACCESS = 48; // i64 (8)
|
||||
const FI_MODFR = 56; // i64 (8)
|
||||
const FI_TOTAL_FR = 64; // i64 (8)
|
||||
const _FI_BINARY = 72; // bool (1 + 7 pad)
|
||||
const FI_SIZE_OF = 80;
|
||||
|
||||
// FffScore (48 bytes)
|
||||
const SC_TOTAL = 0; // i32 (4)
|
||||
const SC_BASE = 4; // i32 (4)
|
||||
const SC_FNAME = 8; // i32 (4)
|
||||
const SC_SPECIAL = 12; // i32 (4)
|
||||
const SC_FREC = 16; // i32 (4)
|
||||
const SC_DIST = 20; // i32 (4)
|
||||
const SC_CURFILE = 24; // i32 (4)
|
||||
const SC_COMBO = 28; // i32 (4)
|
||||
const SC_EXACT = 32; // bool (1 + 7 pad)
|
||||
const SC_MTYPE = 40; // *mut c_char (8)
|
||||
const SC_SIZE_OF = 48;
|
||||
|
||||
/** Cast a number (raw address from pointer math) to Pointer for read.*. */
|
||||
function asPtr(n: number): Pointer {
|
||||
return n as unknown as Pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an FffFileItem struct at the given raw address.
|
||||
*/
|
||||
function readFileItemStruct(p: number): FileItem {
|
||||
const pp = asPtr(p);
|
||||
return {
|
||||
path: readCString(read.ptr(pp, FI_PATH)) ?? "",
|
||||
relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
|
||||
fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
|
||||
gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
|
||||
size: Number(read.u64(pp, FI_SIZE)),
|
||||
modified: Number(read.u64(pp, FI_MODIFIED)),
|
||||
accessFrecencyScore: Number(read.i64(pp, FI_ACCESS)),
|
||||
modificationFrecencyScore: Number(read.i64(pp, FI_MODFR)),
|
||||
totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an FffScore struct at the given raw address.
|
||||
*/
|
||||
function readScoreStruct(p: number): Score {
|
||||
const pp = asPtr(p);
|
||||
return {
|
||||
total: read.i32(pp, SC_TOTAL),
|
||||
baseScore: read.i32(pp, SC_BASE),
|
||||
filenameBonus: read.i32(pp, SC_FNAME),
|
||||
specialFilenameBonus: read.i32(pp, SC_SPECIAL),
|
||||
frecencyBoost: read.i32(pp, SC_FREC),
|
||||
distancePenalty: read.i32(pp, SC_DIST),
|
||||
currentFilePenalty: read.i32(pp, SC_CURFILE),
|
||||
comboMatchBoost: read.i32(pp, SC_COMBO),
|
||||
exactMatch: read.u8(pp, SC_EXACT) !== 0,
|
||||
matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an FffSearchResult from a raw pointer, then free native memory.
|
||||
*/
|
||||
function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
|
||||
if (resultPtr === null) {
|
||||
return err("FFI returned null pointer");
|
||||
}
|
||||
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
|
||||
if (envelope.handlePtr === 0) {
|
||||
return err("fff_search returned null search result");
|
||||
}
|
||||
|
||||
const hp = asPtr(envelope.handlePtr);
|
||||
const count = read.u32(hp, SR_COUNT);
|
||||
const totalMatched = read.u32(hp, SR_MATCHED);
|
||||
const totalFiles = read.u32(hp, SR_TOTAL);
|
||||
const itemsBase = read.ptr(hp, SR_ITEMS);
|
||||
const scoresBase = read.ptr(hp, SR_SCORES);
|
||||
|
||||
// Read location
|
||||
const locTag = read.u8(hp, SR_LOC_TAG);
|
||||
let location: Location | undefined;
|
||||
if (locTag === 1) {
|
||||
location = { type: "line", line: read.i32(hp, SR_LOC_LINE) };
|
||||
} else if (locTag === 2) {
|
||||
location = {
|
||||
type: "position",
|
||||
line: read.i32(hp, SR_LOC_LINE),
|
||||
col: read.i32(hp, SR_LOC_COL),
|
||||
};
|
||||
} else if (locTag === 3) {
|
||||
location = {
|
||||
type: "range",
|
||||
start: { line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) },
|
||||
end: {
|
||||
line: read.i32(hp, SR_LOC_END_LINE),
|
||||
col: read.i32(hp, SR_LOC_END_COL),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Read items and scores arrays using pointer arithmetic
|
||||
const items: FileItem[] = [];
|
||||
const scores: Score[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
items.push(readFileItemStruct(itemsBase + i * FI_SIZE_OF));
|
||||
scores.push(readScoreStruct(scoresBase + i * SC_SIZE_OF));
|
||||
}
|
||||
|
||||
// Free native search result
|
||||
loadLibrary().symbols.fff_free_search_result(hp);
|
||||
|
||||
const result: SearchResult = { items, scores, totalMatched, totalFiles };
|
||||
if (location) {
|
||||
result.location = location;
|
||||
}
|
||||
return { ok: true, value: result };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FffGrepMatch byte offsets (must match #[repr(C)] layout on 64-bit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Pointers (8 bytes each)
|
||||
const GM_PATH = 0;
|
||||
const GM_RELPATH = 8;
|
||||
const GM_FNAME = 16;
|
||||
const GM_GIT = 24;
|
||||
const GM_LINE_CONTENT = 32;
|
||||
const GM_MATCH_RANGES = 40;
|
||||
const GM_CTX_BEFORE = 48;
|
||||
const GM_CTX_AFTER = 56;
|
||||
|
||||
// 8-byte numeric fields
|
||||
const GM_SIZE = 64;
|
||||
const GM_MODIFIED = 72;
|
||||
const GM_TOTAL_FR = 80;
|
||||
const GM_ACCESS_FR = 88;
|
||||
const GM_MOD_FR = 96;
|
||||
const GM_LINE_NUM = 104;
|
||||
const GM_BYTE_OFF = 112;
|
||||
|
||||
// 4-byte fields
|
||||
const GM_COL = 120;
|
||||
const GM_MR_COUNT = 124;
|
||||
const GM_CTX_B_COUNT = 128;
|
||||
const GM_CTX_A_COUNT = 132;
|
||||
|
||||
// 2-byte
|
||||
const GM_FUZZY_SCORE = 136;
|
||||
// 1-byte
|
||||
const GM_HAS_FUZZY = 138;
|
||||
const GM_IS_BINARY = 139;
|
||||
const _GM_IS_DEF = 140;
|
||||
|
||||
// struct size: pad to 8-byte alignment → 144
|
||||
const GM_SIZE_OF = 144;
|
||||
|
||||
// FffGrepResult
|
||||
const GR_ITEMS = 0; // *mut FffGrepMatch (8)
|
||||
const GR_COUNT = 8; // u32 (4)
|
||||
const GR_MATCHED = 12; // u32 (4)
|
||||
const GR_FILES_SEARCHED = 16; // u32 (4)
|
||||
const GR_TOTAL_FILES = 20; // u32 (4)
|
||||
const GR_FILTERED = 24; // u32 (4)
|
||||
const GR_NEXT_OFFSET = 28; // u32 (4)
|
||||
const GR_REGEX_ERR = 32; // *mut c_char (8)
|
||||
|
||||
// FffMatchRange (8 bytes)
|
||||
const MR_START = 0;
|
||||
const MR_END = 4;
|
||||
const MR_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Read a C string array (char**) at the given pointer, with `count` elements.
|
||||
*/
|
||||
function readCStringArray(base: number, count: number): string[] {
|
||||
if (count === 0 || base === 0) return [];
|
||||
const result: string[] = [];
|
||||
const bp = asPtr(base);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const strPtr = read.ptr(bp, i * 8); // each pointer is 8 bytes
|
||||
result.push(readCString(strPtr) ?? "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an FffGrepMatch struct at the given raw address.
|
||||
*/
|
||||
function readGrepMatchStruct(p: number): GrepMatch {
|
||||
const pp = asPtr(p);
|
||||
const matchRangesPtr = read.ptr(pp, GM_MATCH_RANGES);
|
||||
const matchRangesCount = read.u32(pp, GM_MR_COUNT);
|
||||
const matchRanges: [number, number][] = [];
|
||||
for (let i = 0; i < matchRangesCount; i++) {
|
||||
const base = matchRangesPtr + i * MR_SIZE;
|
||||
const bp = asPtr(base);
|
||||
matchRanges.push([read.u32(bp, MR_START), read.u32(bp, MR_END)]);
|
||||
}
|
||||
|
||||
const hasFuzzy = read.u8(pp, GM_HAS_FUZZY) !== 0;
|
||||
const ctxBeforeCount = read.u32(pp, GM_CTX_B_COUNT);
|
||||
const ctxAfterCount = read.u32(pp, GM_CTX_A_COUNT);
|
||||
|
||||
const match: GrepMatch = {
|
||||
path: readCString(read.ptr(pp, GM_PATH)) ?? "",
|
||||
relativePath: readCString(read.ptr(pp, GM_RELPATH)) ?? "",
|
||||
fileName: readCString(read.ptr(pp, GM_FNAME)) ?? "",
|
||||
gitStatus: readCString(read.ptr(pp, GM_GIT)) ?? "",
|
||||
lineContent: readCString(read.ptr(pp, GM_LINE_CONTENT)) ?? "",
|
||||
size: Number(read.u64(pp, GM_SIZE)),
|
||||
modified: Number(read.u64(pp, GM_MODIFIED)),
|
||||
totalFrecencyScore: Number(read.i64(pp, GM_TOTAL_FR)),
|
||||
accessFrecencyScore: Number(read.i64(pp, GM_ACCESS_FR)),
|
||||
modificationFrecencyScore: Number(read.i64(pp, GM_MOD_FR)),
|
||||
isBinary: read.u8(pp, GM_IS_BINARY) !== 0,
|
||||
lineNumber: Number(read.u64(pp, GM_LINE_NUM)),
|
||||
col: read.u32(pp, GM_COL),
|
||||
byteOffset: Number(read.u64(pp, GM_BYTE_OFF)),
|
||||
matchRanges,
|
||||
};
|
||||
|
||||
if (hasFuzzy) {
|
||||
match.fuzzyScore = read.u16(pp, GM_FUZZY_SCORE);
|
||||
}
|
||||
if (ctxBeforeCount > 0) {
|
||||
match.contextBefore = readCStringArray(
|
||||
read.ptr(pp, GM_CTX_BEFORE),
|
||||
ctxBeforeCount,
|
||||
);
|
||||
}
|
||||
if (ctxAfterCount > 0) {
|
||||
match.contextAfter = readCStringArray(
|
||||
read.ptr(pp, GM_CTX_AFTER),
|
||||
ctxAfterCount,
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an FffGrepResult from a raw FffResult pointer, then free native memory.
|
||||
*/
|
||||
function parseGrepResult(resultPtr: Pointer | null): Result<GrepResult> {
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
|
||||
if (envelope.handlePtr === 0) {
|
||||
return err("grep returned null result");
|
||||
}
|
||||
|
||||
const hp = asPtr(envelope.handlePtr);
|
||||
const count = read.u32(hp, GR_COUNT);
|
||||
const totalMatched = read.u32(hp, GR_MATCHED);
|
||||
const totalFilesSearched = read.u32(hp, GR_FILES_SEARCHED);
|
||||
const totalFiles = read.u32(hp, GR_TOTAL_FILES);
|
||||
const filteredFileCount = read.u32(hp, GR_FILTERED);
|
||||
const nextFileOffset = read.u32(hp, GR_NEXT_OFFSET);
|
||||
const regexErrPtr = read.ptr(hp, GR_REGEX_ERR);
|
||||
const regexFallbackError = readCString(regexErrPtr) ?? undefined;
|
||||
const itemsBase = read.ptr(hp, GR_ITEMS);
|
||||
|
||||
const items: GrepMatch[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
items.push(readGrepMatchStruct(itemsBase + i * GM_SIZE_OF));
|
||||
}
|
||||
|
||||
loadLibrary().symbols.fff_free_grep_result(hp);
|
||||
|
||||
const grepResult: GrepResult = {
|
||||
items,
|
||||
totalMatched,
|
||||
totalFilesSearched,
|
||||
totalFiles,
|
||||
filteredFileCount,
|
||||
nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
|
||||
};
|
||||
if (regexFallbackError) {
|
||||
grepResult.regexFallbackError = regexFallbackError;
|
||||
}
|
||||
return { ok: true, value: grepResult };
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform fuzzy search.
|
||||
*/
|
||||
export function ffiSearch(
|
||||
handle: NativeHandle,
|
||||
query: string,
|
||||
optsJson: string,
|
||||
): Result<unknown> {
|
||||
currentFile: string,
|
||||
maxThreads: number,
|
||||
pageIndex: number,
|
||||
pageSize: number,
|
||||
comboBoostMultiplier: number,
|
||||
minComboCount: number,
|
||||
): Result<SearchResult> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_search(
|
||||
handle,
|
||||
ptr(encodeString(query)),
|
||||
ptr(encodeString(optsJson)),
|
||||
ptr(encodeString(currentFile)),
|
||||
maxThreads,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
comboBoostMultiplier,
|
||||
minComboCount,
|
||||
);
|
||||
return parseResult<unknown>(resultPtr);
|
||||
return parseSearchResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live grep - search file contents.
|
||||
*/
|
||||
export function ffiLiveGrep(
|
||||
handle: NativeHandle,
|
||||
query: string,
|
||||
mode: string,
|
||||
maxFileSize: number,
|
||||
maxMatchesPerFile: number,
|
||||
smartCase: boolean,
|
||||
fileOffset: number,
|
||||
pageLimit: number,
|
||||
timeBudgetMs: number,
|
||||
beforeContext: number,
|
||||
afterContext: number,
|
||||
classifyDefinitions: boolean,
|
||||
): Result<GrepResult> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_live_grep(
|
||||
handle,
|
||||
ptr(encodeString(query)),
|
||||
grepModeToU8(mode),
|
||||
BigInt(maxFileSize),
|
||||
maxMatchesPerFile,
|
||||
smartCase,
|
||||
fileOffset,
|
||||
pageLimit,
|
||||
BigInt(timeBudgetMs),
|
||||
beforeContext,
|
||||
afterContext,
|
||||
classifyDefinitions,
|
||||
);
|
||||
return parseGrepResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-pattern grep - Aho-Corasick multi-needle search.
|
||||
*/
|
||||
export function ffiMultiGrep(
|
||||
handle: NativeHandle,
|
||||
patternsJoined: string,
|
||||
constraints: string,
|
||||
maxFileSize: number,
|
||||
maxMatchesPerFile: number,
|
||||
smartCase: boolean,
|
||||
fileOffset: number,
|
||||
pageLimit: number,
|
||||
timeBudgetMs: number,
|
||||
beforeContext: number,
|
||||
afterContext: number,
|
||||
classifyDefinitions: boolean,
|
||||
): Result<GrepResult> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_multi_grep(
|
||||
handle,
|
||||
ptr(encodeString(patternsJoined)),
|
||||
ptr(encodeString(constraints)),
|
||||
BigInt(maxFileSize),
|
||||
maxMatchesPerFile,
|
||||
smartCase,
|
||||
fileOffset,
|
||||
pageLimit,
|
||||
BigInt(timeBudgetMs),
|
||||
beforeContext,
|
||||
afterContext,
|
||||
classifyDefinitions,
|
||||
);
|
||||
return parseGrepResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,7 +812,7 @@ export function ffiSearch(
|
||||
export function ffiScanFiles(handle: NativeHandle): Result<void> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_scan_files(handle);
|
||||
return parseResult<void>(resultPtr);
|
||||
return parseVoidResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,35 +823,54 @@ export function ffiIsScanning(handle: NativeHandle): boolean {
|
||||
return library.symbols.fff_is_scanning(handle) as boolean;
|
||||
}
|
||||
|
||||
// FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1+7pad) }
|
||||
const SP_COUNT = 0; // u64 (8)
|
||||
const SP_SCANNING = 8; // bool (1 + 7 pad)
|
||||
|
||||
/**
|
||||
* Get scan progress.
|
||||
*/
|
||||
export function ffiGetScanProgress(handle: NativeHandle): Result<unknown> {
|
||||
export function ffiGetScanProgress(handle: NativeHandle): Result<{ scannedFilesCount: number; isScanning: boolean }> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_get_scan_progress(handle);
|
||||
return parseResult<unknown>(resultPtr);
|
||||
const envelope = readResultEnvelope(resultPtr);
|
||||
if (!("success" in envelope)) return envelope;
|
||||
|
||||
if (envelope.handlePtr === 0) {
|
||||
return err("scan progress returned null");
|
||||
}
|
||||
|
||||
const hp = asPtr(envelope.handlePtr);
|
||||
const result = {
|
||||
scannedFilesCount: Number(read.u64(hp, SP_COUNT)),
|
||||
isScanning: read.u8(hp, SP_SCANNING) !== 0,
|
||||
};
|
||||
library.symbols.fff_free_scan_progress(hp);
|
||||
return { ok: true, value: result };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for scan to complete.
|
||||
*/
|
||||
export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<boolean> {
|
||||
export function ffiWaitForScan(
|
||||
handle: NativeHandle,
|
||||
timeoutMs: number,
|
||||
): Result<boolean> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
|
||||
const result = parseResult<boolean | string>(resultPtr);
|
||||
if (!result.ok) return result;
|
||||
// JSON.parse("true") returns boolean true, but we also handle
|
||||
// the string case defensively.
|
||||
return { ok: true, value: result.value === true || result.value === "true" };
|
||||
return parseBoolResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart index in new path.
|
||||
*/
|
||||
export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<void> {
|
||||
export function ffiRestartIndex(
|
||||
handle: NativeHandle,
|
||||
newPath: string,
|
||||
): Result<void> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_restart_index(handle, ptr(encodeString(newPath)));
|
||||
return parseResult<void>(resultPtr);
|
||||
return parseVoidResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,13 +879,7 @@ export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<v
|
||||
export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_refresh_git_status(handle);
|
||||
const result = parseResult<number | string>(resultPtr);
|
||||
if (!result.ok) return result;
|
||||
// JSON.parse("3") returns 3 (number), parseInt handles both
|
||||
return {
|
||||
ok: true,
|
||||
value: typeof result.value === "number" ? result.value : parseInt(result.value, 10),
|
||||
};
|
||||
return parseIntResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,9 +896,7 @@ export function ffiTrackQuery(
|
||||
ptr(encodeString(query)),
|
||||
ptr(encodeString(filePath)),
|
||||
);
|
||||
const result = parseResult<boolean | string>(resultPtr);
|
||||
if (!result.ok) return result;
|
||||
return { ok: true, value: result.value === true || result.value === "true" };
|
||||
return parseBoolResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,10 +908,7 @@ export function ffiGetHistoricalQuery(
|
||||
): Result<string | null> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_get_historical_query(handle, BigInt(offset));
|
||||
const result = parseResult<string | null>(resultPtr);
|
||||
if (!result.ok) return result;
|
||||
if (result.value === null || result.value === "null") return { ok: true, value: null };
|
||||
return result as Result<string>;
|
||||
return parseStringResult(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,49 +925,16 @@ export function ffiHealthCheck(
|
||||
handle ?? (0 as unknown as Pointer),
|
||||
ptr(encodeString(testPath)),
|
||||
);
|
||||
return parseResult<unknown>(resultPtr);
|
||||
return parseJsonResult<unknown>(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect workspace roots in the indexed directory.
|
||||
* Ensure the library is loaded.
|
||||
*
|
||||
* Loads the native library from the platform-specific npm package
|
||||
* or a local dev build. Throws if the binary is not found.
|
||||
*/
|
||||
export function ffiDetectWorkspaces(handle: NativeHandle): Result<unknown> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_detect_workspaces(handle);
|
||||
return parseResult<unknown>(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live grep - search file contents.
|
||||
*/
|
||||
export function ffiLiveGrep(
|
||||
handle: NativeHandle,
|
||||
query: string,
|
||||
optsJson: string,
|
||||
): Result<unknown> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_live_grep(
|
||||
handle,
|
||||
ptr(encodeString(query)),
|
||||
ptr(encodeString(optsJson)),
|
||||
);
|
||||
return parseResult<unknown>(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-pattern grep - Aho-Corasick multi-needle search.
|
||||
*/
|
||||
export function ffiMultiGrep(handle: NativeHandle, optsJson: string): Result<unknown> {
|
||||
const library = loadLibrary();
|
||||
const resultPtr = library.symbols.fff_multi_grep(handle, ptr(encodeString(optsJson)));
|
||||
return parseResult<unknown>(resultPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the library is loaded (for preloading).
|
||||
*/
|
||||
export async function ensureLoaded(): Promise<void> {
|
||||
await ensureBinary();
|
||||
export function ensureLoaded(): void {
|
||||
loadLibrary();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,33 +40,7 @@ import type {
|
||||
SearchResult,
|
||||
} from "./types";
|
||||
|
||||
import {
|
||||
createGrepCursor,
|
||||
err,
|
||||
toInternalGrepOptions,
|
||||
toInternalInitOptions,
|
||||
toInternalMultiGrepOptions,
|
||||
toInternalSearchOptions,
|
||||
} from "./types";
|
||||
|
||||
/** Transform raw FFI grep result into typed GrepResult with opaque cursor. */
|
||||
function transformGrepResult(result: Result<unknown>): Result<GrepResult> {
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
const raw = result.value as Record<string, unknown>;
|
||||
const nextFileOffset = raw.nextFileOffset as number;
|
||||
const grepResult: GrepResult = {
|
||||
items: raw.items as GrepResult["items"],
|
||||
totalMatched: raw.totalMatched as number,
|
||||
totalFilesSearched: raw.totalFilesSearched as number,
|
||||
totalFiles: raw.totalFiles as number,
|
||||
filteredFileCount: raw.filteredFileCount as number,
|
||||
nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
|
||||
regexFallbackError: raw.regexFallbackError as string | undefined,
|
||||
};
|
||||
return { ok: true, value: grepResult };
|
||||
}
|
||||
import { err } from "./types";
|
||||
|
||||
/**
|
||||
* FileFinder - Fast file finder with fuzzy search
|
||||
@@ -127,8 +101,14 @@ export class FileFinder {
|
||||
* ```
|
||||
*/
|
||||
static create(options: InitOptions): Result<FileFinder> {
|
||||
const internalOpts = toInternalInitOptions(options);
|
||||
const result = ffiCreate(JSON.stringify(internalOpts));
|
||||
const result = ffiCreate(
|
||||
options.basePath,
|
||||
options.frecencyDbPath ?? "",
|
||||
options.historyDbPath ?? "",
|
||||
options.useUnsafeNoLock ?? false,
|
||||
options.warmupMmapCache ?? false,
|
||||
options.aiMode ?? false,
|
||||
);
|
||||
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
@@ -192,18 +172,20 @@ export class FileFinder {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
search(query: string, options?: SearchOptions): Result<SearchResult> {
|
||||
fileSearch(query: string, options?: SearchOptions): Result<SearchResult> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
const internalOpts = toInternalSearchOptions(options);
|
||||
const result = ffiSearch(guard.value, query, JSON.stringify(internalOpts));
|
||||
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result as Result<SearchResult>;
|
||||
return ffiSearch(
|
||||
guard.value,
|
||||
query,
|
||||
options?.currentFile ?? "",
|
||||
options?.maxThreads ?? 0,
|
||||
options?.pageIndex ?? 0,
|
||||
options?.pageSize ?? 0,
|
||||
options?.comboBoostMultiplier ?? 0,
|
||||
options?.minComboCount ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,28 +210,38 @@ export class FileFinder {
|
||||
* @example
|
||||
* ```typescript
|
||||
* // First page
|
||||
* const result = finder.liveGrep("TODO", { mode: "plain" });
|
||||
* const result = finder.grep("TODO", { mode: "plain" });
|
||||
* if (result.ok) {
|
||||
* for (const match of result.value.items) {
|
||||
* console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
* }
|
||||
* // Fetch next page
|
||||
* if (result.value.nextCursor) {
|
||||
* const page2 = finder.liveGrep("TODO", {
|
||||
* const page2 = finder.grep("TODO", {
|
||||
* cursor: result.value.nextCursor,
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
liveGrep(query: string, options?: GrepOptions): Result<GrepResult> {
|
||||
grep(query: string, options?: GrepOptions): Result<GrepResult> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
const internalOpts = toInternalGrepOptions(options);
|
||||
const result = ffiLiveGrep(guard.value, query, JSON.stringify(internalOpts));
|
||||
|
||||
return transformGrepResult(result);
|
||||
return ffiLiveGrep(
|
||||
guard.value,
|
||||
query,
|
||||
options?.mode ?? "plain",
|
||||
options?.maxFileSize ?? 0,
|
||||
options?.maxMatchesPerFile ?? 0,
|
||||
options?.smartCase ?? true,
|
||||
options?.cursor?._offset ?? 0,
|
||||
0, // page_limit (0 = default 50)
|
||||
options?.timeBudgetMs ?? 0,
|
||||
options?.beforeContext ?? 0,
|
||||
options?.afterContext ?? 0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,10 +277,20 @@ export class FileFinder {
|
||||
return err("patterns array must have at least 1 element");
|
||||
}
|
||||
|
||||
const internalOpts = toInternalMultiGrepOptions(options);
|
||||
const result = ffiMultiGrep(guard.value, JSON.stringify(internalOpts));
|
||||
|
||||
return transformGrepResult(result);
|
||||
return ffiMultiGrep(
|
||||
guard.value,
|
||||
options.patterns.join("\n"),
|
||||
options.constraints ?? "",
|
||||
options.maxFileSize ?? 0,
|
||||
options.maxMatchesPerFile ?? 0,
|
||||
options.smartCase ?? true,
|
||||
options.cursor?._offset ?? 0,
|
||||
0, // page_limit (0 = default 50)
|
||||
options.timeBudgetMs ?? 0,
|
||||
options.beforeContext ?? 0,
|
||||
options.afterContext ?? 0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,14 +414,9 @@ export class FileFinder {
|
||||
return isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the native library is loaded.
|
||||
*
|
||||
* This will download the binary if needed and load it.
|
||||
* Useful for preloading before first use.
|
||||
*/
|
||||
static async ensureLoaded(): Promise<void> {
|
||||
return ensureLoaded();
|
||||
/** Ensure the native library is loaded. */
|
||||
static ensureLoaded(): void {
|
||||
ensureLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -433,3 +430,4 @@ export class FileFinder {
|
||||
return ffiHealthCheck(null, testPath || "") as Result<HealthCheck>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function sleep(ms: number) {
|
||||
}
|
||||
|
||||
function findFile(finder: FileFinder, name: string): FileItem | undefined {
|
||||
const result = finder.search(name, { pageSize: 200 });
|
||||
const result = finder.fileSearch(name, { pageSize: 200 });
|
||||
if (!result.ok) throw new Error(`search failed: ${result.error}`);
|
||||
return result.value.items.find((item) => item.fileName === name);
|
||||
}
|
||||
@@ -99,15 +99,15 @@ async function waitForFileGone(finder: FileFinder, name: string): Promise<boolea
|
||||
async function waitForFileCount(finder: FileFinder, count: number): Promise<number> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
||||
const result = finder.search("", { pageSize: 200 });
|
||||
const result = finder.fileSearch("", { pageSize: 200 });
|
||||
if (result.ok && result.value.totalFiles === count) return count;
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
const result = finder.search("", { pageSize: 200 });
|
||||
const result = finder.fileSearch("", { pageSize: 200 });
|
||||
return result.ok ? result.value.totalFiles : -1;
|
||||
}
|
||||
|
||||
/** Poll liveGrep until predicate on totalMatched is satisfied, or the timeout is exceeded. */
|
||||
/** Poll grep until predicate on totalMatched is satisfied, or the timeout is exceeded. */
|
||||
async function waitForGrep(
|
||||
finder: FileFinder,
|
||||
pattern: string,
|
||||
@@ -116,11 +116,11 @@ async function waitForGrep(
|
||||
) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
||||
const result = finder.liveGrep(pattern, options);
|
||||
const result = finder.grep(pattern, options);
|
||||
if (result.ok && predicate(result.value.totalMatched)) return result;
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
return finder.liveGrep(pattern, options);
|
||||
return finder.grep(pattern, options);
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === "win32")("Git lifecycle integration", () => {
|
||||
@@ -161,7 +161,7 @@ describe.skipIf(process.platform === "win32")("Git lifecycle integration", () =>
|
||||
});
|
||||
|
||||
test("initial scan indexes all committed files", () => {
|
||||
const result = finder.search("", { pageSize: 200 });
|
||||
const result = finder.fileSearch("", { pageSize: 200 });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { findBinary, getDevBinaryPath } from "./download";
|
||||
import { findBinary } from "./download";
|
||||
import { FileFinder } from "./index";
|
||||
import { getLibExtension, getLibFilename, getTriple } from "./platform";
|
||||
|
||||
@@ -46,14 +46,6 @@ describe("Platform Detection", () => {
|
||||
});
|
||||
|
||||
describe("Binary Detection", () => {
|
||||
test("getDevBinaryPath finds local build", () => {
|
||||
const devPath = getDevBinaryPath();
|
||||
expect(devPath).not.toBeNull();
|
||||
// Normalize path for cross-platform comparison (Windows uses backslashes)
|
||||
const normalizedPath = normalizePath(devPath);
|
||||
expect(normalizedPath).toContain("target/release");
|
||||
});
|
||||
|
||||
test("findBinary returns a path", () => {
|
||||
const path = findBinary();
|
||||
expect(path).not.toBeNull();
|
||||
@@ -120,7 +112,7 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
if (progress.ok) {
|
||||
}
|
||||
|
||||
const result = finder.search("");
|
||||
const result = finder.fileSearch("");
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
@@ -140,7 +132,7 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
});
|
||||
|
||||
test("search returns a valid result structure", () => {
|
||||
const result = finder.search("Cargo.toml");
|
||||
const result = finder.fileSearch("Cargo.toml");
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
@@ -152,7 +144,7 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
});
|
||||
|
||||
test("search returns empty for non-matching query", () => {
|
||||
const result = finder.search("xyznonexistentfilenamexyz123456");
|
||||
const result = finder.fileSearch("xyznonexistentfilenamexyz123456");
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
@@ -162,7 +154,7 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
});
|
||||
|
||||
test("search respects pageSize option", () => {
|
||||
const result = finder.search("ts", { pageSize: 3 });
|
||||
const result = finder.fileSearch("ts", { pageSize: 3 });
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (result.ok) {
|
||||
@@ -170,8 +162,8 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("liveGrep plain text returns matching lines", () => {
|
||||
const result = finder.liveGrep("fff-core", {
|
||||
test("grep plain text returns matching lines", () => {
|
||||
const result = finder.grep("fff-core", {
|
||||
mode: "plain",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -205,9 +197,9 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("liveGrep fuzzy mode returns results with scores", () => {
|
||||
test("grep fuzzy mode returns results with scores", () => {
|
||||
// Intentional typo: "depdnency" instead of "dependency" to exercise fuzzy matching
|
||||
const result = finder.liveGrep("depdnency", {
|
||||
const result = finder.grep("depdnency", {
|
||||
mode: "fuzzy",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -273,8 +265,8 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
const finder2 = result2.value;
|
||||
|
||||
// Both should work independently
|
||||
const search1 = finder.search("Cargo");
|
||||
const search2 = finder2.search("Cargo");
|
||||
const search1 = finder.fileSearch("Cargo");
|
||||
const search2 = finder2.fileSearch("Cargo");
|
||||
|
||||
expect(search1.ok).toBe(true);
|
||||
expect(search2.ok).toBe(true);
|
||||
@@ -282,7 +274,7 @@ describe("FileFinder - Full Lifecycle", () => {
|
||||
// Destroying one should not affect the other
|
||||
finder2.destroy();
|
||||
|
||||
const search3 = finder.search("Cargo");
|
||||
const search3 = finder.fileSearch("Cargo");
|
||||
expect(search3.ok).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -297,7 +289,7 @@ describe("FileFinder - Error Handling", () => {
|
||||
const f = createResult.value;
|
||||
f.destroy();
|
||||
|
||||
const result = f.search("test");
|
||||
const result = f.fileSearch("test");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("destroyed");
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* finder.waitForScan(5000);
|
||||
*
|
||||
* // Search for files
|
||||
* const search = finder.search("main.ts");
|
||||
* const search = finder.fileSearch("main.ts");
|
||||
* if (search.ok) {
|
||||
* for (const item of search.value.items) {
|
||||
* console.log(item.relativePath);
|
||||
@@ -37,15 +37,11 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { FileFinder } from "./finder";
|
||||
|
||||
export {
|
||||
binaryExists,
|
||||
downloadBinary,
|
||||
ensureBinary,
|
||||
findBinary,
|
||||
getBinaryPath,
|
||||
} from "./download";
|
||||
export { FileFinder } from "./finder";
|
||||
|
||||
export {
|
||||
getLibExtension,
|
||||
|
||||
@@ -95,20 +95,20 @@ export function getLibFilename(): string {
|
||||
* Map from Rust target triple to npm platform package name
|
||||
*/
|
||||
const TRIPLE_TO_NPM_PACKAGE: Record<string, string> = {
|
||||
"aarch64-apple-darwin": "@ff-labs/fff-bun-darwin-arm64",
|
||||
"x86_64-apple-darwin": "@ff-labs/fff-bun-darwin-x64",
|
||||
"x86_64-unknown-linux-gnu": "@ff-labs/fff-bun-linux-x64-gnu",
|
||||
"aarch64-unknown-linux-gnu": "@ff-labs/fff-bun-linux-arm64-gnu",
|
||||
"x86_64-unknown-linux-musl": "@ff-labs/fff-bun-linux-x64-musl",
|
||||
"aarch64-unknown-linux-musl": "@ff-labs/fff-bun-linux-arm64-musl",
|
||||
"x86_64-pc-windows-msvc": "@ff-labs/fff-bun-win32-x64",
|
||||
"aarch64-pc-windows-msvc": "@ff-labs/fff-bun-win32-arm64",
|
||||
"aarch64-apple-darwin": "@ff-labs/fff-bin-darwin-arm64",
|
||||
"x86_64-apple-darwin": "@ff-labs/fff-bin-darwin-x64",
|
||||
"x86_64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-x64-gnu",
|
||||
"aarch64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-arm64-gnu",
|
||||
"x86_64-unknown-linux-musl": "@ff-labs/fff-bin-linux-x64-musl",
|
||||
"aarch64-unknown-linux-musl": "@ff-labs/fff-bin-linux-arm64-musl",
|
||||
"x86_64-pc-windows-msvc": "@ff-labs/fff-bin-win32-x64",
|
||||
"aarch64-pc-windows-msvc": "@ff-labs/fff-bin-win32-arm64",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the npm package name for the current platform's native binary.
|
||||
*
|
||||
* @returns Package name like "@ff-labs/fff-bun-darwin-arm64"
|
||||
* @returns Package name like "@ff-labs/fff-bin-darwin-arm64"
|
||||
* @throws If the current platform is not supported
|
||||
*/
|
||||
export function getNpmPackageName(): string {
|
||||
|
||||
@@ -208,62 +208,6 @@ export interface HealthCheck {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Options format sent to Rust FFI
|
||||
* @internal
|
||||
*/
|
||||
export interface InitOptionsInternal {
|
||||
base_path: string;
|
||||
frecency_db_path?: string;
|
||||
history_db_path?: string;
|
||||
use_unsafe_no_lock: boolean;
|
||||
warmup_mmap_cache: boolean;
|
||||
ai_mode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Search options format sent to Rust FFI
|
||||
* @internal
|
||||
*/
|
||||
export interface SearchOptionsInternal {
|
||||
max_threads?: number;
|
||||
current_file?: string;
|
||||
combo_boost_multiplier?: number;
|
||||
min_combo_count?: number;
|
||||
page_index?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert public InitOptions to internal format
|
||||
* @internal
|
||||
*/
|
||||
export function toInternalInitOptions(opts: InitOptions): InitOptionsInternal {
|
||||
return {
|
||||
base_path: opts.basePath,
|
||||
frecency_db_path: opts.frecencyDbPath,
|
||||
history_db_path: opts.historyDbPath,
|
||||
use_unsafe_no_lock: opts.useUnsafeNoLock ?? false,
|
||||
warmup_mmap_cache: opts.warmupMmapCache ?? false,
|
||||
ai_mode: opts.aiMode ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert public SearchOptions to internal format
|
||||
* @internal
|
||||
*/
|
||||
export function toInternalSearchOptions(opts?: SearchOptions): SearchOptionsInternal {
|
||||
return {
|
||||
max_threads: opts?.maxThreads,
|
||||
current_file: opts?.currentFile,
|
||||
combo_boost_multiplier: opts?.comboBoostMultiplier,
|
||||
min_combo_count: opts?.minComboCount,
|
||||
page_index: opts?.pageIndex,
|
||||
page_size: opts?.pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grep search mode
|
||||
*/
|
||||
@@ -417,78 +361,3 @@ export interface MultiGrepOptions {
|
||||
afterContext?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Multi-grep options format sent to Rust FFI
|
||||
* @internal
|
||||
*/
|
||||
export interface MultiGrepOptionsInternal {
|
||||
patterns: string[];
|
||||
constraints?: string;
|
||||
max_file_size?: number;
|
||||
max_matches_per_file?: number;
|
||||
smart_case?: boolean;
|
||||
file_offset?: number;
|
||||
page_limit?: number;
|
||||
time_budget_ms?: number;
|
||||
before_context?: number;
|
||||
after_context?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert public MultiGrepOptions to internal format
|
||||
* @internal
|
||||
*/
|
||||
export function toInternalMultiGrepOptions(
|
||||
opts: MultiGrepOptions,
|
||||
pageLimit?: number,
|
||||
): MultiGrepOptionsInternal {
|
||||
return {
|
||||
patterns: opts.patterns,
|
||||
constraints: opts.constraints,
|
||||
max_file_size: opts.maxFileSize,
|
||||
max_matches_per_file: opts.maxMatchesPerFile,
|
||||
smart_case: opts.smartCase,
|
||||
file_offset: opts.cursor?._offset ?? 0,
|
||||
page_limit: pageLimit,
|
||||
time_budget_ms: opts.timeBudgetMs,
|
||||
before_context: opts.beforeContext,
|
||||
after_context: opts.afterContext,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Grep options format sent to Rust FFI
|
||||
* @internal
|
||||
*/
|
||||
export interface GrepOptionsInternal {
|
||||
max_file_size?: number;
|
||||
max_matches_per_file?: number;
|
||||
smart_case?: boolean;
|
||||
file_offset?: number;
|
||||
page_limit?: number;
|
||||
mode?: string;
|
||||
time_budget_ms?: number;
|
||||
before_context?: number;
|
||||
after_context?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert public GrepOptions to internal format
|
||||
* @internal
|
||||
*/
|
||||
export function toInternalGrepOptions(
|
||||
opts?: GrepOptions,
|
||||
pageLimit?: number,
|
||||
): GrepOptionsInternal {
|
||||
return {
|
||||
max_file_size: opts?.maxFileSize,
|
||||
max_matches_per_file: opts?.maxMatchesPerFile,
|
||||
smart_case: opts?.smartCase,
|
||||
file_offset: opts?.cursor?._offset ?? 0,
|
||||
page_limit: pageLimit,
|
||||
mode: opts?.mode,
|
||||
time_budget_ms: opts?.timeBudgetMs,
|
||||
before_context: opts?.beforeContext,
|
||||
after_context: opts?.afterContext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ async function main() {
|
||||
|
||||
// Search test
|
||||
console.log("Searching for 'lib.rs'...");
|
||||
const searchResult = finder.search("lib.rs", { pageSize: 5 });
|
||||
const searchResult = finder.fileSearch("lib.rs", { pageSize: 5 });
|
||||
|
||||
if (searchResult.ok) {
|
||||
console.log(`Found ${searchResult.value.totalMatched} matches (showing first 5):\n`);
|
||||
@@ -94,7 +94,7 @@ async function main() {
|
||||
|
||||
// Search with different query
|
||||
console.log("Searching for 'package.json'...");
|
||||
const searchResult2 = finder.search("package.json", { pageSize: 3 });
|
||||
const searchResult2 = finder.fileSearch("package.json", { pageSize: 3 });
|
||||
|
||||
if (searchResult2.ok) {
|
||||
console.log(`Found ${searchResult2.value.totalMatched} matches:\n`);
|
||||
@@ -127,7 +127,7 @@ async function main() {
|
||||
console.log(" Second instance created successfully");
|
||||
|
||||
finder2.waitForScan(5000);
|
||||
const search2 = finder2.search("Cargo.toml");
|
||||
const search2 = finder2.fileSearch("Cargo.toml");
|
||||
if (search2.ok) {
|
||||
console.log(
|
||||
` Second instance found ${search2.value.totalMatched} matches for 'Cargo.toml'`,
|
||||
@@ -138,7 +138,7 @@ async function main() {
|
||||
console.log(" Second instance destroyed");
|
||||
|
||||
// First instance should still work
|
||||
const search3 = finder.search("Cargo.toml");
|
||||
const search3 = finder.fileSearch("Cargo.toml");
|
||||
if (search3.ok) {
|
||||
console.log(` First instance still works: ${search3.value.totalMatched} matches`);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"lib": ["ESNext"],
|
||||
"types": ["bun"]
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*", "*.ts"],
|
||||
"include": ["src/**/*", "*.ts"],
|
||||
"exclude": ["node_modules", "dist", "bin"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@ff-labs/fff-node",
|
||||
"version": "0.1.37",
|
||||
"private": false,
|
||||
"description": "High-performance fuzzy file finder for Node.js - perfect for LLM agent tools",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node test/e2e.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/dmtrKovalenko/fff.nvim.git",
|
||||
"directory": "packages/fff-node"
|
||||
},
|
||||
"keywords": [
|
||||
"file-finder",
|
||||
"fuzzy-search",
|
||||
"node",
|
||||
"ffi",
|
||||
"llm-tools",
|
||||
"agent-tools",
|
||||
"fast",
|
||||
"rust"
|
||||
],
|
||||
"author": "Dmitry Kovalenko",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dmtrKovalenko/fff.nvim/issues"
|
||||
},
|
||||
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
||||
"dependencies": {
|
||||
"ffi-rs": "^1.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.0.0",
|
||||
"@ff-labs/fff-bin-darwin-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-x64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-linux-arm64-musl": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.0.0",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
Executable → Regular
+30
-24
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* CLI tool for fff package management
|
||||
* CLI tool for fff-node package management
|
||||
*
|
||||
* Usage:
|
||||
* bunx fff download [tag] - Download native binary from GitHub
|
||||
* bunx fff info - Show platform and binary info
|
||||
* npx @ff-labs/fff-node download [tag] - Download native binary from GitHub
|
||||
* npx @ff-labs/fff-node info - Show platform and binary info
|
||||
*/
|
||||
|
||||
import { downloadBinary, getBinaryPath, findBinary } from "../src/download";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { downloadBinary, findBinary, getBinaryPath } from "../src/binary.js";
|
||||
import {
|
||||
getTriple,
|
||||
getLibExtension,
|
||||
getLibFilename,
|
||||
getNpmPackageName,
|
||||
} from "../src/platform";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
getTriple,
|
||||
} from "../src/platform.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
@@ -24,12 +24,12 @@ interface PackageJson {
|
||||
version: string;
|
||||
}
|
||||
|
||||
async function getPackageInfo(): Promise<PackageJson> {
|
||||
function getPackageInfo(): PackageJson {
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = join(currentDir, "..", "package.json");
|
||||
|
||||
try {
|
||||
return await Bun.file(packageJsonPath).json();
|
||||
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
} catch {
|
||||
return { version: "unknown" };
|
||||
}
|
||||
@@ -51,7 +51,7 @@ async function main() {
|
||||
}
|
||||
|
||||
case "info": {
|
||||
const pkg = await getPackageInfo();
|
||||
const pkg = getPackageInfo();
|
||||
let npmPackage: string;
|
||||
try {
|
||||
npmPackage = getNpmPackageName();
|
||||
@@ -59,7 +59,7 @@ async function main() {
|
||||
npmPackage = "unsupported";
|
||||
}
|
||||
|
||||
console.log("fff - Fast File Finder");
|
||||
console.log("fff - Fast File Finder (Node.js)");
|
||||
console.log(`Package version: ${pkg.version}`);
|
||||
console.log("");
|
||||
console.log("Platform Information:");
|
||||
@@ -73,9 +73,9 @@ async function main() {
|
||||
if (existing) {
|
||||
console.log(` Found: ${existing}`);
|
||||
} else {
|
||||
console.log(` Not found`);
|
||||
console.log(" Not found");
|
||||
console.log(` Expected path: ${getBinaryPath()}`);
|
||||
console.log(` Try: bun add ${npmPackage}`);
|
||||
console.log(` Try: npm add ${npmPackage}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -83,26 +83,32 @@ async function main() {
|
||||
case "version":
|
||||
case "--version":
|
||||
case "-v": {
|
||||
const pkg = await getPackageInfo();
|
||||
const pkg = getPackageInfo();
|
||||
console.log(pkg.version);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
const pkg = await getPackageInfo();
|
||||
console.log(`fff - Fast File Finder CLI v${pkg.version}`);
|
||||
const pkg = getPackageInfo();
|
||||
console.log(`fff - Fast File Finder CLI (Node.js) v${pkg.version}`);
|
||||
console.log("");
|
||||
console.log("Usage:");
|
||||
console.log(
|
||||
" bunx fff download [tag] Download native binary from GitHub (fallback)",
|
||||
" npx @ff-labs/fff-node download [tag] Download native binary from GitHub (fallback)",
|
||||
);
|
||||
console.log(" bunx fff info Show platform and binary info");
|
||||
console.log(" bunx fff version Show version");
|
||||
console.log(" bunx fff help Show this help message");
|
||||
console.log(
|
||||
" npx @ff-labs/fff-node info Show platform and binary info",
|
||||
);
|
||||
console.log(" npx @ff-labs/fff-node version Show version");
|
||||
console.log(" npx @ff-labs/fff-node help Show this help message");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" bunx fff download Download latest binary from GitHub");
|
||||
console.log(" bunx fff download abc1234 Download specific release tag");
|
||||
console.log(
|
||||
" npx @ff-labs/fff-node download Download latest binary from GitHub",
|
||||
);
|
||||
console.log(
|
||||
" npx @ff-labs/fff-node download abc1234 Download specific release tag",
|
||||
);
|
||||
console.log("");
|
||||
console.log(
|
||||
"Note: Binaries are normally provided via platform-specific npm packages.",
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Postinstall script - ensures the native binary is available
|
||||
*
|
||||
@@ -8,8 +7,8 @@
|
||||
* 3. Fallback: download from GitHub releases
|
||||
*/
|
||||
|
||||
import { findBinary, downloadBinary } from "../src/download";
|
||||
import { getNpmPackageName } from "../src/platform";
|
||||
import { downloadBinary, findBinary } from "../src/binary.js";
|
||||
import { getNpmPackageName } from "../src/platform.js";
|
||||
|
||||
async function main() {
|
||||
// Check if binary is already available (npm package or dev build)
|
||||
@@ -40,7 +39,9 @@ async function main() {
|
||||
console.error("fff: You can build from source instead:");
|
||||
console.error(" cargo build --release -p fff-c");
|
||||
console.error("");
|
||||
console.error("fff: Or run `bunx fff download` after fixing network issues.");
|
||||
console.error(
|
||||
"fff: Or run `npx @ff-labs/fff-node download` after fixing network issues.",
|
||||
);
|
||||
// Don't exit with error - allow install to complete
|
||||
// The error will surface when the user tries to use the library
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Binary resolution utilities for fff-node
|
||||
*
|
||||
* Resolves the native library from:
|
||||
* 1. Platform-specific npm package (e.g. @ff-labs/fff-bin-darwin-arm64)
|
||||
* 2. Local dev build (target/release or target/debug)
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { getLibFilename, getNpmPackageName } from "./platform.js";
|
||||
|
||||
/**
|
||||
* Get the current file's directory
|
||||
*/
|
||||
function getCurrentDir(): string {
|
||||
const url = import.meta.url;
|
||||
|
||||
if (url.startsWith("file://")) {
|
||||
return dirname(fileURLToPath(url));
|
||||
}
|
||||
return dirname(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package root directory
|
||||
*/
|
||||
function getPackageDir(): string {
|
||||
const currentDir = getCurrentDir();
|
||||
// In dev: src/ -> package root
|
||||
// In dist: dist/src/ -> package root
|
||||
// We look for package.json to find the actual root
|
||||
let dir = currentDir;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (existsSync(join(dir, "package.json"))) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
||||
if (pkg.name === "@ff-labs/fff-node") {
|
||||
return dir;
|
||||
}
|
||||
} catch {
|
||||
// Not our package.json, keep going up
|
||||
}
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
// Fallback: assume we're one level deep in src/
|
||||
return dirname(currentDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the binary exists in any known location
|
||||
*/
|
||||
export function binaryExists(): boolean {
|
||||
return findBinary() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve the binary from the platform-specific npm package.
|
||||
*
|
||||
* When users install @ff-labs/fff-node, npm automatically installs the matching
|
||||
* optionalDependency (e.g. @ff-labs/fff-bin-darwin-arm64). We resolve the binary
|
||||
* path by requiring that package's package.json and looking for the binary
|
||||
* in the same directory.
|
||||
*/
|
||||
function resolveFromNpmPackage(): string | null {
|
||||
const packageName = getNpmPackageName();
|
||||
|
||||
try {
|
||||
// Use createRequire to resolve the platform package's location
|
||||
const require = createRequire(join(getPackageDir(), "package.json"));
|
||||
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
||||
const packageDir = dirname(packageJsonPath);
|
||||
const binaryPath = join(packageDir, getLibFilename());
|
||||
|
||||
if (existsSync(binaryPath)) {
|
||||
return binaryPath;
|
||||
}
|
||||
} catch {
|
||||
// Package not installed - this is expected on unsupported platforms
|
||||
// or when installed without optional dependencies
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the development binary path (for local development)
|
||||
*/
|
||||
function getDevBinaryPath(): string | null {
|
||||
const packageDir = getPackageDir();
|
||||
const workspaceRoot = join(packageDir, "..", "..");
|
||||
|
||||
const possiblePaths = [
|
||||
join(workspaceRoot, "target", "release", getLibFilename()),
|
||||
join(workspaceRoot, "target", "debug", getLibFilename()),
|
||||
];
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
if (existsSync(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isDevWorkspace(): boolean {
|
||||
const packageDir = getPackageDir();
|
||||
const workspaceRoot = join(packageDir, "..", "..");
|
||||
return existsSync(join(workspaceRoot, "Cargo.toml"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the native library binary.
|
||||
*
|
||||
* Resolution order:
|
||||
* - Dev workspace: local dev build first, then npm package
|
||||
* - Production: npm package first, then dev build
|
||||
*
|
||||
* @returns Absolute path to the library, or null if not found
|
||||
*/
|
||||
export function findBinary(): string | null {
|
||||
if (isDevWorkspace()) {
|
||||
// 1. Local bin/ directory (populated by `make prepare-node`)
|
||||
const binPath = join(getPackageDir(), "bin", getLibFilename());
|
||||
if (existsSync(binPath)) return binPath;
|
||||
|
||||
// 2. Local dev build (target/release or target/debug)
|
||||
const devPath = getDevBinaryPath();
|
||||
if (devPath) return devPath;
|
||||
|
||||
// 3. Fallback to npm package
|
||||
const npmPath = resolveFromNpmPackage();
|
||||
if (npmPath) return npmPath;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Production: npm package first
|
||||
const npmPath = resolveFromNpmPackage();
|
||||
if (npmPath) return npmPath;
|
||||
|
||||
// Fallback: local dev build (e.g. user built from source)
|
||||
return getDevBinaryPath();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* FileFinder - High-level API for the fff file finder
|
||||
*
|
||||
* This class provides a type-safe, ergonomic API for file finding operations.
|
||||
* Each instance owns an independent native file picker that can be created
|
||||
* and destroyed independently. Multiple instances can coexist.
|
||||
*
|
||||
* All methods return Result types for explicit error handling.
|
||||
*/
|
||||
|
||||
import {
|
||||
ensureLoaded,
|
||||
ffiCreate,
|
||||
ffiDestroy,
|
||||
ffiGetHistoricalQuery,
|
||||
ffiGetScanProgress,
|
||||
ffiHealthCheck,
|
||||
ffiIsScanning,
|
||||
ffiLiveGrep,
|
||||
ffiMultiGrep,
|
||||
ffiRefreshGitStatus,
|
||||
ffiRestartIndex,
|
||||
ffiScanFiles,
|
||||
ffiSearch,
|
||||
ffiTrackQuery,
|
||||
isAvailable,
|
||||
type NativeHandle,
|
||||
} from "./ffi.js";
|
||||
|
||||
import type {
|
||||
GrepOptions,
|
||||
GrepResult,
|
||||
HealthCheck,
|
||||
InitOptions,
|
||||
MultiGrepOptions,
|
||||
Result,
|
||||
ScanProgress,
|
||||
SearchOptions,
|
||||
SearchResult,
|
||||
} from "./types.js";
|
||||
|
||||
import { err } from "./types.js";
|
||||
|
||||
/**
|
||||
* FileFinder - Fast file finder with fuzzy search
|
||||
*
|
||||
* Each instance is backed by an independent native file picker. Create as many
|
||||
* as you need and destroy them when done.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import { FileFinder } from "@ff-labs/fff-node";
|
||||
*
|
||||
* // Create an instance
|
||||
* const finder = FileFinder.create({ basePath: "/path/to/project" });
|
||||
* if (!finder.ok) {
|
||||
* console.error(finder.error);
|
||||
* process.exit(1);
|
||||
* }
|
||||
*
|
||||
* // Wait for initial scan
|
||||
* finder.value.waitForScan(5000);
|
||||
*
|
||||
* // Search for files
|
||||
* const search = finder.value.search("main.ts");
|
||||
* if (search.ok) {
|
||||
* for (const item of search.value.items) {
|
||||
* console.log(item.relativePath);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Cleanup
|
||||
* finder.value.destroy();
|
||||
* ```
|
||||
*/
|
||||
export class FileFinder {
|
||||
private handle: NativeHandle | null;
|
||||
|
||||
private constructor(handle: NativeHandle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file finder instance.
|
||||
*
|
||||
* @param options - Initialization options
|
||||
* @returns Result containing the new FileFinder instance or an error
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic initialization
|
||||
* const finder = FileFinder.create({ basePath: "/path/to/project" });
|
||||
*
|
||||
* // With custom database paths
|
||||
* const finder = FileFinder.create({
|
||||
* basePath: "/path/to/project",
|
||||
* frecencyDbPath: "/custom/frecency.mdb",
|
||||
* historyDbPath: "/custom/history.mdb",
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static create(options: InitOptions): Result<FileFinder> {
|
||||
const result = ffiCreate(
|
||||
options.basePath,
|
||||
options.frecencyDbPath ?? "",
|
||||
options.historyDbPath ?? "",
|
||||
options.useUnsafeNoLock ?? false,
|
||||
options.warmupMmapCache ?? false,
|
||||
options.aiMode ?? false,
|
||||
);
|
||||
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return { ok: true, value: new FileFinder(result.value) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy and clean up all resources.
|
||||
*
|
||||
* Call this when you're done using the file finder to free memory
|
||||
* and stop background file watching. After calling this, the instance
|
||||
* must not be used again.
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.handle !== null) {
|
||||
ffiDestroy(this.handle);
|
||||
this.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this instance has been destroyed.
|
||||
*/
|
||||
get isDestroyed(): boolean {
|
||||
return this.handle === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard that returns an error if the instance has been destroyed.
|
||||
*/
|
||||
private ensureAlive(): Result<NativeHandle> {
|
||||
if (this.handle === null) {
|
||||
return err("FileFinder instance has been destroyed.");
|
||||
}
|
||||
return { ok: true, value: this.handle };
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for files matching the query.
|
||||
*
|
||||
* The query supports fuzzy matching and special syntax:
|
||||
* - `foo bar` - Match files containing "foo" and "bar"
|
||||
* - `src/` - Match files in src directory
|
||||
* - `file.ts:42` - Match file.ts with line 42
|
||||
* - `file.ts:42:10` - Match file.ts with line 42, column 10
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param options - Search options
|
||||
* @returns Search results with matched files and scores
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = finder.search("main.ts", { pageSize: 10 });
|
||||
* if (result.ok) {
|
||||
* console.log(`Found ${result.value.totalMatched} files`);
|
||||
* for (const item of result.value.items) {
|
||||
* console.log(item.relativePath);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
fileSearch(query: string, options?: SearchOptions): Result<SearchResult> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
return ffiSearch(
|
||||
guard.value,
|
||||
query,
|
||||
options?.currentFile ?? "",
|
||||
options?.maxThreads ?? 0,
|
||||
options?.pageIndex ?? 0,
|
||||
options?.pageSize ?? 0,
|
||||
options?.comboBoostMultiplier ?? 0,
|
||||
options?.minComboCount ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search file contents (live grep).
|
||||
*
|
||||
* Searches through the contents of indexed files using the specified mode:
|
||||
* - `"plain"` (default): SIMD-accelerated literal text matching
|
||||
* - `"regex"`: Regular expression matching
|
||||
* - `"fuzzy"`: Smith-Waterman fuzzy matching per line
|
||||
*
|
||||
* Supports pagination for large result sets. The result includes a `nextCursor`
|
||||
* that can be passed back to fetch the next page.
|
||||
*
|
||||
* The query also supports constraint syntax:
|
||||
* - `*.ts pattern` - Only search in TypeScript files
|
||||
* - `src/ pattern` - Only search in the src directory
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param options - Grep options (mode, pagination, limits)
|
||||
* @returns Grep results with matched lines and file metadata
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // First page
|
||||
* const result = finder.grep("TODO", { mode: "plain" });
|
||||
* if (result.ok) {
|
||||
* for (const match of result.value.items) {
|
||||
* console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
* }
|
||||
* // Fetch next page
|
||||
* if (result.value.nextCursor) {
|
||||
* const page2 = finder.grep("TODO", {
|
||||
* cursor: result.value.nextCursor,
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
grep(query: string, options?: GrepOptions): Result<GrepResult> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
return ffiLiveGrep(
|
||||
guard.value,
|
||||
query,
|
||||
options?.mode ?? "plain",
|
||||
options?.maxFileSize ?? 0,
|
||||
options?.maxMatchesPerFile ?? 0,
|
||||
options?.smartCase ?? true,
|
||||
options?.cursor?._offset ?? 0,
|
||||
0, // page_limit (0 = default 50)
|
||||
options?.timeBudgetMs ?? 0,
|
||||
options?.beforeContext ?? 0,
|
||||
options?.afterContext ?? 0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-pattern OR search using Aho-Corasick.
|
||||
*
|
||||
* Searches for lines matching ANY of the provided patterns using
|
||||
* SIMD-accelerated multi-needle matching. Faster than regex alternation
|
||||
* for literal text searches.
|
||||
*
|
||||
* Supports pagination. The result includes a `nextCursor` that can be
|
||||
* passed back to fetch the next page.
|
||||
*
|
||||
* @param options - Multi-grep options including patterns and optional constraints
|
||||
* @returns Grep results with matched lines and file metadata
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = finder.multiGrep({
|
||||
* patterns: ["VideoFrame", "video_frame", "PreloadedImage"],
|
||||
* });
|
||||
* if (result.ok) {
|
||||
* for (const match of result.value.items) {
|
||||
* console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
multiGrep(options: MultiGrepOptions): Result<GrepResult> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
if (!options.patterns || options.patterns.length === 0) {
|
||||
return err("patterns array must have at least 1 element");
|
||||
}
|
||||
|
||||
return ffiMultiGrep(
|
||||
guard.value,
|
||||
options.patterns.join("\n"),
|
||||
options.constraints ?? "",
|
||||
options.maxFileSize ?? 0,
|
||||
options.maxMatchesPerFile ?? 0,
|
||||
options.smartCase ?? true,
|
||||
options.cursor?._offset ?? 0,
|
||||
0, // page_limit (0 = default 50)
|
||||
options.timeBudgetMs ?? 0,
|
||||
options.beforeContext ?? 0,
|
||||
options.afterContext ?? 0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a rescan of the indexed directory.
|
||||
*
|
||||
* This is useful after major file system changes that the
|
||||
* background watcher might have missed.
|
||||
*/
|
||||
scanFiles(): Result<void> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiScanFiles(guard.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a scan is currently in progress.
|
||||
*/
|
||||
isScanning(): boolean {
|
||||
if (this.handle === null) return false;
|
||||
return ffiIsScanning(this.handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current scan progress.
|
||||
*/
|
||||
getScanProgress(): Result<ScanProgress> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiGetScanProgress(guard.value) as Result<ScanProgress>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the initial file scan to complete.
|
||||
*
|
||||
* Non-blocking — polls `isScanning` and yields to the event loop between checks.
|
||||
*
|
||||
* @param timeoutMs - Maximum time to wait in milliseconds (default: 5000)
|
||||
* @returns true if scan completed, false if timed out
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const finder = FileFinder.create({ basePath: "/path/to/project" });
|
||||
* if (finder.ok) {
|
||||
* const completed = await finder.value.waitForScan(10000);
|
||||
* if (!completed.ok || !completed.value) {
|
||||
* console.warn("Scan did not complete in time");
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async waitForScan(timeoutMs: number = 5000): Promise<Result<boolean>> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (this.isScanning()) {
|
||||
if (Date.now() >= deadline) {
|
||||
return { ok: true, value: false };
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
return { ok: true, value: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the indexed directory to a new path.
|
||||
*
|
||||
* This stops the current file watcher and starts indexing the new directory.
|
||||
*
|
||||
* @param newPath - New directory path to index
|
||||
*/
|
||||
reindex(newPath: string): Result<void> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiRestartIndex(guard.value, newPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the git status cache.
|
||||
*
|
||||
* @returns Number of files with updated git status
|
||||
*/
|
||||
refreshGitStatus(): Result<number> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiRefreshGitStatus(guard.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track query completion for smart suggestions.
|
||||
*
|
||||
* Call this when a user selects a file from search results.
|
||||
* This helps improve future search rankings for similar queries.
|
||||
*
|
||||
* @param query - The search query that was used
|
||||
* @param selectedFilePath - The file path that was selected
|
||||
*/
|
||||
trackQuery(query: string, selectedFilePath: string): Result<boolean> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiTrackQuery(guard.value, query, selectedFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a historical query by offset.
|
||||
*
|
||||
* @param offset - Offset from most recent (0 = most recent)
|
||||
* @returns The historical query string, or null if not found
|
||||
*/
|
||||
getHistoricalQuery(offset: number): Result<string | null> {
|
||||
const guard = this.ensureAlive();
|
||||
if (!guard.ok) return guard;
|
||||
return ffiGetHistoricalQuery(guard.value, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health check information.
|
||||
*
|
||||
* Useful for debugging and verifying the file finder is working correctly.
|
||||
*
|
||||
* @param testPath - Optional path to test git repository detection
|
||||
*/
|
||||
healthCheck(testPath?: string): Result<HealthCheck> {
|
||||
return ffiHealthCheck(this.handle, testPath || "") as Result<HealthCheck>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the native library is available.
|
||||
*/
|
||||
static isAvailable(): boolean {
|
||||
return isAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the native library is loaded.
|
||||
*
|
||||
* Loads the native library from the platform-specific npm package
|
||||
* or a local dev build. Throws if the binary is not found.
|
||||
*/
|
||||
static ensureLoaded(): void {
|
||||
ensureLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a health check without requiring an instance.
|
||||
*
|
||||
* Returns limited info (version + git only, no picker/frecency/query data).
|
||||
*
|
||||
* @param testPath - Optional path to test git repository detection
|
||||
*/
|
||||
static healthCheckStatic(testPath?: string): Result<HealthCheck> {
|
||||
return ffiHealthCheck(null, testPath || "") as Result<HealthCheck>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* fff - Fast File Finder
|
||||
*
|
||||
* High-performance fuzzy file finder for Node.js, powered by Rust.
|
||||
* Perfect for LLM agent tools that need to search through codebases.
|
||||
*
|
||||
* Each `FileFinder` instance is backed by an independent native file picker.
|
||||
* Create as many as you need and destroy them when done.
|
||||
*
|
||||
* Uses ffi-rs to load the same native libfff_c binary used by @ff-labs/fff-bun.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { FileFinder } from "@ff-labs/fff-node";
|
||||
*
|
||||
* // Create a file finder instance
|
||||
* const result = FileFinder.create({ basePath: "/path/to/project" });
|
||||
* if (!result.ok) {
|
||||
* console.error(result.error);
|
||||
* process.exit(1);
|
||||
* }
|
||||
* const finder = result.value;
|
||||
*
|
||||
* // Wait for initial scan
|
||||
* finder.waitForScan(5000);
|
||||
*
|
||||
* // Search for files
|
||||
* const search = finder.fileSearch("main.ts");
|
||||
* if (search.ok) {
|
||||
* for (const item of search.value.items) {
|
||||
* console.log(item.relativePath);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Cleanup when done
|
||||
* finder.destroy();
|
||||
* ```
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
binaryExists,
|
||||
findBinary,
|
||||
} from "./binary.js";
|
||||
export { closeLibrary } from "./ffi.js";
|
||||
export { FileFinder } from "./finder.js";
|
||||
export {
|
||||
getLibExtension,
|
||||
getLibFilename,
|
||||
getNpmPackageName,
|
||||
getTriple,
|
||||
} from "./platform.js";
|
||||
|
||||
export type {
|
||||
DbHealth,
|
||||
FileItem,
|
||||
GrepCursor,
|
||||
GrepMatch,
|
||||
GrepMode,
|
||||
GrepOptions,
|
||||
GrepResult,
|
||||
HealthCheck,
|
||||
InitOptions,
|
||||
Location,
|
||||
MultiGrepOptions,
|
||||
Result,
|
||||
ScanProgress,
|
||||
Score,
|
||||
SearchOptions,
|
||||
SearchResult,
|
||||
} from "./types.js";
|
||||
// Result helpers
|
||||
export { err, ok } from "./types.js";
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Platform detection utilities for downloading the correct binary
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Get the platform triple (e.g., "x86_64-unknown-linux-gnu")
|
||||
*/
|
||||
export function getTriple(): string {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
|
||||
let osName: string;
|
||||
if (platform === "darwin") {
|
||||
osName = "apple-darwin";
|
||||
} else if (platform === "linux") {
|
||||
osName = detectLinuxLibc();
|
||||
} else if (platform === "win32") {
|
||||
osName = "pc-windows-msvc";
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
const archName = normalizeArch(arch);
|
||||
return `${archName}-${osName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether we're on musl or glibc Linux
|
||||
*/
|
||||
function detectLinuxLibc(): string {
|
||||
try {
|
||||
const lddOutput = execSync("ldd --version 2>&1", {
|
||||
encoding: "utf-8",
|
||||
timeout: 5000,
|
||||
});
|
||||
if (lddOutput.toLowerCase().includes("musl")) {
|
||||
return "unknown-linux-musl";
|
||||
}
|
||||
} catch {
|
||||
// ldd failed, assume glibc
|
||||
}
|
||||
return "unknown-linux-gnu";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize architecture name to Rust target format
|
||||
*/
|
||||
function normalizeArch(arch: string): string {
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
case "amd64":
|
||||
return "x86_64";
|
||||
case "arm64":
|
||||
return "aarch64";
|
||||
case "arm":
|
||||
return "arm";
|
||||
default:
|
||||
throw new Error(`Unsupported architecture: ${arch}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the library file extension for the current platform
|
||||
*/
|
||||
export function getLibExtension(): "dylib" | "so" | "dll" {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "dylib";
|
||||
case "win32":
|
||||
return "dll";
|
||||
default:
|
||||
return "so";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the library filename prefix (empty on Windows)
|
||||
*/
|
||||
export function getLibPrefix(): string {
|
||||
return process.platform === "win32" ? "" : "lib";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full library filename for the current platform
|
||||
*/
|
||||
export function getLibFilename(): string {
|
||||
const prefix = getLibPrefix();
|
||||
const ext = getLibExtension();
|
||||
return `${prefix}fff_c.${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map from Rust target triple to npm platform package name.
|
||||
* The @ff-labs/fff-bin-* packages contain the pre-built libfff_c
|
||||
* shared library and are runtime-agnostic (used by both Bun and Node).
|
||||
*/
|
||||
const TRIPLE_TO_NPM_PACKAGE: Record<string, string> = {
|
||||
"aarch64-apple-darwin": "@ff-labs/fff-bin-darwin-arm64",
|
||||
"x86_64-apple-darwin": "@ff-labs/fff-bin-darwin-x64",
|
||||
"x86_64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-x64-gnu",
|
||||
"aarch64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-arm64-gnu",
|
||||
"x86_64-unknown-linux-musl": "@ff-labs/fff-bin-linux-x64-musl",
|
||||
"aarch64-unknown-linux-musl": "@ff-labs/fff-bin-linux-arm64-musl",
|
||||
"x86_64-pc-windows-msvc": "@ff-labs/fff-bin-win32-x64",
|
||||
"aarch64-pc-windows-msvc": "@ff-labs/fff-bin-win32-arm64",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the npm package name for the current platform's native binary.
|
||||
*
|
||||
* @returns Package name like "@ff-labs/fff-bin-darwin-arm64"
|
||||
* @throws If the current platform is not supported
|
||||
*/
|
||||
export function getNpmPackageName(): string {
|
||||
const triple = getTriple();
|
||||
const packageName = TRIPLE_TO_NPM_PACKAGE[triple];
|
||||
if (!packageName) {
|
||||
throw new Error(`No npm package available for platform: ${triple}`);
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* Result type for all operations - follows the Result pattern
|
||||
*/
|
||||
export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Helper to create a successful result
|
||||
*/
|
||||
export function ok<T>(value: T): Result<T> {
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create an error result
|
||||
*/
|
||||
export function err<T>(error: string): Result<T> {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization options for the file finder
|
||||
*/
|
||||
export interface InitOptions {
|
||||
/** Base directory to index (required) */
|
||||
basePath: string;
|
||||
/** Path to frecency database (optional, omit to skip frecency initialization) */
|
||||
frecencyDbPath?: string;
|
||||
/** Path to query history database (optional, omit to skip query tracker initialization) */
|
||||
historyDbPath?: string;
|
||||
/** Use unsafe no-lock mode for databases (optional, defaults to false) */
|
||||
useUnsafeNoLock?: boolean;
|
||||
/**
|
||||
* Pre-populate mmap caches for all files after the initial scan completes.
|
||||
* When enabled, the first grep search will be as fast as subsequent ones
|
||||
* at the cost of a longer scan time and higher initial memory usage.
|
||||
* (default: false)
|
||||
*/
|
||||
warmupMmapCache?: boolean;
|
||||
/** enables optimizations for AI agent assistants. Provide as true if running via mcp/agent */
|
||||
aiMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search options for fuzzy file search
|
||||
*/
|
||||
export interface SearchOptions {
|
||||
/** Maximum threads for parallel search (0 = auto) */
|
||||
maxThreads?: number;
|
||||
/** Current file path (for deprioritization in results) */
|
||||
currentFile?: string;
|
||||
/** Combo boost score multiplier (default: 100) */
|
||||
comboBoostMultiplier?: number;
|
||||
/** Minimum combo count for boost (default: 3) */
|
||||
minComboCount?: number;
|
||||
/** Page index for pagination (default: 0) */
|
||||
pageIndex?: number;
|
||||
/** Page size for pagination (default: 100) */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file item in search results
|
||||
*/
|
||||
export interface FileItem {
|
||||
/** Absolute path to the file */
|
||||
path: string;
|
||||
/** Path relative to the indexed directory */
|
||||
relativePath: string;
|
||||
/** File name only */
|
||||
fileName: string;
|
||||
/** File size in bytes */
|
||||
size: number;
|
||||
/** Last modified timestamp (Unix seconds) */
|
||||
modified: number;
|
||||
/** Frecency score based on access patterns */
|
||||
accessFrecencyScore: number;
|
||||
/** Frecency score based on modification time */
|
||||
modificationFrecencyScore: number;
|
||||
/** Combined frecency score */
|
||||
totalFrecencyScore: number;
|
||||
/** Git status: 'clean', 'modified', 'untracked', 'staged_new', etc. */
|
||||
gitStatus: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score breakdown for a search result
|
||||
*/
|
||||
export interface Score {
|
||||
/** Total combined score */
|
||||
total: number;
|
||||
/** Base fuzzy match score */
|
||||
baseScore: number;
|
||||
/** Bonus for filename match */
|
||||
filenameBonus: number;
|
||||
/** Bonus for special filenames (index.ts, main.rs, etc.) */
|
||||
specialFilenameBonus: number;
|
||||
/** Boost from frecency */
|
||||
frecencyBoost: number;
|
||||
/** Penalty for distance in path */
|
||||
distancePenalty: number;
|
||||
/** Penalty if this is the current file */
|
||||
currentFilePenalty: number;
|
||||
/** Boost from query history combo matching */
|
||||
comboMatchBoost: number;
|
||||
/** Whether this was an exact match */
|
||||
exactMatch: boolean;
|
||||
/** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
|
||||
matchType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location in file (from query like "file.ts:42")
|
||||
*/
|
||||
export type Location =
|
||||
| { type: "line"; line: number }
|
||||
| { type: "position"; line: number; col: number }
|
||||
| {
|
||||
type: "range";
|
||||
start: { line: number; col: number };
|
||||
end: { line: number; col: number };
|
||||
};
|
||||
|
||||
/**
|
||||
* Search result from fuzzy file search
|
||||
*/
|
||||
export interface SearchResult {
|
||||
/** Matched file items */
|
||||
items: FileItem[];
|
||||
/** Corresponding scores for each item */
|
||||
scores: Score[];
|
||||
/** Total number of files that matched */
|
||||
totalMatched: number;
|
||||
/** Total number of indexed files */
|
||||
totalFiles: number;
|
||||
/** Location parsed from query (e.g., "file.ts:42:10") */
|
||||
location?: Location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan progress information
|
||||
*/
|
||||
export interface ScanProgress {
|
||||
/** Number of files scanned so far */
|
||||
scannedFilesCount: number;
|
||||
/** Whether a scan is currently in progress */
|
||||
isScanning: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database health information
|
||||
*/
|
||||
export interface DbHealth {
|
||||
/** Path to the database */
|
||||
path: string;
|
||||
/** Size of the database on disk in bytes */
|
||||
diskSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check result
|
||||
*/
|
||||
export interface HealthCheck {
|
||||
/** Library version */
|
||||
version: string;
|
||||
/** Git integration status */
|
||||
git: {
|
||||
/** Whether git2 library is available */
|
||||
available: boolean;
|
||||
/** Whether a git repository was found */
|
||||
repositoryFound: boolean;
|
||||
/** Git working directory path */
|
||||
workdir?: string;
|
||||
/** libgit2 version string */
|
||||
libgit2Version: string;
|
||||
/** Error message if git detection failed */
|
||||
error?: string;
|
||||
};
|
||||
/** File picker status */
|
||||
filePicker: {
|
||||
/** Whether the file picker is initialized */
|
||||
initialized: boolean;
|
||||
/** Base path being indexed */
|
||||
basePath?: string;
|
||||
/** Whether a scan is in progress */
|
||||
isScanning?: boolean;
|
||||
/** Number of indexed files */
|
||||
indexedFiles?: number;
|
||||
/** Error message if there's an issue */
|
||||
error?: string;
|
||||
};
|
||||
/** Frecency database status */
|
||||
frecency: {
|
||||
/** Whether frecency tracking is initialized */
|
||||
initialized: boolean;
|
||||
/** Database health information */
|
||||
dbHealthcheck?: DbHealth;
|
||||
/** Error message if there's an issue */
|
||||
error?: string;
|
||||
};
|
||||
/** Query tracker status */
|
||||
queryTracker: {
|
||||
/** Whether query tracking is initialized */
|
||||
initialized: boolean;
|
||||
/** Database health information */
|
||||
dbHealthcheck?: DbHealth;
|
||||
/** Error message if there's an issue */
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grep search mode
|
||||
*/
|
||||
export type GrepMode = "plain" | "regex" | "fuzzy";
|
||||
|
||||
/**
|
||||
* Opaque pagination cursor for grep results.
|
||||
* Pass this to `GrepOptions.cursor` to fetch the next page.
|
||||
* Do not construct or modify this — use the `nextCursor` from a previous `GrepResult`.
|
||||
*/
|
||||
export interface GrepCursor {
|
||||
/** @internal */
|
||||
readonly __brand: "GrepCursor";
|
||||
/** @internal */
|
||||
readonly _offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Create a GrepCursor from a raw file offset.
|
||||
*/
|
||||
export function createGrepCursor(offset: number): GrepCursor {
|
||||
return { __brand: "GrepCursor" as const, _offset: offset };
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for live grep (content search)
|
||||
*
|
||||
* Files are searched sequentially in frecency order (most recently/frequently
|
||||
* accessed first). The engine returns a `nextCursor` for fetching the next page.
|
||||
*/
|
||||
export interface GrepOptions {
|
||||
/** Maximum file size to search in bytes. Files larger than this are skipped. (default: 10MB) */
|
||||
maxFileSize?: number;
|
||||
/** Maximum matching lines to collect from a single file (default: 200) */
|
||||
maxMatchesPerFile?: number;
|
||||
/** Smart case: case-insensitive when the query is all lowercase, case-sensitive otherwise (default: true) */
|
||||
smartCase?: boolean;
|
||||
/**
|
||||
* Pagination cursor from a previous `GrepResult.nextCursor`.
|
||||
* Omit (or pass `null`) for the first page.
|
||||
*/
|
||||
cursor?: GrepCursor | null;
|
||||
/** Search mode (default: "plain") */
|
||||
mode?: GrepMode;
|
||||
/**
|
||||
* Maximum wall-clock time in milliseconds to spend searching before returning
|
||||
* partial results. 0 = unlimited. (default: 0)
|
||||
*/
|
||||
timeBudgetMs?: number;
|
||||
/** Number of context lines to include before each match (default: 0) */
|
||||
beforeContext?: number;
|
||||
/** Number of context lines to include after each match (default: 0) */
|
||||
afterContext?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single grep match with file and line information
|
||||
*/
|
||||
export interface GrepMatch {
|
||||
/** Absolute path to the file */
|
||||
path: string;
|
||||
/** Path relative to the indexed directory */
|
||||
relativePath: string;
|
||||
/** File name only */
|
||||
fileName: string;
|
||||
/** Git status */
|
||||
gitStatus: string;
|
||||
/** File size in bytes */
|
||||
size: number;
|
||||
/** Last modified timestamp (Unix seconds) */
|
||||
modified: number;
|
||||
/** Whether the file is binary */
|
||||
isBinary: boolean;
|
||||
/** Combined frecency score */
|
||||
totalFrecencyScore: number;
|
||||
/** Access-based frecency score */
|
||||
accessFrecencyScore: number;
|
||||
/** Modification-based frecency score */
|
||||
modificationFrecencyScore: number;
|
||||
/** 1-based line number of the match */
|
||||
lineNumber: number;
|
||||
/** 0-based byte column of first match start */
|
||||
col: number;
|
||||
/** Absolute byte offset of the matched line from file start */
|
||||
byteOffset: number;
|
||||
/** The matched line text (may be truncated) */
|
||||
lineContent: string;
|
||||
/** Byte offset pairs [start, end] within lineContent for highlighting */
|
||||
matchRanges: [number, number][];
|
||||
/** Fuzzy match score (only in fuzzy mode) */
|
||||
fuzzyScore?: number;
|
||||
/** Lines before the match (context). Empty array when context is 0. */
|
||||
contextBefore?: string[];
|
||||
/** Lines after the match (context). Empty array when context is 0. */
|
||||
contextAfter?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from a grep search
|
||||
*/
|
||||
export interface GrepResult {
|
||||
/** Matched items with file and line information. At most `max_matches_per_file`. */
|
||||
items: GrepMatch[];
|
||||
/** Total number of matches collected (always equal to items.length). */
|
||||
totalMatched: number;
|
||||
/** Number of files actually opened and searched in this call */
|
||||
totalFilesSearched: number;
|
||||
/** Total number of indexed files (before any filtering) */
|
||||
totalFiles: number;
|
||||
/** Number of files eligible for search after filtering out binary files, oversized files, and constraint mismatches */
|
||||
filteredFileCount: number;
|
||||
/**
|
||||
* Cursor for the next page, or `null` if all eligible files have been searched.
|
||||
* Pass this as `GrepOptions.cursor` to continue from where this call left off.
|
||||
*/
|
||||
nextCursor: GrepCursor | null;
|
||||
/** When regex mode fails to compile the pattern, the engine falls back to literal matching and this field contains the compilation error */
|
||||
regexFallbackError?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for multi-pattern grep (Aho-Corasick multi-needle search)
|
||||
*
|
||||
* Searches for lines matching ANY of the provided patterns using
|
||||
* SIMD-accelerated Aho-Corasick multi-pattern matching.
|
||||
*/
|
||||
export interface MultiGrepOptions {
|
||||
/** Patterns to search for (OR logic — matches lines containing any pattern) */
|
||||
patterns: string[];
|
||||
/** File constraints like "*.rs" or "/src/" */
|
||||
constraints?: string;
|
||||
/** Maximum file size to search in bytes (default: 10MB) */
|
||||
maxFileSize?: number;
|
||||
/** Maximum matching lines to collect from a single file (default: 0 = unlimited) */
|
||||
maxMatchesPerFile?: number;
|
||||
/** Smart case: case-insensitive when all patterns are lowercase (default: true) */
|
||||
smartCase?: boolean;
|
||||
/**
|
||||
* Pagination cursor from a previous `GrepResult.nextCursor`.
|
||||
* Omit (or pass `null`) for the first page.
|
||||
*/
|
||||
cursor?: GrepCursor | null;
|
||||
/**
|
||||
* Maximum wall-clock time in milliseconds to spend searching before returning
|
||||
* partial results. 0 = unlimited. (default: 0)
|
||||
*/
|
||||
timeBudgetMs?: number;
|
||||
/** Number of context lines to include before each match (default: 0) */
|
||||
beforeContext?: number;
|
||||
/** Number of context lines to include after each match (default: 0) */
|
||||
afterContext?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FileFinder } from "@ff-labs/fff-node";
|
||||
|
||||
const finder = FileFinder.create({ basePath: "~/dev/linux-root" });
|
||||
if (!finder.ok) {
|
||||
throw new Error(`Failed to create FileFinder: ${finder.error}`);
|
||||
}
|
||||
|
||||
const scan = await finder.value.waitForScan(5_000);
|
||||
if (!scan.ok) {
|
||||
throw new Error(`Failed to wait for scan: ${scan.error}`);
|
||||
}
|
||||
|
||||
// 5ms on m1 mac at the linux kernel repo root
|
||||
const result = finder.value.grep("hardwre", { mode: "plain" });
|
||||
for (const match of result.value.items) {
|
||||
console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
}
|
||||
|
||||
console.log(`\n${result.value.totalMatched} matches across ${result.value.totalFilesSearched} files`);
|
||||
|
||||
finder.value.destroy();
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* End-to-end tests for the fff-node package.
|
||||
*
|
||||
* Indexes the fff.nvim repository itself so the test suite is fully
|
||||
* self-contained — no external projects required.
|
||||
*
|
||||
* Requires:
|
||||
* - A built Rust library (cargo build --release -p fff-c)
|
||||
* - A compiled TS dist (cd packages/fff-node && npx tsc)
|
||||
*
|
||||
* Run:
|
||||
* node --test test/e2e.mjs
|
||||
*/
|
||||
|
||||
import { after, before, describe, it } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileFinder, closeLibrary } from "../dist/src/index.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
|
||||
/** @type {import("../dist/src/finder.js").FileFinder | null} */
|
||||
let finder = null;
|
||||
|
||||
describe("fff-node", { concurrency: 1 }, () => {
|
||||
before(async () => {
|
||||
const result = FileFinder.create({ basePath: REPO_ROOT });
|
||||
assert.ok(result.ok, `create failed: ${!result.ok ? result.error : ""}`);
|
||||
finder = result.value;
|
||||
|
||||
const wait = await finder.waitForScan(5_000);
|
||||
if (!wait.ok) {
|
||||
assert.ok(wait.ok, `waitForScan failed: ${wait.error}`);
|
||||
}
|
||||
|
||||
assert.equal(wait.value, true, "scan should finish within 5s");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (finder && !finder.isDestroyed) finder.destroy();
|
||||
// Skip closeLibrary() — the OS handles cleanup on process exit.
|
||||
// Calling ffi-rs close() during test teardown can cause native crashes
|
||||
// on some platforms (e.g. Windows DLL unload, Linux dlclose).
|
||||
});
|
||||
|
||||
it("isAvailable returns true when the native library is loadable", () => {
|
||||
assert.equal(FileFinder.isAvailable(), true);
|
||||
});
|
||||
|
||||
it("getScanProgress reports >100 indexed files", () => {
|
||||
const r = finder.getScanProgress();
|
||||
assert.ok(r.ok, "getScanProgress failed");
|
||||
assert.equal(typeof r.value.scannedFilesCount, "number");
|
||||
assert.ok(
|
||||
r.value.scannedFilesCount > 100,
|
||||
`expected >100, got ${r.value.scannedFilesCount}`,
|
||||
);
|
||||
|
||||
assert.equal(r.value.isScanning, false);
|
||||
});
|
||||
|
||||
describe("search", { concurrency: 1 }, () => {
|
||||
it("finds the root Cargo.toml", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 10 });
|
||||
assert.ok(r.ok, `search failed: ${!r.ok ? r.error : ""}`);
|
||||
const paths = r.value.items.map((i) => i.relativePath);
|
||||
assert.ok(
|
||||
paths.includes("Cargo.toml"),
|
||||
`expected Cargo.toml in results, got: ${paths}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("finds crates/fff-c/src/lib.rs", () => {
|
||||
const r = finder.fileSearch("lib.rs", { pageSize: 20 });
|
||||
assert.ok(r.ok);
|
||||
const paths = r.value.items.map((i) => i.relativePath);
|
||||
assert.ok(
|
||||
paths.some((p) => p.includes("fff-c") && p.endsWith("lib.rs")),
|
||||
`expected fff-c/src/lib.rs, got: ${paths}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("respects pageSize", () => {
|
||||
const r = finder.fileSearch("lua", { pageSize: 3 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(
|
||||
r.value.items.length <= 3,
|
||||
`expected <=3, got ${r.value.items.length}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("items contain all required fields", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
const item = r.value.items[0];
|
||||
for (const f of ["relativePath", "path", "fileName", "gitStatus"]) {
|
||||
assert.equal(typeof item[f], "string", `${f} should be a string`);
|
||||
}
|
||||
for (const f of ["size", "modified"]) {
|
||||
assert.equal(typeof item[f], "number", `${f} should be a number`);
|
||||
}
|
||||
});
|
||||
|
||||
it("scores contain all required fields", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.value.scores.length > 0);
|
||||
const s = r.value.scores[0];
|
||||
for (const f of ["total", "baseScore"]) {
|
||||
assert.equal(typeof s[f], "number", `${f} should be a number`);
|
||||
}
|
||||
assert.equal(typeof s.matchType, "string");
|
||||
});
|
||||
|
||||
it("totalMatched <= totalFiles", () => {
|
||||
const r = finder.fileSearch("rs", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.value.totalMatched > 0);
|
||||
assert.ok(r.value.totalFiles > 100);
|
||||
assert.ok(r.value.totalFiles >= r.value.totalMatched);
|
||||
});
|
||||
|
||||
it("empty query returns files", () => {
|
||||
const r = finder.fileSearch("", { pageSize: 5 });
|
||||
assert.ok(r.ok, `empty query failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.totalFiles, "number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep", { concurrency: 1 }, () => {
|
||||
it("finds FffResult in Rust sources", () => {
|
||||
const r = finder.grep("FffResult", { mode: "plain" });
|
||||
assert.ok(r.ok, `grep failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0);
|
||||
assert.ok(r.value.items.some((m) => m.relativePath.endsWith(".rs")));
|
||||
});
|
||||
|
||||
it("match items contain all required fields", () => {
|
||||
const r = finder.grep("FffResult", { mode: "plain" });
|
||||
assert.ok(r.ok);
|
||||
const m = r.value.items[0];
|
||||
assert.equal(typeof m.relativePath, "string");
|
||||
assert.equal(typeof m.lineNumber, "number");
|
||||
assert.ok(m.lineNumber > 0, "lineNumber is 1-based");
|
||||
assert.equal(typeof m.lineContent, "string");
|
||||
assert.ok(m.lineContent.includes("FffResult"));
|
||||
assert.equal(typeof m.col, "number");
|
||||
assert.equal(typeof m.byteOffset, "number");
|
||||
assert.ok(Array.isArray(m.matchRanges));
|
||||
console.log(m.matchRanges);
|
||||
});
|
||||
|
||||
it("pagination returns a second page", () => {
|
||||
const r = finder.grep("fn", { mode: "plain" });
|
||||
assert.ok(r.ok);
|
||||
if (r.value.nextCursor) {
|
||||
const r2 = finder.grep("fn", { cursor: r.value.nextCursor });
|
||||
assert.ok(r2.ok, `page 2 failed: ${!r2.ok ? r2.error : ""}`);
|
||||
assert.equal(typeof r2.value.totalMatched, "number");
|
||||
}
|
||||
});
|
||||
|
||||
it("regex mode matches pub fn declarations", () => {
|
||||
const r = finder.grep("pub fn \\w+", { mode: "regex" });
|
||||
assert.ok(r.ok, `regex grep failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiGrep", { concurrency: 1 }, () => {
|
||||
it("finds lines matching any of the C FFI function names", () => {
|
||||
const r = finder.multiGrep({
|
||||
patterns: ["fff_create_instance", "fff_destroy", "fff_search"],
|
||||
});
|
||||
assert.ok(r.ok, `multiGrep failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0);
|
||||
});
|
||||
|
||||
it("rejects empty patterns array", () => {
|
||||
const r = finder.multiGrep({ patterns: [] });
|
||||
assert.ok(!r.ok);
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshGitStatus returns a positive count", () => {
|
||||
const r = finder.refreshGitStatus();
|
||||
assert.ok(r.ok, `refreshGitStatus failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value, "number");
|
||||
assert.ok(r.value > 0);
|
||||
});
|
||||
|
||||
// ── Scan ────────────────────────────────────────────────────────────
|
||||
|
||||
it("isScanning returns a boolean", () => {
|
||||
assert.equal(typeof finder.isScanning(), "boolean");
|
||||
});
|
||||
|
||||
// ── Health check ────────────────────────────────────────────────────
|
||||
|
||||
describe("healthCheck", { concurrency: 1 }, () => {
|
||||
it("reports initialized state with instance", () => {
|
||||
const r = finder.healthCheck(REPO_ROOT);
|
||||
assert.ok(r.ok, `healthCheck failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.version, "string");
|
||||
assert.ok(r.value.version.length > 0);
|
||||
assert.equal(r.value.git.available, true);
|
||||
assert.equal(r.value.git.repositoryFound, true);
|
||||
assert.equal(r.value.filePicker.initialized, true);
|
||||
assert.ok(r.value.filePicker.indexedFiles > 100);
|
||||
});
|
||||
|
||||
it("static check works without instance", () => {
|
||||
const r = FileFinder.healthCheckStatic(REPO_ROOT);
|
||||
assert.ok(r.ok, `static healthCheck failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.version, "string");
|
||||
assert.equal(r.value.filePicker.initialized, false);
|
||||
assert.equal(r.value.git.repositoryFound, true);
|
||||
});
|
||||
});
|
||||
|
||||
it("create rejects a non-existent path", () => {
|
||||
const r = FileFinder.create({ basePath: "/nonexistent/fff-test-path" });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(
|
||||
r.error.toLowerCase().includes("invalid path") ||
|
||||
r.error.toLowerCase().includes("not exist"),
|
||||
`unexpected error: ${r.error}`,
|
||||
);
|
||||
});
|
||||
|
||||
// This must be the last describe block since it destroys the shared instance.
|
||||
describe("after destroy", { concurrency: 1 }, () => {
|
||||
before(() => {
|
||||
finder.destroy();
|
||||
});
|
||||
|
||||
it("isDestroyed is true", () => {
|
||||
assert.equal(finder.isDestroyed, true);
|
||||
});
|
||||
|
||||
it("search returns an error", () => {
|
||||
const r = finder.fileSearch("test");
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.error.includes("destroyed"));
|
||||
});
|
||||
|
||||
it("grep returns an error", () => {
|
||||
const r = finder.grep("test");
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.error.includes("destroyed"));
|
||||
});
|
||||
|
||||
it("double destroy is a safe no-op", () => {
|
||||
finder.destroy();
|
||||
assert.equal(finder.isDestroyed, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "bin"]
|
||||
}
|
||||
+5
-6
@@ -1,10 +1,9 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2026-03-14"
|
||||
channel = "stable"
|
||||
components = [
|
||||
"clippy-preview",
|
||||
"rustfmt-preview",
|
||||
"llvm-tools-preview",
|
||||
"miri",
|
||||
"clippy",
|
||||
"rustfmt",
|
||||
"llvm-tools",
|
||||
"rust-src",
|
||||
"rust-analyzer-preview",
|
||||
"rust-analyzer",
|
||||
]
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Determines the release version based on git context.
|
||||
#
|
||||
# Tagged release (refs/tags/v*) → uses the tag version (e.g. v0.2.4 → 0.2.4)
|
||||
# Push to main → nightly prerelease (e.g. 0.2.5-nightly.abc1234)
|
||||
# Other (PR / dev branch) → dev prerelease (e.g. 0.2.5-dev.abc1234)
|
||||
#
|
||||
# For prerelease builds the patch version is bumped so that the resulting
|
||||
# semver is strictly greater than the current Cargo.toml version.
|
||||
# (In semver 0.2.4-nightly.x < 0.2.4, so we need 0.2.5-nightly.x > 0.2.4.)
|
||||
#
|
||||
# Outputs (appended to $GITHUB_OUTPUT when running in CI):
|
||||
# version – semver string
|
||||
# npm_tag – npm dist-tag (latest | nightly | dev)
|
||||
# is_release – "true" for tagged releases, "false" otherwise
|
||||
#
|
||||
# Can also be run locally for debugging:
|
||||
# GITHUB_REF=refs/tags/v0.3.0 ./scripts/determine-version.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Single source of truth: base version lives in fff-core
|
||||
base_version=$(grep '^version' "$REPO_ROOT/crates/fff-core/Cargo.toml" \
|
||||
| head -1 \
|
||||
| sed 's/version = "\(.*\)"/\1/')
|
||||
|
||||
short_sha=$(git -C "$REPO_ROOT" rev-parse --short HEAD)
|
||||
|
||||
# Bump the patch component: 0.2.4 → 0.2.5
|
||||
IFS='.' read -r major minor patch <<< "$base_version"
|
||||
next_patch_version="${major}.${minor}.$((patch + 1))"
|
||||
|
||||
ref="${GITHUB_REF:-}"
|
||||
|
||||
if [[ "$ref" == refs/tags/v* ]]; then
|
||||
# Tagged release – strip the leading "v"
|
||||
version="${ref#refs/tags/v}"
|
||||
npm_tag="latest"
|
||||
is_release="true"
|
||||
elif [[ "$ref" == "refs/heads/main" || "$ref" == "refs/heads/node" ]]; then
|
||||
version="${next_patch_version}-nightly.${short_sha}"
|
||||
npm_tag="nightly"
|
||||
is_release="false"
|
||||
else
|
||||
version="${next_patch_version}-dev.${short_sha}"
|
||||
npm_tag="dev"
|
||||
is_release="false"
|
||||
fi
|
||||
|
||||
echo "version=${version}"
|
||||
echo "npm_tag=${npm_tag}"
|
||||
echo "is_release=${is_release}"
|
||||
|
||||
# Write to GITHUB_OUTPUT when running in CI
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "is_release=${is_release}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?Usage: ./scripts/release.sh <version> (e.g. 0.3.0)}"
|
||||
|
||||
# Strip leading 'v' if provided
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
TAG="v${VERSION}"
|
||||
|
||||
# Check for clean working tree
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo "Error: Working tree is not clean. Commit or stash changes first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if tag already exists
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Error: Tag $TAG already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Releasing $VERSION..."
|
||||
|
||||
echo "→ Updating Cargo.toml versions to $VERSION"
|
||||
cargo set-version "$VERSION" 2>/dev/null || {
|
||||
echo "cargo-edit not found, installing..."
|
||||
cargo install cargo-edit
|
||||
cargo set-version "$VERSION"
|
||||
}
|
||||
|
||||
git add -A
|
||||
git commit -m "chore: release $VERSION"
|
||||
|
||||
echo "→ Creating tag $TAG"
|
||||
git tag -a "$TAG" -m "Release $VERSION"
|
||||
|
||||
echo "→ Pushing to origin"
|
||||
git push origin
|
||||
git push origin "$TAG"
|
||||
|
||||
echo ""
|
||||
echo "Release $VERSION created and pushed."
|
||||
echo "CI will build and publish: https://github.com/dmtrKovalenko/fff.nvim/actions"
|
||||
Reference in New Issue
Block a user