mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Let callers map between view lines and buffer lines
Everything about a diff's content — which file and line a row belongs to, whether it's a change — is a property of the unwrapped buffer line, while the cursor, clicks and the range selection all speak in view lines, which count wrapped segments. Reading the content under the cursor therefore needs the mapping in both directions, and doing it outside gocui isn't possible: the wrapping is internal, and the caller couldn't take the view's lock across the lookup and the read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
27edfa2008
commit
d65f30caf8
+50
-1
@@ -362,9 +362,18 @@ func (v *View) segmentSpanOf(pos contentPos) (int, int, bool) {
|
||||
if !pos.ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
return v.viewLineSpanOfBufferLine(pos.line)
|
||||
}
|
||||
|
||||
// viewLineSpanOfBufferLine returns the first and last view line drawing the given
|
||||
// buffer line, i.e. the first and last segment it is wrapped into. Both are the
|
||||
// same view line when the line doesn't wrap. ok is false when the line isn't drawn
|
||||
// at all. Only call this with a lock on writeMutex, and with the view lines up to
|
||||
// date.
|
||||
func (v *View) viewLineSpanOfBufferLine(bufferLine int) (int, int, bool) {
|
||||
first, last := -1, -1
|
||||
for i, vline := range v.viewLines {
|
||||
if vline.linesY == pos.line {
|
||||
if vline.linesY == bufferLine {
|
||||
if first == -1 {
|
||||
first = i
|
||||
}
|
||||
@@ -1851,6 +1860,46 @@ func (v *View) BufferLines() []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
// BufferLineForViewLine maps a view line index (which counts wrapped lines) to
|
||||
// the index of the corresponding line in the unwrapped internal buffer (as
|
||||
// returned by BufferLines). Several view lines map to the same buffer line when
|
||||
// that line wraps. Returns false if the view line is out of range.
|
||||
func (v *View) BufferLineForViewLine(y int) (int, bool) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
return v.bufferLineForViewLine(y)
|
||||
}
|
||||
|
||||
// ViewLineForBufferLine maps an unwrapped buffer line index to the index of the
|
||||
// first view line that renders it — the inverse of BufferLineForViewLine, for
|
||||
// turning a line found by examining the buffer into a line to scroll to or
|
||||
// select. Returns false if the buffer line isn't rendered into any view line.
|
||||
func (v *View) ViewLineForBufferLine(bufferLineIdx int) (int, bool) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.refreshViewLinesIfNeeded()
|
||||
|
||||
first, _, ok := v.viewLineSpanOfBufferLine(bufferLineIdx)
|
||||
return first, ok
|
||||
}
|
||||
|
||||
// LastViewLineForBufferLine maps an unwrapped buffer line index to the index of
|
||||
// the last view line that renders it, which for a line that doesn't wrap is the
|
||||
// same as the first. It is where the far end of a range goes: a range is over
|
||||
// buffer lines, so it has to cover the last one of them to its final segment
|
||||
// rather than stopping where that line begins.
|
||||
func (v *View) LastViewLineForBufferLine(bufferLineIdx int) (int, bool) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.refreshViewLinesIfNeeded()
|
||||
|
||||
_, last, ok := v.viewLineSpanOfBufferLine(bufferLineIdx)
|
||||
return last, ok
|
||||
}
|
||||
|
||||
// Buffer returns a string with the contents of the view's internal
|
||||
// buffer.
|
||||
func (v *View) Buffer() string {
|
||||
|
||||
@@ -204,6 +204,61 @@ func TestViewLinesTruncatedByShorterRender(t *testing.T) {
|
||||
assert.Equal(t, []string{"aaa", "bbb", "ccc"}, v.ViewBufferLines())
|
||||
}
|
||||
|
||||
func TestBufferLineForViewLine(t *testing.T) {
|
||||
v := NewView("name", 0, 0, 10, 10, OutputNormal) // InnerWidth is 9
|
||||
v.Wrap = true
|
||||
|
||||
// Buffer line 0 is short (view line 0); buffer line 1 wraps into three view
|
||||
// lines (1, 2, 3); buffer line 2 is short again (view line 4).
|
||||
v.writeString("short\n" + strings.Repeat("b", 27) + "\nlast")
|
||||
|
||||
for viewLine, wantBufferLine := range []int{0, 1, 1, 1, 2} {
|
||||
bufferLine, ok := v.BufferLineForViewLine(viewLine)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, wantBufferLine, bufferLine)
|
||||
}
|
||||
|
||||
_, ok := v.BufferLineForViewLine(5)
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = v.BufferLineForViewLine(-1)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestViewLineForBufferLine(t *testing.T) {
|
||||
v := NewView("name", 0, 0, 10, 10, OutputNormal) // InnerWidth is 9
|
||||
v.Wrap = true
|
||||
|
||||
// A wrapped buffer line maps to the first of the view lines it spans.
|
||||
v.writeString("short\n" + strings.Repeat("b", 27) + "\nlast")
|
||||
|
||||
for bufferLine, wantViewLine := range []int{0, 1, 4} {
|
||||
viewLine, ok := v.ViewLineForBufferLine(bufferLine)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, wantViewLine, viewLine)
|
||||
}
|
||||
|
||||
_, ok := v.ViewLineForBufferLine(3)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestLastViewLineForBufferLine(t *testing.T) {
|
||||
v := NewView("name", 0, 0, 10, 10, OutputNormal) // InnerWidth is 9
|
||||
v.Wrap = true
|
||||
|
||||
// A wrapped buffer line maps to the last of the view lines it spans.
|
||||
v.writeString("short\n" + strings.Repeat("b", 27) + "\nlast")
|
||||
|
||||
for bufferLine, wantViewLine := range []int{0, 3, 4} {
|
||||
viewLine, ok := v.LastViewLineForBufferLine(bufferLine)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, wantViewLine, viewLine)
|
||||
}
|
||||
|
||||
_, ok := v.LastViewLineForBufferLine(3)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
// While an async re-render loads, it swaps in only a partially-filled buffer at
|
||||
// its first paint and keeps appending lines afterwards. The scrollbar must keep
|
||||
// using the pre-load height until the load ends, so the thumb doesn't shrink and
|
||||
|
||||
Reference in New Issue
Block a user