1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: Open-file", () => { 3 it("can be reloaded with empty content", () => { 4 const f = { 5 path: "/a/b/app.ts", 6 content: "let x = 1" 7 }; 8 const projectFileName = "externalProject"; 9 const host = createServerHost([f]); 10 const projectService = createProjectService(host); 11 // create a project 12 projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); 13 projectService.checkNumberOfProjects({ externalProjects: 1 }); 14 15 const p = projectService.externalProjects[0]; 16 // force to load the content of the file 17 p.updateGraph(); 18 19 const scriptInfo = p.getScriptInfo(f.path)!; 20 checkSnapLength(scriptInfo.getSnapshot(), f.content.length); 21 22 // open project and replace its content with empty string 23 projectService.openClientFile(f.path, ""); 24 checkSnapLength(scriptInfo.getSnapshot(), 0); 25 }); 26 function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { 27 assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); 28 } 29 30 function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { 31 const file1: File = { 32 path: "/a/b/src/app.ts", 33 content: "let x = 10;" 34 }; 35 const file2: File = { 36 path: "/a/B/lib/module2.ts", 37 content: "let z = 10;" 38 }; 39 const configFile: File = { 40 path: "/a/b/tsconfig.json", 41 content: "" 42 }; 43 const configFile2: File = { 44 path: "/a/tsconfig.json", 45 content: "" 46 }; 47 const host = createServerHost([file1, file2, configFile, configFile2], { 48 useCaseSensitiveFileNames 49 }); 50 const service = createProjectService(host); 51 52 // Open file1 -> configFile 53 verifyConfigFileName(file1, "/a", configFile); 54 verifyConfigFileName(file1, "/a/b", configFile); 55 verifyConfigFileName(file1, "/a/B", configFile); 56 57 // Open file2 use root "/a/b" 58 verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); 59 verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); 60 verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); 61 62 function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { 63 const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); 64 assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); 65 service.closeClientFile(file.path); 66 } 67 } 68 it("works when project root is used with case-sensitive system", () => { 69 verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); 70 }); 71 72 it("works when project root is used with case-insensitive system", () => { 73 verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); 74 }); 75 76 it("uses existing project even if project refresh is pending", () => { 77 const projectFolder = "/user/someuser/projects/myproject"; 78 const aFile: File = { 79 path: `${projectFolder}/src/a.ts`, 80 content: "export const x = 0;" 81 }; 82 const configFile: File = { 83 path: `${projectFolder}/tsconfig.json`, 84 content: "{}" 85 }; 86 const files = [aFile, configFile, libFile]; 87 const host = createServerHost(files); 88 const service = createProjectService(host); 89 service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); 90 verifyProject(); 91 92 const bFile: File = { 93 path: `${projectFolder}/src/b.ts`, 94 content: `export {}; declare module "./a" { export const y: number; }` 95 }; 96 files.push(bFile); 97 host.writeFile(bFile.path, bFile.content); 98 service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); 99 verifyProject(); 100 101 function verifyProject() { 102 assert.isDefined(service.configuredProjects.get(configFile.path)); 103 const project = service.configuredProjects.get(configFile.path)!; 104 checkProjectActualFiles(project, files.map(f => f.path)); 105 } 106 }); 107 108 it("can open same file again", () => { 109 const projectFolder = "/user/someuser/projects/myproject"; 110 const aFile: File = { 111 path: `${projectFolder}/src/a.ts`, 112 content: "export const x = 0;" 113 }; 114 const configFile: File = { 115 path: `${projectFolder}/tsconfig.json`, 116 content: "{}" 117 }; 118 const files = [aFile, configFile, libFile]; 119 const host = createServerHost(files); 120 const service = createProjectService(host); 121 verifyProject(aFile.content); 122 verifyProject(`${aFile.content}export const y = 10;`); 123 124 function verifyProject(aFileContent: string) { 125 service.openClientFile(aFile.path, aFileContent, ScriptKind.TS, projectFolder); 126 const project = service.configuredProjects.get(configFile.path)!; 127 checkProjectActualFiles(project, files.map(f => f.path)); 128 assert.equal(project.getCurrentProgram()?.getSourceFile(aFile.path)!.text, aFileContent); 129 } 130 }); 131 132 it("when file makes edits to add/remove comment directives, they are handled correcrly", () => { 133 const file: File = { 134 path: `${tscWatch.projectRoot}/file.ts`, 135 content: `const x = 10; 136function foo() { 137 // @ts-ignore 138 let y: string = x; 139 return y; 140} 141function bar() { 142 // @ts-ignore 143 let z : string = x; 144 return z; 145} 146foo(); 147bar();` 148 }; 149 const host = createServerHost([file, libFile]); 150 const session = createSession(host, { canUseEvents: true, }); 151 openFilesForSession([file], session); 152 verifyGetErrRequestNoErrors({ session, host, files: [file] }); 153 154 // Remove first ts-ignore and check only first error is reported 155 const tsIgnoreComment = `// @ts-ignore`; 156 const locationOfTsIgnore = protocolTextSpanFromSubstring(file.content, tsIgnoreComment); 157 session.executeCommandSeq<protocol.UpdateOpenRequest>({ 158 command: protocol.CommandTypes.UpdateOpen, 159 arguments: { 160 changedFiles: [{ 161 fileName: file.path, 162 textChanges: [{ 163 newText: " ", 164 ...locationOfTsIgnore 165 }] 166 }] 167 } 168 }); 169 const locationOfY = protocolTextSpanFromSubstring(file.content, "y"); 170 verifyGetErrRequest({ 171 session, 172 host, 173 expected: [ 174 { 175 file, 176 syntax: [], 177 semantic: [ 178 createDiagnostic(locationOfY.start, locationOfY.end, Diagnostics.Type_0_is_not_assignable_to_type_1, ["number", "string"]), 179 ], 180 suggestion: [] 181 }, 182 ] 183 }); 184 185 // Revert the change and no errors should be reported 186 session.executeCommandSeq<protocol.UpdateOpenRequest>({ 187 command: protocol.CommandTypes.UpdateOpen, 188 arguments: { 189 changedFiles: [{ 190 fileName: file.path, 191 textChanges: [{ 192 newText: tsIgnoreComment, 193 ...locationOfTsIgnore 194 }] 195 }] 196 } 197 }); 198 verifyGetErrRequestNoErrors({ session, host, files: [file] }); 199 }); 200 }); 201} 202