Skip to content

Commit e5ab598

Browse files
committed
fix: cursor scroll on line wrap, Unicode search backspace, and increase test coverage
Fix editor Left/Right arrow keys not calling scrollToCursor() when wrapping across lines, causing the cursor to go off-screen. Fix viewer search backspace corrupting multi-byte Unicode characters by using rune-based truncation. Remove dead code in NewBuffer. Add 41 new tests covering edge cases, boundary conditions, and previously untested code paths. Coverage: 74.2% -> 91.3%.
1 parent 63d1f7e commit e5ab598

File tree

9 files changed

+1001
-16
lines changed

9 files changed

+1001
-16
lines changed

internal/editor/buffer.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ type Buffer struct {
1212

1313
func NewBuffer(content string) *Buffer {
1414
lines := strings.Split(content, "\n")
15-
if len(lines) == 0 {
16-
lines = []string{""}
17-
}
1815
return &Buffer{lines: lines}
1916
}
2017

internal/editor/buffer_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,118 @@ func TestBuffer_Modified(t *testing.T) {
107107
t.Fatal("buffer should be modified after insert")
108108
}
109109
}
110+
111+
func TestBuffer_GetLine_OutOfBounds(t *testing.T) {
112+
b := NewBuffer("hello")
113+
if got := b.GetLine(-1); got != "" {
114+
t.Fatalf("expected empty string for negative row, got %q", got)
115+
}
116+
if got := b.GetLine(5); got != "" {
117+
t.Fatalf("expected empty string for row beyond end, got %q", got)
118+
}
119+
}
120+
121+
func TestBuffer_LineLen_OutOfBounds(t *testing.T) {
122+
b := NewBuffer("hello")
123+
if got := b.LineLen(-1); got != 0 {
124+
t.Fatalf("expected 0 for negative row, got %d", got)
125+
}
126+
if got := b.LineLen(5); got != 0 {
127+
t.Fatalf("expected 0 for row beyond end, got %d", got)
128+
}
129+
}
130+
131+
func TestBuffer_InsertChar_OutOfBounds(t *testing.T) {
132+
b := NewBuffer("hello")
133+
// Invalid row — should be no-op
134+
b.InsertChar(-1, 0, 'x')
135+
b.InsertChar(5, 0, 'x')
136+
if got := b.GetLine(0); got != "hello" {
137+
t.Fatalf("expected unchanged line, got %q", got)
138+
}
139+
// Col beyond line length — should clamp
140+
b.InsertChar(0, 100, '!')
141+
if got := b.GetLine(0); got != "hello!" {
142+
t.Fatalf("expected 'hello!' with clamped col, got %q", got)
143+
}
144+
}
145+
146+
func TestBuffer_DeleteChar_OutOfBounds(t *testing.T) {
147+
b := NewBuffer("hello")
148+
// Invalid row
149+
b.DeleteChar(-1, 0)
150+
b.DeleteChar(5, 0)
151+
// Invalid col
152+
b.DeleteChar(0, -1)
153+
b.DeleteChar(0, 10)
154+
if got := b.GetLine(0); got != "hello" {
155+
t.Fatalf("expected unchanged line, got %q", got)
156+
}
157+
}
158+
159+
func TestBuffer_Backspace_AtStart(t *testing.T) {
160+
b := NewBuffer("hello")
161+
b.Backspace(0, 0) // at start of line — should be no-op
162+
if got := b.GetLine(0); got != "hello" {
163+
t.Fatalf("expected unchanged line, got %q", got)
164+
}
165+
}
166+
167+
func TestBuffer_InsertNewline_OutOfBounds(t *testing.T) {
168+
b := NewBuffer("hello")
169+
b.InsertNewline(-1, 0)
170+
b.InsertNewline(5, 0)
171+
if b.LineCount() != 1 {
172+
t.Fatalf("expected 1 line after out-of-bounds newline inserts, got %d", b.LineCount())
173+
}
174+
}
175+
176+
func TestBuffer_InsertNewline_ColClamp(t *testing.T) {
177+
b := NewBuffer("hello")
178+
b.InsertNewline(0, 100) // col beyond length, should clamp
179+
if b.LineCount() != 2 {
180+
t.Fatalf("expected 2 lines, got %d", b.LineCount())
181+
}
182+
if got := b.GetLine(0); got != "hello" {
183+
t.Fatalf("expected 'hello', got %q", got)
184+
}
185+
if got := b.GetLine(1); got != "" {
186+
t.Fatalf("expected empty second line, got %q", got)
187+
}
188+
}
189+
190+
func TestBuffer_JoinLines_OutOfBounds(t *testing.T) {
191+
b := NewBuffer("hello\nworld")
192+
// row 0 — can't join with row above
193+
col := b.JoinLines(0)
194+
if col != 0 {
195+
t.Fatalf("expected 0 for out-of-bounds join, got %d", col)
196+
}
197+
// row beyond end
198+
col = b.JoinLines(5)
199+
if col != 0 {
200+
t.Fatalf("expected 0 for out-of-bounds join, got %d", col)
201+
}
202+
if b.LineCount() != 2 {
203+
t.Fatalf("expected unchanged line count 2, got %d", b.LineCount())
204+
}
205+
}
206+
207+
func TestBuffer_LineLen_Unicode(t *testing.T) {
208+
b := NewBuffer("héllo 世界")
209+
if got := b.LineLen(0); got != 8 {
210+
t.Fatalf("expected 8 runes, got %d", got)
211+
}
212+
}
213+
214+
func TestBuffer_ResetModified(t *testing.T) {
215+
b := NewBuffer("hello")
216+
b.InsertChar(0, 0, 'x')
217+
if !b.Modified() {
218+
t.Fatal("expected modified after insert")
219+
}
220+
b.ResetModified()
221+
if b.Modified() {
222+
t.Fatal("expected not modified after reset")
223+
}
224+
}

internal/editor/editor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
125125
} else if m.cursorRow > 0 {
126126
m.cursorRow--
127127
m.cursorCol = m.buffer.LineLen(m.cursorRow)
128+
m.scrollToCursor()
128129
}
129130
return m, nil
130131

@@ -135,6 +136,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
135136
} else if m.cursorRow < m.buffer.LineCount()-1 {
136137
m.cursorRow++
137138
m.cursorCol = 0
139+
m.scrollToCursor()
138140
}
139141
return m, nil
140142

0 commit comments

Comments
 (0)