Skip to content

Commit 7979004

Browse files
feat: trim_whitespace option for grep (#356)
* feat: trim_whitespace optin for grep closes #307 * chore: Update docs for - feat: trim_whitespace optin for grep
1 parent 210bc9e commit 7979004

17 files changed

Lines changed: 100 additions & 30 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ require('fff').setup({
289289
smart_case = true, -- Case-insensitive unless query has uppercase
290290
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
291291
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
292+
trim_whitespace = false, -- Strip leading whitespace from matched lines
292293
},
293294
})
294295
```

crates/fff-c/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ pub unsafe extern "C" fn fff_live_grep(
390390
before_context: before_context as usize,
391391
after_context: after_context as usize,
392392
classify_definitions,
393+
trim_whitespace: false,
393394
};
394395

395396
let result = picker.grep(&parsed, &options);
@@ -491,6 +492,7 @@ pub unsafe extern "C" fn fff_multi_grep(
491492
before_context: before_context as usize,
492493
after_context: after_context as usize,
493494
classify_definitions,
495+
trim_whitespace: false,
494496
};
495497

496498
let result = fff::multi_grep_search(

crates/fff-core/src/grep.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,35 @@ pub struct GrepMatch {
279279
pub context_after: Vec<String>,
280280
}
281281

282+
impl GrepMatch {
283+
/// Strip leading whitespace from `line_content` and all context lines,
284+
/// adjusting `col` and `match_byte_offsets` so highlights remain correct.
285+
pub fn trim_leading_whitespace(&mut self) {
286+
let strip_len = self.line_content.len() - self.line_content.trim_start().len();
287+
if strip_len > 0 {
288+
self.line_content.drain(..strip_len);
289+
let off = strip_len as u32;
290+
self.col = self.col.saturating_sub(strip_len);
291+
for range in &mut self.match_byte_offsets {
292+
range.0 = range.0.saturating_sub(off);
293+
range.1 = range.1.saturating_sub(off);
294+
}
295+
}
296+
for line in &mut self.context_before {
297+
let n = line.len() - line.trim_start().len();
298+
if n > 0 {
299+
line.drain(..n);
300+
}
301+
}
302+
for line in &mut self.context_after {
303+
let n = line.len() - line.trim_start().len();
304+
if n > 0 {
305+
line.drain(..n);
306+
}
307+
}
308+
}
309+
}
310+
282311
/// Result of a grep search.
283312
#[derive(Debug, Clone, Default)]
284313
pub struct GrepResult<'a> {
@@ -326,6 +355,28 @@ pub struct GrepSearchOptions {
326355
/// Whether to classify each match as a definition line. Adds ~2% overhead
327356
/// on large repos; disable for interactive grep where it is not needed.
328357
pub classify_definitions: bool,
358+
/// Strip leading whitespace from matched lines and context lines, adjusting
359+
/// highlight byte offsets accordingly. Useful for AI/MCP consumers and UIs
360+
/// that don't need indentation. Default: false.
361+
pub trim_whitespace: bool,
362+
}
363+
364+
impl Default for GrepSearchOptions {
365+
fn default() -> Self {
366+
Self {
367+
max_file_size: 10 * 1024 * 1024,
368+
max_matches_per_file: 200,
369+
smart_case: true,
370+
file_offset: 0,
371+
page_limit: 50,
372+
mode: GrepMode::default(),
373+
time_budget_ms: 0,
374+
before_context: 0,
375+
after_context: 0,
376+
classify_definitions: false,
377+
trim_whitespace: false,
378+
}
379+
}
329380
}
330381

331382
#[derive(Clone, Copy)]
@@ -1157,6 +1208,9 @@ where
11571208

11581209
for mut m in file_matches {
11591210
m.file_index = file_result_idx;
1211+
if options.trim_whitespace {
1212+
m.trim_leading_whitespace();
1213+
}
11601214
all_matches.push(m);
11611215
}
11621216

@@ -1237,6 +1291,9 @@ fn collect_grep_results<'a>(
12371291

12381292
for mut m in file_matches {
12391293
m.file_index = file_result_idx;
1294+
if options.trim_whitespace {
1295+
m.trim_leading_whitespace();
1296+
}
12401297
all_matches.push(m);
12411298
}
12421299

@@ -2158,6 +2215,7 @@ mod tests {
21582215
before_context: 0,
21592216
after_context: 0,
21602217
classify_definitions: false,
2218+
trim_whitespace: false,
21612219
};
21622220

21632221
// Test with 3 patterns

crates/fff-core/tests/bigram_overlay_coherence_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,7 @@ fn grep_opts() -> GrepSearchOptions {
13731373
before_context: 0,
13741374
after_context: 0,
13751375
classify_definitions: false,
1376+
trim_whitespace: false,
13761377
}
13771378
}
13781379

crates/fff-core/tests/bigram_overlay_integration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ fn grep_opts() -> GrepSearchOptions {
302302
before_context: 0,
303303
after_context: 0,
304304
classify_definitions: false,
305+
trim_whitespace: false,
305306
}
306307
}
307308

crates/fff-core/tests/grep_integration.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn plain_opts() -> GrepSearchOptions {
2929
before_context: 0,
3030
after_context: 0,
3131
classify_definitions: false,
32+
trim_whitespace: false,
3233
}
3334
}
3435

@@ -45,6 +46,7 @@ fn regex_opts() -> GrepSearchOptions {
4546
before_context: 0,
4647
after_context: 0,
4748
classify_definitions: false,
49+
trim_whitespace: false,
4850
}
4951
}
5052

@@ -61,6 +63,7 @@ fn fuzzy_opts() -> GrepSearchOptions {
6163
before_context: 0,
6264
after_context: 0,
6365
classify_definitions: false,
66+
trim_whitespace: false,
6467
}
6568
}
6669

crates/fff-mcp/src/output.rs

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -81,34 +81,19 @@ fn trauncate_line_for_ai(
8181
match_ranges: Option<&[(u32, u32)]>,
8282
max_len: usize,
8383
) -> String {
84-
// Strip leading/trailing whitespace to save tokens — the LLM has file:line for location.
85-
let trimmed = line.trim();
84+
// Leading whitespace is already stripped by core (trim_whitespace option).
85+
// Only strip trailing whitespace here.
86+
let trimmed = line.trim_end();
8687
if trimmed.is_empty() {
8788
return String::new();
8889
}
8990

90-
let strip_offset = line.len() - line.trim_start().len();
91-
9291
if trimmed.len() <= max_len {
9392
return trimmed.to_string();
9493
}
9594

96-
// Adjust match ranges for the stripped leading whitespace
97-
let adjusted: Vec<(u32, u32)>;
98-
let ranges = match match_ranges {
99-
Some(r) if strip_offset > 0 => {
100-
let off = strip_offset as u32;
101-
adjusted = r
102-
.iter()
103-
.map(|&(s, e)| (s.saturating_sub(off), e.saturating_sub(off)))
104-
.collect();
105-
Some(adjusted.as_slice())
106-
}
107-
other => other,
108-
};
109-
11095
// Use first match range to center the window
111-
if let Some(ranges) = ranges
96+
if let Some(ranges) = match_ranges
11297
&& let Some(&(match_start, match_end)) = ranges.first()
11398
{
11499
let match_start = match_start as usize;
@@ -569,26 +554,29 @@ mod tests {
569554
use super::*;
570555

571556
#[test]
572-
fn trunc_strips_whitespace() {
573-
assert_eq!(trauncate_line_for_ai(" foo()", None, 180), "foo()");
574-
assert_eq!(trauncate_line_for_ai(" bar ", None, 180), "bar");
557+
fn trunc_strips_trailing_whitespace() {
558+
// Leading whitespace is now stripped by core's trim_whitespace option.
559+
// This function only strips trailing whitespace.
560+
assert_eq!(trauncate_line_for_ai("foo()", None, 180), "foo()");
561+
assert_eq!(trauncate_line_for_ai("bar ", None, 180), "bar");
575562
assert_eq!(trauncate_line_for_ai(" ", None, 180), "");
576563
}
577564

578565
#[test]
579-
fn trunc_adjusts_match_ranges_after_strip() {
580-
// " hello" — match on "hello" at bytes 4..9
581-
let line = " hello";
582-
let ranges = [(4, 9)];
566+
fn trunc_preserves_pre_trimmed_match_ranges() {
567+
// Core already stripped leading whitespace and adjusted offsets,
568+
// so "hello" arrives with match at bytes 0..5.
569+
let line = "hello";
570+
let ranges = [(0, 5)];
583571
let result = trauncate_line_for_ai(line, Some(&ranges), 180);
584-
// After stripping 4 leading spaces, the trimmed line is "hello"
585572
assert_eq!(result, "hello");
586573
}
587574

588575
#[test]
589576
fn trunc_long_line_centered() {
590-
let line = format!("{}match_here{}", " ".repeat(8), "x".repeat(200));
591-
let ranges = [(8u32, 18u32)];
577+
// Core already stripped leading whitespace; offsets are pre-adjusted.
578+
let line = format!("match_here{}", "x".repeat(200));
579+
let ranges = [(0u32, 10u32)];
592580
let result = trauncate_line_for_ai(&line, Some(&ranges), 50);
593581
assert!(result.contains("match_here"));
594582
assert!(result.len() <= 55); // budget + ellipsis chars

crates/fff-mcp/src/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ fn make_grep_options(
6464
before_context: ctx_lines,
6565
after_context: after_ctx,
6666
classify_definitions: true,
67+
trim_whitespace: true,
6768
},
6869
auto_expand,
6970
)

crates/fff-nvim/benches/indexing_and_search.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@ fn bench_grep_search(c: &mut Criterion) {
707707
before_context: 0,
708708
after_context: 0,
709709
classify_definitions: false,
710+
trim_whitespace: false,
710711
};
711712

712713
let test_queries = vec![

crates/fff-nvim/src/bin/bench_grep_query.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ fn run_grep(files: &[fff::FileItem], index: Option<&fff::BigramFilter>, query: &
3030
before_context: 0,
3131
after_context: 0,
3232
classify_definitions: false,
33+
trim_whitespace: false,
3334
};
3435

3536
let parsed = parse_grep_query(query);

0 commit comments

Comments
 (0)