@@ -3,6 +3,13 @@ import * as readline from 'readline';
33import { spawn } from 'child_process' ;
44import { FileInfo , LineData , SearchMatch , SearchOptions } from '../shared/types' ;
55
6+ // Convert wildcard pattern to regex string: * = .*, ? = ., rest escaped
7+ function wildcardToRegex ( pattern : string ) : string {
8+ return pattern . replace ( / [ . + ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
9+ . replace ( / \* / g, '.*' )
10+ . replace ( / \? / g, '.' ) ;
11+ }
12+
613// Shared column filter utility — filters a line to only visible columns
714export interface ColumnConfig {
815 delimiter : string ;
@@ -334,6 +341,25 @@ export class FileHandler {
334341 const matches : SearchMatch [ ] = [ ] ;
335342 const MAX_MATCHES = 50000 ;
336343
344+ // Determine the effective regex pattern and whether ripgrep should use regex mode
345+ const useRegexMode = options . isRegex || options . isWildcard ;
346+ let rgPattern = options . pattern ;
347+ if ( options . isWildcard ) {
348+ rgPattern = wildcardToRegex ( options . pattern ) ;
349+ }
350+
351+ // Pre-compile regex for determining actual match length (ripgrep doesn't report it)
352+ let searchRegex : RegExp | null = null ;
353+ if ( useRegexMode ) {
354+ try {
355+ const flags = options . matchCase ? '' : 'i' ;
356+ searchRegex = new RegExp ( rgPattern , flags ) ;
357+ } catch {
358+ // Invalid regex - ripgrep will also fail, return empty
359+ return [ ] ;
360+ }
361+ }
362+
337363 // Build ripgrep arguments
338364 const args : string [ ] = [
339365 '--line-number' ,
@@ -350,15 +376,15 @@ export class FileHandler {
350376 args . push ( '--word-regexp' ) ;
351377 }
352378
353- if ( ! options . isRegex ) {
379+ if ( ! useRegexMode ) {
354380 args . push ( '--fixed-strings' ) ;
355381 }
356382
357383 // Limit matches
358384 args . push ( '--max-count' , String ( MAX_MATCHES ) ) ;
359385
360386 // Add pattern and file
361- args . push ( '--' , options . pattern , this . filePath ) ;
387+ args . push ( '--' , rgPattern , this . filePath ) ;
362388
363389 return new Promise ( ( resolve ) => {
364390 const proc = spawn ( 'rg' , args ) ;
@@ -396,10 +422,20 @@ export class FileHandler {
396422 const adjustedLineNum = lineNum - 1 - this . headerLineCount ;
397423 if ( adjustedLineNum < 0 ) continue ;
398424
425+ // For regex patterns, determine actual match length from the line text
426+ let matchLength = options . pattern . length ;
427+ if ( searchRegex ) {
428+ const textFromMatch = lineText . substring ( column - 1 ) ;
429+ const reMatch = searchRegex . exec ( textFromMatch ) ;
430+ if ( reMatch && reMatch . index === 0 ) {
431+ matchLength = reMatch [ 0 ] . length ;
432+ }
433+ }
434+
399435 matches . push ( {
400436 lineNumber : adjustedLineNum ,
401437 column : column - 1 , // ripgrep uses 1-based columns
402- length : options . pattern . length ,
438+ length : matchLength ,
403439 lineText,
404440 } ) ;
405441
@@ -464,6 +500,12 @@ export class FileHandler {
464500 const flags = options . matchCase ? 'g' : 'gi' ;
465501 if ( options . isRegex ) {
466502 regex = new RegExp ( options . pattern , flags ) ;
503+ } else if ( options . isWildcard ) {
504+ let converted = wildcardToRegex ( options . pattern ) ;
505+ if ( options . wholeWord ) {
506+ converted = `\\b${ converted } \\b` ;
507+ }
508+ regex = new RegExp ( converted , flags ) ;
467509 } else {
468510 let escaped = options . pattern . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
469511 if ( options . wholeWord ) {
0 commit comments