@@ -61,7 +61,15 @@ impl<C: ParserConfig> QueryParser<C> {
6161 . rev ( )
6262 . take_while ( |& b| b != b':' )
6363 . all ( |b| b. is_ascii_digit ( ) ) ;
64- if !matches ! ( constraint, Constraint :: FilePath ( _) ) && !has_location_suffix {
64+
65+ // for grep we don't want to treat a part of path like pathname
66+ let treat_as_text = matches ! ( constraint, Constraint :: PathSegment ( _) )
67+ && config. treat_lone_path_as_text ( ) ;
68+
69+ if !matches ! ( constraint, Constraint :: FilePath ( _) )
70+ && !has_location_suffix
71+ && !treat_as_text
72+ {
6573 constraints. push ( constraint) ;
6674 return FFFQuery {
6775 raw_query,
@@ -102,6 +110,10 @@ impl<C: ParserConfig> QueryParser<C> {
102110 let tokens = query. split_whitespace ( ) ;
103111
104112 let mut has_file_path = false ;
113+ // Track the FilePath token position in constraints so we can promote
114+ // it back to text if the final query ends up with no fuzzy text.
115+ let mut file_path_constraint_idx: Option < usize > = None ;
116+ let mut file_path_token: Option < & str > = None ;
105117 for token in tokens {
106118 match parse_token ( token, config) {
107119 Some ( Constraint :: FilePath ( _) ) => {
@@ -111,6 +123,8 @@ impl<C: ParserConfig> QueryParser<C> {
111123 // searching for).
112124 text_parts. push ( token) ;
113125 } else {
126+ file_path_constraint_idx = Some ( constraints. len ( ) ) ;
127+ file_path_token = Some ( token) ;
114128 constraints. push ( Constraint :: FilePath ( token) ) ;
115129 has_file_path = true ;
116130 }
@@ -124,6 +138,21 @@ impl<C: ParserConfig> QueryParser<C> {
124138 }
125139 }
126140
141+ // If the query produced a single FilePath and no fuzzy text parts, the
142+ // user isn't filtering by filename suffix — they're fuzzy-searching
143+ // for that name (the only other constraints are path-scoping like
144+ // PathSegment/Extension/Glob). Mirror the single-token rule at
145+ // parser.rs:48-64: promote FilePath → fuzzy text so e.g. `profile.h`
146+ // alongside `chrome/browser/profiles/` fuzzy-matches all `profile*.h`
147+ // files instead of only one file literally ending in `/profile.h`.
148+ if text_parts. is_empty ( )
149+ && let Some ( idx) = file_path_constraint_idx
150+ && let Some ( tok) = file_path_token
151+ {
152+ constraints. remove ( idx) ;
153+ text_parts. push ( tok) ;
154+ }
155+
127156 // Try to extract location from the last fuzzy token
128157 // e.g., "search file:12" -> fuzzy="search file", location=Line(12)
129158 let location = if config. enable_location ( ) && !text_parts. is_empty ( ) {
@@ -916,6 +945,164 @@ mod tests {
916945 assert_eq ! ( result. grep_text( ) , "pattern" ) ;
917946 }
918947
948+ #[ test]
949+ fn test_ai_grep_filename_with_pathsegment_only_promotes_to_text ( ) {
950+ // When the ONLY non-text constraints are path-scoping (PathSegment,
951+ // here), a bare filename token like `profile.h` should NOT be used as
952+ // a FilePath filter — the user is fuzzy-searching within that dir,
953+ // not asking for files named exactly `profile.h`.
954+ use crate :: AiGrepConfig ;
955+ let parser = QueryParser :: new ( AiGrepConfig ) ;
956+ let result = parser. parse ( "chrome/browser/profiles/ profile.h" ) ;
957+ assert_eq ! ( result. constraints. len( ) , 1 ) ;
958+ assert ! (
959+ matches!(
960+ result. constraints[ 0 ] ,
961+ Constraint :: PathSegment ( "chrome/browser/profiles" )
962+ ) ,
963+ "Expected single PathSegment, got {:?}" ,
964+ result. constraints
965+ ) ;
966+ assert_eq ! ( result. grep_text( ) , "profile.h" ) ;
967+ }
968+
969+ #[ test]
970+ fn test_ai_grep_leading_slash_path_alone_is_text_not_path_segment ( ) {
971+ // A leading-slash multi-segment path like `/api/tests/` or `/api/tests`
972+ // used as the sole query token should be treated as fuzzy text, NOT as
973+ // a PathSegment constraint. The user is searching for files matching
974+ // that path string, not trying to scope results to a directory.
975+ use crate :: AiGrepConfig ;
976+ let parser = QueryParser :: new ( AiGrepConfig ) ;
977+
978+ // With trailing slash
979+ let result = parser. parse ( "/api/tests/" ) ;
980+ assert_eq ! (
981+ result. constraints. len( ) ,
982+ 0 ,
983+ "Expected no constraints for '/api/tests/', got {:?}" ,
984+ result. constraints
985+ ) ;
986+ assert ! (
987+ matches!( result. fuzzy_query, FuzzyQuery :: Text ( "/api/tests/" ) ) ,
988+ "Expected FuzzyQuery::Text, got {:?}" ,
989+ result. fuzzy_query
990+ ) ;
991+
992+ // Without trailing slash
993+ let result = parser. parse ( "/api/tests" ) ;
994+ assert_eq ! (
995+ result. constraints. len( ) ,
996+ 0 ,
997+ "Expected no constraints for '/api/tests', got {:?}" ,
998+ result. constraints
999+ ) ;
1000+ assert ! (
1001+ matches!( result. fuzzy_query, FuzzyQuery :: Text ( "/api/tests" ) ) ,
1002+ "Expected FuzzyQuery::Text, got {:?}" ,
1003+ result. fuzzy_query
1004+ ) ;
1005+ }
1006+
1007+ #[ test]
1008+ fn test_grep_leading_slash_path_alone_is_text_not_path_segment ( ) {
1009+ // Same behavior for regular GrepConfig — single-token path-like
1010+ // queries are search terms, not directory filters.
1011+ let parser = QueryParser :: new ( GrepConfig ) ;
1012+
1013+ let result = parser. parse ( "/api/tests/" ) ;
1014+ assert_eq ! (
1015+ result. constraints. len( ) ,
1016+ 0 ,
1017+ "GrepConfig: expected no constraints for '/api/tests/', got {:?}" ,
1018+ result. constraints
1019+ ) ;
1020+ assert ! (
1021+ matches!( result. fuzzy_query, FuzzyQuery :: Text ( "/api/tests/" ) ) ,
1022+ "GrepConfig: expected FuzzyQuery::Text, got {:?}" ,
1023+ result. fuzzy_query
1024+ ) ;
1025+
1026+ let result = parser. parse ( "/api/tests" ) ;
1027+ assert_eq ! (
1028+ result. constraints. len( ) ,
1029+ 0 ,
1030+ "GrepConfig: expected no constraints for '/api/tests', got {:?}" ,
1031+ result. constraints
1032+ ) ;
1033+ assert ! (
1034+ matches!( result. fuzzy_query, FuzzyQuery :: Text ( "/api/tests" ) ) ,
1035+ "GrepConfig: expected FuzzyQuery::Text, got {:?}" ,
1036+ result. fuzzy_query
1037+ ) ;
1038+ }
1039+
1040+ #[ test]
1041+ fn test_file_search_leading_slash_path_alone_stays_path_segment ( ) {
1042+ // FileSearchConfig (fuzzy file finder) should still treat a lone
1043+ // `/api/tests/` as a PathSegment constraint — the user is scoping
1044+ // the file list to that directory.
1045+ let parser = QueryParser :: new ( FileSearchConfig ) ;
1046+
1047+ let result = parser. parse ( "/api/tests/" ) ;
1048+ assert_eq ! (
1049+ result. constraints. len( ) ,
1050+ 1 ,
1051+ "FileSearchConfig: expected PathSegment constraint, got {:?}" ,
1052+ result. constraints
1053+ ) ;
1054+ assert ! (
1055+ matches!( result. constraints[ 0 ] , Constraint :: PathSegment ( "api/tests" ) ) ,
1056+ "FileSearchConfig: expected PathSegment(\" api/tests\" ), got {:?}" ,
1057+ result. constraints[ 0 ]
1058+ ) ;
1059+
1060+ let result = parser. parse ( "/api/tests" ) ;
1061+ assert_eq ! (
1062+ result. constraints. len( ) ,
1063+ 1 ,
1064+ "FileSearchConfig: expected PathSegment constraint, got {:?}" ,
1065+ result. constraints
1066+ ) ;
1067+ assert ! (
1068+ matches!( result. constraints[ 0 ] , Constraint :: PathSegment ( "api/tests" ) ) ,
1069+ "FileSearchConfig: expected PathSegment(\" api/tests\" ), got {:?}" ,
1070+ result. constraints[ 0 ]
1071+ ) ;
1072+ }
1073+
1074+ #[ test]
1075+ fn test_ai_grep_filename_with_extension_only_promotes_to_text ( ) {
1076+ // Same case with an Extension constraint — no fuzzy text means the
1077+ // filename is what the user is searching for.
1078+ use crate :: AiGrepConfig ;
1079+ let parser = QueryParser :: new ( AiGrepConfig ) ;
1080+ let result = parser. parse ( "*.h profile.h" ) ;
1081+ assert_eq ! ( result. constraints. len( ) , 1 ) ;
1082+ assert ! (
1083+ matches!( result. constraints[ 0 ] , Constraint :: Extension ( "h" ) ) ,
1084+ "Expected Extension, got {:?}" ,
1085+ result. constraints
1086+ ) ;
1087+ assert_eq ! ( result. grep_text( ) , "profile.h" ) ;
1088+ }
1089+
1090+ #[ test]
1091+ fn test_ai_grep_filename_with_other_text_keeps_filepath ( ) {
1092+ // Sanity: when there IS fuzzy text alongside the filename, the
1093+ // filename stays a FilePath filter (the documented multi-token case).
1094+ use crate :: AiGrepConfig ;
1095+ let parser = QueryParser :: new ( AiGrepConfig ) ;
1096+ let result = parser. parse ( "main.rs pattern" ) ;
1097+ assert_eq ! ( result. constraints. len( ) , 1 ) ;
1098+ assert ! (
1099+ matches!( result. constraints[ 0 ] , Constraint :: FilePath ( "main.rs" ) ) ,
1100+ "Expected FilePath, got {:?}" ,
1101+ result. constraints
1102+ ) ;
1103+ assert_eq ! ( result. grep_text( ) , "pattern" ) ;
1104+ }
1105+
9191106 #[ test]
9201107 fn test_ai_grep_bare_filename_schema_rs ( ) {
9211108 use crate :: AiGrepConfig ;
@@ -1157,16 +1344,16 @@ mod tests {
11571344 fn test_file_picker_filename_with_extension_constraint ( ) {
11581345 let parser = QueryParser :: new ( FileSearchConfig ) ;
11591346 let result = parser. parse ( "main.rs *.lua" ) ;
1160- // main.rs → FilePath, *.lua → Extension
1161- assert_eq ! ( result. constraints. len( ) , 2 ) ;
1347+ // With only path-scoping constraints (Extension) and no fuzzy text,
1348+ // `main.rs` is promoted to fuzzy text — the user is fuzzy-searching
1349+ // for "main.rs" among `.lua` files, not filtering by literal filename
1350+ // suffix. Only the Extension constraint remains.
1351+ assert_eq ! ( result. constraints. len( ) , 1 ) ;
11621352 assert ! ( matches!(
11631353 result. constraints[ 0 ] ,
1164- Constraint :: FilePath ( "main.rs" )
1165- ) ) ;
1166- assert ! ( matches!(
1167- result. constraints[ 1 ] ,
11681354 Constraint :: Extension ( "lua" )
11691355 ) ) ;
1356+ assert_eq ! ( result. fuzzy_query, FuzzyQuery :: Text ( "main.rs" ) ) ;
11701357 }
11711358
11721359 #[ test]
0 commit comments