From ff2f61a21267460e4a63cfa0f98b51c7df54a383 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:44:36 -0700 Subject: [PATCH 1/4] Clamp LSP position conversions --- internal/ls/lsconv/converters.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/ls/lsconv/converters.go b/internal/ls/lsconv/converters.go index 1728f3f7bcd..efbff9c3b19 100644 --- a/internal/ls/lsconv/converters.go +++ b/internal/ls/lsconv/converters.go @@ -2,7 +2,6 @@ package lsconv import ( "context" - "fmt" "net/url" "slices" "strings" @@ -149,19 +148,34 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter line := core.TextPos(lineAndCharacter.Line) char := core.TextPos(lineAndCharacter.Character) - if line < 0 || int(line) >= len(lineMap.LineStarts) { - panic(fmt.Sprintf("bad line number. Line: %d, lineMap length: %d", line, len(lineMap.LineStarts))) + textLen := core.TextPos(len(script.Text())) + + // Clamp line to valid range. + if line < 0 { + return 0 + } + if int(line) >= len(lineMap.LineStarts) { + return textLen } start := lineMap.LineStarts[line] + + // Determine the end of this line (start of next line, or end of text). + var lineEnd core.TextPos + if int(line)+1 < len(lineMap.LineStarts) { + lineEnd = lineMap.LineStarts[int(line)+1] + } else { + lineEnd = textLen + } + if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { - return start + char + return min(start+char, lineEnd) } var utf8Char core.TextPos var utf16Char core.TextPos - for i, r := range script.Text()[start:] { + for i, r := range script.Text()[start:lineEnd] { u16Len := core.TextPos(utf16.RuneLen(r)) if utf16Char+u16Len > char { break @@ -176,7 +190,7 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position { // UTF-8 offset to UTF-8/16 0-indexed line and character - position = min(position, core.TextPos(len(script.Text()))) + position = max(0, min(position, core.TextPos(len(script.Text())))) lineMap := c.getLineMap(script.FileName()) @@ -184,7 +198,7 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex if !isLineStart { line-- } - line = max(0, line) + line = max(0, min(line, len(lineMap.LineStarts)-1)) // The current line ranges from lineMap.LineStarts[line] (or 0) to lineMap.LineStarts[line+1] (or len(text)). From 007b5d5223aa89aa7fb6a045acf0ae4404580fa7 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:54:28 -0700 Subject: [PATCH 2/4] Drop unused clamp --- internal/ls/lsconv/converters.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/ls/lsconv/converters.go b/internal/ls/lsconv/converters.go index efbff9c3b19..ca4f5b883e8 100644 --- a/internal/ls/lsconv/converters.go +++ b/internal/ls/lsconv/converters.go @@ -151,9 +151,6 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter textLen := core.TextPos(len(script.Text())) // Clamp line to valid range. - if line < 0 { - return 0 - } if int(line) >= len(lineMap.LineStarts) { return textLen } From 6cdf1ff2e187835e0c5ac5657b3d7efe98bb48fb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:56:52 -0700 Subject: [PATCH 3/4] Copilot feedback --- internal/ls/lsconv/converters.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ls/lsconv/converters.go b/internal/ls/lsconv/converters.go index ca4f5b883e8..ef4e7187b08 100644 --- a/internal/ls/lsconv/converters.go +++ b/internal/ls/lsconv/converters.go @@ -166,7 +166,7 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter } if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { - return min(start+char, lineEnd) + return max(start, min(start+char, lineEnd)) } var utf8Char core.TextPos From 4eab4d29784854bb7ba6dd1170251f0db048f0b5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:48:27 -0700 Subject: [PATCH 4/4] Directly restore old content for checking fixes --- internal/fourslash/fourslash.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index c8bd7867709..5dba338b674 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1574,7 +1574,6 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] actualTextArray := make([]string, 0, len(importActions)) for _, action := range importActions { // Apply the code action - var edits []*lsproto.TextEdit if action.Edit != nil && action.Edit.Changes != nil { if len(*action.Edit.Changes) != 1 { t.Fatalf("Expected exactly 1 change, got %d", len(*action.Edit.Changes)) @@ -1583,7 +1582,6 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] if uri != lsconv.FileNameToDocumentURI(f.activeFilename) { t.Fatalf("Expected change to file %s, got %s", f.activeFilename, uri) } - edits = changeEdits f.applyTextEdits(t, changeEdits) } } @@ -1597,14 +1595,8 @@ func (f *FourslashTest) VerifyImportFixAtPosition(t *testing.T, expectedTexts [] } actualTextArray = append(actualTextArray, text) - // Undo changes to perform next fix - for _, textChange := range edits { - start := int(f.converters.LineAndCharacterToPosition(script, textChange.Range.Start)) - end := int(f.converters.LineAndCharacterToPosition(script, textChange.Range.End)) - deletedText := originalContent[start:end] - insertedText := textChange.NewText - f.editScriptAndUpdateMarkers(t, f.activeFilename, start, start+len(insertedText), deletedText) - } + // Restore original content for next fix + f.editScriptAndUpdateMarkers(t, f.activeFilename, 0, len(script.content), originalContent) f.currentCaretPosition = currentCaretPosition }