diff --git a/src/harness/client.ts b/src/harness/client.ts index 27365e3ecf1c4..1c7c0adb83450 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -384,7 +384,8 @@ namespace ts.server { return notImplemented(); } - getRenameInfo(fileName: string, position: number, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + // Not passing along 'options' because server should already have those from the 'configure' command const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; const request = this.processRequest(CommandNames.Rename, args); @@ -428,7 +429,7 @@ namespace ts.server { this.lastRenameEntry.inputs.position !== position || this.lastRenameEntry.inputs.findInStrings !== findInStrings || this.lastRenameEntry.inputs.findInComments !== findInComments) { - this.getRenameInfo(fileName, position, findInStrings, findInComments); + this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); } return this.lastRenameEntry!.locations; diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index db1c326a50440..1276de7de44a7 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1308,8 +1308,8 @@ Actual: ${stringify(fullActual)}`); } } - public verifyRenameInfoSucceeded(displayName: string | undefined, fullDisplayName: string | undefined, kind: string | undefined, kindModifiers: string | undefined, fileToRename: string | undefined, expectedRange: Range | undefined): void { - const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition); + public verifyRenameInfoSucceeded(displayName: string | undefined, fullDisplayName: string | undefined, kind: string | undefined, kindModifiers: string | undefined, fileToRename: string | undefined, expectedRange: Range | undefined, renameInfoOptions: ts.RenameInfoOptions | undefined): void { + const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition, renameInfoOptions || { allowRenameOfImportPath: true }); if (!renameInfo.canRename) { throw this.raiseError("Rename did not succeed"); } @@ -1334,8 +1334,9 @@ Actual: ${stringify(fullActual)}`); } } - public verifyRenameInfoFailed(message?: string) { - const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition); + public verifyRenameInfoFailed(message?: string, allowRenameOfImportPath?: boolean) { + allowRenameOfImportPath = allowRenameOfImportPath === undefined ? true : allowRenameOfImportPath; + const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition, { allowRenameOfImportPath }); if (renameInfo.canRename) { throw this.raiseError("Rename was expected to fail"); } @@ -4091,12 +4092,12 @@ namespace FourSlashInterface { this.state.verifySemanticClassifications(classifications); } - public renameInfoSucceeded(displayName?: string, fullDisplayName?: string, kind?: string, kindModifiers?: string, fileToRename?: string, expectedRange?: FourSlash.Range) { - this.state.verifyRenameInfoSucceeded(displayName, fullDisplayName, kind, kindModifiers, fileToRename, expectedRange); + public renameInfoSucceeded(displayName?: string, fullDisplayName?: string, kind?: string, kindModifiers?: string, fileToRename?: string, expectedRange?: FourSlash.Range, options?: ts.RenameInfoOptions) { + this.state.verifyRenameInfoSucceeded(displayName, fullDisplayName, kind, kindModifiers, fileToRename, expectedRange, options); } - public renameInfoFailed(message?: string) { - this.state.verifyRenameInfoFailed(message); + public renameInfoFailed(message?: string, allowRenameOfImportPath?: boolean) { + this.state.verifyRenameInfoFailed(message, allowRenameOfImportPath); } public renameLocations(startRanges: ArrayOrSingle, options: RenameLocationsOptions) { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 581d5cc045d75..d233ddf458522 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -469,8 +469,8 @@ namespace Harness.LanguageService { getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); } - getRenameInfo(fileName: string, position: number): ts.RenameInfo { - return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position)); + getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); } findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ts.RenameLocation[] { return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments)); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 193621da82a3d..8b9df926d8252 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2905,6 +2905,7 @@ namespace ts.server.protocol { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; + readonly allowRenameOfImportPath?: boolean; } export interface CompilerOptions { diff --git a/src/server/session.ts b/src/server/session.ts index d534f445ee355..8c300b121e1d4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1177,7 +1177,8 @@ namespace ts.server { private getRenameInfo(args: protocol.FileLocationRequestArgs): RenameInfo { const { file, project } = this.getFileAndProject(args); const position = this.getPositionInFile(args, file); - return project.getLanguageService().getRenameInfo(file, position); + const preferences = this.getHostPreferences(); + return project.getLanguageService().getRenameInfo(file, position, { allowRenameOfImportPath: preferences.allowRenameOfImportPath }); } private getProjects(args: protocol.FileRequestArgs, getScriptInfoEnsuringProjectsUptoDate?: boolean, ignoreNoProjectError?: boolean): Projects { @@ -1236,7 +1237,7 @@ namespace ts.server { if (!simplifiedResult) return locations; const defaultProject = this.getDefaultProject(args); - const renameInfo: protocol.RenameInfo = this.mapRenameInfo(defaultProject.getLanguageService().getRenameInfo(file, position), Debug.assertDefined(this.projectService.getScriptInfo(file))); + const renameInfo: protocol.RenameInfo = this.mapRenameInfo(defaultProject.getLanguageService().getRenameInfo(file, position, { allowRenameOfImportPath: this.getHostPreferences().allowRenameOfImportPath }), Debug.assertDefined(this.projectService.getScriptInfo(file))); return { info: renameInfo, locs: this.toSpanGroups(locations) }; } diff --git a/src/services/rename.ts b/src/services/rename.ts index a51d79797bfec..484349acc8683 100644 --- a/src/services/rename.ts +++ b/src/services/rename.ts @@ -1,14 +1,14 @@ /* @internal */ namespace ts.Rename { - export function getRenameInfo(program: Program, sourceFile: SourceFile, position: number): RenameInfo { + export function getRenameInfo(program: Program, sourceFile: SourceFile, position: number, options?: RenameInfoOptions): RenameInfo { const node = getTouchingPropertyName(sourceFile, position); const renameInfo = node && nodeIsEligibleForRename(node) - ? getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) + ? getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()), options) : undefined; return renameInfo || getRenameInfoError(Diagnostics.You_cannot_rename_this_element); } - function getRenameInfoForNode(node: Node, typeChecker: TypeChecker, sourceFile: SourceFile, isDefinedInLibraryFile: (declaration: Node) => boolean): RenameInfo | undefined { + function getRenameInfoForNode(node: Node, typeChecker: TypeChecker, sourceFile: SourceFile, isDefinedInLibraryFile: (declaration: Node) => boolean, options?: RenameInfoOptions): RenameInfo | undefined { const symbol = typeChecker.getSymbolAtLocation(node); if (!symbol) return; // Only allow a symbol to be renamed if it actually has at least one declaration. @@ -26,7 +26,7 @@ namespace ts.Rename { } if (isStringLiteralLike(node) && tryGetImportFromModuleSpecifier(node)) { - return getRenameInfoForModule(node, sourceFile, symbol); + return options && options.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; } const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, node); diff --git a/src/services/services.ts b/src/services/services.ts index 15f9709cb389f..6bf6566da36ac 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2062,9 +2062,9 @@ namespace ts { } } - function getRenameInfo(fileName: string, position: number): RenameInfo { + function getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo { synchronizeHostData(); - return Rename.getRenameInfo(program, getValidSourceFile(fileName), position); + return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); } function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings): RefactorContext { diff --git a/src/services/shims.ts b/src/services/shims.ts index ff66680bdbc69..33ea4333e6f01 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -164,7 +164,7 @@ namespace ts { * Returns a JSON-encoded value of the type: * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } */ - getRenameInfo(fileName: string, position: number): string; + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; /** * Returns a JSON-encoded value of the type: @@ -831,10 +831,10 @@ namespace ts { ); } - public getRenameInfo(fileName: string, position: number): string { + public getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string { return this.forwardJSONCall( `getRenameInfo('${fileName}', ${position})`, - () => this.languageService.getRenameInfo(fileName, position) + () => this.languageService.getRenameInfo(fileName, position, options) ); } diff --git a/src/services/types.ts b/src/services/types.ts index 5100a99930661..b45a816d6e007 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -294,7 +294,7 @@ namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; - getRenameInfo(fileName: string, position: number): RenameInfo; + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -848,6 +848,10 @@ namespace ts { localizedErrorMessage: string; } + export interface RenameInfoOptions { + readonly allowRenameOfImportPath?: boolean; + } + export interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/src/testRunner/unittests/tsserver/rename.ts b/src/testRunner/unittests/tsserver/rename.ts index 75c08bb8bb046..4e95e79e31f31 100644 --- a/src/testRunner/unittests/tsserver/rename.ts +++ b/src/testRunner/unittests/tsserver/rename.ts @@ -7,8 +7,18 @@ namespace ts.projectSystem { const session = createSession(createServerHost([aTs, bTs])); openFilesForSession([bTs], session); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";')); - assert.deepEqual(response, { + const response1 = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";')); + assert.deepEqual(response1, { + info: { + canRename: false, + localizedErrorMessage: "You cannot rename this element." + }, + locs: [{ file: bTs.path, locs: [protocolRenameSpanFromSubstring(bTs.content, "./a")] }], + }); + + session.getProjectService().setHostConfiguration({ preferences: { allowRenameOfImportPath: true } }); + const response2 = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";')); + assert.deepEqual(response2, { info: { canRename: true, fileToRename: aTs.path, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 91f492ddd1d52..30effc9e91a3d 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4706,7 +4706,7 @@ declare namespace ts { getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; - getRenameInfo(fileName: string, position: number): RenameInfo; + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; @@ -5150,6 +5150,9 @@ declare namespace ts { canRename: false; localizedErrorMessage: string; } + interface RenameInfoOptions { + readonly allowRenameOfImportPath?: boolean; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; @@ -7923,6 +7926,7 @@ declare namespace ts.server.protocol { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; + readonly allowRenameOfImportPath?: boolean; } interface CompilerOptions { allowJs?: boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 0e693f698f253..cd6d45a16478a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4706,7 +4706,7 @@ declare namespace ts { getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; - getRenameInfo(fileName: string, position: number): RenameInfo; + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReadonlyArray | undefined; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; @@ -5150,6 +5150,9 @@ declare namespace ts { canRename: false; localizedErrorMessage: string; } + interface RenameInfoOptions { + readonly allowRenameOfImportPath?: boolean; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/tests/cases/fourslash/findAllRefs_importType_exportEquals.ts b/tests/cases/fourslash/findAllRefs_importType_exportEquals.ts index 496fa8a9d3baf..3734b7a2e880f 100644 --- a/tests/cases/fourslash/findAllRefs_importType_exportEquals.ts +++ b/tests/cases/fourslash/findAllRefs_importType_exportEquals.ts @@ -27,4 +27,5 @@ verify.renameLocations(r2, [r0, r1, r2]); for (const range of [r3b, r4b]) { goTo.rangeStart(range); verify.renameInfoSucceeded(/*displayName*/ "/a.ts", /*fullDisplayName*/ "/a.ts", /*kind*/ "module", /*kindModifiers*/ "", /*fileToRename*/ "/a.ts", range); + verify.renameInfoFailed("You cannot rename this element.", /*allowRenameOfImportPath*/ false); } diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 293c8d2157422..ed71f8893b8b5 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -282,8 +282,8 @@ declare namespace FourSlashInterface { text: string; textSpan?: TextSpan; }[]): void; - renameInfoSucceeded(displayName?: string, fullDisplayName?: string, kind?: string, kindModifiers?: string, fileToRename?: string, range?: Range): void; - renameInfoFailed(message?: string): void; + renameInfoSucceeded(displayName?: string, fullDisplayName?: string, kind?: string, kindModifiers?: string, fileToRename?: string, range?: Range, allowRenameOfImportPath?: boolean): void; + renameInfoFailed(message?: string, allowRenameOfImportPath?: boolean): void; renameLocations(startRanges: ArrayOrSingle, options: RenameLocationsOptions): void; /** Verify the quick info available at the current marker. */ diff --git a/tests/cases/fourslash/renameImport.ts b/tests/cases/fourslash/renameImport.ts index 6fdef347205e9..8292da4013d72 100644 --- a/tests/cases/fourslash/renameImport.ts +++ b/tests/cases/fourslash/renameImport.ts @@ -27,6 +27,7 @@ goTo.eachRange(range => { const name = target === "dir" ? "/dir" : target === "dir/index" ? "/dir/index.ts" : "/a.ts"; const kind = target === "dir" ? "directory" : "module"; verify.renameInfoSucceeded(/*displayName*/ name, /*fullDisplayName*/ name, /*kind*/ kind, /*kindModifiers*/ "", /*fileToRename*/ name, range); + verify.renameInfoFailed("You cannot rename this element.", /*allowRenameOfImportPath*/ false); }); goTo.marker("global");