diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c0eb7e257a6f8..30a82cfc6da4c 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1271,6 +1271,10 @@ namespace ts { return Array.isArray ? Array.isArray(value) : value instanceof Array; } + export function toArray(value: T | T[]): T[] { + return isArray(value) ? value : [value]; + } + /** * Tests whether a value is string */ diff --git a/src/server/protocol.ts b/src/server/protocol.ts index f063eb29e340e..1ae65a4004c97 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -582,7 +582,7 @@ namespace ts.server.protocol { errorCodes?: number[]; } - export interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + export interface ApplyCodeActionCommandRequestArgs { /** May also be an array of commands. */ command: {}; } diff --git a/src/server/session.ts b/src/server/session.ts index aca131773182a..614bea592c0c9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1555,15 +1555,14 @@ namespace ts.server { } private applyCodeActionCommand(commandName: string, requestSeq: number, args: protocol.ApplyCodeActionCommandRequestArgs): void { - const { file, project } = this.getFileAndProject(args); - const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message); - const command = args.command as CodeActionCommand | CodeActionCommand[]; // They should be sending back the command we sent them. - - project.getLanguageService().applyCodeActionCommand(file, command).then( - result => { - output(/*success*/ true, isArray(result) ? result.map(res => res.successMessage).join(`${this.host.newLine}${this.host.newLine}`) : result.successMessage); - }, - error => { output(/*success*/ false, error); }); + const commands = args.command as CodeActionCommand | CodeActionCommand[]; // They should be sending back the command we sent them. + for (const command of toArray(commands)) { + const { project } = this.getFileAndProject(command); + const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message); + project.getLanguageService().applyCodeActionCommand(command).then( + result => { output(/*success*/ true, result.successMessage); }, + error => { output(/*success*/ false, error); }); + } } private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { diff --git a/src/services/codefixes/fixCannotFindModule.ts b/src/services/codefixes/fixCannotFindModule.ts index c15ca341cb428..e3bb5d15a299f 100644 --- a/src/services/codefixes/fixCannotFindModule.ts +++ b/src/services/codefixes/fixCannotFindModule.ts @@ -11,12 +11,12 @@ namespace ts.codefix { throw Debug.fail(); // These errors should only happen on the module name. } - const action = tryGetCodeActionForInstallPackageTypes(context.host, token.text); + const action = tryGetCodeActionForInstallPackageTypes(context.host, sourceFile.fileName, token.text); return action && [action]; }, }); - export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, moduleName: string): CodeAction | undefined { + export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, fileName: string, moduleName: string): CodeAction | undefined { const { packageName } = getPackageName(moduleName); if (!host.isKnownTypesPackageName(packageName)) { @@ -28,7 +28,7 @@ namespace ts.codefix { return { description: `Install '${typesPackageName}'`, changes: [], - commands: [{ type: "install package", packageName: typesPackageName }], + commands: [{ type: "install package", file: fileName, packageName: typesPackageName }], }; } } diff --git a/src/services/refactors/installTypesForPackage.ts b/src/services/refactors/installTypesForPackage.ts index b0abe3963f43e..6e80b617c16a9 100644 --- a/src/services/refactors/installTypesForPackage.ts +++ b/src/services/refactors/installTypesForPackage.ts @@ -49,7 +49,7 @@ namespace ts.refactor.installTypesForPackage { const { file, startPosition } = context; const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); if (isStringLiteral(node) && isModuleIdentifier(node) && getResolvedModule(file, node.text) === undefined) { - return codefix.tryGetCodeActionForInstallPackageTypes(context.host, node.text); + return codefix.tryGetCodeActionForInstallPackageTypes(context.host, file.fileName, node.text); } } diff --git a/src/services/services.ts b/src/services/services.ts index 867c85ded3ead..ab9347a2155ef 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1787,18 +1787,21 @@ namespace ts { }); } + function applyCodeActionCommand(action: CodeActionCommand): Promise; + function applyCodeActionCommand(action: CodeActionCommand[]): Promise; + function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise; function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise; - function applyCodeActionCommand(fileName: Path, action: CodeActionCommand | CodeActionCommand[]): Promise { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(path, a))) : applySingleCodeActionCommand(path, action); + function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrUndefined?: CodeActionCommand | CodeActionCommand[]): Promise { + const action = typeof fileName === "string" ? actionOrUndefined! : fileName as CodeActionCommand[]; + return isArray(action) ? Promise.all(action.map(applySingleCodeActionCommand)) : applySingleCodeActionCommand(action); } - function applySingleCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise { + function applySingleCodeActionCommand(action: CodeActionCommand): Promise { switch (action.type) { case "install package": return host.installPackage - ? host.installPackage({ fileName, packageName: action.packageName }) + ? host.installPackage({ fileName: toPath(action.file, currentDirectory, getCanonicalFileName), packageName: action.packageName }) : Promise.reject("Host does not implement `installPackage`"); default: Debug.fail(); diff --git a/src/services/types.ts b/src/services/types.ts index 9b32a42ab4f1b..d82d0fa94603e 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -289,8 +289,14 @@ namespace ts { getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -402,6 +408,7 @@ namespace ts { export type CodeActionCommand = InstallPackageAction; export interface InstallPackageAction { + /* @internal */ file: string; /* @internal */ type: "install package"; /* @internal */ packageName: string; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 55b6a07bf8675..522cfc906c03f 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3967,8 +3967,14 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -5262,7 +5268,7 @@ declare namespace ts.server.protocol { */ errorCodes?: number[]; } - interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + interface ApplyCodeActionCommandRequestArgs { /** May also be an array of commands. */ command: {}; } diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 140078d707728..41baa1770e1c9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3967,8 +3967,14 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; diff --git a/tests/cases/fourslash/codeFixCannotFindModule.ts b/tests/cases/fourslash/codeFixCannotFindModule.ts index d0ace09909ef8..10d4270892e87 100644 --- a/tests/cases/fourslash/codeFixCannotFindModule.ts +++ b/tests/cases/fourslash/codeFixCannotFindModule.ts @@ -19,6 +19,7 @@ verify.codeFixAvailable([{ description: "Install '@types/abs'", commands: [{ type: "install package", + file: "/a.ts", packageName: "@types/abs", }], }]);