diff --git a/internal/ls/lsconv/converters.go b/internal/ls/lsconv/converters.go index 1728f3f7bcd..aba0e9b1140 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,13 +148,16 @@ 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))) + if line < 0 { + line = 0 + } else if int(line) >= len(lineMap.LineStarts) { + line = core.TextPos(len(lineMap.LineStarts) - 1) } + textLen := core.TextPos(len(script.Text())) start := lineMap.LineStarts[line] if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 { - return start + char + return min(start+char, textLen) } var utf8Char core.TextPos @@ -170,7 +172,7 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter utf8Char = core.TextPos(i + utf8.RuneLen(r)) } - return start + utf8Char + return min(start+utf8Char, textLen) } func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position { diff --git a/internal/project/session_test.go b/internal/project/session_test.go index 9ba29853b0a..b27978d23bd 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/testutil" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "github.com/microsoft/typescript-go/internal/tspath" "gotest.tools/v3/assert" @@ -952,4 +953,50 @@ func TestSession(t *testing.T) { assert.DeepEqual(t, *actualConfig2.TS(), *expectedPrefs1) assert.DeepEqual(t, *actualConfig2.JS(), *expectedPrefs2) }) + + t.Run("definition with stale position after file shrink does not panic", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on definition with stale position") + files := map[string]any{ + "/home/projects/TS/p1/tsconfig.json": `{"compilerOptions": {"noLib": true}}`, + "/home/projects/TS/p1/src/index.ts": "0\n1\n", + } + session, _ := projecttestutil.Setup(files) + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, "0\n1\n", lsproto.LanguageKindTypeScript) + + session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ + { + Partial: &lsproto.TextDocumentContentChangePartial{ + Range: lsproto.Range{ + Start: lsproto.Position{Line: 0, Character: 0}, + End: lsproto.Position{Line: 2, Character: 0}, + }, + Text: "0\n", + }, + }, + }) + + ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + + _, err = ls.ProvideDefinition(context.Background(), "file:///home/projects/TS/p1/src/index.ts", lsproto.Position{Line: 2, Character: 0}) + assert.NilError(t, err) + }) + + t.Run("definition with out-of-bounds position does not panic", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on definition with out-of-bounds position") + files := map[string]any{ + "/home/projects/TS/p1/tsconfig.json": `{"compilerOptions": {"noLib": true}}`, + "/home/projects/TS/p1/src/index.ts": "let x = 1;", + } + session, _ := projecttestutil.Setup(files) + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, "let x = 1;", lsproto.LanguageKindTypeScript) + + ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + assert.NilError(t, err) + + _, err = ls.ProvideDefinition(context.Background(), "file:///home/projects/TS/p1/src/index.ts", lsproto.Position{Line: 999, Character: 0}) + assert.NilError(t, err) + }) }