Compare commits
3 Commits
main
...
fix/grep-trim
| Author | SHA1 | Date | |
|---|---|---|---|
| f866463273 | |||
| 6d2a174b38 | |||
| 34a8020a58 |
@@ -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
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user