diff --git a/src/compiler/core.ts b/src/compiler/core.ts index caf6f553baf..8b0e3cfacd3 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1324,6 +1324,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 3761049017d..6e96365a4ac 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -585,7 +585,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 f9a145d16fd..ea95f7a4f42 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1569,15 +1569,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 29f2475f21d..9796bf2fa22 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: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Install_0), [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 996645fc15d..fbefbbcf4e8 100644 --- a/src/services/refactors/installTypesForPackage.ts +++ b/src/services/refactors/installTypesForPackage.ts @@ -47,7 +47,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 8fcd7ef13b8..d62fc88a649 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1887,18 +1887,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 bcebc437892..28de0566a50 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -293,8 +293,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; @@ -406,6 +412,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 0b93c408be4..407e5dec707 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3975,8 +3975,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; @@ -5275,7 +5281,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 8a8f5218f3a..43b668fe2d1 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3975,8 +3975,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 d0ace09909e..10d4270892e 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", }], }]);