• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.projectSystem {
2    describe("unittests:: tsserver:: Projects", () => {
3        it("handles the missing files - that were added to program because they were added with ///<ref", () => {
4            const file1: File = {
5                path: "/a/b/commonFile1.ts",
6                content: `/// <reference path="commonFile2.ts"/>
7                    let x = y`
8            };
9            const host = createServerHost([file1, libFile]);
10            const session = createSession(host);
11            openFilesForSession([file1], session);
12            const projectService = session.getProjectService();
13
14            checkNumberOfInferredProjects(projectService, 1);
15            const project = projectService.inferredProjects[0];
16            checkProjectRootFiles(project, [file1.path]);
17            checkProjectActualFiles(project, [file1.path, libFile.path]);
18            const getErrRequest = makeSessionRequest<server.protocol.SemanticDiagnosticsSyncRequestArgs>(
19                server.CommandNames.SemanticDiagnosticsSync,
20                { file: file1.path }
21            );
22
23            // Two errors: CommonFile2 not found and cannot find name y
24            let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
25            verifyDiagnostics(diags, [
26                { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] },
27                { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] }
28            ]);
29
30            host.writeFile(commonFile2.path, commonFile2.content);
31            host.runQueuedTimeoutCallbacks();
32            checkNumberOfInferredProjects(projectService, 1);
33            assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same");
34            checkProjectRootFiles(project, [file1.path]);
35            checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]);
36            diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[];
37            verifyNoDiagnostics(diags);
38        });
39
40        it("should create new inferred projects for files excluded from a configured project", () => {
41            const configFile: File = {
42                path: "/a/b/tsconfig.json",
43                content: `{
44                    "compilerOptions": {},
45                    "files": ["${commonFile1.path}", "${commonFile2.path}"]
46                }`
47            };
48            const files = [commonFile1, commonFile2, configFile];
49            const host = createServerHost(files);
50            const projectService = createProjectService(host);
51            projectService.openClientFile(commonFile1.path);
52
53            const project = configuredProjectAt(projectService, 0);
54            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
55            configFile.content = `{
56                "compilerOptions": {},
57                "files": ["${commonFile1.path}"]
58            }`;
59            host.writeFile(configFile.path, configFile.content);
60
61            checkNumberOfConfiguredProjects(projectService, 1);
62            checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
63            host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects
64            checkNumberOfConfiguredProjects(projectService, 1);
65            checkProjectRootFiles(project, [commonFile1.path]);
66
67            projectService.openClientFile(commonFile2.path);
68            checkNumberOfInferredProjects(projectService, 1);
69        });
70
71        it("should disable features when the files are too large", () => {
72            const file1 = {
73                path: "/a/b/f1.js",
74                content: "let x =1;",
75                fileSize: 10 * 1024 * 1024
76            };
77            const file2 = {
78                path: "/a/b/f2.js",
79                content: "let y =1;",
80                fileSize: 6 * 1024 * 1024
81            };
82            const file3 = {
83                path: "/a/b/f3.js",
84                content: "let y =1;",
85                fileSize: 6 * 1024 * 1024
86            };
87
88            const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3";
89
90            const host = createServerHost([file1, file2, file3]);
91            const projectService = createProjectService(host);
92
93            projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name });
94            const proj1 = projectService.findProject(proj1name)!;
95            assert.isTrue(proj1.languageServiceEnabled);
96
97            projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name });
98            const proj2 = projectService.findProject(proj2name)!;
99            assert.isTrue(proj2.languageServiceEnabled);
100
101            projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name });
102            const proj3 = projectService.findProject(proj3name)!;
103            assert.isFalse(proj3.languageServiceEnabled);
104        });
105
106        it("should not crash when opening a file in a project with a disabled language service", () => {
107            const file1 = {
108                path: "/a/b/f1.js",
109                content: "let x =1;",
110                fileSize: 50 * 1024 * 1024
111            };
112            const file2 = {
113                path: "/a/b/f2.js",
114                content: "let x =1;",
115                fileSize: 100
116            };
117
118            const projName = "proj1";
119
120            const host = createServerHost([file1, file2]);
121            const projectService = createProjectService(host, { useSingleInferredProject: true, eventHandler: noop });
122
123            projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName });
124            const proj1 = projectService.findProject(projName)!;
125            assert.isFalse(proj1.languageServiceEnabled);
126
127            assert.doesNotThrow(() => projectService.openClientFile(file2.path));
128        });
129
130        describe("ignoreConfigFiles", () => {
131            it("external project including config file", () => {
132                const file1 = {
133                    path: "/a/b/f1.ts",
134                    content: "let x =1;"
135                };
136                const config1 = {
137                    path: "/a/b/tsconfig.json",
138                    content: JSON.stringify(
139                        {
140                            compilerOptions: {},
141                            files: ["f1.ts"]
142                        }
143                    )
144                };
145
146                const externalProjectName = "externalproject";
147                const host = createServerHost([file1, config1]);
148                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
149                projectService.openExternalProject({
150                    rootFiles: toExternalFiles([file1.path, config1.path]),
151                    options: {},
152                    projectFileName: externalProjectName
153                });
154
155                checkNumberOfProjects(projectService, { externalProjects: 1 });
156                const proj = projectService.externalProjects[0];
157                assert.isDefined(proj);
158
159                assert.isTrue(proj.fileExists(file1.path));
160            });
161
162            it("loose file included in config file (openClientFile)", () => {
163                const file1 = {
164                    path: "/a/b/f1.ts",
165                    content: "let x =1;"
166                };
167                const config1 = {
168                    path: "/a/b/tsconfig.json",
169                    content: JSON.stringify(
170                        {
171                            compilerOptions: {},
172                            files: ["f1.ts"]
173                        }
174                    )
175                };
176
177                const host = createServerHost([file1, config1]);
178                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
179                projectService.openClientFile(file1.path, file1.content);
180
181                checkNumberOfProjects(projectService, { inferredProjects: 1 });
182                const proj = projectService.inferredProjects[0];
183                assert.isDefined(proj);
184
185                assert.isTrue(proj.fileExists(file1.path));
186            });
187
188            it("loose file included in config file (applyCodeChanges)", () => {
189                const file1 = {
190                    path: "/a/b/f1.ts",
191                    content: "let x =1;"
192                };
193                const config1 = {
194                    path: "/a/b/tsconfig.json",
195                    content: JSON.stringify(
196                        {
197                            compilerOptions: {},
198                            files: ["f1.ts"]
199                        }
200                    )
201                };
202
203                const host = createServerHost([file1, config1]);
204                const projectService = createProjectService(host, { useSingleInferredProject: true, syntaxOnly: true });
205                projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content }));
206
207                checkNumberOfProjects(projectService, { inferredProjects: 1 });
208                const proj = projectService.inferredProjects[0];
209                assert.isDefined(proj);
210
211                assert.isTrue(proj.fileExists(file1.path));
212            });
213        });
214
215        it("reload regular file after closing", () => {
216            const f1 = {
217                path: "/a/b/app.ts",
218                content: "x."
219            };
220            const f2 = {
221                path: "/a/b/lib.ts",
222                content: "let x: number;"
223            };
224
225            const host = createServerHost([f1, f2, libFile]);
226            const service = createProjectService(host);
227            service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} });
228
229            service.openClientFile(f1.path);
230            service.openClientFile(f2.path, "let x: string");
231
232            service.checkNumberOfProjects({ externalProjects: 1 });
233            checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
234
235            const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!;
236            // should contain completions for string
237            assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'");
238            assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'");
239
240            service.closeClientFile(f2.path);
241            const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!;
242            // should contain completions for string
243            assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'");
244            assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'");
245        });
246
247        it("clear mixed content file after closing", () => {
248            const f1 = {
249                path: "/a/b/app.ts",
250                content: " "
251            };
252            const f2 = {
253                path: "/a/b/lib.html",
254                content: "<html/>"
255            };
256
257            const host = createServerHost([f1, f2, libFile]);
258            const service = createProjectService(host);
259            service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} });
260
261            service.openClientFile(f1.path);
262            service.openClientFile(f2.path, "let somelongname: string");
263
264            service.checkNumberOfProjects({ externalProjects: 1 });
265            checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]);
266
267            const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!;
268            assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'");
269
270            service.closeClientFile(f2.path);
271            const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!;
272            assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'");
273            const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!;
274            assert.equal(sf2.text, "");
275        });
276
277        it("changes in closed files are reflected in project structure", () => {
278            const file1 = {
279                path: "/a/b/f1.ts",
280                content: `export * from "./f2"`
281            };
282            const file2 = {
283                path: "/a/b/f2.ts",
284                content: `export let x = 1`
285            };
286            const file3 = {
287                path: "/a/c/f3.ts",
288                content: `export let y = 1;`
289            };
290            const host = createServerHost([file1, file2, file3]);
291            const projectService = createProjectService(host);
292
293            projectService.openClientFile(file1.path);
294            checkNumberOfProjects(projectService, { inferredProjects: 1 });
295            const inferredProject0 = projectService.inferredProjects[0];
296            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]);
297
298            projectService.openClientFile(file3.path);
299            checkNumberOfProjects(projectService, { inferredProjects: 2 });
300            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
301            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]);
302            const inferredProject1 = projectService.inferredProjects[1];
303            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
304
305            host.writeFile(file2.path, `export * from "../c/f3"`); // now inferred project should inclule file3
306            host.checkTimeoutQueueLengthAndRun(2);
307            checkNumberOfProjects(projectService, { inferredProjects: 2 });
308            assert.strictEqual(projectService.inferredProjects[0], inferredProject0);
309            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
310            assert.strictEqual(projectService.inferredProjects[1], inferredProject1);
311            assert.isTrue(inferredProject1.isOrphan());
312        });
313
314        it("deleted files affect project structure", () => {
315            const file1 = {
316                path: "/a/b/f1.ts",
317                content: `export * from "./f2"`
318            };
319            const file2 = {
320                path: "/a/b/f2.ts",
321                content: `export * from "../c/f3"`
322            };
323            const file3 = {
324                path: "/a/c/f3.ts",
325                content: `export let y = 1;`
326            };
327            const host = createServerHost([file1, file2, file3]);
328            const projectService = createProjectService(host);
329
330            projectService.openClientFile(file1.path);
331
332            checkNumberOfProjects(projectService, { inferredProjects: 1 });
333
334            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
335
336            projectService.openClientFile(file3.path);
337            checkNumberOfProjects(projectService, { inferredProjects: 1 });
338
339            host.deleteFile(file2.path);
340            host.checkTimeoutQueueLengthAndRun(2);
341
342            checkNumberOfProjects(projectService, { inferredProjects: 2 });
343
344            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
345            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
346        });
347
348        it("ignores files excluded by a custom safe type list", () => {
349            const file1 = {
350                path: "/a/b/f1.js",
351                content: "export let x = 5"
352            };
353            const office = {
354                path: "/lib/duckquack-3.min.js",
355                content: "whoa do @@ not parse me ok thanks!!!"
356            };
357            const host = createServerHost([file1, office, customTypesMap]);
358            const projectService = createProjectService(host);
359            try {
360                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) });
361                const proj = projectService.externalProjects[0];
362                assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]);
363                assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]);
364            }
365            finally {
366                projectService.resetSafeList();
367            }
368        });
369
370        it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => {
371            const file1 = {
372                path: "/a/b/f1.js",
373                content: `export let x = 5; import { s } from "s"`
374            };
375            const constructorFile = {
376                path: "/a/b/constructor.js",
377                content: "const x = 10;"
378            };
379            const bliss = {
380                path: "/a/b/bliss.js",
381                content: "export function is() { return true; }"
382            };
383            const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]);
384            let request: string | undefined;
385            const cachePath = "/a/data";
386            const typingsInstaller: server.ITypingsInstaller = {
387                isKnownTypesPackageName: returnFalse,
388                installPackage: notImplemented,
389                enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => {
390                    assert.isUndefined(request);
391                    request = JSON.stringify(server.createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || server.emptyArray, cachePath));
392                },
393                attach: noop,
394                onProjectClosed: noop,
395                globalTypingsCacheLocation: cachePath
396            };
397
398            const projectName = "project";
399            const projectService = createProjectService(host, { typingsInstaller });
400            projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) });
401            assert.equal(request, JSON.stringify({
402                projectName,
403                fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path],
404                compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true },
405                typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true },
406                unresolvedImports: ["s"],
407                projectRootPath: "/",
408                cachePath,
409                kind: "discover"
410            }));
411            const response = JSON.parse(request!);
412            request = undefined;
413            projectService.updateTypingsForProject({
414                kind: "action::set",
415                projectName: response.projectName,
416                typeAcquisition: response.typeAcquisition,
417                compilerOptions: response.compilerOptions,
418                typings: emptyArray,
419                unresolvedImports: response.unresolvedImports,
420            });
421
422            host.checkTimeoutQueueLength(0);
423            assert.isUndefined(request);
424        });
425
426        it("ignores files excluded by the default type list", () => {
427            const file1 = {
428                path: "/a/b/f1.js",
429                content: "export let x = 5"
430            };
431            const minFile = {
432                path: "/c/moment.min.js",
433                content: "unspecified"
434            };
435            const kendoFile1 = {
436                path: "/q/lib/kendo/kendo.all.min.js",
437                content: "unspecified"
438            };
439            const kendoFile2 = {
440                path: "/q/lib/kendo/kendo.ui.min.js",
441                content: "unspecified"
442            };
443            const kendoFile3 = {
444                path: "/q/lib/kendo-ui/kendo.all.js",
445                content: "unspecified"
446            };
447            const officeFile1 = {
448                path: "/scripts/Office/1/excel-15.debug.js",
449                content: "unspecified"
450            };
451            const officeFile2 = {
452                path: "/scripts/Office/1/powerpoint.js",
453                content: "unspecified"
454            };
455            const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2];
456            const host = createServerHost(files);
457            const projectService = createProjectService(host);
458            try {
459                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) });
460                const proj = projectService.externalProjects[0];
461                assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]);
462                assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]);
463            }
464            finally {
465                projectService.resetSafeList();
466            }
467        });
468
469        it("removes version numbers correctly", () => {
470            const testData: [string, string][] = [
471                ["jquery-max", "jquery-max"],
472                ["jquery.min", "jquery"],
473                ["jquery-min.4.2.3", "jquery"],
474                ["jquery.min.4.2.1", "jquery"],
475                ["minimum", "minimum"],
476                ["min", "min"],
477                ["min.3.2", "min"],
478                ["jquery", "jquery"]
479            ];
480            for (const t of testData) {
481                assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]);
482            }
483        });
484
485        it("ignores files excluded by a legacy safe type list", () => {
486            const file1 = {
487                path: "/a/b/bliss.js",
488                content: "let x = 5"
489            };
490            const file2 = {
491                path: "/a/b/foo.js",
492                content: ""
493            };
494            const file3 = {
495                path: "/a/b/Bacon.js",
496                content: "let y = 5"
497            };
498            const host = createServerHost([file1, file2, file3, customTypesMap]);
499            const projectService = createProjectService(host);
500            try {
501                projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } });
502                const proj = projectService.externalProjects[0];
503                assert.deepEqual(proj.getFileNames(), [file2.path]);
504            }
505            finally {
506                projectService.resetSafeList();
507            }
508        });
509
510        it("correctly migrate files between projects", () => {
511            const file1 = {
512                path: "/a/b/f1.ts",
513                content: `
514                export * from "../c/f2";
515                export * from "../d/f3";`
516            };
517            const file2 = {
518                path: "/a/c/f2.ts",
519                content: "export let x = 1;"
520            };
521            const file3 = {
522                path: "/a/d/f3.ts",
523                content: "export let y = 1;"
524            };
525            const host = createServerHost([file1, file2, file3]);
526            const projectService = createProjectService(host);
527
528            projectService.openClientFile(file2.path);
529            checkNumberOfProjects(projectService, { inferredProjects: 1 });
530            checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]);
531            let inferredProjects = projectService.inferredProjects.slice();
532
533            projectService.openClientFile(file3.path);
534            checkNumberOfProjects(projectService, { inferredProjects: 2 });
535            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
536            checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]);
537            checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
538            inferredProjects = projectService.inferredProjects.slice();
539
540            projectService.openClientFile(file1.path);
541            checkNumberOfProjects(projectService, { inferredProjects: 1 });
542            assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]);
543            assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]);
544            checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]);
545            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]);
546            inferredProjects = projectService.inferredProjects.slice();
547
548            projectService.closeClientFile(file1.path);
549            checkNumberOfProjects(projectService, { inferredProjects: 3 });
550            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
551            assert.isTrue(projectService.inferredProjects[0].isOrphan());
552            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
553            checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]);
554            inferredProjects = projectService.inferredProjects.slice();
555
556            projectService.closeClientFile(file3.path);
557            checkNumberOfProjects(projectService, { inferredProjects: 3 });
558            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]);
559            assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
560            assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]);
561            assert.isTrue(projectService.inferredProjects[0].isOrphan());
562            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
563            assert.isTrue(projectService.inferredProjects[2].isOrphan());
564
565            projectService.openClientFile(file3.path);
566            checkNumberOfProjects(projectService, { inferredProjects: 2 });
567            assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]);
568            assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]);
569            checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]);
570            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
571        });
572
573        it("regression test for crash in acquireOrUpdateDocument", () => {
574            const tsFile = {
575                fileName: "/a/b/file1.ts",
576                path: "/a/b/file1.ts",
577                content: ""
578            };
579            const jsFile = {
580                path: "/a/b/file1.js",
581                content: "var x = 10;",
582                fileName: "/a/b/file1.js",
583                scriptKind: "JS" as "JS"
584            };
585
586            const host = createServerHost([]);
587            const projectService = createProjectService(host);
588            projectService.applyChangesInOpenFiles(singleIterator(tsFile));
589            const projs = projectService.synchronizeProjectList([]);
590            projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName);
591            projectService.synchronizeProjectList([projs[0].info!]);
592            projectService.applyChangesInOpenFiles(singleIterator(jsFile));
593        });
594
595        it("config file is deleted", () => {
596            const file1 = {
597                path: "/a/b/f1.ts",
598                content: "let x = 1;"
599            };
600            const file2 = {
601                path: "/a/b/f2.ts",
602                content: "let y = 2;"
603            };
604            const config = {
605                path: "/a/b/tsconfig.json",
606                content: JSON.stringify({ compilerOptions: {} })
607            };
608            const host = createServerHost([file1, file2, config]);
609            const projectService = createProjectService(host);
610
611            projectService.openClientFile(file1.path);
612            checkNumberOfProjects(projectService, { configuredProjects: 1 });
613            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
614
615            projectService.openClientFile(file2.path);
616            checkNumberOfProjects(projectService, { configuredProjects: 1 });
617            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
618
619            host.deleteFile(config.path);
620            host.checkTimeoutQueueLengthAndRun(1);
621            checkNumberOfProjects(projectService, { inferredProjects: 2 });
622            checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]);
623            checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
624        });
625
626        it("loading files with correct priority", () => {
627            const f1 = {
628                path: "/a/main.ts",
629                content: "let x = 1"
630            };
631            const f2 = {
632                path: "/a/main.js",
633                content: "var y = 1"
634            };
635            const f3 = {
636                path: "/main.js",
637                content: "var y = 1"
638            };
639            const config = {
640                path: "/a/tsconfig.json",
641                content: JSON.stringify({
642                    compilerOptions: { allowJs: true }
643                })
644            };
645            const host = createServerHost([f1, f2, f3, config]);
646            const projectService = createProjectService(host);
647            projectService.setHostConfiguration({
648                extraFileExtensions: [
649                    { extension: ".js", isMixedContent: false },
650                    { extension: ".html", isMixedContent: true }
651                ]
652            });
653            projectService.openClientFile(f1.path);
654            projectService.checkNumberOfProjects({ configuredProjects: 1 });
655            checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]);
656
657            // Since f2 refers to config file as the default project, it needs to be kept alive
658            projectService.closeClientFile(f1.path);
659            projectService.openClientFile(f2.path);
660            projectService.checkNumberOfProjects({ inferredProjects: 1, configuredProjects: 1 });
661            assert.isDefined(projectService.configuredProjects.get(config.path));
662            checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
663
664            // Should close configured project with next file open
665            projectService.closeClientFile(f2.path);
666            projectService.openClientFile(f3.path);
667            projectService.checkNumberOfProjects({ inferredProjects: 1 });
668            assert.isUndefined(projectService.configuredProjects.get(config.path));
669            checkProjectActualFiles(projectService.inferredProjects[0], [f3.path]);
670        });
671
672        it("tsconfig script block support", () => {
673            const file1 = {
674                path: "/a/b/f1.ts",
675                content: ` `
676            };
677            const file2 = {
678                path: "/a/b/f2.html",
679                content: `var hello = "hello";`
680            };
681            const config = {
682                path: "/a/b/tsconfig.json",
683                content: JSON.stringify({ compilerOptions: { allowJs: true } })
684            };
685            const host = createServerHost([file1, file2, config]);
686            const session = createSession(host);
687            openFilesForSession([file1], session);
688            const projectService = session.getProjectService();
689
690            // HTML file will not be included in any projects yet
691            checkNumberOfProjects(projectService, { configuredProjects: 1 });
692            const configuredProj = configuredProjectAt(projectService, 0);
693            checkProjectActualFiles(configuredProj, [file1.path, config.path]);
694
695            // Specify .html extension as mixed content
696            const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }];
697            const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
698            session.executeCommand(configureHostRequest);
699
700            // The configured project should now be updated to include html file
701            checkNumberOfProjects(projectService, { configuredProjects: 1 });
702            assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated");
703            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
704
705            // Open HTML file
706            projectService.applyChangesInOpenFiles(singleIterator({
707                fileName: file2.path,
708                hasMixedContent: true,
709                scriptKind: ScriptKind.JS,
710                content: `var hello = "hello";`
711            }));
712            // Now HTML file is included in the project
713            checkNumberOfProjects(projectService, { configuredProjects: 1 });
714            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
715
716            // Check identifiers defined in HTML content are available in .ts file
717            const project = configuredProjectAt(projectService, 0);
718            let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions);
719            assert(completions && completions.entries[1].name === "hello", `expected entry hello to be in completion list`);
720            assert(completions && completions.entries[0].name === "globalThis", `first entry should be globalThis (not strictly relevant for this test).`);
721
722            // Close HTML file
723            projectService.applyChangesInOpenFiles(
724                /*openFiles*/ undefined,
725                /*changedFiles*/ undefined,
726                /*closedFiles*/[file2.path]);
727
728            // HTML file is still included in project
729            checkNumberOfProjects(projectService, { configuredProjects: 1 });
730            checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]);
731
732            // Check identifiers defined in HTML content are not available in .ts file
733            completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions);
734            assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`);
735        });
736
737        it("no tsconfig script block diagnostic errors", () => {
738
739            //  #1. Ensure no diagnostic errors when allowJs is true
740            const file1 = {
741                path: "/a/b/f1.ts",
742                content: ` `
743            };
744            const file2 = {
745                path: "/a/b/f2.html",
746                content: `var hello = "hello";`
747            };
748            const config1 = {
749                path: "/a/b/tsconfig.json",
750                content: JSON.stringify({ compilerOptions: { allowJs: true } })
751            };
752
753            let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
754            let session = createSession(host);
755
756            // Specify .html extension as mixed content in a configure host request
757            const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }];
758            const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
759            session.executeCommand(configureHostRequest);
760
761            openFilesForSession([file1], session);
762            let projectService = session.getProjectService();
763
764            checkNumberOfProjects(projectService, { configuredProjects: 1 });
765
766            let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
767            assert.deepEqual(diagnostics, []);
768
769            //  #2. Ensure no errors when allowJs is false
770            const config2 = {
771                path: "/a/b/tsconfig.json",
772                content: JSON.stringify({ compilerOptions: { allowJs: false } })
773            };
774
775            host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
776            session = createSession(host);
777
778            session.executeCommand(configureHostRequest);
779
780            openFilesForSession([file1], session);
781            projectService = session.getProjectService();
782
783            checkNumberOfProjects(projectService, { configuredProjects: 1 });
784
785            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
786            assert.deepEqual(diagnostics, []);
787
788            //  #3. Ensure no errors when compiler options aren't specified
789            const config3 = {
790                path: "/a/b/tsconfig.json",
791                content: JSON.stringify({})
792            };
793
794            host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
795            session = createSession(host);
796
797            session.executeCommand(configureHostRequest);
798
799            openFilesForSession([file1], session);
800            projectService = session.getProjectService();
801
802            checkNumberOfProjects(projectService, { configuredProjects: 1 });
803
804            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
805            assert.deepEqual(diagnostics, []);
806
807            //  #4. Ensure no errors when files are explicitly specified in tsconfig
808            const config4 = {
809                path: "/a/b/tsconfig.json",
810                content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] })
811            };
812
813            host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
814            session = createSession(host);
815
816            session.executeCommand(configureHostRequest);
817
818            openFilesForSession([file1], session);
819            projectService = session.getProjectService();
820
821            checkNumberOfProjects(projectService, { configuredProjects: 1 });
822
823            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
824            assert.deepEqual(diagnostics, []);
825
826            //  #4. Ensure no errors when files are explicitly excluded in tsconfig
827            const config5 = {
828                path: "/a/b/tsconfig.json",
829                content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] })
830            };
831
832            host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
833            session = createSession(host);
834
835            session.executeCommand(configureHostRequest);
836
837            openFilesForSession([file1], session);
838            projectService = session.getProjectService();
839
840            checkNumberOfProjects(projectService, { configuredProjects: 1 });
841
842            diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
843            assert.deepEqual(diagnostics, []);
844        });
845
846        it("project structure update is deferred if files are not added\removed", () => {
847            const file1 = {
848                path: "/a/b/f1.ts",
849                content: `import {x} from "./f2"`
850            };
851            const file2 = {
852                path: "/a/b/f2.ts",
853                content: "export let x = 1"
854            };
855            const host = createServerHost([file1, file2]);
856            const projectService = createProjectService(host);
857
858            projectService.openClientFile(file1.path);
859            projectService.openClientFile(file2.path);
860
861            checkNumberOfProjects(projectService, { inferredProjects: 1 });
862            projectService.applyChangesInOpenFiles(
863                /*openFiles*/ undefined,
864                /*changedFiles*/singleIterator({ fileName: file1.path, changes: singleIterator({ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }) }),
865                /*closedFiles*/ undefined);
866
867            checkNumberOfProjects(projectService, { inferredProjects: 1 });
868            projectService.ensureInferredProjectsUpToDate_TestOnly();
869            checkNumberOfProjects(projectService, { inferredProjects: 2 });
870        });
871
872        it("files with mixed content are handled correctly", () => {
873            const file1 = {
874                path: "/a/b/f1.html",
875                content: `<html><script language="javascript">var x = 1;</></html>`
876            };
877            const host = createServerHost([file1]);
878            const projectService = createProjectService(host);
879            const projectFileName = "projectFileName";
880            projectService.openExternalProject({ projectFileName, options: {}, rootFiles: [{ fileName: file1.path, scriptKind: ScriptKind.JS, hasMixedContent: true }] });
881
882            checkNumberOfProjects(projectService, { externalProjects: 1 });
883            checkWatchedFiles(host, [libFile.path]); // watching the "missing" lib file
884
885            const project = projectService.externalProjects[0];
886
887            const scriptInfo = project.getScriptInfo(file1.path)!;
888            const snap = scriptInfo.getSnapshot();
889            const actualText = getSnapshotText(snap);
890            assert.equal(actualText, "", `expected content to be empty string, got "${actualText}"`);
891
892            projectService.openClientFile(file1.path, `var x = 1;`);
893            project.updateGraph();
894
895            const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file1.path, 4)!;
896            assert.equal(quickInfo.kind, ScriptElementKind.variableElement);
897
898            projectService.closeClientFile(file1.path);
899
900            const scriptInfo2 = project.getScriptInfo(file1.path)!;
901            const actualText2 = getSnapshotText(scriptInfo2.getSnapshot());
902            assert.equal(actualText2, "", `expected content to be empty string, got "${actualText2}"`);
903        });
904
905        it("syntax tree cache handles changes in project settings", () => {
906            const file1 = {
907                path: "/a/b/app.ts",
908                content: "{x: 1}"
909            };
910            const host = createServerHost([file1]);
911            const projectService = createProjectService(host, { useSingleInferredProject: true });
912            projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: false });
913            projectService.openClientFile(file1.path);
914            projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path);
915            projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES5, allowJs: true });
916            projectService.getScriptInfo(file1.path)!.editContent(0, 0, " ");
917            projectService.inferredProjects[0].getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file1.path);
918            projectService.closeClientFile(file1.path);
919        });
920
921        it("File in multiple projects at opened and closed correctly", () => {
922            const file1 = {
923                path: "/a/b/app.ts",
924                content: "let x = 1;"
925            };
926            const file2 = {
927                path: "/a/c/f.ts",
928                content: `/// <reference path="../b/app.ts"/>`
929            };
930            const tsconfig1 = {
931                path: "/a/c/tsconfig.json",
932                content: "{}"
933            };
934            const tsconfig2 = {
935                path: "/a/b/tsconfig.json",
936                content: "{}"
937            };
938            const host = createServerHost([file1, file2, tsconfig1, tsconfig2]);
939            const projectService = createProjectService(host);
940
941            projectService.openClientFile(file2.path);
942            checkNumberOfProjects(projectService, { configuredProjects: 1 });
943            const project1 = projectService.configuredProjects.get(tsconfig1.path)!;
944            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 1"); // file2
945            assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, "containing projects count");
946            assert.isFalse(project1.isClosed());
947
948            projectService.openClientFile(file1.path);
949            checkNumberOfProjects(projectService, { configuredProjects: 2 });
950            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 2"); // file2
951            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
952            assert.isFalse(project1.isClosed());
953
954            const project2 = projectService.configuredProjects.get(tsconfig2.path)!;
955            assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 2"); // file1
956            assert.isFalse(project2.isClosed());
957
958            assert.equal(project1.getScriptInfo(file1.path)!.containingProjects.length, 2, `${file1.path} containing projects count`);
959            assert.equal(project1.getScriptInfo(file2.path)!.containingProjects.length, 1, `${file2.path} containing projects count`);
960
961            projectService.closeClientFile(file2.path);
962            checkNumberOfProjects(projectService, { configuredProjects: 2 });
963            assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 3"); // No files
964            assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 3"); // file1
965            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
966            assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2);
967            assert.isFalse(project1.isClosed());
968            assert.isFalse(project2.isClosed());
969
970            projectService.closeClientFile(file1.path);
971            checkNumberOfProjects(projectService, { configuredProjects: 2 });
972            assert.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 4"); // No files
973            assert.isFalse(project2.hasOpenRef(), "Has open ref count in project2 - 4"); // No files
974            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
975            assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2);
976            assert.isFalse(project1.isClosed());
977            assert.isFalse(project2.isClosed());
978
979            projectService.openClientFile(file2.path);
980            checkNumberOfProjects(projectService, { configuredProjects: 1 });
981            assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1);
982            assert.isUndefined(projectService.configuredProjects.get(tsconfig2.path));
983            assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 5"); // file2
984            assert.isFalse(project1.isClosed());
985            assert.isTrue(project2.isClosed());
986        });
987
988        it("snapshot from different caches are incompatible", () => {
989            const f1 = {
990                path: "/a/b/app.ts",
991                content: "let x = 1;"
992            };
993            const host = createServerHost([f1]);
994            const projectFileName = "/a/b/proj.csproj";
995            const projectService = createProjectService(host);
996            projectService.openExternalProject({
997                projectFileName,
998                rootFiles: [toExternalFile(f1.path)],
999                options: {}
1000            });
1001            projectService.openClientFile(f1.path, "let x = 1;\nlet y = 2;");
1002
1003            projectService.checkNumberOfProjects({ externalProjects: 1 });
1004            projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path);
1005            projectService.closeClientFile(f1.path);
1006
1007            projectService.openClientFile(f1.path);
1008            projectService.checkNumberOfProjects({ externalProjects: 1 });
1009            const navbar = projectService.externalProjects[0].getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(f1.path);
1010            assert.equal(navbar[0].spans[0].length, f1.content.length);
1011        });
1012
1013        it("Getting errors from closed script info does not throw exception (because of getting project from orphan script info)", () => {
1014            const { logger, hasErrorMsg } = createHasErrorMessageLogger();
1015            const f1 = {
1016                path: "/a/b/app.ts",
1017                content: "let x = 1;"
1018            };
1019            const config = {
1020                path: "/a/b/tsconfig.json",
1021                content: JSON.stringify({ compilerOptions: {} })
1022            };
1023            const host = createServerHost([f1, libFile, config]);
1024            const session = createSession(host, { logger });
1025            session.executeCommandSeq(<protocol.OpenRequest>{
1026                command: server.CommandNames.Open,
1027                arguments: {
1028                    file: f1.path
1029                }
1030            });
1031            session.executeCommandSeq(<protocol.CloseRequest>{
1032                command: server.CommandNames.Close,
1033                arguments: {
1034                    file: f1.path
1035                }
1036            });
1037            session.executeCommandSeq(<protocol.GeterrRequest>{
1038                command: server.CommandNames.Geterr,
1039                arguments: {
1040                    delay: 0,
1041                    files: [f1.path]
1042                }
1043            });
1044            assert.isFalse(hasErrorMsg());
1045        });
1046
1047        it("Properly handle Windows-style outDir", () => {
1048            const configFile: File = {
1049                path: "C:\\a\\tsconfig.json",
1050                content: JSON.stringify({
1051                    compilerOptions: {
1052                        outDir: `C:\\a\\b`
1053                    },
1054                    include: ["*.ts"]
1055                })
1056            };
1057            const file1: File = {
1058                path: "C:\\a\\f1.ts",
1059                content: "let x = 1;"
1060            };
1061
1062            const host = createServerHost([file1, configFile], { windowsStyleRoot: "c:/" });
1063            const projectService = createProjectService(host);
1064
1065            projectService.openClientFile(file1.path);
1066            checkNumberOfProjects(projectService, { configuredProjects: 1 });
1067            const project = configuredProjectAt(projectService, 0);
1068            checkProjectActualFiles(project, [normalizePath(file1.path), normalizePath(configFile.path)]);
1069            const options = project.getCompilerOptions();
1070            assert.equal(options.outDir, "C:/a/b", "");
1071        });
1072
1073        it("files opened, closed affecting multiple projects", () => {
1074            const file: File = {
1075                path: "/a/b/projects/config/file.ts",
1076                content: `import {a} from "../files/file1"; export let b = a;`
1077            };
1078            const config: File = {
1079                path: "/a/b/projects/config/tsconfig.json",
1080                content: ""
1081            };
1082            const filesFile1: File = {
1083                path: "/a/b/projects/files/file1.ts",
1084                content: "export let a = 10;"
1085            };
1086            const filesFile2: File = {
1087                path: "/a/b/projects/files/file2.ts",
1088                content: "export let aa = 10;"
1089            };
1090
1091            const files = [config, file, filesFile1, filesFile2, libFile];
1092            const host = createServerHost(files);
1093            const session = createSession(host);
1094            // Create configured project
1095            session.executeCommandSeq<protocol.OpenRequest>({
1096                command: protocol.CommandTypes.Open,
1097                arguments: {
1098                    file: file.path
1099                }
1100            });
1101
1102            const projectService = session.getProjectService();
1103            const configuredProject = projectService.configuredProjects.get(config.path)!;
1104            verifyConfiguredProject();
1105
1106            // open files/file1 = should not create another project
1107            session.executeCommandSeq<protocol.OpenRequest>({
1108                command: protocol.CommandTypes.Open,
1109                arguments: {
1110                    file: filesFile1.path
1111                }
1112            });
1113            verifyConfiguredProject();
1114
1115            // Close the file = should still have project
1116            session.executeCommandSeq<protocol.CloseRequest>({
1117                command: protocol.CommandTypes.Close,
1118                arguments: {
1119                    file: file.path
1120                }
1121            });
1122            verifyConfiguredProject();
1123
1124            // Open files/file2 - should create inferred project and close configured project
1125            session.executeCommandSeq<protocol.OpenRequest>({
1126                command: protocol.CommandTypes.Open,
1127                arguments: {
1128                    file: filesFile2.path
1129                }
1130            });
1131            checkNumberOfProjects(projectService, { inferredProjects: 1 });
1132            checkProjectActualFiles(projectService.inferredProjects[0], [libFile.path, filesFile2.path]);
1133
1134            // Actions on file1 would result in assert
1135            session.executeCommandSeq<protocol.OccurrencesRequest>({
1136                command: protocol.CommandTypes.Occurrences,
1137                arguments: {
1138                    file: filesFile1.path,
1139                    line: 1,
1140                    offset: filesFile1.content.indexOf("a")
1141                }
1142            });
1143
1144            function verifyConfiguredProject() {
1145                checkNumberOfProjects(projectService, { configuredProjects: 1 });
1146                checkProjectActualFiles(configuredProject, [file.path, filesFile1.path, libFile.path, config.path]);
1147            }
1148        });
1149
1150        it("requests are done on file on pendingReload but has svc for previous version", () => {
1151            const file1: File = {
1152                path: `${tscWatch.projectRoot}/src/file1.ts`,
1153                content: `import { y } from "./file2"; let x = 10;`
1154            };
1155            const file2: File = {
1156                path: `${tscWatch.projectRoot}/src/file2.ts`,
1157                content: "export let y = 10;"
1158            };
1159            const config: File = {
1160                path: `${tscWatch.projectRoot}/tsconfig.json`,
1161                content: "{}"
1162            };
1163            const files = [file1, file2, libFile, config];
1164            const host = createServerHost(files);
1165            const session = createSession(host);
1166            session.executeCommandSeq<protocol.OpenRequest>({
1167                command: protocol.CommandTypes.Open,
1168                arguments: { file: file2.path, fileContent: file2.content }
1169            });
1170            session.executeCommandSeq<protocol.OpenRequest>({
1171                command: protocol.CommandTypes.Open,
1172                arguments: { file: file1.path }
1173            });
1174            session.executeCommandSeq<protocol.CloseRequest>({
1175                command: protocol.CommandTypes.Close,
1176                arguments: { file: file2.path }
1177            });
1178
1179            file2.content += "export let z = 10;";
1180            host.writeFile(file2.path, file2.content);
1181            // Do not let the timeout runs, before executing command
1182            const startOffset = file2.content.indexOf("y") + 1;
1183            session.executeCommandSeq<protocol.GetApplicableRefactorsRequest>({
1184                command: protocol.CommandTypes.GetApplicableRefactors,
1185                arguments: { file: file2.path, startLine: 1, startOffset, endLine: 1, endOffset: startOffset + 1 }
1186            });
1187        });
1188
1189        describe("includes deferred files in the project context", () => {
1190            function verifyDeferredContext(lazyConfiguredProjectsFromExternalProject: boolean) {
1191                const file1 = {
1192                    path: "/a.deferred",
1193                    content: "const a = 1;"
1194                };
1195                // Deferred extensions should not affect JS files.
1196                const file2 = {
1197                    path: "/b.js",
1198                    content: "const b = 1;"
1199                };
1200                const tsconfig = {
1201                    path: "/tsconfig.json",
1202                    content: ""
1203                };
1204
1205                const host = createServerHost([file1, file2, tsconfig]);
1206                const session = createSession(host);
1207                const projectService = session.getProjectService();
1208                session.executeCommandSeq<protocol.ConfigureRequest>({
1209                    command: protocol.CommandTypes.Configure,
1210                    arguments: { preferences: { lazyConfiguredProjectsFromExternalProject } }
1211                });
1212
1213                // Configure the deferred extension.
1214                const extraFileExtensions = [{ extension: ".deferred", scriptKind: ScriptKind.Deferred, isMixedContent: true }];
1215                const configureHostRequest = makeSessionRequest<protocol.ConfigureRequestArguments>(CommandNames.Configure, { extraFileExtensions });
1216                session.executeCommand(configureHostRequest);
1217
1218                // Open external project
1219                const projectName = "/proj1";
1220                projectService.openExternalProject({
1221                    projectFileName: projectName,
1222                    rootFiles: toExternalFiles([file1.path, file2.path, tsconfig.path]),
1223                    options: {}
1224                });
1225
1226                // Assert
1227                checkNumberOfProjects(projectService, { configuredProjects: 1 });
1228
1229                const configuredProject = configuredProjectAt(projectService, 0);
1230                if (lazyConfiguredProjectsFromExternalProject) {
1231                    // configured project is just created and not yet loaded
1232                    checkProjectActualFiles(configuredProject, emptyArray);
1233                    projectService.ensureInferredProjectsUpToDate_TestOnly();
1234                }
1235                checkProjectActualFiles(configuredProject, [file1.path, tsconfig.path]);
1236
1237                // Allow allowNonTsExtensions will be set to true for deferred extensions.
1238                assert.isTrue(configuredProject.getCompilerOptions().allowNonTsExtensions);
1239            }
1240
1241            it("when lazyConfiguredProjectsFromExternalProject not set", () => {
1242                verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ false);
1243            });
1244            it("when lazyConfiguredProjectsFromExternalProject is set", () => {
1245                verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ true);
1246            });
1247        });
1248
1249        it("Orphan source files are handled correctly on watch trigger", () => {
1250            const file1: File = {
1251                path: `${tscWatch.projectRoot}/src/file1.ts`,
1252                content: `export let x = 10;`
1253            };
1254            const file2: File = {
1255                path: `${tscWatch.projectRoot}/src/file2.ts`,
1256                content: "export let y = 10;"
1257            };
1258            const configContent1 = JSON.stringify({
1259                files: ["src/file1.ts", "src/file2.ts"]
1260            });
1261            const config: File = {
1262                path: `${tscWatch.projectRoot}/tsconfig.json`,
1263                content: configContent1
1264            };
1265            const files = [file1, file2, libFile, config];
1266            const host = createServerHost(files);
1267            const service = createProjectService(host);
1268            service.openClientFile(file1.path);
1269            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, file2.path, libFile.path, config.path]);
1270
1271            const configContent2 = JSON.stringify({
1272                files: ["src/file1.ts"]
1273            });
1274            config.content = configContent2;
1275            host.writeFile(config.path, config.content);
1276            host.runQueuedTimeoutCallbacks();
1277
1278            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]);
1279            verifyFile2InfoIsOrphan();
1280
1281            file2.content += "export let z = 10;";
1282            host.writeFile(file2.path, file2.content);
1283            host.runQueuedTimeoutCallbacks();
1284
1285            checkProjectActualFiles(service.configuredProjects.get(config.path)!, [file1.path, libFile.path, config.path]);
1286            verifyFile2InfoIsOrphan();
1287
1288            function verifyFile2InfoIsOrphan() {
1289                const info = Debug.checkDefined(service.getScriptInfoForPath(file2.path as Path));
1290                assert.equal(info.containingProjects.length, 0);
1291            }
1292        });
1293
1294        it("no project structure update on directory watch invoke on open file save", () => {
1295            const projectRootPath = "/users/username/projects/project";
1296            const file1: File = {
1297                path: `${projectRootPath}/a.ts`,
1298                content: "export const a = 10;"
1299            };
1300            const config: File = {
1301                path: `${projectRootPath}/tsconfig.json`,
1302                content: "{}"
1303            };
1304            const files = [file1, config];
1305            const host = createServerHost(files);
1306            const service = createProjectService(host);
1307            service.openClientFile(file1.path);
1308            checkNumberOfProjects(service, { configuredProjects: 1 });
1309
1310            host.modifyFile(file1.path, file1.content, { invokeFileDeleteCreateAsPartInsteadOfChange: true });
1311            host.checkTimeoutQueueLength(0);
1312        });
1313
1314        it("synchronizeProjectList provides redirect info when requested", () => {
1315            const projectRootPath = "/users/username/projects/project";
1316            const fileA: File = {
1317                path: `${projectRootPath}/A/a.ts`,
1318                content: "export const foo: string = 5;"
1319            };
1320            const configA: File = {
1321                path: `${projectRootPath}/A/tsconfig.json`,
1322                content: `{
1323  "compilerOptions": {
1324    "composite": true,
1325    "declaration": true
1326  }
1327}`
1328            };
1329            const fileB: File = {
1330                path: `${projectRootPath}/B/b.ts`,
1331                content: "import { foo } from \"../A/a\"; console.log(foo);"
1332            };
1333            const configB: File = {
1334                path: `${projectRootPath}/B/tsconfig.json`,
1335                content: `{
1336  "compilerOptions": {
1337    "composite": true,
1338    "declaration": true
1339  },
1340  "references": [
1341    { "path": "../A" }
1342  ]
1343}`
1344            };
1345            const files = [fileA, fileB, configA, configB, libFile];
1346            const host = createServerHost(files);
1347            const projectService = createProjectService(host);
1348            projectService.openClientFile(fileA.path);
1349            projectService.openClientFile(fileB.path);
1350            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1351            assert.deepEqual(knownProjects[0].files, [
1352                {
1353                    fileName: libFile.path,
1354                    isSourceOfProjectReferenceRedirect: false
1355                },
1356                {
1357                    fileName: fileA.path,
1358                    isSourceOfProjectReferenceRedirect: false
1359                },
1360                {
1361                    fileName: configA.path,
1362                    isSourceOfProjectReferenceRedirect: false
1363                }
1364            ]);
1365            assert.deepEqual(knownProjects[1].files, [
1366                {
1367                    fileName: libFile.path,
1368                    isSourceOfProjectReferenceRedirect: false
1369                },
1370                {
1371                    fileName: fileA.path,
1372                    isSourceOfProjectReferenceRedirect: true,
1373                },
1374                {
1375                    fileName: fileB.path,
1376                    isSourceOfProjectReferenceRedirect: false,
1377                },
1378                {
1379                    fileName: configB.path,
1380                    isSourceOfProjectReferenceRedirect: false
1381                }
1382            ]);
1383        });
1384
1385        it("synchronizeProjectList provides updates to redirect info when requested", () => {
1386            const projectRootPath = "/users/username/projects/project";
1387            const fileA: File = {
1388                path: `${projectRootPath}/A/a.ts`,
1389                content: "export const foo: string = 5;"
1390            };
1391            const configA: File = {
1392                path: `${projectRootPath}/A/tsconfig.json`,
1393                content: `{
1394  "compilerOptions": {
1395    "composite": true,
1396    "declaration": true
1397  }
1398}`
1399            };
1400            const fileB: File = {
1401                path: `${projectRootPath}/B/b.ts`,
1402                content: "import { foo } from \"../B/b2\"; console.log(foo);"
1403            };
1404            const fileB2: File = {
1405                path: `${projectRootPath}/B/b2.ts`,
1406                content: "export const foo: string = 5;"
1407            };
1408            const configB: File = {
1409                path: `${projectRootPath}/B/tsconfig.json`,
1410                content: `{
1411  "compilerOptions": {
1412    "composite": true,
1413    "declaration": true
1414  },
1415  "references": [
1416    { "path": "../A" }
1417  ]
1418}`
1419            };
1420            const files = [fileA, fileB, fileB2, configA, configB, libFile];
1421            const host = createServerHost(files);
1422            const projectService = createProjectService(host);
1423            projectService.openClientFile(fileA.path);
1424            projectService.openClientFile(fileB.path);
1425            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1426            assert.deepEqual(knownProjects[0].files, [
1427                {
1428                    fileName: libFile.path,
1429                    isSourceOfProjectReferenceRedirect: false
1430                },
1431                {
1432                    fileName: fileA.path,
1433                    isSourceOfProjectReferenceRedirect: false
1434                },
1435                {
1436                    fileName: configA.path,
1437                    isSourceOfProjectReferenceRedirect: false
1438                }
1439            ]);
1440            assert.deepEqual(knownProjects[1].files, [
1441                {
1442                    fileName: libFile.path,
1443                    isSourceOfProjectReferenceRedirect: false
1444                },
1445                {
1446                    fileName: fileB2.path,
1447                    isSourceOfProjectReferenceRedirect: false,
1448                },
1449                {
1450                    fileName: fileB.path,
1451                    isSourceOfProjectReferenceRedirect: false,
1452                },
1453                {
1454                    fileName: configB.path,
1455                    isSourceOfProjectReferenceRedirect: false
1456                }
1457            ]);
1458
1459            host.modifyFile(configA.path, `{
1460  "compilerOptions": {
1461    "composite": true,
1462    "declaration": true
1463  },
1464  "include": [
1465      "**/*",
1466      "../B/b2.ts"
1467  ]
1468}`);
1469            const newKnownProjects = projectService.synchronizeProjectList(knownProjects.map(proj => proj.info!), /*includeProjectReferenceRedirectInfo*/ true);
1470            assert.deepEqual(newKnownProjects[0].changes?.added, [
1471                {
1472                    fileName: fileB2.path,
1473                    isSourceOfProjectReferenceRedirect: false
1474                }
1475            ]);
1476            assert.deepEqual(newKnownProjects[1].changes?.updatedRedirects, [
1477                {
1478                    fileName: fileB2.path,
1479                    isSourceOfProjectReferenceRedirect: true
1480                }
1481            ]);
1482        });
1483
1484        it("synchronizeProjectList returns correct information when base configuration file cannot be resolved", () => {
1485            const file: File = {
1486                path: `${tscWatch.projectRoot}/index.ts`,
1487                content: "export const foo = 5;"
1488            };
1489            const config: File = {
1490                path: `${tscWatch.projectRoot}/tsconfig.json`,
1491                content: JSON.stringify({ extends: "./tsconfig_base.json" })
1492            };
1493            const host = createServerHost([file, config, libFile]);
1494            const projectService = createProjectService(host);
1495            projectService.openClientFile(file.path);
1496            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ false);
1497            assert.deepEqual(knownProjects[0].files, [
1498                libFile.path,
1499                file.path,
1500                config.path,
1501                `${tscWatch.projectRoot}/tsconfig_base.json`,
1502            ]);
1503        });
1504
1505        it("synchronizeProjectList returns correct information when base configuration file cannot be resolved and redirect info is requested", () => {
1506            const file: File = {
1507                path: `${tscWatch.projectRoot}/index.ts`,
1508                content: "export const foo = 5;"
1509            };
1510            const config: File = {
1511                path: `${tscWatch.projectRoot}/tsconfig.json`,
1512                content: JSON.stringify({ extends: "./tsconfig_base.json" })
1513            };
1514            const host = createServerHost([file, config, libFile]);
1515            const projectService = createProjectService(host);
1516            projectService.openClientFile(file.path);
1517            const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true);
1518            assert.deepEqual(knownProjects[0].files, [
1519                { fileName: libFile.path, isSourceOfProjectReferenceRedirect: false },
1520                { fileName: file.path, isSourceOfProjectReferenceRedirect: false },
1521                { fileName: config.path, isSourceOfProjectReferenceRedirect: false },
1522                { fileName: `${tscWatch.projectRoot}/tsconfig_base.json`, isSourceOfProjectReferenceRedirect: false },
1523            ]);
1524        });
1525
1526        it("handles delayed directory watch invoke on file creation", () => {
1527            const projectRootPath = "/users/username/projects/project";
1528            const fileB: File = {
1529                path: `${projectRootPath}/b.ts`,
1530                content: "export const b = 10;"
1531            };
1532            const fileA: File = {
1533                path: `${projectRootPath}/a.ts`,
1534                content: "export const a = 10;"
1535            };
1536            const fileSubA: File = {
1537                path: `${projectRootPath}/sub/a.ts`,
1538                content: fileA.content
1539            };
1540            const config: File = {
1541                path: `${projectRootPath}/tsconfig.json`,
1542                content: "{}"
1543            };
1544            const files = [fileSubA, fileB, config, libFile];
1545            const host = createServerHost(files);
1546            const { logger, hasErrorMsg } = createHasErrorMessageLogger();
1547            const session = createSession(host, { canUseEvents: true, noGetErrOnBackgroundUpdate: true, logger });
1548            openFile(fileB);
1549            openFile(fileSubA);
1550
1551            const services = session.getProjectService();
1552            checkNumberOfProjects(services, { configuredProjects: 1 });
1553            checkProjectActualFiles(services.configuredProjects.get(config.path)!, files.map(f => f.path));
1554            host.checkTimeoutQueueLengthAndRun(0);
1555
1556            // This should schedule 2 timeouts for ensuring project structure and ensuring projects for open file
1557            const filesWithFileA = files.map(f => f === fileSubA ? fileA : f);
1558            host.deleteFile(fileSubA.path);
1559            host.deleteFolder(getDirectoryPath(fileSubA.path));
1560            host.writeFile(fileA.path, fileA.content);
1561            host.checkTimeoutQueueLength(2);
1562
1563            closeFilesForSession([fileSubA], session);
1564            // This should cancel existing updates and schedule new ones
1565            host.checkTimeoutQueueLength(2);
1566            checkNumberOfProjects(services, { configuredProjects: 1 });
1567            checkProjectActualFiles(services.configuredProjects.get(config.path)!, files.map(f => f.path));
1568
1569            // Open the fileA (as if rename)
1570            openFile(fileA);
1571
1572            // config project is updated to check if fileA is present in it
1573            checkNumberOfProjects(services, { configuredProjects: 1 });
1574            checkProjectActualFiles(services.configuredProjects.get(config.path)!, filesWithFileA.map(f => f.path));
1575
1576            // Run the timeout for updating configured project and ensuring projects for open file
1577            host.checkTimeoutQueueLengthAndRun(2);
1578            checkProjectActualFiles(services.configuredProjects.get(config.path)!, filesWithFileA.map(f => f.path));
1579
1580            // file is deleted but watches are not yet invoked
1581            const originalFileExists = host.fileExists;
1582            host.fileExists = s => s === fileA.path ? false : originalFileExists.call(host, s);
1583            closeFilesForSession([fileA], session);
1584            host.checkTimeoutQueueLength(2); // Update configured project and projects for open file
1585            checkProjectActualFiles(services.configuredProjects.get(config.path)!, filesWithFileA.map(f => f.path));
1586
1587            // host.fileExists = originalFileExists;
1588            openFile(fileSubA);
1589            // This should create inferred project since fileSubA not on the disk
1590            checkProjectActualFiles(services.configuredProjects.get(config.path)!, mapDefined(filesWithFileA, f => f === fileA ? undefined : f.path));
1591            checkProjectActualFiles(services.inferredProjects[0], [fileSubA.path, libFile.path]);
1592
1593            host.checkTimeoutQueueLengthAndRun(2); // Update configured project and projects for open file
1594            host.fileExists = originalFileExists;
1595
1596            // Actually trigger the file move
1597            host.deleteFile(fileA.path);
1598            host.ensureFileOrFolder(fileSubA);
1599            host.checkTimeoutQueueLength(2);
1600
1601            verifyGetErrRequestNoErrors({
1602                session,
1603                host,
1604                files: [fileB, fileSubA],
1605                existingTimeouts: 2,
1606                onErrEvent: () => assert.isFalse(hasErrorMsg())
1607            });
1608
1609            function openFile(file: File) {
1610                openFilesForSession([{ file, projectRootPath }], session);
1611            }
1612        });
1613
1614        it("assert when removing project", () => {
1615            const host = createServerHost([commonFile1, commonFile2, libFile]);
1616            const service = createProjectService(host);
1617            service.openClientFile(commonFile1.path);
1618            const project = service.inferredProjects[0];
1619            checkProjectActualFiles(project, [commonFile1.path, libFile.path]);
1620            // Intentionally create scriptinfo and attach it to project
1621            const info = service.getOrCreateScriptInfoForNormalizedPath(commonFile2.path as server.NormalizedPath, /*openedByClient*/ false)!;
1622            info.attachToProject(project);
1623            try {
1624                service.applyChangesInOpenFiles(/*openFiles*/ undefined, /*changedFiles*/ undefined, [commonFile1.path]);
1625            }
1626            catch (e) {
1627                assert.isTrue(e.message.indexOf("Debug Failure. False expression: Found script Info still attached to project") === 0);
1628            }
1629        });
1630        it("does not look beyond node_modules folders for default configured projects", () => {
1631            const rootFilePath = server.asNormalizedPath("/project/index.ts");
1632            const rootProjectPath = server.asNormalizedPath("/project/tsconfig.json");
1633            const nodeModulesFilePath1 = server.asNormalizedPath("/project/node_modules/@types/a/index.d.ts");
1634            const nodeModulesProjectPath1 = server.asNormalizedPath("/project/node_modules/@types/a/tsconfig.json");
1635            const nodeModulesFilePath2 = server.asNormalizedPath("/project/node_modules/@types/b/index.d.ts");
1636            const serverHost = createServerHost([
1637                { path: rootFilePath, content: "import 'a'; import 'b';" },
1638                { path: rootProjectPath, content: "{}" },
1639                { path: nodeModulesFilePath1, content: "{}" },
1640                { path: nodeModulesProjectPath1, content: "{}" },
1641                { path: nodeModulesFilePath2, content: "{}" },
1642            ]);
1643            const projectService = createProjectService(serverHost, { useSingleInferredProject: true });
1644
1645            const openRootFileResult = projectService.openClientFile(rootFilePath);
1646            assert.strictEqual(openRootFileResult.configFileName?.toString(), rootProjectPath);
1647
1648            const openNodeModulesFileResult1 = projectService.openClientFile(nodeModulesFilePath1);
1649            assert.strictEqual(openNodeModulesFileResult1.configFileName?.toString(), nodeModulesProjectPath1);
1650
1651            const openNodeModulesFileResult2 = projectService.openClientFile(nodeModulesFilePath2);
1652            assert.isUndefined(openNodeModulesFileResult2.configFileName);
1653
1654            const rootProject = projectService.findProject(rootProjectPath)!;
1655            checkProjectActualFiles(rootProject, [rootProjectPath, rootFilePath, nodeModulesFilePath1, nodeModulesFilePath2]);
1656
1657            checkNumberOfInferredProjects(projectService, 0);
1658        });
1659    });
1660}
1661