Compare commits

...

3 Commits

Author SHA1 Message Date
Dmitriy Kovalenko f866463273 Merge branch 'main' into fix/grep-trim
docs / docs (push) Has been cancelled
2026-04-08 21:11:39 -07:00
Dmitriy Kovalenko 6d2a174b38 chore: Update docs for - feat: trim_whitespace optin for grep 2026-04-09 00:17:42 +00:00
Dmitriy Kovalenko 34a8020a58 feat: trim_whitespace optin for grep
closes https://github.com/dmtrKovalenko/fff.nvim/issues/307
2026-04-08 17:17:00 -07:00
17 changed files with 100 additions and 30 deletions
+1
View File
@@ -289,6 +289,7 @@ require('fff').setup({
smart_case = true, -- Case-insensitive unless query has uppercase
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
trim_whitespace = false, -- Strip leading whitespace from matched lines
},
})
```
+2
View File
@@ -390,6 +390,7 @@ pub unsafe extern "C" fn fff_live_grep(
before_context: before_context as usize,
after_context: after_context as usize,
classify_definitions,
trim_whitespace: false,
};
let result = picker.grep(&parsed, &options);
@@ -491,6 +492,7 @@ pub unsafe extern "C" fn fff_multi_grep(
before_context: before_context as usize,
after_context: after_context as usize,
classify_definitions,
trim_whitespace: false,
};
let result = fff::multi_grep_search(
+58
View File
@@ -279,6 +279,35 @@ pub struct GrepMatch {
pub context_after: Vec<String>,
}
impl GrepMatch {
/// Strip leading whitespace from `line_content` and all context lines,
/// adjusting `col` and `match_byte_offsets` so highlights remain correct.
pub fn trim_leading_whitespace(&mut self) {
let strip_len = self.line_content.len() - self.line_content.trim_start().len();
if strip_len > 0 {
self.line_content.drain(..strip_len);
let off = strip_len as u32;
self.col = self.col.saturating_sub(strip_len);
for range in &mut self.match_byte_offsets {
range.0 = range.0.saturating_sub(off);
range.1 = range.1.saturating_sub(off);
}
}
for line in &mut self.context_before {
let n = line.len() - line.trim_start().len();
if n > 0 {
line.drain(..n);
}
}
for line in &mut self.context_after {
let n = line.len() - line.trim_start().len();
if n > 0 {
line.drain(..n);
}
}
}
}
/// Result of a grep search.
#[derive(Debug, Clone, Default)]
pub struct GrepResult<'a> {
@@ -326,6 +355,28 @@ pub struct GrepSearchOptions {
/// Whether to classify each match as a definition line. Adds ~2% overhead
/// on large repos; disable for interactive grep where it is not needed.
pub classify_definitions: bool,
/// Strip leading whitespace from matched lines and context lines, adjusting
/// highlight byte offsets accordingly. Useful for AI/MCP consumers and UIs
/// that don't need indentation. Default: false.
pub trim_whitespace: bool,
}
impl Default for GrepSearchOptions {
fn default() -> Self {
Self {
max_file_size: 10 * 1024 * 1024,
max_matches_per_file: 200,
smart_case: true,
file_offset: 0,
page_limit: 50,
mode: GrepMode::default(),
time_budget_ms: 0,
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
}
#[derive(Clone, Copy)]
@@ -1157,6 +1208,9 @@ where
for mut m in file_matches {
m.file_index = file_result_idx;
if options.trim_whitespace {
m.trim_leading_whitespace();
}
all_matches.push(m);
}
@@ -1237,6 +1291,9 @@ fn collect_grep_results<'a>(
for mut m in file_matches {
m.file_index = file_result_idx;
if options.trim_whitespace {
m.trim_leading_whitespace();
}
all_matches.push(m);
}
@@ -2158,6 +2215,7 @@ mod tests {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
// Test with 3 patterns
@@ -1373,6 +1373,7 @@ fn grep_opts() -> GrepSearchOptions {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
@@ -302,6 +302,7 @@ fn grep_opts() -> GrepSearchOptions {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
@@ -29,6 +29,7 @@ fn plain_opts() -> GrepSearchOptions {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
@@ -45,6 +46,7 @@ fn regex_opts() -> GrepSearchOptions {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
@@ -61,6 +63,7 @@ fn fuzzy_opts() -> GrepSearchOptions {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
}
}
+17 -29
View File
@@ -81,34 +81,19 @@ fn trauncate_line_for_ai(
match_ranges: Option<&[(u32, u32)]>,
max_len: usize,
) -> String {
// Strip leading/trailing whitespace to save tokens — the LLM has file:line for location.
let trimmed = line.trim();
// Leading whitespace is already stripped by core (trim_whitespace option).
// Only strip trailing whitespace here.
let trimmed = line.trim_end();
if trimmed.is_empty() {
return String::new();
}
let strip_offset = line.len() - line.trim_start().len();
if trimmed.len() <= max_len {
return trimmed.to_string();
}
// Adjust match ranges for the stripped leading whitespace
let adjusted: Vec<(u32, u32)>;
let ranges = match match_ranges {
Some(r) if strip_offset > 0 => {
let off = strip_offset as u32;
adjusted = r
.iter()
.map(|&(s, e)| (s.saturating_sub(off), e.saturating_sub(off)))
.collect();
Some(adjusted.as_slice())
}
other => other,
};
// Use first match range to center the window
if let Some(ranges) = ranges
if let Some(ranges) = match_ranges
&& let Some(&(match_start, match_end)) = ranges.first()
{
let match_start = match_start as usize;
@@ -569,26 +554,29 @@ mod tests {
use super::*;
#[test]
fn trunc_strips_whitespace() {
assert_eq!(trauncate_line_for_ai(" foo()", None, 180), "foo()");
assert_eq!(trauncate_line_for_ai(" bar ", None, 180), "bar");
fn trunc_strips_trailing_whitespace() {
// Leading whitespace is now stripped by core's trim_whitespace option.
// This function only strips trailing whitespace.
assert_eq!(trauncate_line_for_ai("foo()", None, 180), "foo()");
assert_eq!(trauncate_line_for_ai("bar ", None, 180), "bar");
assert_eq!(trauncate_line_for_ai(" ", None, 180), "");
}
#[test]
fn trunc_adjusts_match_ranges_after_strip() {
// " hello" — match on "hello" at bytes 4..9
let line = " hello";
let ranges = [(4, 9)];
fn trunc_preserves_pre_trimmed_match_ranges() {
// Core already stripped leading whitespace and adjusted offsets,
// so "hello" arrives with match at bytes 0..5.
let line = "hello";
let ranges = [(0, 5)];
let result = trauncate_line_for_ai(line, Some(&ranges), 180);
// After stripping 4 leading spaces, the trimmed line is "hello"
assert_eq!(result, "hello");
}
#[test]
fn trunc_long_line_centered() {
let line = format!("{}match_here{}", " ".repeat(8), "x".repeat(200));
let ranges = [(8u32, 18u32)];
// Core already stripped leading whitespace; offsets are pre-adjusted.
let line = format!("match_here{}", "x".repeat(200));
let ranges = [(0u32, 10u32)];
let result = trauncate_line_for_ai(&line, Some(&ranges), 50);
assert!(result.contains("match_here"));
assert!(result.len() <= 55); // budget + ellipsis chars
+1
View File
@@ -64,6 +64,7 @@ fn make_grep_options(
before_context: ctx_lines,
after_context: after_ctx,
classify_definitions: true,
trim_whitespace: true,
},
auto_expand,
)
@@ -707,6 +707,7 @@ fn bench_grep_search(c: &mut Criterion) {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let test_queries = vec![
@@ -30,6 +30,7 @@ fn run_grep(files: &[fff::FileItem], index: Option<&fff::BigramFilter>, query: &
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let parsed = parse_grep_query(query);
@@ -79,6 +79,7 @@ fn run_fuzzy_query(files: &[FileItem], query: &str, label: &str) {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let parsed = parse_grep_query(query);
+2
View File
@@ -146,6 +146,7 @@ impl<'a> GrepBench<'a> {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
},
}
}
@@ -505,6 +506,7 @@ fn main() {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let start = Instant::now();
let result = grep_search(
+3
View File
@@ -213,6 +213,7 @@ fn run_fff_full(files: &[FileItem], query: &str) -> (usize, Duration) {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let start = Instant::now();
let result = grep_search(
@@ -241,6 +242,7 @@ fn benchmark_fff_smart_case(files: &[FileItem], parsed: &FFFQuery<'_>) -> (usize
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let start = Instant::now();
let result = grep_search(
@@ -270,6 +272,7 @@ fn run_fff_page(files: &[FileItem], query: &str) -> (usize, Duration) {
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: false,
};
let start = Instant::now();
let result = grep_search(
+3
View File
@@ -280,6 +280,7 @@ pub fn live_grep(
smart_case,
grep_mode,
time_budget_ms,
trim_whitespace,
): (
String,
Option<usize>,
@@ -289,6 +290,7 @@ pub fn live_grep(
Option<bool>,
Option<String>,
Option<u64>,
Option<bool>,
),
) -> LuaResult<LuaValue> {
let file_picker_guard = FILE_PICKER.read().into_lua_result()?;
@@ -314,6 +316,7 @@ pub fn live_grep(
before_context: 0,
after_context: 0,
classify_definitions: false,
trim_whitespace: trim_whitespace.unwrap_or(false),
};
let result = picker.grep(&parsed, &options);
+1
View File
@@ -300,6 +300,7 @@ all available options:
smart_case = true, -- Case-insensitive unless query has uppercase
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
trim_whitespace = false, -- Strip leading whitespace from matched lines
},
})
<
+2
View File
@@ -54,6 +54,7 @@ local M = {}
--- @field smart_case boolean
--- @field time_budget_ms number
--- @field modes string[]
--- @field trim_whitespace boolean
--- @class FffConfig
--- @field base_path string
@@ -332,6 +333,7 @@ local function init()
smart_case = true, -- Case-insensitive unless query has uppercase
time_budget_ms = 150, -- Max search time in ms per call (prevents UI freeze, 0 = no limit)
modes = { 'plain', 'regex', 'fuzzy' }, -- Available grep modes and their cycling order
trim_whitespace = false, -- Strip leading whitespace from matched lines (useful for cleaner display)
},
}
+2 -1
View File
@@ -33,7 +33,8 @@ function M.search(query, file_offset, page_size, config, grep_mode)
conf.max_matches_per_file,
conf.smart_case,
grep_mode or 'plain',
conf.time_budget_ms
conf.time_budget_ms,
conf.trim_whitespace
)
return last_result
end