Compare commits

...

2 Commits

Author SHA1 Message Date
Dmitriy Kovalenko cfd7be7c76 chore: Update docs for - feat(mcp): Add file path fallback for grep
docs / docs (push) Has been cancelled
2026-03-13 22:25:33 +00:00
Dmitriy Kovalenko 740506993c feat(mcp): Add file path fallback for grep 2026-03-13 15:24:35 -07:00
5 changed files with 89 additions and 12 deletions
+1
View File
@@ -327,6 +327,7 @@ pub fn match_and_score_files<'a>(
match_type: match filename_match {
Some(filename_match) if filename_match.exact => "exact_filename",
Some(_) => "fuzzy_filename",
None if path_match.exact => "exact_path",
None => "fuzzy_path",
},
};
+34 -3
View File
@@ -22,8 +22,8 @@ use crate::cursor::CursorStore;
use crate::output::{GrepFormatter, OutputMode, file_suffix};
/// Strip common delimiters for fuzzy fallback queries.
fn strip_delimiters(s: &str) -> String {
let mut out = String::with_capacity(s.len());
fn cleanup_fuzzy_query(s: &str) -> String {
let mut out = s.to_lowercase();
for c in s.chars() {
if !matches!(c, ':' | '-' | '_') {
out.push(c);
@@ -305,7 +305,7 @@ impl FffServer {
}
// Fuzzy fallback for typo tolerance
let fuzzy_query = strip_delimiters(&query.to_lowercase());
let fuzzy_query = cleanup_fuzzy_query(&query);
let (fuzzy_options, _) = make_grep_options(output_mode, GrepMode::Fuzzy, 0, Some(0));
let fuzzy_parsed = parser.parse(&fuzzy_query);
let fuzzy_result =
@@ -331,6 +331,37 @@ impl FffServer {
)]));
}
// File path fallback: if query looks like a path, suggest the matching file
if query.contains('/') {
let file_parser = QueryParser::default();
let file_query = file_parser.parse(query);
let file_opts = FuzzySearchOptions {
max_threads: 0,
current_file: None,
project_path: Some(picker.base_path()),
last_same_query_match: None,
combo_boost_score_multiplier: 100,
min_combo_count: 3,
pagination: PaginationArgs {
offset: 0,
limit: 1,
},
};
let file_result = FilePicker::fuzzy_search(files, query, file_query, file_opts);
if let (Some(top), Some(score)) =
(file_result.items.first(), file_result.scores.first())
{
// Only suggest when the match is strong enough.
let query_len = query.len() as i32;
if score.base_score > query_len * 10 {
return Ok(CallToolResult::success(vec![Content::text(format!(
"0 content matches. But there is a relevant file path: {}",
top.relative_path
))]));
}
}
}
let hint = match &parsed {
Some(q)
if q.constraints
+15
View File
@@ -73,6 +73,13 @@ pub trait ParserConfig {
true
}
/// Should parse location suffixes (e.g., file:12, file:12:4)
/// Disabled for grep modes where colon-number patterns like localhost:8080
/// are search text, not file locations.
fn enable_location(&self) -> bool {
true
}
/// Determine whether a token should be treated as a glob constraint.
///
/// The default implementation delegates to `zlob::has_wildcards` with
@@ -126,6 +133,10 @@ impl ParserConfig for GrepConfig {
false
}
fn enable_location(&self) -> bool {
false
}
/// Only recognise globs that are clearly directory/path oriented.
///
/// Characters like `?`, `[`, and bare `*` (without `/`) are extremely
@@ -177,6 +188,10 @@ impl ParserConfig for AiGrepConfig {
false
}
fn enable_location(&self) -> bool {
false
}
fn is_glob_pattern(&self, token: &str) -> bool {
// First check GrepConfig's strict rules (path globs, brace expansion)
if GrepConfig.is_glob_pattern(token) {
+38 -8
View File
@@ -58,13 +58,15 @@ impl<C: ParserConfig> QueryParser<C> {
}
// Try to extract location from single token (e.g., "file:12")
let (query_without_loc, location) = parse_location(query);
if location.is_some() {
return Some(FFFQuery {
constraints,
fuzzy_query: FuzzyQuery::Text(query_without_loc),
location,
});
if config.enable_location() {
let (query_without_loc, location) = parse_location(query);
if location.is_some() {
return Some(FFFQuery {
constraints,
fuzzy_query: FuzzyQuery::Text(query_without_loc),
location,
});
}
}
// Plain text single token - return None (caller handles as simple fuzzy match)
@@ -99,7 +101,7 @@ impl<C: ParserConfig> QueryParser<C> {
// Try to extract location from the last fuzzy token
// e.g., "search file:12" -> fuzzy="search file", location=Line(12)
let location = if !text_parts.is_empty() {
let location = if config.enable_location() && !text_parts.is_empty() {
let last_idx = text_parts.len() - 1;
let (without_loc, loc) = parse_location(text_parts[last_idx]);
if loc.is_some() {
@@ -1010,6 +1012,34 @@ mod tests {
);
}
#[test]
fn test_grep_no_location_parsing_single_token() {
let parser = QueryParser::new(GrepConfig);
// localhost:8080 should NOT be parsed as location — it's a search pattern
let result = parser.parse("localhost:8080");
assert!(
result.is_none(),
"Single-token grep query with colon-number should return None (plain text), got {:?}",
result
);
}
#[test]
fn test_grep_no_location_parsing_multi_token() {
let q = QueryParser::new(GrepConfig)
.parse("*.rs localhost:8080")
.expect("should parse");
assert_eq!(
q.grep_text(),
"localhost:8080",
"Colon-number suffix should be preserved in grep text"
);
assert!(
q.location.is_none(),
"Grep should not parse location from colon-number"
);
}
#[test]
fn test_grep_config_star_text_star_not_glob() {
use crate::GrepConfig;
+1 -1
View File
@@ -1,4 +1,4 @@
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 March 12
*fff.nvim.txt* For Neovim >= 0.10.0 Last change: 2026 March 13
==============================================================================
Table of Contents *fff.nvim-table-of-contents*