Skip to content

feat(editor): show git change gutter in the editor#921

Open
wang-mingxue wants to merge 2 commits into
crynta:mainfrom
wang-mingxue:feat/editor-git-gutter
Open

feat(editor): show git change gutter in the editor#921
wang-mingxue wants to merge 2 commits into
crynta:mainfrom
wang-mingxue:feat/editor-git-gutter

Conversation

@wang-mingxue

@wang-mingxue wang-mingxue commented Jul 2, 2026

Copy link
Copy Markdown

What

Add a CodeMirror gutter to the editable editor that marks added / modified / deleted lines relative to git.

Why

The editable editor gives no indication of which lines you've changed relative to git — you have to switch to the read-only diff view to see your own edits. A change gutter (VS Code style) closes that gap directly in the editor.

How

  • New src/modules/editor/lib/gitGutter.ts: a CodeMirror gutter extension + a small parser that turns a unified git diff into per-line change kinds (added / modified / deleted-boundary), stored in a StateField the gutter reads.
  • EditorPane.tsx: recompute the diff on open (via onCreateEditor) and after each save, then push the result into the state field. Resolves the file's repo, derives the repo-relative path, and calls the existing git_diff command.
  • No backend change — reuses the existing git_diff IPC command.

Deliberately scoped as an MVP: diff is worktree-vs-index, refreshed on open/save (not on every keystroke). Marked with a code comment; live per-keystroke diffing and hunk-revert can follow later if desired.

Testing

  • pnpm exec tsc --noEmit clean
  • pnpm test — added src/modules/editor/lib/gitGutter.test.ts, 5 cases locking the unified-diff → per-line mapping (insert / replace / delete-boundary / empty / added line starting with - not mistaken for a header). All pass.
  • Manual smoke-test: opened a modified file — green/blue/red markers appear immediately on open; edited lines and saved — markers refresh; opening a file in a non-repo dir shows no markers and doesn't error.
  • Platforms tested: macOS

Screenshots / GIFs

Left gutter shows a green bar (added), blue bar (modified), red underline (deletion boundary) next to changed lines.

Notes for reviewer

The parser is the load-bearing bit and is covered by the unit test. The gutter refreshes on open + save only (documented in-code); I kept it off the keystroke path on purpose to avoid perf impact on the editor hot path. Happy to tune colors/marker style.

Summary by CodeRabbit

  • New Features

    • Added a Git diff gutter in the editor that highlights added, modified, and deleted lines for the currently opened file, updating automatically as you save or run.
  • Bug Fixes

    • Improved gutter refresh reliability with safeguards to prevent stale updates when switching files or while the editor is initializing.
  • Tests

    • Added unit tests covering unified diff parsing edge cases.

The editable editor showed no indication of which lines changed relative to
git, so you had to switch to the read-only diff view to see your edits. Add a
CodeMirror gutter that marks added / modified / deleted lines from the file's
`git diff`, recomputed on open and after each save (reusing the existing
git_diff command; no backend change).

Adds a unit test locking the unified-diff -> per-line change mapping.
@wang-mingxue wang-mingxue requested a review from crynta as a code owner July 2, 2026 06:05
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a CodeMirror git diff gutter that parses unified diff text into added, modified, and deleted line markers, tests the parser behavior, and refreshes the gutter from EditorPane after ready, save, and run paths using Tauri native git helpers.

Changes

Git Diff Gutter Feature

Layer / File(s) Summary
Gutter data model and diff parser
src/modules/editor/lib/gitGutter.ts
Defines GitChanges, parses unified diff hunks into added/modified/deleted line sets, and stores gutter data through a CodeMirror state effect and field.
Gutter rendering and theming
src/modules/editor/lib/gitGutter.ts
Defines gutter markers for each change kind, selects markers from stored line sets, and exports the themed gitChangeGutter extension.
Parser test suite
src/modules/editor/lib/gitGutter.test.ts
Tests parseUnifiedDiff for empty input, insertion, modification, deletion, and embedded diff-content edge cases.
EditorPane gutter refresh wiring
src/modules/editor/EditorPane.tsx
Imports gutter helpers, adds refreshGitGutter with Tauri native git lookups and stale-view guards, and triggers refresh on ready, save, and Mod-s flows.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Related PRs: None specified.

Suggested labels: review_needed_junior_swe, review_depth_standard

Suggested reviewers: None specified.

Poem:
A rabbit peeks at lines turned red and green,
Gutter markers show where diffs have been.
Save, run, or open — the refresh takes flight,
Parsing hunks of old and new file light.
Hop along, reviewer, the diff trail's clean! 🐇

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and accurately summarizes the editor git gutter change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/modules/editor/lib/gitGutter.ts (2)

114-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Gutter colors are hardcoded, bypassing the theme engine.

With ~15 built-in theme presets (some very light, some very dark), fixed hex values may clash visually with certain themes. GitHub-style fixed diff colors are a common convention though, so this is a nice-to-have rather than a blocker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/editor/lib/gitGutter.ts` around lines 114 - 120, The diff gutter
styling in changeGutterTheme uses hardcoded hex colors, so it should be wired to
the editor theme system instead of fixed values. Update the EditorView.baseTheme
entries for the .cm-change-added, .cm-change-modified, and .cm-change-deleted
selectors to use theme-aware colors or variables consistent with the rest of the
editor styling, while keeping the existing changeGutterTheme structure intact.

24-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trailing pure-deletion-at-EOF branch (line 73) is untested.

res.deleted.add(Math.max(1, newLine - 1)) only fires when a hunk ends mid-deletion with no trailing context/addition line, but none of the four test cases exercise a diff ending on a deletion. Given this is git-diff parsing logic, worth locking the invariant per the project's testing standard for git-adjacent changes.

As per path instructions, "A change to a core subsystem (terminal/shell spawn, workspace auth, git, fs, IPC or AI tool surface) needs a test that locks the invariant."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/editor/lib/gitGutter.ts` around lines 24 - 75, Add a test for the
trailing pure-deletion-at-EOF path in parseUnifiedDiff, since the current
coverage does not exercise a diff that ends with only “-” lines and no trailing
context/addition. Create a case that hits the final pendingDel handling in
gitGutter.ts and assert the deleted line is recorded as expected, so the
invariant for the parseUnifiedDiff function stays locked down.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/editor/EditorPane.tsx`:
- Around line 196-201: The EditorPane refresh is triggered twice on file
open/reload because both onCreateEditor and the doc.status === "ready" useEffect
path call refreshGitGutter. Update EditorPane so only one of these entry points
fires the initial git gutter refresh, preferably by relying on the existing
doc.status effect and removing the onCreateEditor invocation, or by adding a
guard in onCreateEditor so it does not duplicate the ready-state refresh.
- Around line 170-193: The git gutter refresh is being triggered twice on mount
from both onCreateEditor and the doc.status === "ready" effect, causing
duplicate gitResolveRepo and gitDiff calls before the gutter renders. Update
EditorPane’s refreshGitGutter flow so only one of those mount-time paths invokes
it, and keep the other as a fallback for editor initialization timing; use the
existing refreshGitGutter callback and the onCreateEditor / ready-effect logic
to locate and deduplicate the trigger.

In `@src/modules/editor/lib/gitGutter.ts`:
- Around line 18-23: The doc comment in gitGutter.ts contains a stray
nonsensical word, so update the comment near the unified patch parsing
description to replace “ponytail” with the intended note marker (likely “note:”)
and keep the rest of the explanation intact. This is a documentation-only fix in
the top-of-file comment for the git diff/gutter logic, so edit the existing
comment text rather than changing any behavior in gitGutter or its parsing
functions.

---

Nitpick comments:
In `@src/modules/editor/lib/gitGutter.ts`:
- Around line 114-120: The diff gutter styling in changeGutterTheme uses
hardcoded hex colors, so it should be wired to the editor theme system instead
of fixed values. Update the EditorView.baseTheme entries for the
.cm-change-added, .cm-change-modified, and .cm-change-deleted selectors to use
theme-aware colors or variables consistent with the rest of the editor styling,
while keeping the existing changeGutterTheme structure intact.
- Around line 24-75: Add a test for the trailing pure-deletion-at-EOF path in
parseUnifiedDiff, since the current coverage does not exercise a diff that ends
with only “-” lines and no trailing context/addition. Create a case that hits
the final pendingDel handling in gitGutter.ts and assert the deleted line is
recorded as expected, so the invariant for the parseUnifiedDiff function stays
locked down.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c394ba94-77c2-4178-99f2-554574000282

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7f3e4 and 129d6e9.

📒 Files selected for processing (3)
  • src/modules/editor/EditorPane.tsx
  • src/modules/editor/lib/gitGutter.test.ts
  • src/modules/editor/lib/gitGutter.ts

Comment thread src/modules/editor/EditorPane.tsx Outdated
Comment thread src/modules/editor/EditorPane.tsx
Comment thread src/modules/editor/lib/gitGutter.ts
Address review feedback on the git change gutter:
- Drop the onCreateEditor trigger; a single doc.status effect now waits for the
  CodeMirror view via requestAnimationFrame, so open/reload fires one
  gitResolveRepo + gitDiff round trip instead of two.
- Normalize backslashes in the file path and repo root before deriving the
  repo-relative path, so it works on Windows.
- Tidy a stray comment marker.
@wang-mingxue

Copy link
Copy Markdown
Author

Thanks for the review — addressed in 35925ef:

  • Redundant refresh on open (Major): dropped the onCreateEditor trigger. A single doc.status === "ready" effect now waits for the CodeMirror view via requestAnimationFrame before refreshing, so open/reload does one gitResolveRepo + gitDiff round trip instead of two — while still handling the case where the view isn't mounted yet on first open.
  • Path normalization: the file path and repoRoot are now normalized (\/) before deriving the repo-relative path, so it holds on Windows.
  • Stray comment marker: removed.

tsc, pnpm test (gutter parser, 5 cases), and biome all clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/modules/editor/EditorPane.tsx (1)

226-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate save-and-refresh sequence in vim save handler and Mod-s keymap.

Both spots run the identical await saveRef.current(); onSavedRef.current?.(); refreshGitGutterRef.current(); sequence. Worth collapsing into one stable callback so a future addition (or bugfix) to the post-save flow doesn't need to be applied twice.

♻️ Proposed refactor
+    const saveAndRefresh = useCallback(() => {
+      void (async () => {
+        await saveRef.current();
+        onSavedRef.current?.();
+        refreshGitGutterRef.current();
+      })();
+    }, []);
+
     const extensions = useMemo(
       () => [
         ...
         vimHandlersExtension(() => ({
-          save: () => {
-            void (async () => {
-              await saveRef.current();
-              onSavedRef.current?.();
-              refreshGitGutterRef.current();
-            })();
-          },
+          save: saveAndRefresh,
           close: () => onCloseRef.current?.(),
         })),
         ...
         keymap.of([
           {
             key: "Mod-s",
             preventDefault: true,
             run: () => {
-              void (async () => {
-                await saveRef.current();
-                onSavedRef.current?.();
-                refreshGitGutterRef.current();
-              })();
+              saveAndRefresh();
               return true;
             },
           },
         ]),
       ],
       [],
     );

Also applies to: 278-291

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/editor/EditorPane.tsx` around lines 226 - 236, The vim save
handler and the Mod-s keymap both duplicate the same post-save flow, so
consolidate the shared save-and-refresh logic into a single stable callback and
reuse it from both places. Update the save path in EditorPane by extracting the
sequence used in vimHandlersExtension’s save callback and the Mod-s binding into
one function or ref-backed handler, then have both triggers call that shared
implementation so future changes to saveRef.current, onSavedRef.current, or
refreshGitGutterRef.current only need to be made once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/modules/editor/EditorPane.tsx`:
- Around line 226-236: The vim save handler and the Mod-s keymap both duplicate
the same post-save flow, so consolidate the shared save-and-refresh logic into a
single stable callback and reuse it from both places. Update the save path in
EditorPane by extracting the sequence used in vimHandlersExtension’s save
callback and the Mod-s binding into one function or ref-backed handler, then
have both triggers call that shared implementation so future changes to
saveRef.current, onSavedRef.current, or refreshGitGutterRef.current only need to
be made once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56071d78-e252-42ac-99bb-c5a1c55cd445

📥 Commits

Reviewing files that changed from the base of the PR and between 129d6e9 and 35925ef.

📒 Files selected for processing (2)
  • src/modules/editor/EditorPane.tsx
  • src/modules/editor/lib/gitGutter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/editor/lib/gitGutter.ts

@wang-mingxue

Copy link
Copy Markdown
Author

Heads-up per CONTRIBUTING: this feature wasn't pre-discussed. It's scoped small (frontend-only, reuses the existing git_diff command, no backend change) and ships an MVP change gutter refreshed on open/save. If you'd prefer to align on direction first — or on whether a change gutter fits the roadmap at all — I'm glad to take it to an issue or Discord, or close it. Whatever works for you.

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.

1 participant