@@ -38,7 +38,14 @@ pub fn byteOffsetToPosition(text: []const u8, byte_offset: usize, encoding: Posi
3838 const line_bytes = text [line_start .. offset ];
3939 const character : u32 = switch (encoding ) {
4040 .utf8 = > @intCast (line_bytes .len ),
41+ // `byte_offset` comes from UI-side hit-testing, not zls, so it isn't guaranteed to
42+ // land on a UTF-8 character boundary — `Utf8View.initUnchecked` trusts its input is
43+ // valid UTF-8 and its iterator does `catch unreachable` on a bad byte sequence
44+ // (crashed on Windows: "reached unreachable code" on every hover). Validate first and
45+ // fall back to a byte count, which is wrong by at most a few units for the rare
46+ // misaligned/invalid case, rather than panicking.
4147 .utf16 = > blk : {
48+ if (! std .unicode .utf8ValidateSlice (line_bytes )) break :blk @intCast (line_bytes .len );
4249 var units : u32 = 0 ;
4350 var view = std .unicode .Utf8View .initUnchecked (line_bytes );
4451 var it = view .iterator ();
@@ -67,7 +74,10 @@ pub fn positionToByteOffset(text: []const u8, pos: Position, encoding: PositionE
6774
6875 switch (encoding ) {
6976 .utf8 = > return line_start + @min (pos .character , line_bytes .len ),
77+ // See `byteOffsetToPosition`'s matching `.utf16` branch for why this validates first
78+ // instead of trusting `initUnchecked`.
7079 .utf16 = > {
80+ if (! std .unicode .utf8ValidateSlice (line_bytes )) return line_start + @min (pos .character , line_bytes .len );
7181 var units : u32 = 0 ;
7282 var byte_off : usize = 0 ;
7383 var view = std .unicode .Utf8View .initUnchecked (line_bytes );
0 commit comments