renderer: fix preedit range width (#12479)

Related to #12466

`Preedit.range()` returns an inclusive range, but the end position was
calculated as `start + w`. For wide preedit text, this covers one extra
cell.

In Debug builds, Korean IME composition between existing Hangul
characters can panic with:
`index out of bounds: index 2, len 2`

I reproduced this reliably when there are two Hangul characters to the
right of the cursor. For example, type `가나다`, move the cursor between
`가` and `나`, then start a new Korean IME composition. With the old range
calculation, the renderer skips the first wide character plus the head
cell of the next wide character, then resumes on that character's spacer
tail.

This changes the inclusive end to `start + (w - 1)` and adds focused
tests for narrow, wide, and right-edge preedit ranges.

This does not fully fix the visual behavior reported in #12466. The
adjacent character can still disappear during composition, so this PR
only fixes the crash side of the problem.
This commit is contained in:
Mitchell Hashimoto
2026-04-27 09:24:55 -07:00
committed by GitHub

View File

@@ -112,7 +112,7 @@ pub const Preedit = struct {
// If our preedit goes off the end of the screen, we adjust it so
// that it shifts left.
const end = start + w;
const end = if (w > 0) start + (w - 1) else start;
const start_offset = if (end > max) end - max else 0;
return .{
.start = start -| start_offset,
@@ -121,3 +121,41 @@ pub const Preedit = struct {
};
}
};
const test_hangul_ga: u21 = 0xAC00; // U+AC00 HANGUL SYLLABLE GA
test "preedit range covers exact cell width" {
const testing = std.testing;
{
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = 'a' }},
};
const range = p.range(2, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}
{
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = test_hangul_ga, .wide = true }},
};
const range = p.range(2, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 2), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 3), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}
}
test "preedit range shifts left at right edge" {
const testing = std.testing;
const p: Preedit = .{
.codepoints = &.{.{ .codepoint = test_hangul_ga, .wide = true }},
};
const range = p.range(9, 9);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 8), range.start);
try testing.expectEqual(@as(terminalpkg.size.CellCountInt, 9), range.end);
try testing.expectEqual(@as(usize, 0), range.cp_offset);
}