Skip to content
Open
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
81 changes: 81 additions & 0 deletions crates/fff-core/src/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,25 @@ fn collect_grep_results<'a>(
}
}

// Prioritize definition matches within each file while preserving
// contiguous per-file grouping for downstream renderers.
// A global sort would split matches from the same file into non-contiguous
// blocks, causing duplicate file headers in the Lua renderer.
if options.classify_definitions {
let mut start = 0;
while start < all_matches.len() {
let file_index = all_matches[start].file_index;
let mut end = start + 1;
while end < all_matches.len() && all_matches[end].file_index == file_index {
end += 1;
}
crate::sort_buffer::sort_by_key_with_buffer(&mut all_matches[start..end], |m| {
!m.is_definition
});
start = end;
}
}

// If no file had any match, we searched the entire slice.
if result_files.is_empty() {
files_consumed = files_to_search_len;
Expand Down Expand Up @@ -2197,6 +2216,68 @@ mod tests {
"Single pattern should find 1 match"
);

// Test that classify_definitions sorts defs first within each file group
// without breaking contiguous per-file ordering.
let options_with_defs = super::GrepSearchOptions {
classify_definitions: true,
..options.clone()
};
// file1 has: "pub enum GrepMode" (def), "pub struct GrepMatch" (def)
// file2 has: "struct PlainTextMatcher" (def)
// Search for both files using a pattern that hits non-def lines too.
let result_defs = super::multi_grep_search(
&files,
&["pub"],
&[],
&options_with_defs,
&ContentCacheBudget::unlimited(),
None,
);
// All matches for a given file must be contiguous (no interleaving).
if !result_defs.matches.is_empty() {
let mut last_file = result_defs.matches[0].file_index;
let mut seen_files = std::collections::HashSet::new();
seen_files.insert(last_file);
for m in result_defs.matches.iter().skip(1) {
if m.file_index != last_file {
assert!(
!seen_files.contains(&m.file_index),
"classify_definitions broke file contiguity: file {} appeared non-contiguously",
m.file_index
);
seen_files.insert(m.file_index);
last_file = m.file_index;
}
}
}
// Within each file group, definition matches must precede non-definition matches.
{
let mut i = 0;
while i < result_defs.matches.len() {
let file_index = result_defs.matches[i].file_index;
let group_start = i;
while i < result_defs.matches.len()
&& result_defs.matches[i].file_index == file_index
{
i += 1;
}
let group = &result_defs.matches[group_start..i];
// Definitions should not appear after a non-definition in the same file.
let mut saw_non_def = false;
for m in group {
if !m.is_definition {
saw_non_def = true;
} else {
assert!(
!saw_non_def,
"classify_definitions: definition appeared after non-definition in file {}",
file_index
);
}
}
}
}

// Test with empty patterns
let result3 = super::multi_grep_search(
&files,
Expand Down
4 changes: 3 additions & 1 deletion crates/fff-nvim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ pub fn live_grep(
smart_case,
grep_mode,
time_budget_ms,
classify_definitions,
): (
String,
Option<usize>,
Expand All @@ -288,6 +289,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()?;
Expand All @@ -312,7 +314,7 @@ pub fn live_grep(
time_budget_ms: time_budget_ms.unwrap_or(0),
before_context: 0,
after_context: 0,
classify_definitions: false,
classify_definitions: classify_definitions.unwrap_or(false),
};

let result = picker.grep(&parsed, &options);
Expand Down
1 change: 1 addition & 0 deletions crates/fff-nvim/src/lua_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ impl IntoLua for GrepResultLua<'_> {
item.set("col", m.col)?;
item.set("byte_offset", m.byte_offset)?;
item.set("line_content", m.line_content.as_str())?;
item.set("is_definition", m.is_definition)?;

// Match byte ranges within line_content
let ranges = lua.create_table()?;
Expand Down
2 changes: 2 additions & 0 deletions lua/fff/conf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ local M = {}
--- @field smart_case boolean
--- @field time_budget_ms number
--- @field modes string[]
--- @field classify_definitions boolean

--- @class FffConfig
--- @field base_path string
Expand Down Expand Up @@ -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
classify_definitions = false, -- Mark definition lines like fn/struct/class
},
}

Expand Down
10 changes: 10 additions & 0 deletions lua/fff/grep/grep_renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ end
local function render_match_line(item, ctx)
local location = string.format(':%d:%d', item.line_number or 0, (item.col or 0) + 1)
local separator = ' '

-- vim.json.decode may return Blobs for strings with NUL bytes; coerce to string.
local raw_content = item.line_content
if type(raw_content) ~= 'string' then raw_content = raw_content and tostring(raw_content) or '' end
Expand Down Expand Up @@ -193,6 +194,15 @@ local function apply_match_highlights(item, ctx, item_idx, buf, ns_id, row, line
})
end
end

-- 7. Definition indicator as virtual text
if item.is_definition then
pcall(vim.api.nvim_buf_set_extmark, buf, ns_id, row, 0, {
virt_text = { { ' [def]', config.hl.combo_header or 'Number' } },
virt_text_pos = 'right_align',
priority = 250,
})
end
end

--- Render a single item's lines (called by list_renderer's generate_item_lines).
Expand Down
3 changes: 2 additions & 1 deletion lua/fff/grep/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.classify_definitions
)
return last_result
end
Expand Down