Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion internal/display/bufwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,19 @@ func (w *BufWindow) displayBuffer() {
bline := b.LineBytes(bloc.Y)
blineLen := util.CharacterCount(bline)

// An exact-width softwrapped line isn't wrapped to an extra row, so a
// cursor at its end has no row to be drawn on. If a cursor is there,
// keep the wrapped row for this frame so it stays visible.
cursorAtEOL := false
if w.active && softwrap {
for _, c := range cursors {
if !c.HasSelection() && c.Y == bloc.Y && c.X == blineLen {
cursorAtEOL = true
break
}
}
}

leadingwsEnd := len(util.GetLeadingWhitespace(bline))
trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline))

Expand Down Expand Up @@ -791,7 +804,7 @@ func (w *BufWindow) displayBuffer() {
wordwidth = 0

// If we reach the end of the window then we either stop or we wrap for softwrap
if vloc.X >= maxWidth {
if vloc.X >= maxWidth && (len(line) > 0 || cursorAtEOL) {
if !softwrap {
break
} else {
Expand Down
107 changes: 107 additions & 0 deletions internal/display/bufwindow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package display

import (
"strings"
"testing"

"github.com/micro-editor/micro/v2/internal/buffer"
"github.com/micro-editor/micro/v2/internal/config"
ulua "github.com/micro-editor/micro/v2/internal/lua"
"github.com/micro-editor/micro/v2/internal/screen"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert"
lua "github.com/yuin/gopher-lua"
)

func newSoftwrapTestWindow(t *testing.T, text string, width, height int) *BufWindow {
t.Helper()

// buffer.NewBuffer needs a Lua state to expose the buffer to plugins.
if ulua.L == nil {
ulua.L = lua.NewState()
}

if err := config.InitGlobalSettings(); err != nil {
t.Fatalf("InitGlobalSettings: %v", err)
}
// Use the real terminal cursor (not a drawn "fake" cell) so tests can
// read the cursor position via SimulationScreen.GetCursor(). fakecursor
// defaults to true on non-Windows-Terminal Windows consoles.
config.GlobalSettings["fakecursor"] = false
if _, err := screen.InitSimScreen(); err != nil {
t.Fatalf("InitSimScreen: %v", err)
}

buf := buffer.NewBufferFromString(text, "", buffer.BTDefault)
buf.Settings["softwrap"] = true
// Turn off the ruler so gutterOffset is 0 and the window's content
// width is exactly the width passed in.
buf.Settings["ruler"] = false

w := NewBufWindow(0, 0, width, height, buf)
w.Resize(width, height)
return w
}

// A line whose rendered width exactly fills the window must not push the
// next line down by an extra, blank wrapped row.
func TestDisplayBufferNoExtraRowAtExactWidth(t *testing.T) {
tests := []struct {
name string
width int
expectedRows int
}{
{"one narrower than width: no wrap", 9, 1},
{"exactly window width: must not wrap to an extra row", 10, 1},
{"one wider than width: wraps with 1 overflow char", 11, 2},
{"exactly two window widths: wraps once, no extra row", 20, 2},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
w := newSoftwrapTestWindow(t, strings.Repeat("x", tc.width)+"\nSECOND", 10, 5)
w.displayBuffer()

r, _, _, _ := screen.Screen.GetContent(0, tc.expectedRows)
assert.Equal(t, 'S', r, "second buffer line should render right after the first line's %d row(s), with no extra blank rows between them", tc.expectedRows)
})
}
}

// When the cursor is at the end of a softwrapped line whose rendered width
// exactly fills the window, it must still be drawn somewhere visible on
// screen rather than off the right edge (or off screen entirely).
func TestDisplayBufferCursorVisibleAtExactWidthEOL(t *testing.T) {
tests := []struct {
name string
width int
}{
{"one narrower than width", 9},
{"exactly window width", 10},
{"one wider than width", 11},
{"exactly two window widths", 20},
}

const winWidth = 10

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
w := newSoftwrapTestWindow(t, strings.Repeat("x", tc.width)+"\nSECOND", winWidth, 5)
w.active = true

c := w.Buf.GetActiveCursor()
c.GotoLoc(buffer.Loc{X: tc.width, Y: 0})

w.displayBuffer()

sim := screen.Screen.(tcell.SimulationScreen)
cx, cy, vis := sim.GetCursor()

assert.True(t, vis, "cursor should be visible")
assert.GreaterOrEqual(t, cx, 0)
assert.Less(t, cx, winWidth, "cursor x must be within the window, not off the right edge")
assert.GreaterOrEqual(t, cy, 0)
assert.Less(t, cy, 5, "cursor y must be within the window")
})
}
}
8 changes: 6 additions & 2 deletions internal/display/softwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (w *BufWindow) getVLocFromLoc(loc buffer.Loc) VLoc {
wordwidth = 0
wordoffset = 0

if vloc.VisualX >= w.bufWidth {
if vloc.VisualX >= w.bufWidth && len(line) > 0 {
vloc.Row++
vloc.VisualX = 0
}
Expand Down Expand Up @@ -215,7 +215,11 @@ func (w *BufWindow) getLocFromVLoc(svloc VLoc) buffer.Loc {
widths = widths[:0]
wordwidth = 0

if vloc.VisualX >= w.bufWidth {
// len(line) > 0 currently has no effect here since loc, not vloc, is
// returned below and the loop exits right after on its own condition.
// Kept in sync with the equivalent guard in getVLocFromLoc in case
// this function is ever changed to read vloc past the loop.
if vloc.VisualX >= w.bufWidth && len(line) > 0 {
vloc.Row++
vloc.VisualX = 0
}
Expand Down
38 changes: 38 additions & 0 deletions internal/display/softwrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package display

import (
"strings"
"testing"

"github.com/micro-editor/micro/v2/internal/buffer"
"github.com/stretchr/testify/assert"
)

// A line whose rendered width is exactly the window's content width must
// not be counted as needing an extra, empty row.
func TestGetRowCountExactWidth(t *testing.T) {
tests := []struct {
name string
width int
expectedRows int
expectedLastRowX int
}{
{"one narrower than width: no wrap", 79, 1, 79},
{"exactly window width: must not wrap to an extra row", 80, 1, 80},
{"one wider than width: wraps with 1 overflow char", 81, 2, 1},
{"two wider than width: wraps with 2 overflow chars", 82, 2, 2},
{"exactly two window widths: wraps once, no extra row", 160, 2, 80},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
w := newSoftwrapTestWindow(t, strings.Repeat("x", tc.width), 80, 24)

assert.Equal(t, tc.expectedRows, w.getRowCount(0))

vloc := w.getVLocFromLoc(buffer.Loc{X: tc.width, Y: 0})
assert.Equal(t, tc.expectedRows-1, vloc.Row)
assert.Equal(t, tc.expectedLastRowX, vloc.VisualX)
})
}
}