Skip to content

feat: add git gutter markers, tree syncing and diff preview#42

Open
Queaxtra wants to merge 9 commits into
cloudmanic:mainfrom
Queaxtra:feature/git-gutter
Open

feat: add git gutter markers, tree syncing and diff preview#42
Queaxtra wants to merge 9 commits into
cloudmanic:mainfrom
Queaxtra:feature/git-gutter

Conversation

@Queaxtra

Copy link
Copy Markdown

This PR introduces line-level git change tracking with colored gutter markers in the editor, enabling users to see modified, added, and deleted lines at a glance. It adds bidirectional sync between the active tab and the file tree sidebar, and automatically reveals and scrolls to files opened via the finder or CLI. Dirty file paths are normalized relative to the repo root, and folder-level change summaries are recalculated accordingly. It also adds a diff preview panel that opens when clicking a gutter marker, colorizing the output in the info modal. Additionally, tokenization and cursor highlighting are now limited to the visible viewport for better performance. Comprehensive tests are included for all new functionality.

Copilot AI review requested due to automatic review settings June 21, 2026 19:46
@Queaxtra

Copy link
Copy Markdown
Author

@cloudmanic

@Queaxtra

Copy link
Copy Markdown
Author
image image image

Copilot AI 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.

Pull request overview

This PR extends SpiceEdit’s UI/editor integration with Git-aware visuals and tighter sidebar/editor synchronization, adding per-line change markers (with clickable diff previews) and a new tree reveal/sync behavior to keep the file tree aligned with the active tab.

Changes:

  • Added Git-themed colors and filetree git-status kinds for richer “dirty” rendering (added/modified/deleted/renamed/mixed).
  • Implemented file tree active-file syncing plus Reveal() to expand ancestors and scroll opened files into view.
  • Limited syntax highlighting computation to the visible viewport and added gutter git markers + diff preview coloring in the info modal.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/theme/theme.go Adds theme colors for git states used by gutter/tree/modal diff rendering.
internal/filetree/filetree.go Adds git change kinds, active-file tracking, per-kind coloring, and Reveal() scrolling/expansion.
internal/filetree/filetree_test.go Updates/extends tree tests for active-file highlighting, root dirty color, and reveal behavior.
internal/editor/tab.go Adds gutter git markers and switches highlighting to viewport-limited rendering.
internal/editor/highlight.go Adds HighlightVisible and refactors highlighting internals.
internal/editor/highlight_test.go Adds coverage for viewport-limited highlighting behavior.
internal/app/modals.go Colorizes diff preview lines in the info modal.
internal/app/modals_test.go Tests diff preview line coloring and includes a small formatting tweak.
internal/app/gitstatus.go Expands git status parsing to typed kinds; adds diff parsing and hunk preview extraction.
internal/app/gitstatus_test.go Extends tests for status kinds, diff-line parsing, hunk preview selection, and path rebasing.
internal/app/finder_test.go Adds an integration-style test asserting finder-open reveals the file in the tree.
internal/app/app.go Wires tree active-file syncing, reveal-on-open, gutter click-to-preview, and git line marker refresh.
internal/app/app_test.go Adds tests for gutter-marker click behavior opening the info modal.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/app/gitstatus.go
Comment on lines +250 to +259
func loadGitLineChanges(rootDir, path string) map[int]editor.GitLineChange {
if rootDir == "" || path == "" {
return nil
}
out, err := exec.Command("git", "-C", rootDir, "diff", "--unified=0", "--", path).Output()
if err != nil || len(out) == 0 {
return nil
}
return parseGitDiffLines(out)
}
Comment thread internal/app/gitstatus.go
Comment on lines +262 to +271
func loadGitHunkPreview(rootDir, path string, line int) []string {
if rootDir == "" || path == "" || line < 0 {
return nil
}
out, err := exec.Command("git", "-C", rootDir, "diff", "--unified=3", "--", path).Output()
if err != nil || len(out) == 0 {
return nil
}
return parseGitHunkPreview(out, line)
}
Comment thread internal/app/gitstatus.go
Comment on lines +323 to +332
if newCount == 0 {
mark := newStart
if mark < 0 {
mark = 0
}
changes[mark] = editor.GitLineDeleted
_ = oldStart
_ = oldCount
continue
}
Comment thread internal/editor/highlight.go Outdated
Comment on lines +32 to +56
// HighlightVisible returns a style grid for the current viewport. Only visible
// rows are tokenised so keystroke cost follows terminal height, not file size.
func HighlightVisible(filename string, lines []string, startLine, height int, t theme.Theme) [][]tcell.Style {
styles := make([][]tcell.Style, len(lines))
if height <= 0 || startLine >= len(lines) {
return styles
}
if startLine < 0 {
startLine = 0
}
endLine := startLine + height
if endLine > len(lines) {
endLine = len(lines)
}
visible := strings.Join(lines[startLine:endLine], "\n")
visibleStyles := highlightSource(filename, visible, t)
for i, row := range visibleStyles {
lineIdx := startLine + i
if lineIdx >= endLine || lineIdx >= len(styles) {
break
}
styles[lineIdx] = row
}
return styles
}
Comment thread internal/editor/tab.go
Comment on lines 571 to 585
// Gutter / line number, right-aligned with one trailing space.
numStr := fmt.Sprintf("%*d", gutterWidth-1, lineIdx+1)
gutterStyle := tcell.StyleDefault.Background(lineBg).Foreground(th.Muted)
if isCursorLine {
gutterStyle = gutterStyle.Foreground(th.AccentSoft)
}
if marker, ok := t.GitLines[lineIdx]; ok && marker != GitLineNone {
scr.SetContent(x, cy, gitLineMarkerRune(marker), nil, gutterStyle.Foreground(gitLineMarkerColor(th, marker)))
}
for i, r := range numStr {
if i == 0 && t.GitLines[lineIdx] != GitLineNone {
continue
}
scr.SetContent(x+i, cy, r, nil, gutterStyle)
}

@cloudmanic cloudmanic left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

First off — I'm really sorry it took me so long to get to this review. Thank you for your patience, and thank you for such a thorough, well-tested PR. This is genuinely nice work: the git gutter bars, the sidebar syncing, and the diff popup all feel great, and I appreciate that every piece comes with tests.

I'd like to request a few changes before merging. Three things came up in review, explained plainly:

1. Syntax colors can go wrong when you scroll into the middle of a long comment or text block.
To keep things fast, the editor now only works out the coloring for the lines currently on screen instead of the whole file. The catch: if you scroll down into the middle of a big multi-line comment (or a long run of quoted text), the editor doesn't realize it's still "inside" that comment, because it can't see where the comment began further up. So it colors those lines as if they were normal code instead of as a comment. Nothing breaks and no data is lost — it's purely a visual glitch — but it's a step down from how coloring works today. This is the main one I'd like addressed. A common fix is to start the coloring pass a little bit above the top of the visible area so it can "see" that it's inside a comment.

2. The editor recalculates the colors more often than it needs to.
Previously it only recomputed the on-screen colors when something actually changed. Now it recomputes every time it redraws — even on a mouse move. Because it's only doing the visible lines it's still fast, and you won't notice it, but there's now a leftover "does this need recalculating?" flag that gets set but never actually checked. Minor cleanup — worth either using that flag again or removing it.

3. On very long files, the change-bar can cover up a digit of the line number.
The little colored change-bar sits at the far-left edge of the margin. For normal files there's blank space there, so it fits fine. But once a file passes 10,000 lines, the line numbers get wide enough to reach that edge, and the bar ends up sitting on top of the first digit — so a line numbered "10000" could show as "0000" with a bar in front. Rare and cosmetic only, but worth a small tweak so the marker never overlaps the digits.

None of these take away from how solid the overall PR is — #1 is the one I really care about, and #2 and #3 are small polish. Thanks again for the great contribution, and sorry again for the slow turnaround!

Thanks for the thorough review, and no worries at all about the
turnaround time. Genuinely appreciated, and glad the gutter bars,
sidebar syncing, and diff popup landed well.

All three requested points are addressed:

1. Multi-line comment / string colouring mid-scroll
   HighlightVisible now tokenises a bounded lead (256 rows) both
   above and below the viewport, then keeps only the visible rows in
   the output grid. The lead is bidirectional because chroma needs
   both delimiters of a multi-line block in range to colour the body;
   an upper-only lead still left the body uncoloured when the closer
   sat below the viewport. Keystroke cost stays tied to terminal
   height plus the fixed lead, not file size.

2. Highlight recompute cadence
   Render now re-tokenises only when the content changed (StyleStale,
   which was being set by every edit but never actually read) or the
   viewport shifted (scroll / height). Mouse moves and other no-op
   redraws reuse the cached grid. The grid is indexed by absolute line
   number and only carries the visible rows, so a scroll change still
   triggers a recompute.

3. Change-bar overlapping line numbers on 10000+ line files
   Replaced the fixed gutterWidth = 6 with gutterWidthFor(lineCount)
   = max(6, digits + 2), so the gutter grows by one cell per extra
   digit and the change-bar always sits in a blank leading cell
   instead of overwriting the first digit. EnsureVisible, Render, and
   HitTest all use the dynamic width.

Tests: added TestHighlightVisible_LeadRecoversCommentState,
TestTab_RenderSkipsHighlightWhenNotStale,
TestTab_RenderRecomputesHighlightOnScroll, TestGutterWidthFor, and
TestTab_Render_GitMarkerDoesNotOverlapLineNumber. Verified with
`go test -race ./...` (all packages pass), `go vet ./...`, and
`gofmt` clean.

Amp-Thread-ID: https://ampcode.com/threads/T-019f3c09-f41d-77f2-8a3d-198764673a66
@Queaxtra Queaxtra requested a review from cloudmanic July 7, 2026 10:23
@Queaxtra

Queaxtra commented Jul 7, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review, and no worries at all about the
turnaround time. Genuinely appreciated, and glad the gutter bars,
sidebar syncing, and diff popup landed well.

All three requested points are addressed:

  1. Multi-line comment / string colouring mid-scroll
    HighlightVisible now tokenises a bounded lead (256 rows) both
    above and below the viewport, then keeps only the visible rows in
    the output grid. The lead is bidirectional because chroma needs
    both delimiters of a multi-line block in range to colour the body;
    an upper-only lead still left the body uncoloured when the closer
    sat below the viewport. Keystroke cost stays tied to terminal
    height plus the fixed lead, not file size.

  2. Highlight recompute cadence
    Render now re-tokenises only when the content changed (StyleStale,
    which was being set by every edit but never actually read) or the
    viewport shifted (scroll / height). Mouse moves and other no-op
    redraws reuse the cached grid. The grid is indexed by absolute line
    number and only carries the visible rows, so a scroll change still
    triggers a recompute.

  3. Change-bar overlapping line numbers on 10000+ line files
    Replaced the fixed gutterWidth = 6 with gutterWidthFor(lineCount)
    = max(6, digits + 2), so the gutter grows by one cell per extra
    digit and the change-bar always sits in a blank leading cell
    instead of overwriting the first digit. EnsureVisible, Render, and
    HitTest all use the dynamic width.

Tests: added TestHighlightVisible_LeadRecoversCommentState,
TestTab_RenderSkipsHighlightWhenNotStale,
TestTab_RenderRecomputesHighlightOnScroll, TestGutterWidthFor, and
TestTab_Render_GitMarkerDoesNotOverlapLineNumber. Verified with
go test -race ./... (all packages pass), go vet ./..., and
gofmt clean.

@cloudmanic

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.

3 participants