diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 9be969991..0485f2e48 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -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 { diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 930602657..493e83f30 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -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