diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 568a1d1fb60..56b81731b61 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1774,6 +1774,15 @@ namespace ts.server { return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } + /* @internal */ + getScriptInfoOrConfig(uncheckedFileName: string): ScriptInfoOrConfig | undefined { + const path = toNormalizedPath(uncheckedFileName); + const info = this.getScriptInfoForNormalizedPath(path); + if (info) return info; + const configProject = this.configuredProjects.get(uncheckedFileName); + return configProject && configProject.getCompilerOptions().configFile; + } + /** * Returns the projects that contain script info through SymLink * Note that this does not return projects in info.containingProjects @@ -2542,4 +2551,11 @@ namespace ts.server { return false; } } + + /* @internal */ + export type ScriptInfoOrConfig = ScriptInfo | TsConfigSourceFile; + /* @internal */ + export function isConfigFile(config: ScriptInfoOrConfig): config is TsConfigSourceFile { + return (config as TsConfigSourceFile).kind !== undefined; + } } diff --git a/src/server/session.ts b/src/server/session.ts index 0440d8cecee..fe57abe6b98 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1763,7 +1763,7 @@ namespace ts.server { this.projectService, project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences), (a, b) => a.fileName === b.fileName); - return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEditUsingScriptInfo(c)) : changes; + return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEditUsingScriptInfoOrConfigFile(c)) : changes; } private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray | undefined { @@ -1840,8 +1840,8 @@ namespace ts.server { return mapTextChangesToCodeEditsForFile(change, project.getSourceFileOrConfigFile(this.normalizePath(change.fileName))); } - private mapTextChangeToCodeEditUsingScriptInfo(change: FileTextChanges): protocol.FileCodeEdits { - return mapTextChangesToCodeEditsUsingScriptInfo(change, this.projectService.getScriptInfo(this.normalizePath(change.fileName))); + private mapTextChangeToCodeEditUsingScriptInfoOrConfigFile(change: FileTextChanges): protocol.FileCodeEdits { + return mapTextChangesToCodeEditsUsingScriptInfoOrConfig(change, this.projectService.getScriptInfoOrConfig(this.normalizePath(change.fileName))); } private normalizePath(fileName: string) { @@ -2358,7 +2358,7 @@ namespace ts.server { } function mapTextChangesToCodeEditsForFile(textChanges: FileTextChanges, sourceFile: SourceFile | undefined): protocol.FileCodeEdits { - Debug.assert(!!textChanges.isNewFile === !sourceFile, "Expected isNewFile for (only) new files", () => JSON.stringify({ isNewFile: textChanges.isNewFile, hasSourceFile: !!sourceFile })); + Debug.assert(!!textChanges.isNewFile === !sourceFile, "Expected isNewFile for (only) new files", () => JSON.stringify({ isNewFile: !!textChanges.isNewFile, hasSourceFile: !!sourceFile })); if (sourceFile) { return { fileName: textChanges.fileName, @@ -2370,10 +2370,10 @@ namespace ts.server { } } - function mapTextChangesToCodeEditsUsingScriptInfo(textChanges: FileTextChanges, scriptInfo: ScriptInfo | undefined): protocol.FileCodeEdits { - Debug.assert(!!textChanges.isNewFile === !scriptInfo); + function mapTextChangesToCodeEditsUsingScriptInfoOrConfig(textChanges: FileTextChanges, scriptInfo: ScriptInfoOrConfig | undefined): protocol.FileCodeEdits { + Debug.assert(!!textChanges.isNewFile === !scriptInfo, "Expected isNewFile for (only) new files", () => JSON.stringify({ isNewFile: !!textChanges.isNewFile, hasScriptInfo: !!scriptInfo })); return scriptInfo - ? { fileName: textChanges.fileName, textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEditUsingScriptInfo(textChange, scriptInfo)) } + ? { fileName: textChanges.fileName, textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEditUsingScriptInfoOrConfig(textChange, scriptInfo)) } : convertNewFileTextChangeToCodeEdit(textChanges); } @@ -2385,8 +2385,16 @@ namespace ts.server { }; } - function convertTextChangeToCodeEditUsingScriptInfo(change: TextChange, scriptInfo: ScriptInfo) { - return { start: scriptInfo.positionToLineOffset(change.span.start), end: scriptInfo.positionToLineOffset(textSpanEnd(change.span)), newText: change.newText }; + function convertTextChangeToCodeEditUsingScriptInfoOrConfig(change: TextChange, scriptInfo: ScriptInfoOrConfig): protocol.CodeEdit { + return { start: positionToLineOffset(scriptInfo, change.span.start), end: positionToLineOffset(scriptInfo, textSpanEnd(change.span)), newText: change.newText }; + } + + function positionToLineOffset(info: ScriptInfoOrConfig, position: number): protocol.Location { + return isConfigFile(info) ? locationFromLineAndCharacter(info.getLineAndCharacterOfPosition(position)) : info.positionToLineOffset(position); + } + + function locationFromLineAndCharacter(lc: LineAndCharacter): protocol.Location { + return { line: lc.line + 1, offset: lc.character + 1 }; } function convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 932c777907e..75dd608e6c4 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -151,7 +151,7 @@ namespace ts { const toImport = oldFromNew !== undefined // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location. // TODO:GH#18217 - ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, program) + ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, host) : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew); // Need an update if the imported file moved, or the importing file moved and was using a relative path. @@ -192,18 +192,18 @@ namespace ts { const resolved = host.resolveModuleNames ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName) : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName); - return getSourceFileToImportFromResolved(resolved, oldToNew, program); + return getSourceFileToImportFromResolved(resolved, oldToNew, host); } } - function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, program: Program): ToImport | undefined { + function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, host: LanguageServiceHost): ToImport | undefined { return resolved && ( - (resolved.resolvedModule && getIfInProgram(resolved.resolvedModule.resolvedFileName)) || firstDefined(resolved.failedLookupLocations, getIfInProgram)); + (resolved.resolvedModule && getIfExists(resolved.resolvedModule.resolvedFileName)) || firstDefined(resolved.failedLookupLocations, getIfExists)); - function getIfInProgram(oldLocation: string): ToImport | undefined { + function getIfExists(oldLocation: string): ToImport | undefined { const newLocation = oldToNew(oldLocation); - return program.getSourceFile(oldLocation) || newLocation !== undefined && program.getSourceFile(newLocation) + return host.fileExists!(oldLocation) || newLocation !== undefined && host.fileExists!(newLocation) // TODO: GH#18217 ? newLocation !== undefined ? { newFileName: newLocation, updated: true } : { newFileName: oldLocation, updated: false } : undefined; } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index d2a398849c9..00cbda97151 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -8688,7 +8688,7 @@ export const x = 10;` }; const aTsconfig: File = { path: "/a/tsconfig.json", - content: "{}", + content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), }; const bUserTs: File = { path: "/b/user.ts", @@ -8703,12 +8703,15 @@ export const x = 10;` const session = createSession(host); openFilesForSession([aUserTs, bUserTs], session); - const renameRequest = makeSessionRequest(CommandNames.GetEditsForFileRename, { - oldFilePath: "/a/old.ts", + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aOldTs.path, newFilePath: "/a/new.ts", }); - const response = session.executeCommand(renameRequest).response as protocol.GetEditsForFileRenameResponse["body"]; - assert.deepEqual(response, [ + assert.deepEqual>(response, [ + { + fileName: aTsconfig.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], + }, { fileName: aUserTs.path, textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], @@ -8719,6 +8722,31 @@ export const x = 10;` }, ]); }); + + it("works with file moved to inferred project", () => { + const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; + const cTs: File = { path: "/c.ts", content: "export {};" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; + + const host = createServerHost([aTs, cTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs, cTs], session); + + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: "/b.ts", + newFilePath: cTs.path, + }); + assert.deepEqual>(response, [ + { + fileName: "/tsconfig.json", + textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], + }, + { + fileName: "/a.ts", + textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], + }, + ]); + }); }); describe("tsserverProjectSystem document registry in project service", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 43fa1f0b475..57deee2ab98 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8893,7 +8893,7 @@ declare namespace ts.server { private mapCodeFixAction; private mapTextChangesToCodeEdits; private mapTextChangeToCodeEdit; - private mapTextChangeToCodeEditUsingScriptInfo; + private mapTextChangeToCodeEditUsingScriptInfoOrConfigFile; private normalizePath; private convertTextChangeToCodeEdit; private getBraceMatching;