Skip to content
Merged
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
4 changes: 2 additions & 2 deletions crates/mcp-servers/src/coding/tools/ast_grep/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::coding::error::AstGrepError;
use crate::coding::tools::glob_filter::{PathGlobMatcher, build_path_matcher};
use crate::coding::tools::glob_filter::{CaseSensitivity, PathGlobMatcher, build_path_matcher};
use ast_grep_core::Doc;
use ast_grep_core::matcher::{NodeMatch, Pattern};
use ast_grep_language::{Language, LanguageExt, SupportLang};
Expand Down Expand Up @@ -99,7 +99,7 @@ pub async fn perform_ast_grep(mut args: AstGrepInput) -> Result<AstGrepOutput, A
let pattern = Pattern::try_new(&args.pattern, lang).map_err(|e| AstGrepError::InvalidPattern(e.to_string()))?;

let constraint_regexes = compile_constraints(args.constraints.as_ref())?;
let path_matcher = build_path_matcher(args.glob.as_deref(), false)?;
let path_matcher = build_path_matcher(args.glob.as_deref(), CaseSensitivity::Sensitive)?;
let search_path = args.path.as_deref().unwrap_or(".");
let path = Path::new(search_path);
if !path.exists() {
Expand Down
5 changes: 3 additions & 2 deletions crates/mcp-servers/src/coding/tools/find/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::coding::error::FindError;
use crate::coding::tools::glob_filter::PathGlobMatcher;
use crate::coding::tools::glob_filter::{CaseSensitivity, PathGlobMatcher};
use ignore::{WalkBuilder, WalkState};
use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
use schemars::JsonSchema;
Expand Down Expand Up @@ -54,7 +54,8 @@ pub async fn find_files(args: FindInput) -> Result<FindOutput, FindError> {
return Err(FindError::PathNotFound(search_path.to_string()));
}

let path_matcher = Arc::new(PathGlobMatcher::new(&args.pattern, args.case_insensitive.unwrap_or(false))?);
let path_matcher =
Arc::new(PathGlobMatcher::new(&args.pattern, CaseSensitivity::from_optional(args.case_insensitive))?);
let state = Arc::new(FindState::new(args.limit));

let mut walker_builder = WalkBuilder::new(search_root);
Expand Down
46 changes: 35 additions & 11 deletions crates/mcp-servers/src/coding/tools/glob_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ use crate::coding::error::GlobError;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use std::path::Path;

/// Whether glob matching distinguishes uppercase from lowercase.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaseSensitivity {
/// Match patterns exactly, respecting case.
Sensitive,
/// Match patterns ignoring case differences.
Insensitive,
}

impl CaseSensitivity {
/// Map a nullable "case-insensitive" flag (e.g. from a tool input) onto a
/// concrete sensitivity, treating `None`/`Some(false)` as case-sensitive.
pub fn from_optional(case_insensitive: Option<bool>) -> Self {
if case_insensitive.unwrap_or(false) { Self::Insensitive } else { Self::Sensitive }
}

fn is_insensitive(self) -> bool {
matches!(self, Self::Insensitive)
}
}

#[derive(Debug, Clone)]
pub struct PathGlobMatcher {
matcher: GlobSet,
Expand All @@ -14,12 +35,15 @@ enum PathGlobKind {
RelativePath,
}

pub fn build_path_matcher(glob: Option<&str>, case_insensitive: bool) -> Result<Option<PathGlobMatcher>, GlobError> {
glob.map(|glob| PathGlobMatcher::new(glob, case_insensitive)).transpose()
pub fn build_path_matcher(
glob: Option<&str>,
case_sensitivity: CaseSensitivity,
) -> Result<Option<PathGlobMatcher>, GlobError> {
glob.map(|glob| PathGlobMatcher::new(glob, case_sensitivity)).transpose()
}

impl PathGlobMatcher {
pub fn new(pattern: &str, case_insensitive: bool) -> Result<Self, GlobError> {
pub fn new(pattern: &str, case_sensitivity: CaseSensitivity) -> Result<Self, GlobError> {
if pattern.is_empty() {
return Err(GlobError::InvalidPattern {
pattern: pattern.to_string(),
Expand All @@ -29,15 +53,15 @@ impl PathGlobMatcher {

let kind = if contains_separator(pattern) { PathGlobKind::RelativePath } else { PathGlobKind::Basename };
let mut builder = GlobSetBuilder::new();
add_glob(&mut builder, pattern, case_insensitive)?;
add_glob(&mut builder, pattern, case_sensitivity)?;

// globset's `**/` requires at least one leading directory, so `**/*.rs`
// on its own misses files at the search root. Also register the
// `**/`-stripped pattern so depth-zero matches (e.g. `lib.rs`) are found.
if let Some(root_pattern) = pattern.strip_prefix("**/")
&& !root_pattern.is_empty()
{
add_glob(&mut builder, root_pattern, case_insensitive)?;
add_glob(&mut builder, root_pattern, case_sensitivity)?;
}

Ok(Self { matcher: builder.build().map_err(|e| GlobError::BuildFailed(e.to_string()))?, kind })
Expand All @@ -53,9 +77,9 @@ impl PathGlobMatcher {
}
}

fn add_glob(builder: &mut GlobSetBuilder, pattern: &str, case_insensitive: bool) -> Result<(), GlobError> {
fn add_glob(builder: &mut GlobSetBuilder, pattern: &str, case_sensitivity: CaseSensitivity) -> Result<(), GlobError> {
let glob = GlobBuilder::new(pattern)
.case_insensitive(case_insensitive)
.case_insensitive(case_sensitivity.is_insensitive())
.build()
.map_err(|e| GlobError::InvalidPattern { pattern: pattern.to_string(), reason: e.to_string() })?;
builder.add(glob);
Expand All @@ -72,7 +96,7 @@ mod tests {

#[test]
fn bare_patterns_match_basenames_at_any_depth() {
let matcher = PathGlobMatcher::new("README*", false).unwrap();
let matcher = PathGlobMatcher::new("README*", CaseSensitivity::Sensitive).unwrap();
let root = Path::new("/workspace");

assert!(matcher.matches(Path::new("/workspace/README.md"), root));
Expand All @@ -82,7 +106,7 @@ mod tests {

#[test]
fn slash_patterns_match_relative_paths_only() {
let matcher = PathGlobMatcher::new("crates/**/*.rs", false).unwrap();
let matcher = PathGlobMatcher::new("crates/**/*.rs", CaseSensitivity::Sensitive).unwrap();
let root = Path::new("/workspace");

assert!(matcher.matches(Path::new("/workspace/crates/app/src/lib.rs"), root));
Expand All @@ -92,7 +116,7 @@ mod tests {

#[test]
fn recursive_slash_patterns_match_root_files() {
let matcher = PathGlobMatcher::new("**/*.rs", false).unwrap();
let matcher = PathGlobMatcher::new("**/*.rs", CaseSensitivity::Sensitive).unwrap();
let root = Path::new("/workspace");

assert!(matcher.matches(Path::new("/workspace/lib.rs"), root));
Expand All @@ -101,7 +125,7 @@ mod tests {

#[test]
fn case_insensitive_patterns_match_basenames() {
let matcher = PathGlobMatcher::new("readme*", true).unwrap();
let matcher = PathGlobMatcher::new("readme*", CaseSensitivity::Insensitive).unwrap();
assert!(matcher.matches(Path::new("/workspace/README.md"), Path::new("/workspace")));
}
}
4 changes: 2 additions & 2 deletions crates/mcp-servers/src/coding/tools/grep/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub mod common;

use crate::coding::error::GrepError;
use crate::coding::tools::glob_filter::{PathGlobMatcher, build_path_matcher};
use crate::coding::tools::glob_filter::{CaseSensitivity, PathGlobMatcher, build_path_matcher};
use aether_lspd::extensions_for_alias as extensions_for_type;
use common::{CountSink, HasMatchSink, MatchCollectorSink, MatchData, OutputMode};
use grep::{
Expand Down Expand Up @@ -156,7 +156,7 @@ fn perform_grep_sync(mut args: GrepInput) -> Result<GrepOutput, GrepError> {
args.path = None;
}

let path_matcher = build_path_matcher(args.glob.as_deref(), false)?;
let path_matcher = build_path_matcher(args.glob.as_deref(), CaseSensitivity::Sensitive)?;

let matcher = build_matcher(&args.pattern, args.case_insensitive, args.multiline)?;

Expand Down