Skip to content

Commit 6c3e68a

Browse files
fix: copy widened cell to next line when combining char triggers wrap
When a combining character widens the preceding cell past the line boundary during wraparound mode, the character needs to be moved to the next line. Previously buf.X was set to 0 unconditionally and the cell copy/clear was missing, causing duplicated or corrupted characters at line boundaries. Now saves the old row/col before wrapping, sets buf.X = oldWidth, copies the widened cell to the start of the new line, and clears the old cells. Fixes #41 Co-authored-by: Ona <no-reply@ona.com>
1 parent f433168 commit 6c3e68a

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

inputhandler.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,9 @@ func (h *InputHandler) Print(data []uint32, start, end int) {
458458
if buf.X+chWidth-oldWidth > cols {
459459
// autowrap - DECAWM
460460
if wraparoundMode {
461-
buf.X = 0
461+
oldRow := bufferRow
462+
oldCol := buf.X - oldWidth
463+
buf.X = oldWidth
462464
buf.Y++
463465
if buf.Y == buf.ScrollBottom+1 {
464466
buf.Y--
@@ -477,6 +479,15 @@ func (h *InputHandler) Print(data []uint32, start, end int) {
477479
if bufferRow == nil {
478480
return
479481
}
482+
// When a combining character widens the preceding cell past the
483+
// line boundary, copy the widened cell to the new line and clear
484+
// the old cells on the previous line.
485+
if oldWidth > 0 {
486+
bufferRow.CopyCellsFrom(oldRow, oldCol, 0, oldWidth, false)
487+
for c := oldCol; c < cols; c++ {
488+
oldRow.SetCellFromCodepoint(c, 0, 1, curAttr)
489+
}
490+
}
480491
} else {
481492
buf.X = cols - 1
482493
// FIXME: check for xterm behavior

inputhandler_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,115 @@ func TestPrintCombiningCharacters(t *testing.T) {
377377
}
378378
})
379379
}
380+
381+
// ---------------------------------------------------------------------------
382+
// Print wrap + combining character (issue #41)
383+
// ---------------------------------------------------------------------------
384+
385+
func TestPrintWrapCombiningCharWidensCell(t *testing.T) {
386+
t.Parallel()
387+
388+
t.Run("combining_at_last_column_no_wrap", func(t *testing.T) {
389+
// A combining mark on the last character of a line should NOT wrap;
390+
// it should join in-place.
391+
t.Parallel()
392+
term := newTestTerminal(5, 5)
393+
defer term.Dispose()
394+
// Fill 5 columns: "abcde", then add combining accent to 'e'.
395+
term.WriteString("abcde\u0301")
396+
cell := getCellInfo(term, 0, 4)
397+
want := cellInfo{Chars: "e\u0301", Width: 1, Combined: true}
398+
if diff := cmp.Diff(want, cell); diff != "" {
399+
t.Errorf("last col cell (-want +got):\n%s", diff)
400+
}
401+
// Cursor should still be at col 5 (past end), row 0.
402+
if term.CursorX() != 5 {
403+
t.Errorf("cursorX = %d, want 5", term.CursorX())
404+
}
405+
if term.CursorY() != 0 {
406+
t.Errorf("cursorY = %d, want 0", term.CursorY())
407+
}
408+
})
409+
410+
t.Run("wide_char_wrap_then_combining", func(t *testing.T) {
411+
// A wide (width-2) char that wraps to the next line, followed by a
412+
// combining mark, should show the combined character on the new line.
413+
t.Parallel()
414+
term := newTestTerminal(5, 5)
415+
defer term.Dispose()
416+
// Fill 4 columns, then write a wide char (U+4E16 '世', width 2).
417+
// The wide char doesn't fit at col 4 (needs 2 cells), so it wraps.
418+
term.WriteString("abcd\u4e16\u0301")
419+
// Row 0 should be "abcd" (wide char wrapped away).
420+
line0 := term.GetLine(0)
421+
if line0 != "abcd" {
422+
t.Errorf("row 0 = %q, want %q", line0, "abcd")
423+
}
424+
// Row 1 should have the wide char with combining mark at col 0.
425+
cell := getCellInfo(term, 1, 0)
426+
want := cellInfo{Chars: "世\u0301", Width: 2, Combined: true}
427+
if diff := cmp.Diff(want, cell); diff != "" {
428+
t.Errorf("row 1 col 0 (-want +got):\n%s", diff)
429+
}
430+
})
431+
432+
t.Run("oldWidth_combining_joins_in_place", func(t *testing.T) {
433+
// When cursor is at cols (past end) and a combining mark joins the
434+
// preceding width-1 char, no wrap should occur (chWidth - oldWidth = 0).
435+
t.Parallel()
436+
term := newTestTerminal(5, 5)
437+
defer term.Dispose()
438+
term.WriteString("1234A")
439+
if term.CursorX() != 5 {
440+
t.Fatalf("setup: cursorX = %d, want 5", term.CursorX())
441+
}
442+
term.WriteString("\u0301")
443+
cell := getCellInfo(term, 0, 4)
444+
want := cellInfo{Chars: "A\u0301", Width: 1, Combined: true}
445+
if diff := cmp.Diff(want, cell); diff != "" {
446+
t.Errorf("cell at (0,4) (-want +got):\n%s", diff)
447+
}
448+
if term.CursorX() != 5 {
449+
t.Errorf("cursorX = %d, want 5", term.CursorX())
450+
}
451+
if term.CursorY() != 0 {
452+
t.Errorf("cursorY = %d, want 0", term.CursorY())
453+
}
454+
})
455+
456+
t.Run("wrap_sets_bufX_to_oldWidth", func(t *testing.T) {
457+
// Verify that after a normal (non-combining) wrap, buf.X is set
458+
// correctly. The fix changes buf.X = 0 to buf.X = oldWidth; for
459+
// non-combining chars oldWidth is 0, so behavior is identical.
460+
t.Parallel()
461+
term := newTestTerminal(5, 5)
462+
defer term.Dispose()
463+
term.WriteString("12345X")
464+
line0 := term.GetLine(0)
465+
if line0 != "12345" {
466+
t.Errorf("row 0 = %q, want %q", line0, "12345")
467+
}
468+
cell := getCellInfo(term, 1, 0)
469+
want := cellInfo{Chars: "X", Width: 1, Combined: false}
470+
if diff := cmp.Diff(want, cell); diff != "" {
471+
t.Errorf("row 1 col 0 (-want +got):\n%s", diff)
472+
}
473+
if term.CursorX() != 1 {
474+
t.Errorf("cursorX = %d, want 1", term.CursorX())
475+
}
476+
if term.CursorY() != 1 {
477+
t.Errorf("cursorY = %d, want 1", term.CursorY())
478+
}
479+
// Verify the second line is marked as wrapped.
480+
buf := term.bufferService.Buffer()
481+
wrappedLine := buf.Lines.Get(buf.YBase + 1)
482+
if wrappedLine == nil {
483+
t.Fatal("row 1 line is nil")
484+
}
485+
if !wrappedLine.IsWrapped {
486+
t.Error("row 1 should be marked as wrapped")
487+
}
488+
})
489+
}
490+
491+

0 commit comments

Comments
 (0)