package project_test import ( "context" "io/fs" "maps" "slices" "strings" "testing" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "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/projecttestutil" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/vfstest" "gotest.tools/v3/assert" ) func TestSession(t *testing.T) { t.Parallel() if !bundled.Embedded { t.Skip("bundled files are not embedded") } defaultFiles := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`, "/home/projects/TS/p1/src/x.ts": `export const x = 1;`, "/home/projects/TS/p1/config.ts": `let x = 1, y = 2;`, } t.Run("DidOpenFile", func(t *testing.T) { t.Parallel() t.Run("create configured project", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) assert.Assert(t, configuredProject != nil) // Get language service to access the program ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "export const x = 1;") }) t.Run("create inferred project", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/config.ts", 1, defaultFiles["/home/projects/TS/p1/config.ts"].(string), lsproto.LanguageKindTypeScript) // Find tsconfig, load, notice config.ts is not included, create inferred project snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) // Should have both configured project (for tsconfig.json) and inferred project configuredProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) inferredProject := snapshot.ProjectCollection.InferredProject() assert.Assert(t, configuredProject != nil) assert.Assert(t, inferredProject != nil) }) t.Run("inferred project for in-memory files", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/config.ts", 1, defaultFiles["/home/projects/TS/p1/config.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "x", lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "untitled:Untitled-2", 1, "y", lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) }) t.Run("inferred project JS file", func(t *testing.T) { t.Parallel() jsFiles := map[string]any{ "/home/projects/TS/p1/index.js": `import { x } from "./x";`, } session, _ := projecttestutil.Setup(jsFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.js", 1, jsFiles["/home/projects/TS/p1/index.js"].(string), lsproto.LanguageKindJavaScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.js") assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/index.js") != nil) }) }) t.Run("watchChange and didOpen in same batch rebuilds program", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "strict": true } }`, "/home/projects/TS/p1/src/a.ts": "export const a = 1;\n", "/home/projects/TS/p1/src/b.ts": "export const b = 1;\n", } session, utils := projecttestutil.Setup(files) oldContent := files["/home/projects/TS/p1/src/a.ts"].(string) // Open b.ts to create the project; a.ts is included via tsconfig. session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/b.ts", 1, files["/home/projects/TS/p1/src/b.ts"].(string), lsproto.LanguageKindTypeScript) // Verify a.ts is in the program with the original content. ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/b.ts") assert.NilError(t, err) assert.Equal(t, ls.GetProgram().GetSourceFile("/home/projects/TS/p1/src/a.ts").Text(), oldContent) // Modify a.ts on disk (simulate a build tool or git checkout). newContent := "export const a = 2;\nexport const extra = true;\n" err = utils.FS().WriteFile("/home/projects/TS/p1/src/a.ts", newContent) assert.NilError(t, err) // Queue a watch event for the disk change (not flushed yet). session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ {Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/a.ts"}, }) // Open a.ts in the editor—flushes both watch event and didOpen together. // Before the fix, processChanges would discard the watch event, // leaving the project with a stale SourceFile and a mismatched line map. session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/a.ts", 1, newContent, lsproto.LanguageKindTypeScript) // The program's SourceFile must reflect the overlay (new) content. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/a.ts") assert.NilError(t, err) assert.Equal(t, ls.GetProgram().GetSourceFile("/home/projects/TS/p1/src/a.ts").Text(), newContent) }) t.Run("DidChangeFile", func(t *testing.T) { t.Parallel() t.Run("update file and program", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ { Partial: new(lsproto.TextDocumentContentChangePartial{ Range: lsproto.Range{ Start: lsproto.Position{ Line: 0, Character: 17, }, End: lsproto.Position{ Line: 0, Character: 18, }, }, Text: "2", }), }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() // Program should change due to the file content change assert.Check(t, programAfter != programBefore) assert.Equal(t, programAfter.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "export const x = 2;") }) t.Run("update untitled file", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "let x = 1;", lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "untitled:Untitled-1") assert.NilError(t, err) programBefore := lsBefore.GetProgram() untitledFileName := lsproto.DocumentUri("untitled:Untitled-1").FileName() assert.Equal(t, programBefore.GetSourceFile(untitledFileName).Text(), "let x = 1;") session.DidChangeFile(context.Background(), "untitled:Untitled-1", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ { Partial: new(lsproto.TextDocumentContentChangePartial{ Range: lsproto.Range{ Start: lsproto.Position{ Line: 0, Character: 8, }, End: lsproto.Position{ Line: 0, Character: 9, }, }, Text: "2", }), }, }) lsAfter, err := session.GetLanguageService(context.Background(), "untitled:Untitled-1") assert.NilError(t, err) programAfter := lsAfter.GetProgram() assert.Check(t, programAfter != programBefore) assert.Equal(t, programAfter.GetSourceFile(untitledFileName).Text(), "let x = 2;") }) t.Run("unchanged source files are reused", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() indexFileBefore := programBefore.GetSourceFile("/home/projects/TS/p1/src/index.ts") session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ { Partial: new(lsproto.TextDocumentContentChangePartial{ Range: lsproto.Range{ Start: lsproto.Position{ Line: 0, Character: 0, }, End: lsproto.Position{ Line: 0, Character: 0, }, }, Text: ";", }), }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() // Unchanged file should be reused assert.Equal(t, programAfter.GetSourceFile("/home/projects/TS/p1/src/index.ts"), indexFileBefore) }) t.Run("change can pull in new files", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) files["/home/projects/TS/p1/y.ts"] = `export const y = 2;` session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) // Verify y.ts is not initially in the program lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() assert.Check(t, programBefore.GetSourceFile("/home/projects/TS/p1/y.ts") == nil) session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ { Partial: new(lsproto.TextDocumentContentChangePartial{ Range: lsproto.Range{ Start: lsproto.Position{ Line: 0, Character: 0, }, End: lsproto.Position{ Line: 0, Character: 0, }, }, Text: `import { y } from "../y";\n`, }), }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() // y.ts should now be included in the program assert.Assert(t, programAfter.GetSourceFile("/home/projects/TS/p1/y.ts") != nil) }) t.Run("single-file change followed by config change reloads program", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) files["/home/projects/TS/p1/tsconfig.json"] = `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true }, "include": ["src/index.ts"] }` session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() assert.Equal(t, len(programBefore.GetSourceFiles()), 2) session.DidChangeFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{ { Partial: new(lsproto.TextDocumentContentChangePartial{ Range: lsproto.Range{ Start: lsproto.Position{ Line: 0, Character: 0, }, End: lsproto.Position{ Line: 0, Character: 0, }, }, Text: "\n", }), }, }) err = utils.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true }, "include": ["./**/*"] }`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/tsconfig.json", }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() assert.Equal(t, len(programAfter.GetSourceFiles()), 3) }) }) t.Run("DidCloseFile", func(t *testing.T) { t.Parallel() t.Run("Configured projects", func(t *testing.T) { t.Parallel() t.Run("delete a file, close it, recreate it", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) assert.NilError(t, utils.FS().Remove("/home/projects/TS/p1/src/x.ts")) session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts") ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "") assert.NilError(t, err) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "") }) }) t.Run("Inferred projects", func(t *testing.T) { t.Parallel() t.Run("delete a file, close it, recreate it", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) delete(files, "/home/projects/TS/p1/tsconfig.json") session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) err := utils.FS().Remove("/home/projects/TS/p1/src/x.ts") assert.NilError(t, err) session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts") ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", "") assert.NilError(t, err) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) assert.Equal(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "") }) t.Run("close untitled file", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "let x = 1;", lsproto.LanguageKindTypeScript) session.DidCloseFile(context.Background(), "untitled:Untitled-1") session.DidOpenFile(context.Background(), "untitled:Untitled-2", 1, "", lsproto.LanguageKindTypeScript) }) }) }) t.Run("DidSaveFile", func(t *testing.T) { t.Parallel() t.Run("save event first", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, snapshot.ID(), uint64(1)) session.DidSaveFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts") session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/index.ts", }, }) session.WaitForBackgroundTasks() snapshot = session.Snapshot() // We didn't need a snapshot change, but the session overlays should be updated. assert.Equal(t, snapshot.ID(), uint64(1)) // Open another file to force a snapshot update so we can see the changes. session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() assert.Equal(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts").MatchesDiskText(), true) }) t.Run("watch event first", func(t *testing.T) { t.Parallel() session, _ := projecttestutil.Setup(defaultFiles) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, defaultFiles["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, snapshot.ID(), uint64(1)) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/index.ts", }, }) session.DidSaveFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts") session.WaitForBackgroundTasks() snapshot = session.Snapshot() // We didn't need a snapshot change, but the session overlays should be updated. assert.Equal(t, snapshot.ID(), uint64(1)) // Open another file to force a snapshot update so we can see the changes. session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() assert.Equal(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts").MatchesDiskText(), true) }) }) t.Run("Source file sharing", func(t *testing.T) { t.Parallel() t.Run("projects with similar options share source files", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) files["/home/projects/TS/p2/tsconfig.json"] = `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true, "noCheck": true } }` files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p2/src/index.ts", 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program1 := ls1.GetProgram() ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") assert.NilError(t, err) program2 := ls2.GetProgram() assert.Equal( t, program1.GetSourceFile("/home/projects/TS/p1/src/x.ts"), program2.GetSourceFile("/home/projects/TS/p1/src/x.ts"), ) }) t.Run("projects with different options do not share source files", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) files["/home/projects/TS/p2/tsconfig.json"] = `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true, "moduleDetection": "auto" }, "include": ["src"] }` files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p2/src/index.ts", 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program1 := ls1.GetProgram() ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") assert.NilError(t, err) program2 := ls2.GetProgram() x1 := program1.GetSourceFile("/home/projects/TS/p1/src/x.ts") x2 := program2.GetSourceFile("/home/projects/TS/p1/src/x.ts") assert.Assert(t, x1 != nil && x2 != nil) assert.Assert(t, x1 != x2) }) }) t.Run("DidChangeWatchedFiles", func(t *testing.T) { t.Parallel() t.Run("change open file", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) // Program should remain the same since the file is open and changes are handled through DidChangeTextDocument assert.Equal(t, programBefore, lsAfter.GetProgram()) }) t.Run("change closed program file", func(t *testing.T) { t.Parallel() files := maps.Clone(defaultFiles) session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() err = utils.FS().WriteFile("/home/projects/TS/p1/src/x.ts", `export const x = 2;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) assert.Check(t, lsAfter.GetProgram() != programBefore) }) t.Run("change program file not in tsconfig root files", func(t *testing.T) { t.Parallel() for _, workspaceDir := range []string{"/", "/home/projects/TS/p1", "/somewhere/else/entirely"} { t.Run("workspaceDir="+strings.ReplaceAll(workspaceDir, "/", "_"), func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "module": "nodenext", "strict": true }, "files": ["src/index.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { x } from "../../x";`, "/home/projects/TS/x.ts": `export const x = 1;`, } session, utils := projecttestutil.SetupWithOptions(files, &project.SessionOptions{ CurrentDirectory: workspaceDir, DefaultLibraryPath: bundled.LibPath(), TypingsLocation: projecttestutil.TestTypingsLocation, PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, LoggingEnabled: true, }) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() session.WaitForBackgroundTasks() assert.Check(t, utils.WatchesFile("/home/projects/ts/x.ts")) err = utils.FS().WriteFile("/home/projects/TS/x.ts", `export const x = 2;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/x.ts", }, }) lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) assert.Check(t, lsAfter.GetProgram() != programBefore) }) } }) t.Run("change config file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "strict": false } }`, "/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`, "/home/projects/TS/p1/src/index.ts": ` import { x } from "./x"; let y: number = x;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) err = utils.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", `{ "compilerOptions": { "noLib": false, "strict": true } }`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/tsconfig.json", }, }) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) }) t.Run("delete explicitly included file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts", "src/x.ts"] }`, "/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`, "/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/x.ts")) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) err = utils.FS().Remove("/home/projects/TS/p1/src/x.ts") assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/x.ts", }, }) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() // File name is still in the command line, was explicitly included assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/x.ts")) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) // Open file to trigger cleanup session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "", lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Check(t, snapshot.GetFile("/home/projects/TS/p1/src/x.ts") == nil) }) t.Run("delete wildcard included file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `let x = 2;`, "/home/projects/TS/p1/src/x.ts": `let y = x;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/index.ts")) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0) err = utils.FS().Remove("/home/projects/TS/p1/src/index.ts") assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/index.ts", }, }) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() // File name is gone from the command line, was originally included via wildcard assert.Check(t, !slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/index.ts")) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) // Open file to trigger cleanup session.DidOpenFile(context.Background(), "untitled:Untitled-1", 1, "", lsproto.LanguageKindTypeScript) snapshot := session.Snapshot() assert.Check(t, snapshot.GetFile("/home/projects/TS/p1/src/index.ts") == nil) }) t.Run("delete directory with wildcard included files", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `import { x } from "./sub/x";`, "/home/projects/TS/p1/src/sub/x.ts": `export const x = 1;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/sub/x.ts")) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) // Delete the entire subdirectory from the file system. err = utils.FS().Remove("/home/projects/TS/p1/src/sub") assert.NilError(t, err) // When a directory is deleted, the client typically sends a single deletion // event for the directory itself. Because the registered glob pattern includes // file extensions (e.g. **/*.{ts,...}), the directory path does not match // and the event is filtered out, so the server is never notified. // Simulate this by sending a delete event for the directory URI. session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/sub", }, }) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() // The directory was deleted, so the file should no longer be in the program. assert.Check(t, !slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/sub/x.ts")) // The import should now be an error since the module is missing. assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) }) t.Run("delete directory with program-only files", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { x } from "./sub/x";`, "/home/projects/TS/p1/src/sub/x.ts": `export const x = 1;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/index.ts")) // x.ts is not in "files" but is pulled in via the import. assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/sub/x.ts") != nil) assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) // Delete the entire subdirectory from the file system. err = utils.FS().Remove("/home/projects/TS/p1/src/sub") assert.NilError(t, err) // Send a delete event for the directory URI. session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/sub", }, }) ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() // The directory was deleted, so the file should no longer be resolvable. assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/sub/x.ts") == nil) // The import should now be an error since the module is missing. assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) }) t.Run("delete sibling folder schedules diagnostics refresh", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["index.ts"] }`, "/home/projects/TS/p1/index.ts": `import { content } from "./f/content"; export const value = content;`, "/home/projects/TS/p1/f/content.ts": `export const content = 1;`, } session, utils := projecttestutil.Setup(files) contentURI := lsproto.DocumentUri("file:///home/projects/TS/p1/f/content.ts") session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.ts", 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), contentURI, 1, files["/home/projects/TS/p1/f/content.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.ts") assert.NilError(t, err) session.WaitForBackgroundTasks() baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls()) err = utils.FS().Remove("/home/projects/TS/p1/f") assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/f", }, }) session.DidCloseFile(context.Background(), contentURI) session.WaitForBackgroundTasks() refreshCount := len(utils.Client().RefreshDiagnosticsCalls()) assert.Assert(t, refreshCount > baselineRefreshCount, "expected RefreshDiagnostics to be called after deleting /home/projects/TS/p1/f, got %d calls (baseline %d)", refreshCount, baselineRefreshCount) }) t.Run("delete sibling folder schedules diagnostics refresh after opening third file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["index.ts", "third.ts"] }`, "/home/projects/TS/p1/index.ts": `import { content } from "./f/content"; export const value = content;`, "/home/projects/TS/p1/f/content.ts": `export const content = 1;`, "/home/projects/TS/p1/third.ts": `export const third = 3;`, } session, utils := projecttestutil.Setup(files) contentURI := lsproto.DocumentUri("file:///home/projects/TS/p1/f/content.ts") thirdURI := lsproto.DocumentUri("file:///home/projects/TS/p1/third.ts") session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.ts", 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), contentURI, 1, files["/home/projects/TS/p1/f/content.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.ts") assert.NilError(t, err) session.WaitForBackgroundTasks() baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls()) err = utils.FS().Remove("/home/projects/TS/p1/f") assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/f", }, }) session.DidOpenFile(context.Background(), thirdURI, 1, files["/home/projects/TS/p1/third.ts"].(string), lsproto.LanguageKindTypeScript) session.WaitForBackgroundTasks() refreshCount := len(utils.Client().RefreshDiagnosticsCalls()) assert.Assert(t, refreshCount > baselineRefreshCount, "expected RefreshDiagnostics to be called after deleting /home/projects/TS/p1/f and opening /home/projects/TS/p1/third.ts, got %d calls (baseline %d)", refreshCount, baselineRefreshCount) }) t.Run("create explicitly included file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts", "src/y.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { y } from "./y";`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() // Initially should have an error because y.ts is missing assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Add the missing file err = utils.FS().WriteFile("/home/projects/TS/p1/src/y.ts", `export const y = 1;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/y.ts", }, }) // Error should be resolved ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil) }) t.Run("create failed lookup location", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { z } from "./z";`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() // Initially should have an error because z.ts is missing assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Add a new file through failed lookup watch err = utils.FS().WriteFile("/home/projects/TS/p1/src/z.ts", `export const z = 1;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/z.ts", }, }) // Error should be resolved and the new file should be included in the program ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/z.ts") != nil) }) t.Run("create wildcard included file", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `a;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() // Initially should have an error because declaration for 'a' is missing assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Add a new file through wildcard watch err = utils.FS().WriteFile("/home/projects/TS/p1/src/a.ts", `const a = 1;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/a.ts", }, }) // Error should be resolved and the new file should be included in the program ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/a.ts") != nil) }) t.Run("irrelevant extension changes are filtered out", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `export const x = 1;`, "/home/projects/TS/p1/src/data.txt": `some text`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) oldProgram := program // Modify an irrelevant file and send change/create events for files with // extensions that are not relevant to TypeScript compilation. err = utils.FS().WriteFile("/home/projects/TS/p1/src/data.txt", `updated text`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/data.txt", }, { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/styles.css", }, { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/image.png", }, }) // The program should not have been rebuilt since all events had irrelevant extensions. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, program, oldProgram, "program should not be rebuilt for irrelevant extension changes") }) t.Run("pnpm install links local package", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/pnpm/pnpm-workspace.yaml": `packages: - 'packages/*'`, "/home/projects/pnpm/packages/alpha/package.json": `{ "name": "@repo/alpha", "main": "index.ts" }`, "/home/projects/pnpm/packages/alpha/tsconfig.json": `{ "compilerOptions": { "noLib": true, "composite": true } }`, "/home/projects/pnpm/packages/alpha/index.ts": `export const alpha = 1;`, "/home/projects/pnpm/packages/beta/package.json": `{ "name": "@repo/beta" }`, "/home/projects/pnpm/packages/beta/tsconfig.json": `{ "compilerOptions": { "noLib": true } }`, "/home/projects/pnpm/packages/beta/index.ts": `import { alpha } from "@repo/alpha";`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/pnpm/packages/beta/index.ts", 1, files["/home/projects/pnpm/packages/beta/index.ts"].(string), lsproto.LanguageKindTypeScript) // Before pnpm install: the import is unresolved because node_modules/@repo/alpha doesn't exist. ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/pnpm/packages/beta/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/pnpm/packages/beta/index.ts"))), 1) // Simulate pnpm install: create a symlink from beta's node_modules/@repo/alpha to packages/alpha. mapFS := utils.FsFromFileMap().FSys().(*vfstest.MapFS) err = mapFS.MkdirAll("home/projects/pnpm/packages/beta/node_modules/@repo", fs.ModePerm) assert.NilError(t, err) mapFS.AddSymlink("home/projects/pnpm/packages/beta/node_modules/@repo/alpha", "home/projects/pnpm/packages/alpha") // Fire watch events mimicking what VS Code sends for a pnpm install. session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ {Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/pnpm/packages/beta/node_modules"}, {Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/pnpm/packages/beta/node_modules/%40repo"}, {Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/pnpm/packages/beta/node_modules/%40repo/alpha"}, {Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/pnpm/pnpm-lock.yaml"}, {Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/pnpm/packages/beta/node_modules/.bin/tsc"}, {Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/pnpm/packages/beta/node_modules/.bin/tsserver"}, }) // After pnpm install: the import should resolve. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/pnpm/packages/beta/index.ts") assert.NilError(t, err) program = ls.GetProgram() diags := program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/pnpm/packages/beta/index.ts")) for _, d := range diags { t.Logf("diagnostic: %s", d.String()) } assert.Equal(t, len(diags), 0) }) t.Run("symlinked node_modules package.json change invalidates resolution", func(t *testing.T) { t.Parallel() // Set up a project that imports a symlinked node_modules package. // The package resolves via "main" in package.json to dist/index.js. files := map[string]any{ "/home/projects/myproject/tsconfig.json": `{ "compilerOptions": { "noLib": true, "module": "nodenext", "moduleResolution": "nodenext" }, "files": ["src/index.ts"] }`, "/home/projects/myproject/src/index.ts": `import { foo } from "mylib";`, // The real package lives as a sibling directory "/home/projects/mylib/package.json": `{ "name": "mylib", "main": "dist/index.js" }`, "/home/projects/mylib/dist/index.js": `exports.foo = function() { return 1; };`, "/home/projects/mylib/dist/index.d.ts": `export declare function foo(): number;`, // node_modules/mylib is a symlink to the sibling "/home/projects/myproject/node_modules/mylib": vfstest.Symlink("/home/projects/mylib"), } session, utils := projecttestutil.SetupWithOptions(files, &project.SessionOptions{ CurrentDirectory: "/home/projects/myproject", DefaultLibraryPath: bundled.LibPath(), TypingsLocation: projecttestutil.TestTypingsLocation, PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, LoggingEnabled: true, }) session.DidOpenFile(context.Background(), "file:///home/projects/myproject/src/index.ts", 1, files["/home/projects/myproject/src/index.ts"].(string), lsproto.LanguageKindTypeScript) // Initial state: import resolves successfully via package.json main -> dist/index.d.ts ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/myproject/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() session.WaitForBackgroundTasks() diags := program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/myproject/src/index.ts")) for _, d := range diags { t.Logf("initial diagnostic: %s", d.String()) } assert.Equal(t, len(diags), 0, "import should resolve initially") // Assert: watched file globs cover the realpath of package.json and dist/index.d.ts. // With a workspace dir set, watchers use RelativePattern with a base URI. assert.Check(t, utils.WatchesFile("/home/projects/mylib/package.json"), "realpath of package.json should be watched") assert.Check(t, utils.WatchesFile("/home/projects/mylib/dist/index.d.ts"), "realpath of dist/index.d.ts should be watched") // Edit package.json to remove "main" field err = utils.FS().WriteFile("/home/projects/mylib/package.json", `{ "name": "mylib" }`) assert.NilError(t, err) // Fire watch event for the realpath of the changed package.json. // A real editor would fire this for the realpath since it watches realpaths. session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/mylib/package.json", }, }) // After removing "main" from package.json, the import should no longer resolve. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/myproject/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() diags = program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/myproject/src/index.ts")) assert.Assert(t, len(diags) > 0, "import should fail after removing main from package.json") }) t.Run("create file in non-existent directory", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { helper } from "./lib/helper";`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) // Initially should have an error because lib/helper.ts doesn't exist // and src/lib/ directory doesn't exist either. ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Create the directory and file. err = utils.FS().WriteFile("/home/projects/TS/p1/src/lib/helper.ts", `export const helper = 1;`) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/lib/helper.ts", }, }) // Error should be resolved. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/lib/helper.ts") != nil) }) t.Run("create symlink directory matching include pattern", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `export const x = 1;`, "/home/projects/TS/shared/utils.ts": `export const util = "hello";`, "/home/projects/TS/shared/helpers.ts": `export const helper = 42;`, } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() // Initially, project only has the one file in src/. assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/index.ts")) assert.Check(t, !slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/linked/utils.ts")) assert.Check(t, !slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/linked/helpers.ts")) // Create a symlink directory inside src/ that points to the shared directory. mapFS := utils.FsFromFileMap().FSys().(*vfstest.MapFS) mapFS.AddSymlink("home/projects/TS/p1/src/linked", "home/projects/TS/shared") // Send directory creation event (what VS Code sends when a symlink directory appears). session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/linked", }, }) // After the symlink directory is created, the files inside it should be // picked up by the wildcard include pattern. ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/index.ts")) assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/linked/utils.ts")) assert.Check(t, slices.Contains(program.CommandLine().ParsedConfig.FileNames, "/home/projects/TS/p1/src/linked/helpers.ts")) }) }) t.Run("refreshes code lenses and inlay hints when relevant user preferences change", func(t *testing.T) { t.Parallel() files := map[string]any{ "/src/tsconfig.json": "{}", "/src/index.ts": "export const x = 1;", } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) session.Configure(lsutil.NewDefaultUserPreferences()) // Change user preferences for code lens and inlay hints. newPrefs := session.Config() newPrefs.CodeLens.ReferencesCodeLensEnabled = core.TSTrue newPrefs.InlayHints.IncludeInlayFunctionLikeReturnTypeHints = core.TSTrue session.Configure(newPrefs) codeLensRefreshCalls := utils.Client().RefreshCodeLensCalls() inlayHintsRefreshCalls := utils.Client().RefreshInlayHintsCalls() assert.Equal(t, len(codeLensRefreshCalls), 1, "expected one RefreshCodeLens call after code lens preference change") assert.Equal(t, len(inlayHintsRefreshCalls), 1, "expected one RefreshInlayHints call after inlay hints preference change") }) t.Run("schedules diagnostics refresh when reportStyleChecksAsWarnings changes", func(t *testing.T) { t.Parallel() files := map[string]any{ "/src/tsconfig.json": "{}", "/src/index.ts": "export const x = 1;", } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) session.WaitForBackgroundTasks() // Record the baseline count of RefreshDiagnostics calls. baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls()) // Toggle reportStyleChecksAsWarnings (default is true, so set it to false). prefs := lsutil.NewDefaultUserPreferences() prefs.ReportStyleChecksAsWarnings = core.TSFalse session.Configure(prefs) session.WaitForBackgroundTasks() refreshCount := len(utils.Client().RefreshDiagnosticsCalls()) assert.Assert(t, refreshCount > baselineRefreshCount, "expected RefreshDiagnostics to be called after reportStyleChecksAsWarnings change, got %d calls (baseline %d)", refreshCount, baselineRefreshCount) }) t.Run("config parsing", func(t *testing.T) { t.Parallel() files := map[string]any{ "/src/tsconfig.json": "{}", "/src/index.ts": "export const x = 1;", } session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) configMap1 := map[string]any{ "preferences": map[string]any{ "useAliasesForRenames": true, "quoteStyle": "single", }, "unstable": map[string]any{ "organizeImportsSort": "ordinalIgnoreCase", }, } session.Configure(lsutil.ParseUserPreferences(map[string]any{"js/ts": configMap1})) actualConfig1 := session.Config() expectedPrefs1 := lsutil.NewDefaultUserPreferences() expectedPrefs1.UseAliasesForRename = core.TSTrue expectedPrefs1.QuotePreference = lsutil.QuotePreferenceSingle expectedPrefs1.OrganizeImportsSort = lsutil.OrganizeImportsSortOrdinalIgnoreCase assert.DeepEqual(t, actualConfig1, expectedPrefs1) configMap2 := map[string]any{ "preferences": map[string]any{ "useAliasesForRenames": false, "quoteStyle": "double", }, "unstable": map[string]any{ "organizeImportsSort": "ordinal", }, } session.Configure(lsutil.ParseUserPreferences(map[string]any{"js/ts": configMap2})) actualConfig2 := session.Config() expectedPrefs2 := lsutil.NewDefaultUserPreferences() expectedPrefs2.UseAliasesForRename = core.TSFalse expectedPrefs2.QuotePreference = lsutil.QuotePreferenceDouble expectedPrefs2.OrganizeImportsSort = lsutil.OrganizeImportsSortOrdinal assert.DeepEqual(t, actualConfig2, expectedPrefs2) }) t.Run("language service for closed files", func(t *testing.T) { t.Parallel() t.Run("closed file in configured project not yet opened", func(t *testing.T) { t.Parallel() // Set up a project where a tsconfig exists but no files have been opened. // Requesting language service for a file covered by that tsconfig should // work even though the file was never opened via didOpen. files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "strict": true }, "include": ["src"] }`, "/home/projects/TS/p1/src/index.ts": `export const x: number = 1;`, } session, _ := projecttestutil.Setup(files) // Do NOT open any file. Directly request language service for a closed file // that belongs to the configured project. ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) assert.Assert(t, ls != nil) program := ls.GetProgram() assert.Assert(t, program != nil) sourceFile := program.GetSourceFile("/home/projects/TS/p1/src/index.ts") assert.Assert(t, sourceFile != nil) assert.Equal(t, sourceFile.Text(), `export const x: number = 1;`) }) t.Run("closed file with no configured project creates inferred project", func(t *testing.T) { t.Parallel() // Set up a file that has no tsconfig. Requesting language service for it // should create an inferred project even though the file was never opened. files := map[string]any{ "/home/projects/TS/loose/index.ts": `const greeting: string = "hello";`, } session, _ := projecttestutil.Setup(files) // Do NOT open any file. Directly request language service for a closed file // that has no configured project. ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/loose/index.ts") assert.NilError(t, err) assert.Assert(t, ls != nil) program := ls.GetProgram() assert.Assert(t, program != nil) sourceFile := program.GetSourceFile("/home/projects/TS/loose/index.ts") assert.Assert(t, sourceFile != nil) assert.Equal(t, sourceFile.Text(), `const greeting: string = "hello";`) }) }) t.Run("jsconfig.json used for JS files when tsconfig.json exists in same directory", func(t *testing.T) { t.Parallel() files := map[string]any{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, "strict": true } }`, "/home/projects/TS/p1/jsconfig.json": `{ "compilerOptions": { "noLib": true, "checkJs": true } }`, "/home/projects/TS/p1/index.ts": `export const x: number = 1;`, "/home/projects/TS/p1/app.js": `/** @type {number} */ var y = "not a number";`, } session, _ := projecttestutil.Setup(files) // Open the JS file - it should be assigned to the jsconfig.json project, not tsconfig.json session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/app.js", 1, files["/home/projects/TS/p1/app.js"].(string), lsproto.LanguageKindJavaScript) snapshot := session.Snapshot() jsURI := lsproto.DocumentUri("file:///home/projects/TS/p1/app.js") defaultProject := snapshot.GetDefaultProject(jsURI) assert.Assert(t, defaultProject != nil, "JS file should have a default project") assert.Equal(t, defaultProject.Name(), "/home/projects/TS/p1/jsconfig.json", "JS file should belong to jsconfig.json project, not tsconfig.json") // Open the TS file - it should be assigned to tsconfig.json project session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.ts", 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) snapshot = session.Snapshot() tsURI := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts") defaultTSProject := snapshot.GetDefaultProject(tsURI) assert.Assert(t, defaultTSProject != nil, "TS file should have a default project") assert.Equal(t, defaultTSProject.Name(), "/home/projects/TS/p1/tsconfig.json", "TS file should belong to tsconfig.json project") }) }