mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Add an on-demand inclusion gutter to gocui views
A reserved left-hand column that draws a per-line marker glyph and shifts the content right past it, for the custom-patch inclusion overlay (which needs to show which change lines are in the patch over arbitrary pager output, where it can't touch the rendered bytes). It's pure draw-time decoration: the content buffer, and so the diff-line metadata, click resolution and wrapping inputs, are untouched. The marker is drawn on a line's first wrapped segment only, and the gutter narrows the content wrap width while shown. Off by default; SetInclusionGutter turns it on per view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
01882cf2fd
commit
12523c8a20
|
|
@ -158,6 +158,17 @@ type View struct {
|
|||
|
||||
HighlightInset int
|
||||
|
||||
// InclusionGutterMarker is the glyph drawn in the on-demand inclusion gutter
|
||||
// (see SetInclusionGutter) on marked lines; InclusionGutterMarkerColor is its
|
||||
// color. Both are set once at view creation.
|
||||
InclusionGutterMarker string
|
||||
InclusionGutterMarkerColor Attribute
|
||||
// showInclusionGutter reserves the gutter column at the left of every line, and
|
||||
// inclusionGutterMarks (indexed by buffer line) selects which lines get the
|
||||
// marker. Set together via SetInclusionGutter.
|
||||
showInclusionGutter bool
|
||||
inclusionGutterMarks []bool
|
||||
|
||||
// If Frame is true, a border will be drawn around the view.
|
||||
Frame bool
|
||||
|
||||
|
|
@ -654,6 +665,31 @@ func (v *View) Name() string {
|
|||
return v.name
|
||||
}
|
||||
|
||||
// SetInclusionGutter configures the on-demand inclusion marker gutter: a fixed-width
|
||||
// column reserved at the left of every line, used by the custom-patch inclusion
|
||||
// overlay to show which change lines are in the patch. When show is true the gutter
|
||||
// is reserved and the content is shifted right to make room; marks, indexed by
|
||||
// buffer line, selects which lines get InclusionGutterMarker drawn (on every wrapped
|
||||
// segment of the line). It is pure draw-time decoration — the content buffer (and so the
|
||||
// diff-line metadata, click resolution, etc.) is untouched. Toggling show changes the
|
||||
// wrap width, so the view is re-wrapped.
|
||||
func (v *View) SetInclusionGutter(show bool, marks []bool) {
|
||||
if v.showInclusionGutter != show {
|
||||
v.showInclusionGutter = show
|
||||
v.tainted = true
|
||||
}
|
||||
v.inclusionGutterMarks = marks
|
||||
}
|
||||
|
||||
// inclusionGutterWidth is the number of columns the inclusion gutter occupies when
|
||||
// shown (the marker glyph plus a one-cell separator), or 0 when hidden.
|
||||
func (v *View) inclusionGutterWidth() int {
|
||||
if !v.showInclusionGutter {
|
||||
return 0
|
||||
}
|
||||
return uniseg.StringWidth(v.InclusionGutterMarker) + 1
|
||||
}
|
||||
|
||||
// setCharacter sets a character (grapheme cluster) at the given point relative to the view. It applies
|
||||
// the specified colors, taking into account if the cell must be highlighted. Also, it checks if the
|
||||
// position is valid.
|
||||
|
|
@ -1500,6 +1536,10 @@ func (v *View) draw() {
|
|||
|
||||
emptyCell := cell{chr: " ", width: 1, fgColor: ColorDefault, bgColor: ColorDefault}
|
||||
|
||||
// The inclusion gutter (when shown) reserves the leftmost columns; content is
|
||||
// drawn shifted right past it. See SetInclusionGutter.
|
||||
gutterWidth := v.inclusionGutterWidth()
|
||||
|
||||
for y, vline := range v.viewLines[start:] {
|
||||
if y >= maxY {
|
||||
break
|
||||
|
|
@ -1514,10 +1554,21 @@ func (v *View) draw() {
|
|||
trailingCell.bgColor = attrs.bg
|
||||
}
|
||||
|
||||
// Paint the inclusion gutter: blanks across its width, with the marker on
|
||||
// every wrapped segment of a marked buffer line.
|
||||
if gutterWidth > 0 {
|
||||
for gx := range gutterWidth {
|
||||
v.setCharacter(gx, y, " ", v.FgColor, v.BgColor)
|
||||
}
|
||||
if vline.linesY < len(v.inclusionGutterMarks) && v.inclusionGutterMarks[vline.linesY] {
|
||||
v.setCharacter(0, y, v.InclusionGutterMarker, v.InclusionGutterMarkerColor, v.BgColor)
|
||||
}
|
||||
}
|
||||
|
||||
// x tracks the current x position in the view, and cellIdx tracks the
|
||||
// index of the cell. If we print a double-sized rune, we increment cellIdx
|
||||
// by one but x by two.
|
||||
x := -v.ox
|
||||
x := gutterWidth - v.ox
|
||||
cellIdx := 0
|
||||
|
||||
var c cell
|
||||
|
|
@ -1566,7 +1617,7 @@ func (v *View) refreshViewLinesIfNeeded() {
|
|||
return
|
||||
}
|
||||
|
||||
maxX := v.InnerWidth()
|
||||
maxX := v.InnerWidth() - v.inclusionGutterWidth()
|
||||
wrap := 0
|
||||
if v.Wrap {
|
||||
wrap = maxX
|
||||
|
|
|
|||
|
|
@ -884,3 +884,76 @@ func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) {
|
|||
"trailing cell at (%d, 2) should have green bg", x)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInclusionGutter verifies the on-demand inclusion gutter reserves a
|
||||
// left-hand column, draws the marker glyph on marked lines only, and shifts the
|
||||
// content right past it.
|
||||
func TestInclusionGutter(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
// InnerWidth=10; the frame inset of 1 places view x=0 at screen x=1.
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal)
|
||||
v.Wrap = true
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
v.writeString("aaa\nbbb\nccc\n")
|
||||
|
||||
// The gutter is 2 columns wide (marker + separator); mark the middle line.
|
||||
v.SetInclusionGutter(true, []bool{false, true, false})
|
||||
v.draw()
|
||||
|
||||
// The marker appears at the gutter's first column (view x=0 → screen x=1) on
|
||||
// the marked line only.
|
||||
chr, _, _ := Screen.Get(1, 1)
|
||||
assert.Equal(t, " ", chr, "unmarked line has no gutter marker")
|
||||
chr, _, _ = Screen.Get(1, 2)
|
||||
assert.Equal(t, "✓", chr, "marked line shows the gutter marker")
|
||||
chr, _, _ = Screen.Get(1, 3)
|
||||
assert.Equal(t, " ", chr, "unmarked line has no gutter marker")
|
||||
|
||||
// The content is shifted right past the 2-column gutter (view x=2 → screen x=3).
|
||||
chr, _, _ = Screen.Get(3, 1)
|
||||
assert.Equal(t, "a", chr, "content is shifted past the gutter")
|
||||
chr, _, _ = Screen.Get(3, 2)
|
||||
assert.Equal(t, "b", chr)
|
||||
chr, _, _ = Screen.Get(3, 3)
|
||||
assert.Equal(t, "c", chr)
|
||||
|
||||
// Hiding the gutter again returns the content flush left (view x=0 → screen x=1).
|
||||
v.SetInclusionGutter(false, nil)
|
||||
v.draw()
|
||||
chr, _, _ = Screen.Get(1, 1)
|
||||
assert.Equal(t, "a", chr, "content is flush left with no gutter")
|
||||
}
|
||||
|
||||
// TestInclusionGutterMarkerOnEverySegment verifies that a marked buffer line that
|
||||
// wraps shows the marker on every wrapped segment, and that the gutter narrows the
|
||||
// content wrap width.
|
||||
func TestInclusionGutterMarkerOnEverySegment(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth=10
|
||||
v.Wrap = true
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
// 10 cells; with a 2-column gutter the content wrap width is 8, so this wraps
|
||||
// to "01234567" / "89".
|
||||
v.writeString("0123456789\n")
|
||||
v.SetInclusionGutter(true, []bool{true})
|
||||
v.draw()
|
||||
|
||||
// First segment: marker present, content starts at screen x=3 and the eighth
|
||||
// content cell ("7") sits at the right edge (screen x=10).
|
||||
chr, _, _ := Screen.Get(1, 1)
|
||||
assert.Equal(t, "✓", chr)
|
||||
chr, _, _ = Screen.Get(3, 1)
|
||||
assert.Equal(t, "0", chr)
|
||||
chr, _, _ = Screen.Get(10, 1)
|
||||
assert.Equal(t, "7", chr)
|
||||
|
||||
// Continuation segment: marker too, content resumes at screen x=3.
|
||||
chr, _, _ = Screen.Get(1, 2)
|
||||
assert.Equal(t, "✓", chr, "continuation segment also shows the marker")
|
||||
chr, _, _ = Screen.Get(3, 2)
|
||||
assert.Equal(t, "8", chr)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue