okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Bradley Ericksonfix(render): flush trailing empty cells in renderLine (#128)d9e819d

main
19.5 KiB763 linesraw
1package uv
2
3import (
4	"strings"
5	"testing"
6
7	"github.com/charmbracelet/x/ansi"
8	"github.com/rivo/uniseg"
9)
10
11func TestBufferUniseg(t *testing.T) {
12	cases := []struct {
13		name     string
14		input    string
15		expected string
16	}{
17		{
18			name:     "empty buffer",
19			input:    "",
20			expected: "",
21		},
22		{
23			name:     "single line",
24			input:    "Hello, World!",
25			expected: "Hello, World!",
26		},
27		{
28			name:     "multiple lines",
29			input:    "Hello, World!\nThis is a test.\nGoodbye!",
30			expected: "Hello, World!\nThis is a test.\nGoodbye!",
31		},
32	}
33
34	for _, c := range cases {
35		t.Run(c.name, func(t *testing.T) {
36			buf := NewBuffer(width(c.input), height(c.input))
37			for y, line := range strings.Split(c.input, "\n") {
38				var x int
39				seg := uniseg.NewGraphemes(line)
40				for seg.Next() {
41					cell := &Cell{
42						Content: seg.Str(),
43						Width:   seg.Width(),
44					}
45					buf.SetCell(x, y, cell)
46					x += cell.Width
47				}
48			}
49
50			if got := TrimSpace(buf.String()); got != c.expected {
51				t.Errorf("expected %q, got %q", c.expected, got)
52			}
53		})
54	}
55}
56
57// TestLineSet tests the Set method of Line
58func TestLineSet(t *testing.T) {
59	tests := []struct {
60		name     string
61		lineLen  int
62		x        int
63		cell     *Cell
64		validate func(t *testing.T, l Line)
65	}{
66		{
67			name:    "set simple cell",
68			lineLen: 10,
69			x:       5,
70			cell:    &Cell{Content: "a", Width: 1},
71			validate: func(t *testing.T, l Line) {
72				if l[5].Content != "a" {
73					t.Errorf("expected 'a' at position 5, got %q", l[5].Content)
74				}
75			},
76		},
77		{
78			name:    "set out of bounds negative",
79			lineLen: 10,
80			x:       -1,
81			cell:    &Cell{Content: "a", Width: 1},
82			validate: func(t *testing.T, l Line) {
83				// Should not panic, just return
84			},
85		},
86		{
87			name:    "set out of bounds positive",
88			lineLen: 10,
89			x:       10,
90			cell:    &Cell{Content: "a", Width: 1},
91			validate: func(t *testing.T, l Line) {
92				// Should not panic, just return
93			},
94		},
95		{
96			name:    "overwrite wide cell",
97			lineLen: 10,
98			x:       2,
99			cell:    &Cell{Content: "你", Width: 2},
100			validate: func(t *testing.T, l Line) {
101				// First set a wide cell
102				l.Set(2, &Cell{Content: "你", Width: 2})
103				// Then overwrite it
104				l.Set(2, &Cell{Content: "a", Width: 1})
105				if l[2].Content != "a" {
106					t.Errorf("expected 'a' at position 2, got %q", l[2].Content)
107				}
108			},
109		},
110		{
111			name:    "overwrite middle of wide cell",
112			lineLen: 10,
113			x:       3,
114			cell:    &Cell{Content: "a", Width: 1},
115			validate: func(t *testing.T, l Line) {
116				// First set a wide cell at position 2
117				l.Set(2, &Cell{Content: "你", Width: 2})
118				// Then overwrite the middle
119				l.Set(3, &Cell{Content: "a", Width: 1})
120				if l[3].Content != "a" {
121					t.Errorf("expected 'a' at position 3, got %q", l[3].Content)
122				}
123				// The wide cell should be cleared
124				if l[2].Content != " " {
125					t.Errorf("expected empty at position 2, got %q", l[2].Content)
126				}
127			},
128		},
129		{
130			name:    "set wide cell at end",
131			lineLen: 10,
132			x:       9,
133			cell:    &Cell{Content: "你", Width: 2},
134			validate: func(t *testing.T, l Line) {
135				l.Set(9, &Cell{Content: "你", Width: 2})
136				if l[9].Content != " " {
137					// We replace the wide cell with empty spaces because it
138					// doesn't fit in the line.
139					t.Errorf("expected ' ' at position 9, got %q", l[9].Content)
140				}
141			},
142		},
143	}
144
145	for _, tt := range tests {
146		t.Run(tt.name, func(t *testing.T) {
147			l := make(Line, tt.lineLen)
148			l.Set(tt.x, tt.cell)
149			if tt.validate != nil {
150				tt.validate(t, l)
151			}
152		})
153	}
154}
155
156// TestLineString tests the String method of Line
157func TestLineString(t *testing.T) {
158	tests := []struct {
159		name     string
160		setup    func() Line
161		expected string
162	}{
163		{
164			name: "empty line",
165			setup: func() Line {
166				return NewLine(5)
167			},
168			expected: "",
169		},
170		{
171			name: "simple text",
172			setup: func() Line {
173				l := make(Line, 5)
174				l[0] = Cell{Content: "H", Width: 1}
175				l[1] = Cell{Content: "e", Width: 1}
176				l[2] = Cell{Content: "l", Width: 1}
177				l[3] = Cell{Content: "l", Width: 1}
178				l[4] = Cell{Content: "o", Width: 1}
179				return l
180			},
181			expected: "Hello",
182		},
183		{
184			name: "with wide characters",
185			setup: func() Line {
186				l := make(Line, 6)
187				l[0] = Cell{Content: "你", Width: 2}
188				l[1] = Cell{} // continuation
189				l[2] = Cell{Content: "好", Width: 2}
190				l[3] = Cell{} // continuation
191				l[4] = Cell{Content: "!", Width: 1}
192				l[5] = Cell{Content: " ", Width: 1}
193				return l
194			},
195			expected: "你好!",
196		},
197		{
198			name: "trailing spaces trimmed",
199			setup: func() Line {
200				l := make(Line, 10)
201				l[0] = Cell{Content: "H", Width: 1}
202				l[1] = Cell{Content: "i", Width: 1}
203				// Rest are empty
204				return l
205			},
206			expected: "Hi",
207		},
208	}
209
210	for _, tt := range tests {
211		t.Run(tt.name, func(t *testing.T) {
212			l := tt.setup()
213			got := l.String()
214			if got != tt.expected {
215				t.Errorf("String() = %q, want %q", got, tt.expected)
216			}
217		})
218	}
219}
220
221// TestBufferMethods tests various Buffer methods
222func TestBufferMethods(t *testing.T) {
223	t.Run("Width", func(t *testing.T) {
224		// Test empty buffer
225		b := &Buffer{}
226		if w := b.Width(); w != 0 {
227			t.Errorf("Width() = %d, want 0", w)
228		}
229
230		// Test buffer with lines
231		b = NewBuffer(10, 5)
232		if w := b.Width(); w != 10 {
233			t.Errorf("Width() = %d, want 10", w)
234		}
235	})
236
237	t.Run("CellAt", func(t *testing.T) {
238		b := NewBuffer(10, 5)
239		b.SetCell(2, 1, &Cell{Content: "X", Width: 1})
240
241		// Test valid position
242		if c := b.CellAt(2, 1); c == nil || c.Content != "X" {
243			t.Errorf("CellAt(2, 1) = %v, want X", c)
244		}
245
246		// Test out of bounds
247		if c := b.CellAt(-1, 0); c != nil {
248			t.Errorf("CellAt(-1, 0) = %v, want nil", c)
249		}
250		if c := b.CellAt(0, -1); c != nil {
251			t.Errorf("CellAt(0, -1) = %v, want nil", c)
252		}
253		if c := b.CellAt(10, 0); c != nil {
254			t.Errorf("CellAt(10, 0) = %v, want nil", c)
255		}
256		if c := b.CellAt(0, 5); c != nil {
257			t.Errorf("CellAt(0, 5) = %v, want nil", c)
258		}
259	})
260
261	t.Run("SetCell", func(t *testing.T) {
262		b := NewBuffer(10, 5)
263
264		// Test setting cell
265		b.SetCell(2, 1, &Cell{Content: "A", Width: 1})
266		if c := b.CellAt(2, 1); c == nil || c.Content != "A" {
267			t.Errorf("SetCell failed, got %v", c)
268		}
269
270		// Test out of bounds
271		b.SetCell(-1, 0, &Cell{Content: "B", Width: 1})
272		b.SetCell(0, -1, &Cell{Content: "C", Width: 1})
273		b.SetCell(10, 0, &Cell{Content: "D", Width: 1})
274		b.SetCell(0, 5, &Cell{Content: "E", Width: 1})
275		// Should not panic
276
277		// Test nil cell
278		b.SetCell(3, 1, nil)
279		// Should not panic
280	})
281
282	t.Run("Resize", func(t *testing.T) {
283		b := NewBuffer(10, 5)
284		b.SetCell(2, 1, &Cell{Content: "X", Width: 1})
285
286		// Resize smaller
287		b.Resize(5, 3)
288		if b.Width() != 5 || b.Height() != 3 {
289			t.Errorf("Resize(5, 3) failed, got %dx%d", b.Width(), b.Height())
290		}
291
292		// Resize larger
293		b.Resize(15, 10)
294		if b.Width() != 15 || b.Height() != 10 {
295			t.Errorf("Resize(15, 10) failed, got %dx%d", b.Width(), b.Height())
296		}
297
298		// Resize to same size
299		b.Resize(15, 10)
300		if b.Width() != 15 || b.Height() != 10 {
301			t.Errorf("Resize(15, 10) failed, got %dx%d", b.Width(), b.Height())
302		}
303	})
304
305	t.Run("FillArea", func(t *testing.T) {
306		b := NewBuffer(10, 5)
307		cell := &Cell{Content: "X", Width: 1}
308
309		// Fill a rectangle
310		b.FillArea(cell, Rect(2, 1, 3, 2))
311
312		// Check filled area
313		for y := 1; y < 3; y++ {
314			for x := 2; x < 5; x++ {
315				if c := b.CellAt(x, y); c == nil || c.Content != "X" {
316					t.Errorf("FillArea failed at (%d, %d)", x, y)
317				}
318			}
319		}
320
321		// Check unfilled area
322		if c := b.CellAt(1, 1); c != nil && c.Content == "X" {
323			t.Error("FillArea filled outside rectangle")
324		}
325
326		// Test with nil cell
327		b.FillArea(nil, Rect(0, 0, 2, 2))
328		// Should not panic
329	})
330
331	t.Run("TouchLine", func(t *testing.T) {
332		b := NewRenderBuffer(10, 5)
333
334		// Touch valid line
335		b.TouchLine(2, 1, 3)
336		// Should mark line as dirty
337
338		// Touch out of bounds
339		b.TouchLine(0, -1, 1)
340		b.TouchLine(0, 5, 1)
341		// Should not panic
342	})
343
344	t.Run("Touch", func(t *testing.T) {
345		b := NewRenderBuffer(10, 5)
346		b.Touch(2, 1)
347		// Should mark cell as dirty
348	})
349
350	t.Run("Clear", func(t *testing.T) {
351		b := NewBuffer(10, 5)
352		b.SetCell(2, 1, &Cell{Content: "X", Width: 1})
353		b.Clear()
354
355		// Check all cells are cleared
356		for y := 0; y < b.Height(); y++ {
357			for x := 0; x < b.Width(); x++ {
358				if c := b.CellAt(x, y); c != nil && c.Content != " " {
359					t.Errorf("Clear failed at (%d, %d)", x, y)
360				}
361			}
362		}
363	})
364
365	t.Run("Clone", func(t *testing.T) {
366		b := NewBuffer(10, 5)
367		b.SetCell(2, 1, &Cell{Content: "X", Width: 1})
368
369		clone := b.Clone()
370
371		// Check clone has same content
372		if c := clone.CellAt(2, 1); c == nil || c.Content != "X" {
373			t.Error("Clone failed to copy content")
374		}
375
376		// Modify clone shouldn't affect original
377		clone.SetCell(2, 1, &Cell{Content: "Y", Width: 1})
378		if c := b.CellAt(2, 1); c == nil || c.Content != "X" {
379			t.Error("Clone is not independent")
380		}
381	})
382
383	t.Run("CloneArea", func(t *testing.T) {
384		b := NewBuffer(10, 5)
385		b.SetCell(2, 1, &Cell{Content: "X", Width: 1})
386		b.SetCell(3, 2, &Cell{Content: "Y", Width: 1})
387
388		clone := b.CloneArea(Rect(2, 1, 2, 2))
389
390		// Check clone has correct size
391		if clone.Width() != 2 || clone.Height() != 2 {
392			t.Errorf("CloneArea size = %dx%d, want 2x2", clone.Width(), clone.Height())
393		}
394
395		// Check content
396		if c := clone.CellAt(0, 0); c == nil || c.Content != "X" {
397			t.Error("CloneArea failed to copy X")
398		}
399		if c := clone.CellAt(1, 1); c == nil || c.Content != "Y" {
400			t.Error("CloneArea failed to copy Y")
401		}
402	})
403
404	t.Run("Draw", func(t *testing.T) {
405		// Create source buffer
406		src := NewBuffer(3, 3)
407		src.SetCell(1, 1, &Cell{Content: "S", Width: 1})
408
409		// Create destination screen buffer (which implements Screen interface)
410		dst := NewScreenBuffer(10, 5)
411		dst.SetCell(2, 2, &Cell{Content: "D", Width: 1})
412
413		// Draw source onto destination at area (1,1) to (4,4)
414		src.Draw(&dst, Rect(1, 1, 4, 4))
415
416		// Check drawn content
417		if c := dst.CellAt(2, 2); c == nil || c.Content != "S" {
418			t.Error("Draw failed to copy content")
419		}
420
421		// Check original destination content is preserved outside draw area
422		if c := dst.CellAt(0, 0); c != nil && c.Content != " " {
423			t.Error("Draw modified content outside draw area")
424		}
425	})
426
427	t.Run("Render", func(t *testing.T) {
428		b := NewBuffer(5, 2)
429		b.SetCell(0, 0, &Cell{Content: "H", Width: 1})
430		b.SetCell(1, 0, &Cell{Content: "i", Width: 1})
431		b.SetCell(0, 1, &Cell{Content: "!", Width: 1})
432
433		got := b.Render()
434
435		// The exact output depends on the rendering logic
436		if !strings.Contains(got, "Hi") {
437			t.Errorf("Render output doesn't contain expected text")
438		}
439	})
440}
441
442// TestBufferLineOperations tests line insertion and deletion
443func TestBufferLineOperations(t *testing.T) {
444	t.Run("InsertLine", func(t *testing.T) {
445		b := NewBuffer(5, 3)
446		b.SetCell(0, 0, &Cell{Content: "A", Width: 1})
447		b.SetCell(0, 1, &Cell{Content: "B", Width: 1})
448		b.SetCell(0, 2, &Cell{Content: "C", Width: 1})
449
450		// Insert line at position 1
451		b.InsertLine(1, 1, nil)
452
453		// Check that B moved to line 2
454		if c := b.CellAt(0, 2); c == nil || c.Content != "B" {
455			t.Error("InsertLine failed to move line down")
456		}
457
458		// Check that new line is empty
459		if c := b.CellAt(0, 1); c != nil && c.Content != " " {
460			t.Error("InsertLine didn't create empty line")
461		}
462	})
463
464	t.Run("InsertLineArea", func(t *testing.T) {
465		b := NewBuffer(5, 5)
466		b.SetCell(0, 1, &Cell{Content: "A", Width: 1})
467		b.SetCell(0, 2, &Cell{Content: "B", Width: 1})
468
469		// Insert line in area
470		b.InsertLineArea(2, 1, nil, Rect(0, 1, 5, 4))
471
472		// Check that B moved down
473		if c := b.CellAt(0, 3); c == nil || c.Content != "B" {
474			t.Error("InsertLineArea failed to move line down")
475		}
476	})
477
478	t.Run("DeleteLine", func(t *testing.T) {
479		b := NewBuffer(5, 3)
480		b.SetCell(0, 0, &Cell{Content: "A", Width: 1})
481		b.SetCell(0, 1, &Cell{Content: "B", Width: 1})
482		b.SetCell(0, 2, &Cell{Content: "C", Width: 1})
483
484		// Delete line at position 1
485		b.DeleteLine(1, 1, nil)
486
487		// Check that C moved to line 1
488		if c := b.CellAt(0, 1); c == nil || c.Content != "C" {
489			t.Error("DeleteLine failed to move line up")
490		}
491
492		// Check that last line is empty
493		if c := b.CellAt(0, 2); c != nil && c.Content != " " {
494			t.Error("DeleteLine didn't clear last line")
495		}
496	})
497
498	t.Run("DeleteLineArea", func(t *testing.T) {
499		b := NewBuffer(5, 5)
500		b.SetCell(0, 1, &Cell{Content: "A", Width: 1})
501		b.SetCell(0, 2, &Cell{Content: "B", Width: 1})
502		b.SetCell(0, 3, &Cell{Content: "C", Width: 1})
503
504		// Delete line in area
505		b.DeleteLineArea(2, 1, nil, Rect(0, 1, 5, 4))
506
507		// Check that C moved up
508		if c := b.CellAt(0, 2); c == nil || c.Content != "C" {
509			t.Error("DeleteLineArea failed to move line up")
510		}
511	})
512}
513
514// TestBufferCellOperations tests cell insertion and deletion
515func TestBufferCellOperations(t *testing.T) {
516	t.Run("InsertCell", func(t *testing.T) {
517		b := NewBuffer(5, 2)
518		l := b.Line(0)
519		l[0] = Cell{Content: "A", Width: 1}
520		l[1] = Cell{Content: "B", Width: 1}
521		l[2] = Cell{Content: "C", Width: 1}
522
523		// Insert cell at position 1
524		b.InsertCell(1, 0, 1, nil)
525
526		// Check that B moved right
527		if c := b.CellAt(2, 0); c == nil || c.Content != "B" {
528			t.Error("InsertCell failed to move cell right")
529		}
530	})
531
532	t.Run("InsertCellArea", func(t *testing.T) {
533		b := NewBuffer(5, 3)
534		l := b.Line(1)
535		l[1] = Cell{Content: "A", Width: 1}
536		l[2] = Cell{Content: "B", Width: 1}
537
538		// Insert cell in area
539		b.InsertCellArea(1, 1, 1, nil, Rect(1, 1, 4, 2))
540
541		// Check that A moved right
542		if c := b.CellAt(2, 1); c == nil || c.Content != "A" {
543			t.Error("InsertCellArea failed to move cell right")
544		}
545	})
546
547	t.Run("DeleteCell", func(t *testing.T) {
548		b := NewBuffer(5, 2)
549		l := b.Line(0)
550		l[0] = Cell{Content: "A", Width: 1}
551		l[1] = Cell{Content: "B", Width: 1}
552		l[2] = Cell{Content: "C", Width: 1}
553
554		// Delete cell at position 1
555		b.DeleteCell(1, 0, 1, nil)
556
557		// Check that C moved left
558		if c := b.CellAt(1, 0); c == nil || c.Content != "C" {
559			t.Error("DeleteCell failed to move cell left")
560		}
561	})
562
563	t.Run("DeleteCellArea", func(t *testing.T) {
564		b := NewBuffer(5, 3)
565		l := b.Line(1)
566		l[1] = Cell{Content: "A", Width: 1}
567		l[2] = Cell{Content: "B", Width: 1}
568		l[3] = Cell{Content: "C", Width: 1}
569
570		// Delete cell in area
571		b.DeleteCellArea(2, 1, 1, nil, Rect(1, 1, 4, 2))
572
573		// Check that C moved left
574		if c := b.CellAt(2, 1); c == nil || c.Content != "C" {
575			t.Error("DeleteCellArea failed to move cell left")
576		}
577	})
578}
579
580// TestScreenBuffer tests ScreenBuffer specific functionality
581func TestScreenBuffer(t *testing.T) {
582	t.Run("NewScreenBuffer", func(t *testing.T) {
583		sb := NewScreenBuffer(10, 5)
584		if sb.Width() != 10 || sb.Height() != 5 {
585			t.Errorf("NewScreenBuffer size = %dx%d, want 10x5", sb.Width(), sb.Height())
586		}
587	})
588
589	t.Run("WidthMethod", func(t *testing.T) {
590		sb := NewScreenBuffer(10, 5)
591
592		// Get the default width method
593		method := sb.WidthMethod()
594		if method == nil {
595			t.Error("WidthMethod returned nil")
596		}
597
598		// The method should be set to ansi.WcWidth by default
599		// We can test it works by checking a simple character
600		if width := method.StringWidth("a"); width != 1 {
601			t.Errorf("Default width method returned %d for 'a', want 1", width)
602		}
603	})
604}
605
606// TestLineRenderLine tests the renderLine function
607func TestLineRenderLine(t *testing.T) {
608	tests := []struct {
609		name     string
610		setup    func() Line
611		validate func(t *testing.T, output string)
612	}{
613		{
614			name: "render with styles",
615			setup: func() Line {
616				l := make(Line, 5)
617				l[0] = Cell{Content: "H", Width: 1, Style: Style{Fg: ansi.Red}}
618				l[1] = Cell{Content: "i", Width: 1}
619				return l
620			},
621			validate: func(t *testing.T, output string) {
622				// Should contain style sequences
623				if !strings.Contains(output, "H") || !strings.Contains(output, "i") {
624					t.Error("renderLine failed to include content")
625				}
626			},
627		},
628		{
629			name: "render with hyperlink",
630			setup: func() Line {
631				l := make(Line, 5)
632				l[0] = Cell{Content: "L", Width: 1, Link: Link{URL: "http://example.com"}}
633				l[1] = Cell{Content: "i", Width: 1, Link: Link{URL: "http://example.com"}}
634				l[2] = Cell{Content: "n", Width: 1, Link: Link{URL: "http://example.com"}}
635				l[3] = Cell{Content: "k", Width: 1, Link: Link{URL: "http://example.com"}}
636				return l
637			},
638			validate: func(t *testing.T, output string) {
639				if !strings.Contains(output, "Link") {
640					t.Error("renderLine failed to include link content")
641				}
642			},
643		},
644	}
645
646	for _, tt := range tests {
647		t.Run(tt.name, func(t *testing.T) {
648			l := tt.setup()
649			var sb strings.Builder
650			renderLine(&sb, l)
651			output := sb.String()
652			if tt.validate != nil {
653				tt.validate(t, output)
654			}
655		})
656	}
657}
658
659func TestRenderLinePreservesTrailingSpaces(t *testing.T) {
660	// renderLine should preserve trailing empty cells as spaces.
661	// A line of width 10 with "Hi" at positions 0-1 and empty cells at 2-9
662	// should render all 10 visible characters: "Hi" followed by 8 spaces.
663	l := NewLine(10)
664	l[0] = Cell{Content: "H", Width: 1}
665	l[1] = Cell{Content: "i", Width: 1}
666	// Positions 2-9 remain EmptyCell (space, width 1, no style)
667
668	output := l.Render()
669
670	// Strip any ANSI sequences to count visible characters.
671	visible := ansi.Strip(output)
672	if len(visible) != 10 {
673		t.Errorf("Render() produced %d visible chars, want 10; output=%q visible=%q",
674			len(visible), output, visible)
675	}
676}
677
678func TestRenderLinePreservesStyledTrailingSpaces(t *testing.T) {
679	// When trailing cells have explicit styling (e.g. background color),
680	// renderLine must emit them with their ANSI attributes, not drop them.
681	l := NewLine(5)
682	l[0] = Cell{Content: "A", Width: 1}
683	// Positions 1-4: spaces with a red background.
684	for i := 1; i < 5; i++ {
685		l[i] = Cell{Content: " ", Width: 1, Style: Style{Bg: ansi.Red}}
686	}
687
688	output := l.Render()
689
690	visible := ansi.Strip(output)
691	if len(visible) != 5 {
692		t.Errorf("Render() produced %d visible chars, want 5; output=%q visible=%q",
693			len(visible), output, visible)
694	}
695	// The output must contain the background color sequence for the trailing
696	// spaces. Derive the expected sequence from the style itself rather than
697	// hardcoding an SGR number.
698	red := Style{Bg: ansi.Red}
699	wantSeq := red.Diff(&Style{})
700	if !strings.Contains(output, wantSeq) {
701		t.Errorf("Render() missing background SGR %q for trailing styled spaces; output=%q", wantSeq, output)
702	}
703}
704
705func TestRenderLineWideCellTail(t *testing.T) {
706	// A line ending in a wide cell's continuation column (a zero cell) must
707	// not disturb pending empty cells: the zero cell is skipped, and the
708	// buffered spaces are still flushed at end of line.
709	l := NewLine(6)
710	l[0] = Cell{Content: "A", Width: 1}
711	// Positions 1-3 remain EmptyCell.
712	l[4] = Cell{Content: "你", Width: 2}
713	l[5] = Cell{} // continuation column of the wide cell
714
715	output := l.Render()
716
717	visible := ansi.Strip(output)
718	// "A", 3 buffered spaces, then the wide grapheme. The continuation
719	// column contributes nothing.
720	if want := "A   你"; visible != want {
721		t.Errorf("Render() = %q, want %q; output=%q", visible, want, output)
722	}
723	if w := uniseg.StringWidth(visible); w != 6 {
724		t.Errorf("Render() visible width = %d, want 6; output=%q", w, output)
725	}
726}
727
728func BenchmarkBufferSetCell(b *testing.B) {
729	buf := NewBuffer(80, 24)
730	cell := &Cell{Content: "A", Width: 1}
731	b.ResetTimer()
732	for i := 0; i < b.N; i++ {
733		x := i % 80
734		y := (i / 80) % 24
735		buf.SetCell(x, y, cell)
736	}
737}
738
739func BenchmarkBufferResize(b *testing.B) {
740	buf := NewBuffer(80, 24)
741	b.ResetTimer()
742	for i := 0; i < b.N; i++ {
743		width := 80 + (i % 20)
744		height := 24 + (i % 10)
745		buf.Resize(width, height)
746		bufWidth, bufHeight := buf.Width(), buf.Height()
747		if bufWidth != width || bufHeight != height {
748			b.Errorf("Resize failed: got %dx%d, want %dx%d", bufWidth, bufHeight, width, height)
749		}
750	}
751}
752
753func width(s string) int {
754	width := 0
755	for _, line := range strings.Split(s, "\n") {
756		width = max(width, ansi.StringWidth(line))
757	}
758	return width
759}
760
761func height(s string) int {
762	return strings.Count(s, "\n") + 1
763}