487 lines
23 KiB
Go
487 lines
23 KiB
Go
package project_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/microsoft/typescript-go/internal/bundled"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
func TestProjectLifetime(t *testing.T) {
|
|
t.Parallel()
|
|
if !bundled.Embedded {
|
|
t.Skip("bundled files are not embedded")
|
|
}
|
|
|
|
t.Run("configured project", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := 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;`,
|
|
"/home/projects/TS/p2/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true,
|
|
"module": "nodenext",
|
|
"strict": true
|
|
},
|
|
"include": ["src"]
|
|
}`,
|
|
"/home/projects/TS/p2/src/index.ts": `import { x } from "./x";`,
|
|
"/home/projects/TS/p2/src/x.ts": `export const x = 1;`,
|
|
"/home/projects/TS/p2/config.ts": `let x = 1, y = 2;`,
|
|
"/home/projects/TS/p3/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true,
|
|
"module": "nodenext",
|
|
"strict": true
|
|
},
|
|
"include": ["src"]
|
|
}`,
|
|
"/home/projects/TS/p3/src/index.ts": `import { x } from "./x";`,
|
|
"/home/projects/TS/p3/src/x.ts": `export const x = 1;`,
|
|
"/home/projects/TS/p3/config.ts": `let x = 1, y = 2;`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
snapshot := session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0)
|
|
|
|
// Open files in two projects
|
|
uri1 := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
|
uri2 := lsproto.DocumentUri("file:///home/projects/TS/p2/src/index.ts")
|
|
session.DidOpenFile(context.Background(), uri1, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.DidOpenFile(context.Background(), uri2, 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.WaitForBackgroundTasks()
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
|
|
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
|
|
|
|
// Close p1 file and open p3 file
|
|
session.DidCloseFile(context.Background(), uri1)
|
|
uri3 := lsproto.DocumentUri("file:///home/projects/TS/p3/src/index.ts")
|
|
session.DidOpenFile(context.Background(), uri3, 1, files["/home/projects/TS/p3/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.WaitForBackgroundTasks()
|
|
// Should still have two projects, but p1 replaced by p3
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p3/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) != nil)
|
|
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
|
|
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0)
|
|
|
|
// Close p2 and p3 files, open p1 file again
|
|
session.DidCloseFile(context.Background(), uri2)
|
|
session.DidCloseFile(context.Background(), uri3)
|
|
session.DidOpenFile(context.Background(), uri1, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.WaitForBackgroundTasks()
|
|
// Should have one project (p1)
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) == nil)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) == nil)
|
|
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
|
|
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0)
|
|
})
|
|
|
|
t.Run("unrooted inferred projects", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/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;`,
|
|
"/home/projects/TS/p2/src/index.ts": `import { x } from "./x";`,
|
|
"/home/projects/TS/p2/src/x.ts": `export const x = 1;`,
|
|
"/home/projects/TS/p2/config.ts": `let x = 1, y = 2;`,
|
|
"/home/projects/TS/p3/src/index.ts": `import { x } from "./x";`,
|
|
"/home/projects/TS/p3/src/x.ts": `export const x = 1;`,
|
|
"/home/projects/TS/p3/config.ts": `let x = 1, y = 2;`,
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
snapshot := session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 0)
|
|
|
|
// Open files without workspace roots (empty string) - should create single inferred project
|
|
uri1 := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
|
uri2 := lsproto.DocumentUri("file:///home/projects/TS/p2/src/index.ts")
|
|
session.DidOpenFile(context.Background(), uri1, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.DidOpenFile(context.Background(), uri2, 1, files["/home/projects/TS/p2/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should have one inferred project
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
|
|
// Close p1 file and open p3 file
|
|
session.DidCloseFile(context.Background(), uri1)
|
|
uri3 := lsproto.DocumentUri("file:///home/projects/TS/p3/src/index.ts")
|
|
session.DidOpenFile(context.Background(), uri3, 1, files["/home/projects/TS/p3/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should still have one inferred project
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
|
|
// Close p2 and p3 files, open p1 file again
|
|
session.DidCloseFile(context.Background(), uri2)
|
|
session.DidCloseFile(context.Background(), uri3)
|
|
session.DidOpenFile(context.Background(), uri1, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should still have one inferred project
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
})
|
|
|
|
t.Run("file moves from inferred to configured project", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := map[string]any{
|
|
"/home/projects/ts/foo.ts": `export const foo = 1;`,
|
|
"/home/projects/ts/p1/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true,
|
|
"module": "nodenext",
|
|
"strict": true
|
|
},
|
|
"include": ["main.ts"]
|
|
}`,
|
|
"/home/projects/ts/p1/main.ts": `import { foo } from "../foo"; console.log(foo);`,
|
|
}
|
|
session, _ := projecttestutil.Setup(files)
|
|
|
|
// Open foo.ts first - should create inferred project since no tsconfig found initially
|
|
fooUri := lsproto.DocumentUri("file:///home/projects/ts/foo.ts")
|
|
session.DidOpenFile(context.Background(), fooUri, 1, files["/home/projects/ts/foo.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should have one inferred project
|
|
snapshot := session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil)
|
|
|
|
// Now open main.ts - should trigger discovery of tsconfig.json and move foo.ts to configured project
|
|
mainUri := lsproto.DocumentUri("file:///home/projects/ts/p1/main.ts")
|
|
session.DidOpenFile(context.Background(), mainUri, 1, files["/home/projects/ts/p1/main.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should now have one configured project and no inferred project
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
|
|
// Config file should be present
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
|
|
// Close main.ts - configured project should remain because foo.ts is still open
|
|
session.DidCloseFile(context.Background(), mainUri)
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
|
|
// Close foo.ts - configured project should be retained until next file open
|
|
session.DidCloseFile(context.Background(), fooUri)
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
})
|
|
|
|
t.Run("file move from inferred to configured via didOpen/didClose sequence", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Start with tsconfig.json that includes "src" but file is at root level
|
|
files := map[string]any{
|
|
"/home/projects/TS/p1/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true
|
|
},
|
|
"include": ["src"]
|
|
}`,
|
|
"/home/projects/TS/p1/index.ts": `export const x = 1;`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
|
|
// Open index.ts at root level - should create inferred project since it's not under src/
|
|
// Creates config file registry entry, but has no files
|
|
indexUri := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts")
|
|
session.DidOpenFile(context.Background(), indexUri, 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should have one inferred project only (file is not included by tsconfig)
|
|
snapshot := session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil)
|
|
|
|
// Simulate file move: create src/index.ts on disk
|
|
err := utils.FS().WriteFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/index.ts"].(string))
|
|
assert.NilError(t, err)
|
|
err = utils.FS().Remove("/home/projects/TS/p1/index.ts")
|
|
assert.NilError(t, err)
|
|
|
|
// Simulate file move sequence as it would happen in an editor:
|
|
// 1. didOpen src/index.ts (new location)
|
|
// Open comes in before file create event, so the config file is not marked as needing a file name reload,
|
|
// so it's not turned into a configured project yet. This is probably not ideal, but it should sort itself
|
|
// out momentarily after the file watcher events are processed. When we try the config file, we mark it
|
|
// as "retained by src/index.ts" so the config entry doesn't get deleted before src/index.ts is closed.
|
|
// Even though we currently think src/index.ts doesn't belong to the config, the config is in its directory
|
|
// path, so we'll always see it as a candidate for containing src/index.ts.
|
|
srcIndexUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
|
session.DidOpenFile(context.Background(), srcIndexUri, 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// 2. didClose index.ts (old location)
|
|
session.DidCloseFile(context.Background(), indexUri)
|
|
|
|
// 3. didChangeWatchedFiles: create src/index.ts and delete index.ts
|
|
// The creation event for src/index.ts now hits the config file registry, and we should notice we
|
|
// got a creation event for a file that retained the config, triggering a filename reload.
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
|
|
{
|
|
Uri: srcIndexUri,
|
|
Type: lsproto.FileChangeTypeCreated,
|
|
},
|
|
{
|
|
Uri: indexUri,
|
|
Type: lsproto.FileChangeTypeDeleted,
|
|
},
|
|
})
|
|
|
|
// Should now have one configured project only (file is now under src/)
|
|
_, err = session.GetLanguageService(context.Background(), srcIndexUri)
|
|
assert.NilError(t, err)
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
})
|
|
|
|
// Regression test for https://github.com/microsoft/typescript-go/issues/3733
|
|
//
|
|
// "./"-prefixed include specs combined with a dot-directory exclude ("**/.*/")
|
|
// used to drop all wildcard directories for the config, so a newly created file
|
|
// never triggered a root file reload. The stale nested config never claimed the
|
|
// file, and default-project resolution assigned it to the ancestor project
|
|
// (parsed fresh from disk, so it did contain the file) — or the inferred project
|
|
// when no ancestor config matched — until the session was restarted. The file
|
|
// then had the wrong compiler options (e.g. no Temporal from lib ESNext).
|
|
// The failure was independent of the order in which didOpen and the create
|
|
// watch event arrived, so both orderings are covered.
|
|
runNewFileScenario := func(t *testing.T, openBeforeCreateEvent bool) {
|
|
files := map[string]any{
|
|
"/home/projects/TS/monorepo/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "ES2015",
|
|
"lib": ["DOM"]
|
|
},
|
|
"include": ["apps"]
|
|
}`,
|
|
"/home/projects/TS/monorepo/apps/web/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"target": "ESNext",
|
|
"lib": ["ESNext"]
|
|
},
|
|
"include": ["./app/**/*.ts"],
|
|
"exclude": ["**/.*/"]
|
|
}`,
|
|
"/home/projects/TS/monorepo/apps/web/app/existing.ts": `export const existing = 1;`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
|
|
// Open an existing file so the nested configured project loads.
|
|
existingUri := lsproto.DocumentUri("file:///home/projects/TS/monorepo/apps/web/app/existing.ts")
|
|
session.DidOpenFile(context.Background(), existingUri, 1, files["/home/projects/TS/monorepo/apps/web/app/existing.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
ls, err := session.GetLanguageService(context.Background(), existingUri)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext)
|
|
|
|
// Create a new file on disk in a directory matched by the nested config's
|
|
// wildcard include, then simulate the editor events in the given order.
|
|
newFileContent := `export const newFile = 1;`
|
|
newFileUri := lsproto.DocumentUri("file:///home/projects/TS/monorepo/apps/web/app/subdir/new.ts")
|
|
assert.NilError(t, utils.FS().WriteFile("/home/projects/TS/monorepo/apps/web/app/subdir/new.ts", newFileContent))
|
|
openNewFile := func() {
|
|
session.DidOpenFile(context.Background(), newFileUri, 1, newFileContent, lsproto.LanguageKindTypeScript)
|
|
}
|
|
sendCreateEvent := func() {
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
|
|
{Uri: newFileUri, Type: lsproto.FileChangeTypeCreated},
|
|
})
|
|
}
|
|
if openBeforeCreateEvent {
|
|
openNewFile()
|
|
sendCreateEvent()
|
|
} else {
|
|
sendCreateEvent()
|
|
openNewFile()
|
|
}
|
|
|
|
// The new file should be served by the nested configured project, not the
|
|
// root project (ES2015 target) or an inferred project.
|
|
ls, err = session.GetLanguageService(context.Background(), newFileUri)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext)
|
|
assert.Assert(t, session.Snapshot().ProjectCollection.InferredProject() == nil)
|
|
}
|
|
|
|
t.Run("newly created file joins configured project with ./-prefixed includes (didOpen before create event)", func(t *testing.T) {
|
|
t.Parallel()
|
|
runNewFileScenario(t, true)
|
|
})
|
|
|
|
t.Run("newly created file joins configured project with ./-prefixed includes (create event before didOpen)", func(t *testing.T) {
|
|
t.Parallel()
|
|
runNewFileScenario(t, false)
|
|
})
|
|
|
|
t.Run("tsconfig move from subdirectory to parent via didChangeWatchedFiles", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Start with tsconfig.json in src/ that includes "src" - file won't be included initially
|
|
files := map[string]any{
|
|
"/home/projects/TS/p1/src/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true
|
|
},
|
|
"include": ["src"]
|
|
}`,
|
|
"/home/projects/TS/p1/src/index.ts": `export const x = 1;`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
|
|
// Open src/index.ts - should create inferred project since tsconfig.json includes "src"
|
|
// relative to its location (src/src/ which doesn't exist)
|
|
indexUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
|
session.DidOpenFile(context.Background(), indexUri, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Should have one inferred project only (file is not included by tsconfig at src/tsconfig.json)
|
|
snapshot := session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/src/tsconfig.json")) == nil)
|
|
|
|
// Simulate tsconfig.json move: create tsconfig.json at parent level, delete from src/
|
|
tsconfigContent := files["/home/projects/TS/p1/src/tsconfig.json"].(string)
|
|
err := utils.FS().WriteFile("/home/projects/TS/p1/tsconfig.json", tsconfigContent)
|
|
assert.NilError(t, err)
|
|
err = utils.FS().Remove("/home/projects/TS/p1/src/tsconfig.json")
|
|
assert.NilError(t, err)
|
|
|
|
// Simulate file move via didChangeWatchedFiles
|
|
newTsconfigUri := lsproto.DocumentUri("file:///home/projects/TS/p1/tsconfig.json")
|
|
oldTsconfigUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/tsconfig.json")
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
|
|
{
|
|
Uri: newTsconfigUri,
|
|
Type: lsproto.FileChangeTypeCreated,
|
|
},
|
|
{
|
|
Uri: oldTsconfigUri,
|
|
Type: lsproto.FileChangeTypeDeleted,
|
|
},
|
|
})
|
|
|
|
// Should now have one configured project only (tsconfig.json now includes src/index.ts)
|
|
_, err = session.GetLanguageService(context.Background(), indexUri)
|
|
assert.NilError(t, err)
|
|
snapshot = session.Snapshot()
|
|
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1)
|
|
assert.Assert(t, snapshot.ProjectCollection.InferredProject() == nil)
|
|
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
|
|
})
|
|
|
|
t.Run("deleted open file remains in project until closed", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Scenario:
|
|
// 1. Start with two files included by a tsconfig, both open
|
|
// 2. In a single batch change, delete one of the files but leave it open, and create a new file included by the tsconfig
|
|
// 3. Request a LS for the deleted but open file
|
|
// 4. Project should include both the new file and the deleted open file
|
|
// 5. Close the deleted file
|
|
// 6. On next LS request, the project should exclude the deleted file
|
|
|
|
files := map[string]any{
|
|
"/home/projects/TS/p1/tsconfig.json": `{
|
|
"compilerOptions": {
|
|
"noLib": true
|
|
},
|
|
"include": ["src"]
|
|
}`,
|
|
"/home/projects/TS/p1/src/index.ts": ``,
|
|
"/home/projects/TS/p1/src/x.ts": `export const x = 1;`,
|
|
}
|
|
session, utils := projecttestutil.Setup(files)
|
|
|
|
// Step 1: Open both files
|
|
indexUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/index.ts")
|
|
xUri := lsproto.DocumentUri("file:///home/projects/TS/p1/src/x.ts")
|
|
session.DidOpenFile(context.Background(), indexUri, 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
session.DidOpenFile(context.Background(), xUri, 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript)
|
|
|
|
// Verify initial state - both files should be in the project
|
|
ls, err := session.GetLanguageService(context.Background(), indexUri)
|
|
assert.NilError(t, err)
|
|
program := ls.GetProgram()
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/index.ts") != nil, "index.ts should be in project")
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil, "x.ts should be in project")
|
|
|
|
// Step 2: In a single batch change:
|
|
// - Delete x.ts from disk (but leave it open)
|
|
// - Create a new file y.ts on disk
|
|
err = utils.FS().Remove("/home/projects/TS/p1/src/x.ts")
|
|
assert.NilError(t, err)
|
|
err = utils.FS().WriteFile("/home/projects/TS/p1/src/y.ts", `export const y = 2;`)
|
|
assert.NilError(t, err)
|
|
|
|
// Send both events in a single batch
|
|
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
|
|
{
|
|
Uri: xUri,
|
|
Type: lsproto.FileChangeTypeDeleted,
|
|
},
|
|
{
|
|
Uri: lsproto.DocumentUri("file:///home/projects/TS/p1/src/y.ts"),
|
|
Type: lsproto.FileChangeTypeCreated,
|
|
},
|
|
})
|
|
|
|
// Step 3 & 4: Request LS for the deleted but still open file
|
|
// Project should include: index.ts, x.ts (open overlay), y.ts (new disk file)
|
|
ls, err = session.GetLanguageService(context.Background(), xUri)
|
|
assert.NilError(t, err)
|
|
program = ls.GetProgram()
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/index.ts") != nil, "index.ts should still be in project")
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil, "x.ts should still be in project (open overlay)")
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil, "y.ts should be in project (new file)")
|
|
|
|
// Step 5: Close the deleted file
|
|
session.DidCloseFile(context.Background(), xUri)
|
|
|
|
// Step 6: On next LS request, x.ts should be excluded
|
|
ls, err = session.GetLanguageService(context.Background(), indexUri)
|
|
assert.NilError(t, err)
|
|
program = ls.GetProgram()
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/index.ts") != nil, "index.ts should still be in project")
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil, "x.ts should no longer be in project (closed and deleted)")
|
|
assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil, "y.ts should still be in project")
|
|
})
|
|
}
|