Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions crates/fff-c/include/fff.h
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,94 @@ bool fff_grep_match_get_is_definition(const struct FffGrepMatch *m);
*/
bool fff_grep_match_get_is_binary(const struct FffGrepMatch *m);

/**
* Final combined score used for ranking. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_total(const struct FffScore *s);

/**
* Raw fuzzy match score before bonuses and penalties. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_base_score(const struct FffScore *s);

/**
* Bonus for matching the file name rather than only the path. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_filename_bonus(const struct FffScore *s);

/**
* Bonus for special file names (e.g. `README`, `main.rs`). Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_special_filename_bonus(const struct FffScore *s);

/**
* Boost from the frecency database. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_frecency_boost(const struct FffScore *s);

/**
* Penalty based on directory distance from the current file. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_distance_penalty(const struct FffScore *s);

/**
* Penalty applied to the currently open file. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_current_file_penalty(const struct FffScore *s);

/**
* Boost when several scoring signals agree. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_combo_match_boost(const struct FffScore *s);

/**
* Bonus for query segments aligning with path segments. Returns `0` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
int32_t fff_score_get_path_alignment_bonus(const struct FffScore *s);

/**
* `true` if the query matched the file name or directory exactly; `false` if `s` is null.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
bool fff_score_get_exact_match(const struct FffScore *s);

/**
* Match type label (e.g. `"exact_filename"`, `"fuzzy_path"`); null if `s` is null. Do not free.
*
* ## Safety
* `s` must be a valid `FffScore` pointer or null.
*/
const char *fff_score_get_match_type(const struct FffScore *s);

/**
* Number of items in the result; `0` if `r` is null.
*
Expand Down
136 changes: 135 additions & 1 deletion crates/fff-c/src/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::ffi::c_char;
use std::ptr;

use crate::ffi_types::{
FffFileItem, FffGrepMatch, FffGrepResult, FffMatchRange, FffResult, FffSearchResult,
FffFileItem, FffGrepMatch, FffGrepResult, FffMatchRange, FffResult, FffScore, FffSearchResult,
};

// ── FffResult ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -474,6 +474,140 @@ pub unsafe extern "C" fn fff_grep_match_get_is_binary(m: *const FffGrepMatch) ->
unsafe { (*m).is_binary }
}

// ── FffScore ─────────────────────────────────────────────────────────────────

/// Final combined score used for ranking. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_total(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).total }
}

/// Raw fuzzy match score before bonuses and penalties. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_base_score(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).base_score }
}

/// Bonus for matching the file name rather than only the path. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_filename_bonus(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).filename_bonus }
}

/// Bonus for special file names (e.g. `README`, `main.rs`). Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_special_filename_bonus(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).special_filename_bonus }
}

/// Boost from the frecency database. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_frecency_boost(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).frecency_boost }
}

/// Penalty based on directory distance from the current file. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_distance_penalty(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).distance_penalty }
}

/// Penalty applied to the currently open file. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_current_file_penalty(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).current_file_penalty }
}

/// Boost when several scoring signals agree. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_combo_match_boost(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).combo_match_boost }
}

/// Bonus for query segments aligning with path segments. Returns `0` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_path_alignment_bonus(s: *const FffScore) -> i32 {
if s.is_null() {
return 0;
}
unsafe { (*s).path_alignment_bonus }
}

/// `true` if the query matched the file name or directory exactly; `false` if `s` is null.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_exact_match(s: *const FffScore) -> bool {
if s.is_null() {
return false;
}
unsafe { (*s).exact_match }
}

/// Match type label (e.g. `"exact_filename"`, `"fuzzy_path"`); null if `s` is null. Do not free.
///
/// ## Safety
/// `s` must be a valid `FffScore` pointer or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn fff_score_get_match_type(s: *const FffScore) -> *const c_char {
if s.is_null() {
return ptr::null();
}
unsafe { (*s).match_type }
}

// ── FffSearchResult ──────────────────────────────────────────────────────────

/// Number of items in the result; `0` if `r` is null.
Expand Down
2 changes: 2 additions & 0 deletions packages/fff-bun/src/fff-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export interface Score {
currentFilePenalty: number;
/** Boost from query history combo matching */
comboMatchBoost: number;
/** Bonus for query segments aligning with path segments */
pathAlignmentBonus: number;
/** Whether this was an exact match */
exactMatch: boolean;
/** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
Expand Down
4 changes: 3 additions & 1 deletion packages/fff-bun/src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ 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_PATH_ALIGN = 32; // i32 (4)
const SC_EXACT = 36; // bool (1 + 3 pad)
const SC_MTYPE = 40; // *mut c_char (8)
const SC_SIZE_OF = 48;

Expand Down Expand Up @@ -701,6 +702,7 @@ function readScoreStruct(p: number): Score {
distancePenalty: read.i32(pp, SC_DIST),
currentFilePenalty: read.i32(pp, SC_CURFILE),
comboMatchBoost: read.i32(pp, SC_COMBO),
pathAlignmentBonus: read.i32(pp, SC_PATH_ALIGN),
exactMatch: read.u8(pp, SC_EXACT) !== 0,
matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
};
Expand Down
13 changes: 13 additions & 0 deletions packages/fff-bun/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ describe("FileFinder - Full Lifecycle", () => {
}
});

test("exact filename query decodes exactMatch=true (#888)", () => {
const result = finder.fileSearch("package.json", { pageSize: 5 });
expect(result.ok).toBe(true);
if (result.ok) {
const idx = result.value.items.findIndex((i) => i.relativePath === "package.json");
expect(idx).toBeGreaterThanOrEqual(0);
const score = result.value.scores[idx];
expect(score.matchType).toBe("exact_filename");
expect(score.exactMatch).toBe(true);
expect(typeof score.pathAlignmentBonus).toBe("number");
}
});

test("search returns empty for non-matching query", () => {
const result = finder.fileSearch("xyznonexistentfilenamexyz123456");
expect(result.ok).toBe(true);
Expand Down
2 changes: 2 additions & 0 deletions packages/fff-node/src/fff-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export interface Score {
currentFilePenalty: number;
/** Boost from query history combo matching */
comboMatchBoost: number;
/** Bonus for query segments aligning with path segments */
pathAlignmentBonus: number;
/** Whether this was an exact match */
exactMatch: boolean;
/** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
Expand Down
7 changes: 5 additions & 2 deletions packages/fff-node/src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ const FFF_SCORE_STRUCT = {
distance_penalty: DataType.I32,
current_file_penalty: DataType.I32,
combo_match_boost: DataType.I32,
path_alignment_bonus: DataType.I32,
exact_match: DataType.U8,
match_type: DataType.External,
};
Expand All @@ -487,6 +488,7 @@ interface FffScoreRaw {
distance_penalty: number;
current_file_penalty: number;
combo_match_boost: number;
path_alignment_bonus: number;
exact_match: number;
match_type: JsExternal;
}
Expand Down Expand Up @@ -623,7 +625,7 @@ const FFF_GREP_MATCH_STRUCT = {
match_ranges_count: DataType.U32,
context_before_count: DataType.U32,
context_after_count: DataType.U32,
fuzzy_score: DataType.U32, // actually u16 in C, but ffi-rs doesn't so we read it as u32 with padding
fuzzy_score: DataType.I16, // u16 in C; ffi-rs has no U16, so read I16 and mask (#888)
has_fuzzy_score: DataType.U8,
is_binary: DataType.U8,
is_definition: DataType.U8,
Expand Down Expand Up @@ -713,6 +715,7 @@ function readScoreFromRaw(raw: FffScoreRaw): Score {
distancePenalty: raw.distance_penalty,
currentFilePenalty: raw.current_file_penalty,
comboMatchBoost: raw.combo_match_boost,
pathAlignmentBonus: raw.path_alignment_bonus,
exactMatch: raw.exact_match !== 0,
matchType: readCString(raw.match_type) ?? "",
};
Expand Down Expand Up @@ -841,7 +844,7 @@ function readGrepMatchFromRaw(raw: FffGrepMatchRaw): GrepMatch {
};

if (raw.has_fuzzy_score !== 0) {
match.fuzzyScore = raw.fuzzy_score;
match.fuzzyScore = raw.fuzzy_score & 0xffff;
}
if (raw.context_before_count > 0) {
match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count);
Expand Down
32 changes: 32 additions & 0 deletions packages/fff-node/test/e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ describe("fff-node", { concurrency: 1 }, () => {
assert.equal(typeof s.matchType, "string");
});

it("exact filename query decodes exactMatch=true (#888)", () => {
const r = finder.fileSearch("Cargo.toml", { pageSize: 5 });
assert.ok(r.ok);
const idx = r.value.items.findIndex((i) => i.relativePath === "Cargo.toml");
assert.ok(idx >= 0, "root Cargo.toml should be in results");
const s = r.value.scores[idx];
assert.equal(s.matchType, "exact_filename");
assert.equal(s.exactMatch, true);
assert.equal(typeof s.pathAlignmentBonus, "number");
});

it("totalMatched <= totalFiles", () => {
const r = finder.fileSearch("rs", { pageSize: 1 });
assert.ok(r.ok);
Expand Down Expand Up @@ -194,6 +205,27 @@ describe("fff-node", { concurrency: 1 }, () => {
console.log(m.matchRanges);
});

it("plain grep on text files never reports binary/definition/fuzzy flags (#888)", () => {
const r = finder.grep("FffResult", { mode: "plain", pageSize: 100 });
assert.ok(r.ok);
assert.ok(r.value.items.length > 1);
for (const m of r.value.items) {
assert.equal(m.isBinary, false, `${m.relativePath}:${m.lineNumber} flagged binary`);
assert.equal(m.isDefinition, undefined, `${m.relativePath}:${m.lineNumber} flagged definition`);
assert.equal(m.fuzzyScore, undefined, `${m.relativePath}:${m.lineNumber} has fuzzyScore in plain mode`);
}
});

it("fuzzy grep decodes fuzzyScore as u16", () => {
const r = finder.grep("FfRslt", { mode: "fuzzy", pageSize: 20 });
assert.ok(r.ok);
assert.ok(r.value.items.length > 0);
for (const m of r.value.items) {
assert.equal(typeof m.fuzzyScore, "number");
assert.ok(m.fuzzyScore >= 0 && m.fuzzyScore <= 0xffff, `fuzzyScore out of u16 range: ${m.fuzzyScore}`);
}
});

it("pagination returns a second page", () => {
const r = finder.grep("fn", { mode: "plain" });
assert.ok(r.ok);
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/fff-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export interface Score {
currentFilePenalty: number;
/** Boost from query history combo matching */
comboMatchBoost: number;
/** Bonus for query segments aligning with path segments */
pathAlignmentBonus: number;
/** Whether this was an exact match */
exactMatch: boolean;
/** Type of match: 'fuzzy', 'exact', 'prefix', etc. */
Expand Down
Loading