ripgrepSearch method

Future<SearchResult> ripgrepSearch(
  1. String pattern, {
  2. List<String> args = const [],
  3. String? directory,
})

Run rg (ripgrep) with pattern and additional args, parsing output into SearchMatch objects.

Falls back to the built-in search method if rg is not available.

Implementation

Future<SearchResult> ripgrepSearch(
  String pattern, {
  List<String> args = const [],
  String? directory,
}) async {
  final sw = Stopwatch()..start();
  final dir = directory ?? projectRoot;

  try {
    final result = await Process.run('rg', [
      '--json',
      '--line-number',
      ...args,
      pattern,
      dir,
    ]);

    if (result.exitCode != 0 && result.exitCode != 1) {
      // Exit code 1 = no matches; other codes are errors.
      throw ProcessException(
        'rg',
        args,
        result.stderr.toString(),
        result.exitCode,
      );
    }

    final matches = <SearchMatch>[];
    var filesSearched = 0;
    final seenFiles = <String>{};

    for (final line in LineSplitter.split(result.stdout.toString())) {
      if (line.isEmpty) continue;
      try {
        final json = jsonDecode(line) as Map<String, dynamic>;
        final type = json['type'] as String?;

        if (type == 'match') {
          final data = json['data'] as Map<String, dynamic>;
          final path =
              (data['path'] as Map<String, dynamic>)['text'] as String;
          final lineNum = data['line_number'] as int;
          final lineText =
              (data['lines'] as Map<String, dynamic>)['text'] as String;
          final submatches = data['submatches'] as List<dynamic>;

          seenFiles.add(path);

          for (final sub in submatches) {
            final s = sub as Map<String, dynamic>;
            matches.add(
              SearchMatch(
                filePath: path,
                lineNumber: lineNum,
                column: s['start'] as int,
                matchLength: (s['end'] as int) - (s['start'] as int),
                lineContent: lineText.trimRight(),
              ),
            );
          }
        } else if (type == 'summary') {
          final data = json['data'] as Map<String, dynamic>;
          final stats = data['stats'] as Map<String, dynamic>?;
          if (stats != null) {
            filesSearched = stats['searches'] as int? ?? seenFiles.length;
          }
        }
      } catch (_) {
        // Skip malformed JSON lines.
      }
    }

    sw.stop();
    return SearchResult(
      matches: matches,
      totalMatches: matches.length,
      filesSearched: filesSearched > 0 ? filesSearched : seenFiles.length,
      duration: sw.elapsed,
    );
  } on ProcessException {
    // rg not found — fall back to built-in search.
    sw.stop();
    return search(
      pattern,
      SearchScope.project,
      const SearchOptions(),
      targetPath: dir,
    );
  }
}