feat: add git gutter markers, tree syncing and diff preview#42
feat: add git gutter markers, tree syncing and diff preview#42Queaxtra wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
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.
| 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) | ||
| } |
| 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) | ||
| } |
| if newCount == 0 { | ||
| mark := newStart | ||
| if mark < 0 { | ||
| mark = 0 | ||
| } | ||
| changes[mark] = editor.GitLineDeleted | ||
| _ = oldStart | ||
| _ = oldCount | ||
| continue | ||
| } |
| // 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 | ||
| } |
| // 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
left a comment
There was a problem hiding this comment.
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
|
Thanks for the thorough review, and no worries at all about the All three requested points are addressed:
Tests: added TestHighlightVisible_LeadRecoversCommentState, |



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.