Skip to content

--json --replace rewrites source text and offsets instead of preserving source matches with replacement metadata #199

Closed

Description

@wuliangtdi

Summary

With --json -r, tgrep emits the replacement text as lines.text and as the submatch text, with offsets into that rewritten text. ripgrep preserves the source line and source match offsets, and includes the expanded replacement in the submatch's replacement field.

This difference can mislead consumers that use the JSON match record to locate the original match or prepare a replacement. It is different from the JSON summary/statistics differences in #101.

Reproduce

mkdir repro-json-replace && cd repro-json-replace
mkdir data
printf 'hello\n' > data/a.txt

tgrep --no-index --json -r R -- '(hello)' data
rg               --json -r R -- '(hello)' data

Both commands exit 0. Relevant fields from the match event (path and timing fields omitted):

Actual tgrep:

{"lines":{"text":"R\n"},"submatches":[{"match":{"text":"R"},"start":0,"end":1}]}

Expected, as emitted by rg 15.1.0:

{"lines":{"text":"hello\n"},"submatches":[{"match":{"text":"hello"},"replacement":{"text":"R"},"start":0,"end":5}]}

The content-search cases reproduce with --no-index, an on-disk index, and tgrep serve --no-watch.

To repeat the indexed cases, run tgrep index data, remove --no-index, and keep the directory path data. For the server cases, start tgrep serve --no-watch data in another terminal and repeat those directory searches. --stats was used in the Windows harness to verify the selected backend, including (via server).

Environment

  • tgrep 1.2.0, official Windows x86_64 release.
  • Source inspected at release commit 3ba6ee02537281b68ee1366bb40df9f8b7cc54f0.
  • Windows; test harness run with PowerShell 7.
  • ripgrep 15.1.0 (af60c2de9d).
  • Verified on 2026-10-12. The shell examples describe the same fixture bytes and arguments tested on Windows.

Cause

tgrep-cli/src/search.rs:328 forwards replace to match rendering. The JSON branch in tgrep-cli/src/output.rs:599 constructs lines.text and submatches from the rendered content and spans, and emits no separate replacement field.

Possible fix

For JSON output, preserve the original source line, source match text, and source-relative offsets. Carry expanded replacements separately and emit them as submatches[].replacement, matching the observed rg 15.1.0 format.

A regression could compare match-event source fields and replacement metadata, including a replacement whose length differs from the source match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions