@@ -217,4 +217,131 @@ func TestProjectLifetime(t *testing.T) {
217217 assert .Equal (t , len (snapshot .ProjectCollection .Projects ()), 1 )
218218 assert .Assert (t , snapshot .ConfigFileRegistry .GetConfig (tspath .Path ("/home/projects/ts/p1/tsconfig.json" )) != nil )
219219 })
220+
221+ t .Run ("file move from inferred to configured via didOpen/didClose sequence" , func (t * testing.T ) {
222+ t .Parallel ()
223+ // Start with tsconfig.json that includes "src" but file is at root level
224+ files := map [string ]any {
225+ "/home/projects/TS/p1/tsconfig.json" : `{
226+ "compilerOptions": {
227+ "noLib": true
228+ },
229+ "include": ["src"]
230+ }` ,
231+ "/home/projects/TS/p1/index.ts" : `export const x = 1;` ,
232+ }
233+ session , utils := projecttestutil .Setup (files )
234+
235+ // Open index.ts at root level - should create inferred project since it's not under src/
236+ // Creates config file registry entry, but has no files
237+ indexUri := lsproto .DocumentUri ("file:///home/projects/TS/p1/index.ts" )
238+ session .DidOpenFile (context .Background (), indexUri , 1 , files ["/home/projects/TS/p1/index.ts" ].(string ), lsproto .LanguageKindTypeScript )
239+
240+ // Should have one inferred project only (file is not included by tsconfig)
241+ snapshot , release := session .Snapshot ()
242+ defer release ()
243+ assert .Equal (t , len (snapshot .ProjectCollection .Projects ()), 1 )
244+ assert .Assert (t , snapshot .ProjectCollection .InferredProject () != nil )
245+ assert .Assert (t , snapshot .ProjectCollection .ConfiguredProject (tspath .Path ("/home/projects/ts/p1/tsconfig.json" )) == nil )
246+
247+ // Simulate file move: create src/index.ts on disk
248+ err := utils .FS ().WriteFile ("/home/projects/TS/p1/src/index.ts" , files ["/home/projects/TS/p1/index.ts" ].(string ), false )
249+ assert .NilError (t , err )
250+ err = utils .FS ().Remove ("/home/projects/TS/p1/index.ts" )
251+ assert .NilError (t , err )
252+
253+ // Simulate file move sequence as it would happen in an editor:
254+ // 1. didOpen src/index.ts (new location)
255+ // Open comes in before file create event, so the config file is not marked as needing a file name reload,
256+ // so it's not turned into a configured project yet. This is probably not ideal, but it should sort itself
257+ // out momentarily after the file watcher events are processed. When we try the config file, we mark it
258+ // as "retained by src/index.ts" so the config entry doesn't get deleted before src/index.ts is closed.
259+ // Even though we currently think src/index.ts doesn't belong to the config, the config is in its directory
260+ // path, so we'll always see it as a candidate for containing src/index.ts.
261+ srcIndexUri := lsproto .DocumentUri ("file:///home/projects/TS/p1/src/index.ts" )
262+ session .DidOpenFile (context .Background (), srcIndexUri , 1 , files ["/home/projects/TS/p1/index.ts" ].(string ), lsproto .LanguageKindTypeScript )
263+
264+ // 2. didClose index.ts (old location)
265+ session .DidCloseFile (context .Background (), indexUri )
266+
267+ // 3. didChangeWatchedFiles: create src/index.ts and delete index.ts
268+ // The creation event for src/index.ts now hits the config file registry, and we should notice we
269+ // got a creation event for a file that retained the config, triggering a filename reload.
270+ session .DidChangeWatchedFiles (context .Background (), []* lsproto.FileEvent {
271+ {
272+ Uri : srcIndexUri ,
273+ Type : lsproto .FileChangeTypeCreated ,
274+ },
275+ {
276+ Uri : indexUri ,
277+ Type : lsproto .FileChangeTypeDeleted ,
278+ },
279+ })
280+
281+ // Should now have one configured project only (file is now under src/)
282+ _ , err = session .GetLanguageService (context .Background (), srcIndexUri )
283+ assert .NilError (t , err )
284+ snapshot , release = session .Snapshot ()
285+ defer release ()
286+ assert .Equal (t , len (snapshot .ProjectCollection .Projects ()), 1 )
287+ assert .Assert (t , snapshot .ProjectCollection .InferredProject () == nil )
288+ assert .Assert (t , snapshot .ProjectCollection .ConfiguredProject (tspath .Path ("/home/projects/ts/p1/tsconfig.json" )) != nil )
289+ })
290+
291+ t .Run ("tsconfig move from subdirectory to parent via didChangeWatchedFiles" , func (t * testing.T ) {
292+ t .Parallel ()
293+ // Start with tsconfig.json in src/ that includes "src" - file won't be included initially
294+ files := map [string ]any {
295+ "/home/projects/TS/p1/src/tsconfig.json" : `{
296+ "compilerOptions": {
297+ "noLib": true
298+ },
299+ "include": ["src"]
300+ }` ,
301+ "/home/projects/TS/p1/src/index.ts" : `export const x = 1;` ,
302+ }
303+ session , utils := projecttestutil .Setup (files )
304+
305+ // Open src/index.ts - should create inferred project since tsconfig.json includes "src"
306+ // relative to its location (src/src/ which doesn't exist)
307+ indexUri := lsproto .DocumentUri ("file:///home/projects/TS/p1/src/index.ts" )
308+ session .DidOpenFile (context .Background (), indexUri , 1 , files ["/home/projects/TS/p1/src/index.ts" ].(string ), lsproto .LanguageKindTypeScript )
309+
310+ // Should have one inferred project only (file is not included by tsconfig at src/tsconfig.json)
311+ snapshot , release := session .Snapshot ()
312+ defer release ()
313+ assert .Equal (t , len (snapshot .ProjectCollection .Projects ()), 1 )
314+ assert .Assert (t , snapshot .ProjectCollection .InferredProject () != nil )
315+ assert .Assert (t , snapshot .ProjectCollection .ConfiguredProject (tspath .Path ("/home/projects/ts/p1/src/tsconfig.json" )) == nil )
316+
317+ // Simulate tsconfig.json move: create tsconfig.json at parent level, delete from src/
318+ tsconfigContent := files ["/home/projects/TS/p1/src/tsconfig.json" ].(string )
319+ err := utils .FS ().WriteFile ("/home/projects/TS/p1/tsconfig.json" , tsconfigContent , false )
320+ assert .NilError (t , err )
321+ err = utils .FS ().Remove ("/home/projects/TS/p1/src/tsconfig.json" )
322+ assert .NilError (t , err )
323+
324+ // Simulate file move via didChangeWatchedFiles
325+ newTsconfigUri := lsproto .DocumentUri ("file:///home/projects/TS/p1/tsconfig.json" )
326+ oldTsconfigUri := lsproto .DocumentUri ("file:///home/projects/TS/p1/src/tsconfig.json" )
327+ session .DidChangeWatchedFiles (context .Background (), []* lsproto.FileEvent {
328+ {
329+ Uri : newTsconfigUri ,
330+ Type : lsproto .FileChangeTypeCreated ,
331+ },
332+ {
333+ Uri : oldTsconfigUri ,
334+ Type : lsproto .FileChangeTypeDeleted ,
335+ },
336+ })
337+
338+ // Should now have one configured project only (tsconfig.json now includes src/index.ts)
339+ _ , err = session .GetLanguageService (context .Background (), indexUri )
340+ assert .NilError (t , err )
341+ snapshot , release = session .Snapshot ()
342+ defer release ()
343+ assert .Equal (t , len (snapshot .ProjectCollection .Projects ()), 1 )
344+ assert .Assert (t , snapshot .ProjectCollection .InferredProject () == nil )
345+ assert .Assert (t , snapshot .ProjectCollection .ConfiguredProject (tspath .Path ("/home/projects/ts/p1/tsconfig.json" )) != nil )
346+ })
220347}
0 commit comments