From ce3f9d82460c17ca0700e527cfa644a8eef5121a Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Sat, 9 Jul 2022 22:15:09 +0800 Subject: [PATCH] feat(server): support code action --- package.json | 2 +- server/package.json | 2 +- server/src/session.ts | 89 ++++++++++++++++++++++++++++++++++++++++++- server/src/utils.ts | 26 +++++++++++++ yarn.lock | 7 ++-- 5 files changed, 119 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index aad2cd2782..831d890be7 100644 --- a/package.json +++ b/package.json @@ -216,7 +216,7 @@ "test:syntaxes": "yarn compile:syntaxes-test && yarn build:syntaxes && jasmine dist/syntaxes/test/driver.js" }, "dependencies": { - "@angular/language-service": "14.1.0-next.0", + "@angular/language-service": "https://output.circle-artifacts.com/output/job/ad7b0112-3d86-4f40-a595-a399e55bc276/artifacts/0/angular/language-service-pr46764-e7f1b32d29.tgz", "typescript": "4.7.4", "vscode-jsonrpc": "6.0.0", "vscode-languageclient": "7.0.0", diff --git a/server/package.json b/server/package.json index 728d0c3880..520c211bb4 100644 --- a/server/package.json +++ b/server/package.json @@ -15,7 +15,7 @@ "ngserver": "./bin/ngserver" }, "dependencies": { - "@angular/language-service": "14.0.0-next.0", + "@angular/language-service": "https://output.circle-artifacts.com/output/job/ad7b0112-3d86-4f40-a595-a399e55bc276/artifacts/0/angular/language-service-pr46764-e7f1b32d29.tgz", "vscode-jsonrpc": "6.0.0", "vscode-languageserver": "7.0.0", "vscode-uri": "3.0.3" diff --git a/server/src/session.ts b/server/src/session.ts index 56c37edf60..5bb45ae2b1 100644 --- a/server/src/session.ts +++ b/server/src/session.ts @@ -19,7 +19,7 @@ import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './comp import {tsDiagnosticToLspDiagnostic} from './diagnostic'; import {resolveAndRunNgcc} from './ngcc'; import {ServerHost} from './server_host'; -import {filePathToUri, getMappedDefinitionInfo, isConfiguredProject, isDebugMode, lspPositionToTsPosition, lspRangeToTsPositions, MruTracker, tsDisplayPartsToText, tsTextSpanToLspRange, uriToFilePath} from './utils'; +import {filePathToUri, fileTextChangesToWorkspaceEdit, getMappedDefinitionInfo, isConfiguredProject, isDebugMode, lspPositionToTsPosition, lspRangeToTsPositions, MruTracker, tsDisplayPartsToText, tsTextSpanToLspRange, uriToFilePath} from './utils'; export interface SessionOptions { host: ServerHost; @@ -197,6 +197,59 @@ export class Session { conn.onCodeLens(p => this.onCodeLens(p)); conn.onCodeLensResolve(p => this.onCodeLensResolve(p)); conn.onSignatureHelp(p => this.onSignatureHelp(p)); + conn.onCodeAction(p => this.codeAction(p)); + conn.onCodeActionResolve(p => this.onCodeActionResolve(p)); + } + + private codeAction(params: lsp.CodeActionParams): lsp.CodeAction[]|null { + const filePath = uriToFilePath(params.textDocument.uri); + const lsInfo = this.getLSAndScriptInfo(params.textDocument); + if (!lsInfo) { + return null; + } + const start = lspPositionToTsPosition(lsInfo.scriptInfo, params.range.start); + const end = lspPositionToTsPosition(lsInfo.scriptInfo, params.range.end); + const errorCodes = params.context.diagnostics.map(diag => diag.code) + .filter((code): code is number => typeof code === 'number'); + + const codeActions = + lsInfo.languageService.getCodeFixesAtPosition(filePath, start, end, errorCodes, {}, {}); + return codeActions + .map(codeAction => { + return { + title: codeAction.description, + kind: lsp.CodeActionKind.QuickFix, + diagnostics: params.context.diagnostics, + edit: fileTextChangesToWorkspaceEdit( + codeAction.changes, (path: string) => this.projectService.getScriptInfo(path)), + }; + }) + .concat(getCodeFixesAll(codeActions, params.textDocument)); + } + + private onCodeActionResolve(param: lsp.CodeAction): lsp.CodeAction { + const codeActionResolve = param.data as unknown as CodeActionResolve; + const isCodeFixesAll = codeActionResolve.fixId !== undefined; + if (!isCodeFixesAll) { + return param; + } + const filePath = uriToFilePath(codeActionResolve.document.uri); + const lsInfo = this.getLSAndScriptInfo(codeActionResolve.document); + if (!lsInfo) { + return param; + } + const fixesAllChanges = lsInfo.languageService.getCombinedCodeFix( + { + type: 'file', + fileName: filePath, + }, + codeActionResolve.fixId as {}, {}, {}); + + return { + title: codeActionResolve.title, + edit: fileTextChangesToWorkspaceEdit( + fixesAllChanges.changes, (path) => this.projectService.getScriptInfo(path)), + }; } private isInAngularProject(params: IsInAngularProjectParams): boolean|null { @@ -663,6 +716,10 @@ export class Session { workspace: { workspaceFolders: {supported: true}, }, + codeActionProvider: this.ivy ? { + resolveProvider: true, + } : + undefined, }, serverOptions, }; @@ -1239,3 +1296,33 @@ function isTypeScriptFile(path: string): boolean { function isExternalTemplate(path: string): boolean { return !isTypeScriptFile(path); } + +interface CodeActionResolve { + fixId?: string; + document: lsp.TextDocumentIdentifier; + title: string; +} + +function getCodeFixesAll( + codeActions: readonly ts.CodeFixAction[], + document: lsp.TextDocumentIdentifier): lsp.CodeAction[] { + const seen = new Set(); + const lspCodeActions: lsp.CodeAction[] = []; + for (const codeAction of codeActions) { + const fixId = codeAction.fixId as string | undefined; + if (fixId === undefined || codeAction.fixAllDescription === undefined || seen.has(fixId)) { + continue; + } + seen.add(fixId); + const codeActionResolve: CodeActionResolve = { + fixId, + document, + title: codeAction.fixAllDescription, + }; + lspCodeActions.push({ + title: codeAction.fixAllDescription, + data: codeActionResolve, + }); + } + return lspCodeActions; +} diff --git a/server/src/utils.ts b/server/src/utils.ts index 658068b63a..7fc52b1239 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -39,6 +39,32 @@ export function filePathToUri(filePath: string): lsp.DocumentUri { return URI.file(filePath).toString(); } +export function fileTextChangesToWorkspaceEdit( + changes: readonly ts.FileTextChanges[], + getScriptInfo: (path: string) => ts.server.ScriptInfo | undefined): lsp.WorkspaceEdit { + const workspaceChanges: {[uri: string]: lsp.TextEdit[]} = {}; + for (const change of changes) { + const scriptInfo = getScriptInfo(change.fileName); + const uri = filePathToUri(change.fileName); + if (scriptInfo === undefined) { + continue; + } + if (!workspaceChanges[uri]) { + workspaceChanges[uri] = []; + } + for (const textChange of change.textChanges) { + const textEdit: lsp.TextEdit = { + newText: textChange.newText, + range: tsTextSpanToLspRange(scriptInfo, textChange.span), + }; + workspaceChanges[uri].push(textEdit); + } + } + return { + changes: workspaceChanges, + }; +} + /** * Convert ts.TextSpan to lsp.TextSpan. TypeScript keeps track of offset using * 1-based index whereas LSP uses 0-based index. diff --git a/yarn.lock b/yarn.lock index a3acba69e9..6ff61aff15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -158,10 +158,9 @@ uuid "^8.3.2" yargs "^17.0.0" -"@angular/language-service@14.1.0-next.0": - version "14.1.0-next.0" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-14.1.0-next.0.tgz#2cc7c30a7fe641ee2d255f03ef23028e8e574ab5" - integrity sha512-tMsrL/Ug35hnH14BjpLYdjy44F3Tzrfqem38vxWUyATwV1YLiqJrvzsUhvzMffyBAkMMgdfykrb50wyguUg/fQ== +"@angular/language-service@https://output.circle-artifacts.com/output/job/ad7b0112-3d86-4f40-a595-a399e55bc276/artifacts/0/angular/language-service-pr46764-e7f1b32d29.tgz": + version "14.1.0-next.4" + resolved "https://output.circle-artifacts.com/output/job/ad7b0112-3d86-4f40-a595-a399e55bc276/artifacts/0/angular/language-service-pr46764-e7f1b32d29.tgz#28b6dcb0af3ce46512981a5f3acfaef742ed2626" "@assemblyscript/loader@^0.10.1": version "0.10.1"