Skip to content

feat: surface git's color-moved highlights for moved lines#323

Open
mvanhorn wants to merge 1 commit into
modem-dev:mainfrom
mvanhorn:fix/261-surface-git-s-color-moved-highlights-for-moved-lin
Open

feat: surface git's color-moved highlights for moved lines#323
mvanhorn wants to merge 1 commit into
modem-dev:mainfrom
mvanhorn:fix/261-surface-git-s-color-moved-highlights-for-moved-lin

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

Summary

Honors git config color.moved and color.movedBlock so moved lines render with a distinct color rather than appearing as a paired add/remove. This matches the behavior of git diff --color-moved and makes large refactors much easier to read in hunk.

Why this matters

#261 specifically asked for this. Today, a function moved from one file to another shows up as a delete + add and the reviewer has to mentally pair the pieces. Surfacing the move highlights closes that gap.

Changes

  • src/core/git.ts + src/core/git.test.ts - read the new git config keys; parse moved-line metadata out of git diff --color-moved=zebra --no-color output and emit it as a moved lane in the parsed diff.
  • src/core/config.ts + src/core/config.test.ts + src/core/types.ts - thread the color_moved setting through.
  • src/core/loaders.ts + src/core/loaders.test.ts - apply the loader logic for the new lane.
  • src/ui/diff/pierre.ts + src/ui/diff/pierre.test.ts + src/ui/diff/renderRows.tsx + src/ui/diff/rowStyle.ts + src/ui/staticDiffPager.ts - render the moved lane with the configured palette.
  • src/ui/themes.ts - add default moved-from / moved-to colors that match git's zebra palette.

Testing

Targeted tests at each layer + the existing diff/UI test suites pass locally.

Fixes #261

modem-dev#261 asked for hunk to honor git's color.moved + color.movedBlock so
moved code in a diff is highlighted distinctly from added/removed.
Loads the relevant git config keys, threads a 'moved' lane through
the diff renderer, and renders moved-from / moved-to lines with the
configured palette.

Fixes modem-dev#261
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 16, 2026

Greptile Summary

This PR surfaces git's --color-moved output in hunk's diff viewer, so lines that were relocated (rather than truly added/removed) render with a distinct cyan/magenta palette instead of the standard green/red. The implementation threads a colorMoved option from config through the git command builder, parses ANSI escape sequences emitted by git before stripping them for normal diff parsing, and maps the resulting per-line move metadata into the existing row-rendering pipeline.

  • src/core/git.ts: New resolveGitColorMovedOptions reads diff.colorMoved / diff.colorMovedWS from git config, with a fallback to hunk's own colorMoved setting; the three diff command builders now forward deterministic -c color.diff.* overrides and --color=always --color-moved=<mode> when the feature is active.
  • src/core/loaders.ts: collectLineMoveKinds scans the raw ANSI-colored patch text for magenta (35) / cyan (36) SGR parameters before the normal stripTerminalControl pass, building per-file arrays of "moved" | undefined indexed in lock-step with @pierre/diffs's additionLines / deletionLines arrays.
  • src/ui/diff/ + src/ui/themes.ts: splitCellPalette / stackCellPalette now accept an optional moveKind parameter and switch to movedAddedBg / movedRemovedBg; four built-in themes gain defaults that mirror git's zebra palette.

Confidence Score: 4/5

Safe to merge; the core ANSI-parsing pipeline is logically sound and the two minor code-quality findings have no impact on correctness.

The index-alignment strategy between collectLineMoveKinds and @pierre/diffs is correct: context lines increment both counters, additions increment only additionLineIndex, and deletions only deletionLineIndex, matching the per-hunk cursor arithmetic in pierre.ts. The only real findings are a dead-code branch in normalizeGitColorMovedMode and a redundant string allocation in collectLineMoveKinds — both harmless.

src/core/loaders.ts and src/core/git.ts have the two minor nits; all UI and theme files are straightforward.

Important Files Changed

Filename Overview
src/core/git.ts Adds resolveGitColorMovedOptions (reads diff.colorMoved / diff.colorMovedWS), gitPatchColorArgs, and deterministic color config injection; minor: redundant "no" check in normalizeGitColorMovedMode.
src/core/loaders.ts Core ANSI parsing via collectLineMoveKinds and per-line index tracking look correct; minor: redundant replaceAll inside collectLineMoveKinds; function runs unconditionally even when color-moved is disabled.
src/core/types.ts Adds DiffLineMoveKind, DiffLineMoveKinds, and colorMoved to CommonOptions; clean, minimal additions.
src/core/config.ts Threads colorMoved through readConfigPreferences and mergeOptions; intentionally has no ?? default (opt-in feature), consistent with how resolveGitColorMovedOptions treats undefined as disabled.
src/ui/diff/pierre.ts Adds moveKind to SplitLineCell / StackLineCell and threads lineMoveKinds indices into makeSplitCell / makeStackCell; index alignment with @pierre/diffs arrays appears correct.
src/ui/diff/rowStyle.ts splitCellPalette and stackCellPalette accept optional moveKind and switch to movedAddedBg / movedRemovedBg; straightforward and correct.
src/ui/themes.ts Adds movedAddedBg and movedRemovedBg to AppTheme interface and all four built-in themes; colours are visually distinct from add/remove backgrounds.
src/ui/diff/renderRows.tsx Four palette call sites updated to pass cell.moveKind; change is mechanical and correct.
src/ui/staticDiffPager.ts Single palette call site updated to pass cell.moveKind; no issues.

Sequence Diagram

sequenceDiagram
    participant Config as config.ts
    participant Loaders as loaders.ts
    participant Git as git.ts
    participant Pierre as pierre.ts
    participant Render as renderRows.tsx / staticDiffPager.ts

    Config->>Loaders: "resolvedOptions.colorMoved (bool | undefined)"
    Loaders->>Git: resolveGitColorMovedOptions(input, cwd)
    Git-->>Loaders: "GitColorMovedOptions | null"
    Loaders->>Git: buildGitDiffArgs(input, excludes, colorMoved)
    Note over Git: Appends --color=always<br/>--color-moved=zebra<br/>-c color.diff.*=magenta/cyan
    Git-->>Loaders: string[] (git args with ANSI color flags)
    Loaders->>Loaders: collectLineMoveKinds(rawAnsiPatchText)
    Note over Loaders: Scans SGR params (35=magenta=moved deletion,<br/>36=cyan=moved addition) before strip
    Loaders->>Loaders: stripTerminalControl → parsePatchFiles
    Loaders-->>Pierre: "DiffFile { lineMoveKinds?: DiffLineMoveKinds }"
    Pierre->>Pierre: buildSplitRows / buildStackRows
    Note over Pierre: Passes lineMoveKinds[i] as moveKind<br/>into makeSplitCell / makeStackCell
    Pierre-->>Render: "DiffRow[] { cell.moveKind? }"
    Render->>Render: splitCellPalette / stackCellPalette(kind, theme, moveKind)
    Note over Render: moveKind → movedAddedBg / movedRemovedBg<br/>instead of addedBg / removedBg
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/core/git.ts:425-427
`"no"` is already a member of `GIT_BOOLEAN_FALSE_VALUES`, so the `|| normalized === "no"` guard is dead code. This is harmless today, but if `GIT_BOOLEAN_FALSE_VALUES` is ever trimmed, the intent could be lost. Removing it keeps the two constant sets as the single source of truth.

```suggestion
  if (GIT_BOOLEAN_FALSE_VALUES.has(normalized)) {
    return null;
  }
```

### Issue 2 of 2
src/core/loaders.ts:131
`collectLineMoveKinds` receives `rawPatchText`, which the caller already produced with `.replaceAll("
", "
")`. The second `.replaceAll` inside the loop is therefore a guaranteed no-op on every call. Dropping it removes a full string allocation for every diff parse, including the common path where color-moved is disabled.

```suggestion
  for (const rawLine of patchText.split("\n")) {
```

Reviews (1): Last reviewed commit: "feat: surface git's color-moved highligh..." | Re-trigger Greptile

Comment thread src/core/git.ts
Comment on lines +425 to +427
if (GIT_BOOLEAN_FALSE_VALUES.has(normalized) || normalized === "no") {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "no" is already a member of GIT_BOOLEAN_FALSE_VALUES, so the || normalized === "no" guard is dead code. This is harmless today, but if GIT_BOOLEAN_FALSE_VALUES is ever trimmed, the intent could be lost. Removing it keeps the two constant sets as the single source of truth.

Suggested change
if (GIT_BOOLEAN_FALSE_VALUES.has(normalized) || normalized === "no") {
return null;
}
if (GIT_BOOLEAN_FALSE_VALUES.has(normalized)) {
return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/git.ts
Line: 425-427

Comment:
`"no"` is already a member of `GIT_BOOLEAN_FALSE_VALUES`, so the `|| normalized === "no"` guard is dead code. This is harmless today, but if `GIT_BOOLEAN_FALSE_VALUES` is ever trimmed, the intent could be lost. Removing it keeps the two constant sets as the single source of truth.

```suggestion
  if (GIT_BOOLEAN_FALSE_VALUES.has(normalized)) {
    return null;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/core/loaders.ts
return moveKinds;
};

for (const rawLine of patchText.replaceAll("\r\n", "\n").split("\n")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 collectLineMoveKinds receives rawPatchText, which the caller already produced with .replaceAll(" ", " "). The second .replaceAll inside the loop is therefore a guaranteed no-op on every call. Dropping it removes a full string allocation for every diff parse, including the common path where color-moved is disabled.

Suggested change
for (const rawLine of patchText.replaceAll("\r\n", "\n").split("\n")) {
for (const rawLine of patchText.split("\n")) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/loaders.ts
Line: 131

Comment:
`collectLineMoveKinds` receives `rawPatchText`, which the caller already produced with `.replaceAll("
", "
")`. The second `.replaceAll` inside the loop is therefore a guaranteed no-op on every call. Dropping it removes a full string allocation for every diff parse, including the common path where color-moved is disabled.

```suggestion
  for (const rawLine of patchText.split("\n")) {
```

How can I resolve this? If you propose a fix, please make it concise.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support gits colorMoved option

1 participant