diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index f630b9329aaa7..79a46c9077563 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -273,6 +273,8 @@ namespace ts.server { /*@internal*/ export function isDynamicFileName(fileName: NormalizedPath) { return fileName[0] === "^" || + ((stringContains(fileName, "walkThroughSnippet:/") || stringContains(fileName, "untitled:/")) && + getBaseFileName(fileName)[0] === "^") || (stringContains(fileName, ":^") && !stringContains(fileName, directorySeparator)); } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 73ccca0bac2ef..909b2fe8c4ddc 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -149,6 +149,7 @@ "unittests/tsserver/declarationFileMaps.ts", "unittests/tsserver/documentRegistry.ts", "unittests/tsserver/duplicatePackages.ts", + "unittests/tsserver/dynamicFiles.ts", "unittests/tsserver/events/largeFileReferenced.ts", "unittests/tsserver/events/projectLanguageServiceState.ts", "unittests/tsserver/events/projectLoading.ts", @@ -189,7 +190,6 @@ "unittests/tsserver/typeOnlyImportChains.ts", "unittests/tsserver/typeReferenceDirectives.ts", "unittests/tsserver/typingsInstaller.ts", - "unittests/tsserver/untitledFiles.ts", "unittests/tsserver/versionCache.ts", "unittests/tsserver/watchEnvironment.ts" ] diff --git a/src/testRunner/unittests/tsserver/dynamicFiles.ts b/src/testRunner/unittests/tsserver/dynamicFiles.ts new file mode 100644 index 0000000000000..92f495412cc3e --- /dev/null +++ b/src/testRunner/unittests/tsserver/dynamicFiles.ts @@ -0,0 +1,214 @@ +namespace ts.projectSystem { + export function verifyDynamic(service: server.ProjectService, path: string) { + const info = Debug.assertDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); + assert.isTrue(info.isDynamic); + } + + function verifyPathRecognizedAsDynamic(path: string) { + const file: File = { + path, + content: `/// +/// +var x = 10;` + }; + const host = createServerHost([libFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file.path, file.content); + verifyDynamic(projectService, projectService.toPath(file.path)); + + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file.path]); + checkProjectActualFiles(project, [file.path, libFile.path]); + } + + describe("unittests:: tsserver:: dynamicFiles:: Untitled files", () => { + const untitledFile = "untitled:^Untitled-1"; + it("Can convert positions to locations", () => { + const aTs: File = { path: "/proj/a.ts", content: "" }; + const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; + const session = createSession(createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); + + openFilesForSession([aTs], session); + + executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { + file: untitledFile, + fileContent: `/// \nlet foo = 1;\nfooo/**/`, + scriptKindName: "TS", + projectRootPath: "/proj", + }); + verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: untitledFile, + startLine: 3, + startOffset: 1, + endLine: 3, + endOffset: 5, + errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], + }); + assert.deepEqual(response, [ + { + description: "Change spelling to 'foo'", + fixName: "spelling", + changes: [{ + fileName: untitledFile, + textChanges: [{ + start: { line: 3, offset: 1 }, + end: { line: 3, offset: 5 }, + newText: "foo", + }], + }], + commands: undefined, + fixId: undefined, + fixAllDescription: undefined + }, + ]); + }); + + it("opening untitled files", () => { + const config: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = createServerHost([config, libFile], { useCaseSensitiveFileNames: true, currentDirectory: tscWatch.projectRoot }); + const service = createProjectService(host); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); + + const untitled: File = { + path: `${tscWatch.projectRoot}/Untitled-1.ts`, + content: "const x = 10;" + }; + host.writeFile(untitled.path, untitled.content); + host.checkTimeoutQueueLength(0); + service.openClientFile(untitled.path, untitled.content, /*scriptKind*/ undefined, tscWatch.projectRoot); + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + + service.closeClientFile(untitledFile); + checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); + verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); + checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + }); + }); + + describe("unittests:: tsserver:: dynamicFiles:: ", () => { + it("dynamic file without external project", () => { + const file: File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const host = createServerHost([libFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ + module: ModuleKind.CommonJS, + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true + }); + projectService.openClientFile(file.path, "var x = 10;"); + + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file.path]); + checkProjectActualFiles(project, [file.path, libFile.path]); + verifyDynamic(projectService, `/${file.path}`); + + assert.strictEqual(projectService.ensureDefaultProjectForFile(server.toNormalizedPath(file.path)), project); + const indexOfX = file.content.indexOf("x"); + assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { + kind: ScriptElementKind.variableElement, + kindModifiers: "", + textSpan: { start: indexOfX, length: 1 }, + displayParts: [ + { text: "var", kind: "keyword" }, + { text: " ", kind: "space" }, + { text: "x", kind: "localName" }, + { text: ":", kind: "punctuation" }, + { text: " ", kind: "space" }, + { text: "number", kind: "keyword" } + ], + documentation: [], + tags: undefined, + }); + }); + + it("dynamic file with reference paths without external project", () => { + verifyPathRecognizedAsDynamic("^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js"); + }); + + describe("dynamic file with projectRootPath", () => { + const file: File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const configFile: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const configProjectFile: File = { + path: `${tscWatch.projectRoot}/a.ts`, + content: "let y = 10;" + }; + it("with useInferredProjectPerProjectRoot", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const session = createSession(host, { useInferredProjectPerProjectRoot: true }); + openFilesForSession([{ file: file.path, projectRootPath: tscWatch.projectRoot }], session); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + verifyDynamic(projectService, `${tscWatch.projectRoot}/${file.path}`); + + session.executeCommandSeq({ + command: protocol.CommandTypes.GetOutliningSpans, + arguments: { + file: file.path + } + }); + + // Without project root + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, libFile.path]); + }); + + it("fails when useInferredProjectPerProjectRoot is false", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + try { + projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, tscWatch.projectRoot); + } + catch (e) { + assert.strictEqual( + e.message.replace(/\r?\n/, "\n"), + `Debug Failure. False expression: \nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.` + ); + } + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, libFile.path]); + }); + }); + + describe("verify accepts known schemas as dynamic file", () => { + it("walkThroughSnippet", () => { + verifyPathRecognizedAsDynamic("walkThroughSnippet:/usr/share/code/resources/app/out/vs/workbench/contrib/welcome/walkThrough/browser/editor/^vs_code_editor_walkthrough.md#1.ts"); + }); + + it("untitled", () => { + verifyPathRecognizedAsDynamic("untitled:/Users/matb/projects/san/^newFile.ts"); + }); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index ae2573852684e..97f9343f983b8 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1075,121 +1075,6 @@ namespace ts.projectSystem { assert.equal(options.outDir, "C:/a/b", ""); }); - it("dynamic file without external project", () => { - const file: File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const host = createServerHost([libFile], { useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ - module: ModuleKind.CommonJS, - allowJs: true, - allowSyntheticDefaultImports: true, - allowNonTsExtensions: true - }); - projectService.openClientFile(file.path, "var x = 10;"); - - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file.path]); - checkProjectActualFiles(project, [file.path, libFile.path]); - verifyDynamic(projectService, `/${file.path}`); - - assert.strictEqual(projectService.ensureDefaultProjectForFile(server.toNormalizedPath(file.path)), project); - const indexOfX = file.content.indexOf("x"); - assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { - kind: ScriptElementKind.variableElement, - kindModifiers: "", - textSpan: { start: indexOfX, length: 1 }, - displayParts: [ - { text: "var", kind: "keyword" }, - { text: " ", kind: "space" }, - { text: "x", kind: "localName" }, - { text: ":", kind: "punctuation" }, - { text: " ", kind: "space" }, - { text: "number", kind: "keyword" } - ], - documentation: [], - tags: undefined, - }); - }); - - it("dynamic file with reference paths without external project", () => { - const file: File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: `/// -/// -var x = 10;` - }; - const host = createServerHost([libFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file.path, file.content); - verifyDynamic(projectService, projectService.toPath(file.path)); - - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file.path]); - checkProjectActualFiles(project, [file.path, libFile.path]); - }); - - describe("dynamic file with projectRootPath", () => { - const file: File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const configFile: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const configProjectFile: File = { - path: `${tscWatch.projectRoot}/a.ts`, - content: "let y = 10;" - }; - it("with useInferredProjectPerProjectRoot", () => { - const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const session = createSession(host, { useInferredProjectPerProjectRoot: true }); - openFilesForSession([{ file: file.path, projectRootPath: tscWatch.projectRoot }], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); - verifyDynamic(projectService, `${tscWatch.projectRoot}/${file.path}`); - - session.executeCommandSeq({ - command: protocol.CommandTypes.GetOutliningSpans, - arguments: { - file: file.path - } - }); - - // Without project root - const file2Path = file.path.replace("#1", "#2"); - projectService.openClientFile(file2Path, file.content); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, libFile.path]); - }); - - it("fails when useInferredProjectPerProjectRoot is false", () => { - const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - try { - projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, tscWatch.projectRoot); - } - catch (e) { - assert.strictEqual( - e.message.replace(/\r?\n/, "\n"), - `Debug Failure. False expression: \nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.` - ); - } - const file2Path = file.path.replace("#1", "#2"); - projectService.openClientFile(file2Path, file.content); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, libFile.path]); - }); - }); - it("files opened, closed affecting multiple projects", () => { const file: File = { path: "/a/b/projects/config/file.ts", diff --git a/src/testRunner/unittests/tsserver/untitledFiles.ts b/src/testRunner/unittests/tsserver/untitledFiles.ts deleted file mode 100644 index 6ba8da3cf398f..0000000000000 --- a/src/testRunner/unittests/tsserver/untitledFiles.ts +++ /dev/null @@ -1,83 +0,0 @@ -namespace ts.projectSystem { - export function verifyDynamic(service: server.ProjectService, path: string) { - const info = Debug.assertDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); - assert.isTrue(info.isDynamic); - } - - describe("unittests:: tsserver:: Untitled files", () => { - const untitledFile = "untitled:^Untitled-1"; - it("Can convert positions to locations", () => { - const aTs: File = { path: "/proj/a.ts", content: "" }; - const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; - const session = createSession(createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); - - openFilesForSession([aTs], session); - - executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { - file: untitledFile, - fileContent: `/// \nlet foo = 1;\nfooo/**/`, - scriptKindName: "TS", - projectRootPath: "/proj", - }); - verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); - const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { - file: untitledFile, - startLine: 3, - startOffset: 1, - endLine: 3, - endOffset: 5, - errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], - }); - assert.deepEqual(response, [ - { - description: "Change spelling to 'foo'", - fixName: "spelling", - changes: [{ - fileName: untitledFile, - textChanges: [{ - start: { line: 3, offset: 1 }, - end: { line: 3, offset: 5 }, - newText: "foo", - }], - }], - commands: undefined, - fixId: undefined, - fixAllDescription: undefined - }, - ]); - }); - - it("opening untitled files", () => { - const config: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = createServerHost([config, libFile], { useCaseSensitiveFileNames: true, currentDirectory: tscWatch.projectRoot }); - const service = createProjectService(host); - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); - - const untitled: File = { - path: `${tscWatch.projectRoot}/Untitled-1.ts`, - content: "const x = 10;" - }; - host.writeFile(untitled.path, untitled.content); - host.checkTimeoutQueueLength(0); - service.openClientFile(untitled.path, untitled.content, /*scriptKind*/ undefined, tscWatch.projectRoot); - checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - - service.closeClientFile(untitledFile); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); - verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - }); - }); -}