diff --git a/internal/ls/converters.go b/internal/ls/converters.go index 18238662f3..e01dadd72b 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -228,12 +228,17 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex start := lineMap.LineStarts[line] + // Ensure position doesn't exceed text length to avoid slice bounds errors + text := script.Text() + textLen := core.TextPos(len(text)) + position = min(position, textLen) + var character core.TextPos if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { character = position - start } else { // We need to rescan the text as UTF-16 to find the character offset. - for _, r := range script.Text()[start:position] { + for _, r := range text[start:position] { character += core.TextPos(utf16.RuneLen(r)) } } diff --git a/internal/ls/converters_bounds_test.go b/internal/ls/converters_bounds_test.go new file mode 100644 index 0000000000..e7743afe41 --- /dev/null +++ b/internal/ls/converters_bounds_test.go @@ -0,0 +1,56 @@ +package ls + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" +) + +type mockScript struct { + fileName string + text string +} + +func (m *mockScript) FileName() string { + return m.fileName +} + +func (m *mockScript) Text() string { + return m.text +} + +func TestPositionToLineAndCharacterBoundsCheck(t *testing.T) { + t.Parallel() + + // Test case that reproduces the panic with multi-byte characters and trailing newlines + text := "\"→\" ;\n\n\n" + + lineMap := ComputeLineStarts(text) + + converters := NewConverters(lsproto.PositionEncodingKindUTF16, func(fileName string) *LineMap { + return lineMap + }) + + script := &mockScript{ + fileName: "test.ts", + text: text, + } + + // This should not panic even if position is beyond text length + textLen := len(text) + position := core.TextPos(textLen) // position at end of text + + // This should work + result := converters.PositionToLineAndCharacter(script, position) + + t.Logf("Text length: %d, Position: %d", textLen, position) + t.Logf("Result: line=%d, char=%d", result.Line, result.Character) + + // This should also not panic, even though position is beyond text length + beyondEndPosition := core.TextPos(textLen + 1) + result2 := converters.PositionToLineAndCharacter(script, beyondEndPosition) + + t.Logf("Beyond end position: %d", beyondEndPosition) + t.Logf("Result2: line=%d, char=%d", result2.Line, result2.Character) +} \ No newline at end of file