Skip to content

Commit

Permalink
feat: generate the import declaration for the completion item code ac…
Browse files Browse the repository at this point in the history
…tions

In the completion item, the `additionalTextEdits` can only be included the changes about the
current file, the other changes should be inserted by the vscode command.

For example, when the user selects a component in an HTML file, the extension inserts the
selector in the HTML file and auto-generates the import declaration in the TS file.
  • Loading branch information
ivanwonder authored and atscott committed Jun 27, 2024
1 parent 4ab3481 commit 68f0d60
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Input hashes for repository rule npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml").
# This file should be checked into version control along with the pnpm-lock.yaml file.
.npmrc=974837034
pnpm-lock.yaml=730915817
yarn.lock=1032276408
package.json=-257701941
pnpm-lock.yaml=1771343819
yarn.lock=1590538245
package.json=1973544585
pnpm-workspace.yaml=1711114604
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
type: enum
enum: ['package.json', 'builds-repo']
docker:
- image: cimg/node:18.13.0
- image: cimg/node:18.19.1
environment:
# TODO: Remove when pnpm is exclusively used.
ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1'
Expand Down
10 changes: 10 additions & 0 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ export class AngularLanguageClient implements vscode.Disposable {
};
}

async applyWorkspaceEdits(workspaceEdits: lsp.WorkspaceEdit[]) {
for (const edit of workspaceEdits) {
const workspaceEdit = this.client?.protocol2CodeConverter.asWorkspaceEdit(edit);
if (workspaceEdit === undefined) {
continue;
}
await vscode.workspace.applyEdit(workspaceEdit);
}
}

private async isInAngularProject(doc: vscode.TextDocument): Promise<boolean> {
if (this.client === null) {
return false;
Expand Down
12 changes: 12 additions & 0 deletions client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import * as vscode from 'vscode';
import * as lsp from 'vscode-languageclient/node';

import {OpenJsDocLinkCommand_Args, OpenJsDocLinkCommandId, ServerOptions} from '../../common/initialize';

Expand Down Expand Up @@ -191,6 +192,16 @@ function openJsDocLinkCommand(): Command<OpenJsDocLinkCommand_Args> {
};
}

function applyCodeActionCommand(ngClient: AngularLanguageClient): Command {
return {
id: 'angular.applyCompletionCodeAction',
isTextEditorCommand: false,
async execute(args: lsp.WorkspaceEdit[]) {
await ngClient.applyWorkspaceEdits(args);
},
};
}

/**
* Register all supported vscode commands for the Angular extension.
* @param client language client
Expand All @@ -205,6 +216,7 @@ export function registerCommands(
goToComponentWithTemplateFile(client),
goToTemplateForComponent(client),
openJsDocLinkCommand(),
applyCodeActionCommand(client),
];

for (const command of commands) {
Expand Down
52 changes: 51 additions & 1 deletion integration/lsp/ivy_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {URI} from 'vscode-uri';

import {ProjectLanguageService, ProjectLanguageServiceParams, SuggestStrictMode, SuggestStrictModeParams} from '../../common/notifications';
import {GetComponentsWithTemplateFile, GetTcbRequest, GetTemplateLocationForComponent, IsInAngularProject} from '../../common/requests';
import {APP_COMPONENT, APP_COMPONENT_URI, FOO_COMPONENT, FOO_COMPONENT_URI, FOO_TEMPLATE, FOO_TEMPLATE_URI, IS_BAZEL, PROJECT_PATH, TSCONFIG} from '../test_constants';
import {APP_COMPONENT, APP_COMPONENT_MODULE_URI, APP_COMPONENT_URI, BAR_COMPONENT, BAR_COMPONENT_URI, FOO_COMPONENT, FOO_COMPONENT_URI, FOO_TEMPLATE, FOO_TEMPLATE_URI, IS_BAZEL, PROJECT_PATH, TSCONFIG} from '../test_constants';

import {convertPathToFileUrl, createConnection, createTracer, initializeServer, openTextDocument} from './test_utils';

Expand Down Expand Up @@ -580,6 +580,56 @@ export class AppComponent {
});
expect(componentResponse).toBe(true);
})

describe('auto-import component', () => {
it('should generate import in the different file', async () => {
openTextDocument(client, FOO_TEMPLATE, `<bar-`);
const response = await client.sendRequest(lsp.CompletionRequest.type, {
textDocument: {
uri: FOO_TEMPLATE_URI,
},
position: {line: 0, character: 5},
}) as lsp.CompletionItem[];
const libPostResponse = response.find(res => res.label === 'bar-component')!;
const detail = await client.sendRequest(lsp.CompletionResolveRequest.type, libPostResponse);
expect(detail.command?.command).toEqual('angular.applyCompletionCodeAction');
expect(detail.command?.arguments?.[0])
.toEqual(([{
'changes': {
[APP_COMPONENT_MODULE_URI]: [
{
'newText': '\nimport { BarComponent } from "./bar.component";',
'range':
{'start': {'line': 5, 'character': 45}, 'end': {'line': 5, 'character': 45}}
},
{
'newText': 'imports: [\n CommonModule,\n PostModule,\n BarComponent\n]',
'range':
{'start': {'line': 8, 'character': 2}, 'end': {'line': 11, 'character': 3}}
}
]
}
}]

));
});

it('should generate import in the current file', async () => {
openTextDocument(client, BAR_COMPONENT);
const response = await client.sendRequest(lsp.CompletionRequest.type, {
textDocument: {
uri: BAR_COMPONENT_URI,
},
position: {line: 13, character: 16},
}) as lsp.CompletionItem[];
const libPostResponse = response.find(res => res.label === 'baz-component')!;
const detail = await client.sendRequest(lsp.CompletionResolveRequest.type, libPostResponse);
expect(detail.additionalTextEdits).toEqual([{
'newText': ',\n imports: [BazComponent]',
'range': {'start': {'line': 14, 'character': 20}, 'end': {'line': 14, 'character': 20}}
}]);
});
});
});

describe('auto-apply optional chaining', () => {
Expand Down
19 changes: 19 additions & 0 deletions integration/project/app/bar.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component } from '@angular/core';

@Component({
selector: 'baz-component',
template: `<h1>Hello {{name}}</h1>`,
standalone: true
})
export class BazComponent {
name = 'Angular';
}

@Component({
selector: 'bar-component',
template: `<`,
standalone: true
})
export class BarComponent {
name = 'Angular';
}
4 changes: 4 additions & 0 deletions integration/test_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export const SERVER_PATH = IS_BAZEL ? join(PACKAGE_ROOT, 'server', 'index.js') :
export const PROJECT_PATH = join(PACKAGE_ROOT, 'integration', 'project');
export const APP_COMPONENT = join(PROJECT_PATH, 'app', 'app.component.ts');
export const APP_COMPONENT_URI = convertPathToFileUrl(APP_COMPONENT);
export const BAR_COMPONENT = join(PROJECT_PATH, 'app', 'bar.component.ts');
export const BAR_COMPONENT_URI = convertPathToFileUrl(BAR_COMPONENT);
export const APP_COMPONENT_MODULE = join(PROJECT_PATH, 'app', 'app.module.ts');
export const APP_COMPONENT_MODULE_URI = convertPathToFileUrl(APP_COMPONENT_MODULE);
export const FOO_TEMPLATE = join(PROJECT_PATH, 'app', 'foo.component.html');
export const FOO_TEMPLATE_URI = convertPathToFileUrl(FOO_TEMPLATE);
export const FOO_COMPONENT = join(PROJECT_PATH, 'app', 'foo.component.ts');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@
"test:legacy-syntaxes": "yarn compile:syntaxes-test && yarn build:syntaxes && jasmine dist/syntaxes/test/driver.js"
},
"dependencies": {
"@angular/language-service": "18.1.0-next.0",
"@angular/language-service": "18.1.0-next.2",
"typescript": "5.4.5",
"vscode-html-languageservice": "^4.2.5",
"vscode-jsonrpc": "6.0.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"ngserver": "./bin/ngserver"
},
"dependencies": {
"@angular/language-service": "18.1.0-next.1",
"@angular/language-service": "18.1.0-next.2",
"vscode-html-languageservice": "^4.2.5",
"vscode-jsonrpc": "6.0.0",
"vscode-languageserver": "7.0.0",
Expand Down
70 changes: 69 additions & 1 deletion server/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,9 @@ export class Session {
return item;
}

const {kind, kindModifiers, displayParts, documentation, tags} = details;
const {kind, kindModifiers, displayParts, documentation, tags, codeActions} = details;
const codeActionsDetail = generateCommandAndTextEditsFromCodeActions(
codeActions ?? [], filePath, (path: string) => this.projectService.getScriptInfo(path));
let desc = kindModifiers ? kindModifiers + ' ' : '';
if (displayParts && displayParts.length > 0) {
// displayParts does not contain info about kindModifiers
Expand All @@ -1206,6 +1208,8 @@ export class Session {
documentation, tags, (fileName) => this.getLSAndScriptInfo(fileName)?.scriptInfo)
.join('\n'),
};
item.additionalTextEdits = codeActionsDetail.additionalTextEdits;
item.command = codeActionsDetail.command;
return item;
}

Expand Down Expand Up @@ -1340,3 +1344,67 @@ function getCodeFixesAll(
}
return lspCodeActions;
}

/**
* In the completion item, the `additionalTextEdits` can only be included the changes about the
* current file, the other changes should be inserted by the vscode command.
*
* For example, when the user selects a component in an HTML file, the extension inserts the
* selector in the HTML file and auto-generates the import declaration in the TS file.
*
* The code is copied from
* [here](https://github.com/microsoft/vscode/blob/4608b378a8101ff273fa5db36516da6022f66bbf/extensions/typescript-language-features/src/languageFeatures/completions.ts#L304)
*/
function generateCommandAndTextEditsFromCodeActions(
codeActions: ts.CodeAction[], currentFilePath: string,
getScriptInfo: (path: string) => ts.server.ScriptInfo |
undefined): {command?: lsp.Command; additionalTextEdits?: lsp.TextEdit[]} {
if (codeActions.length === 0) {
return {};
}

// Try to extract out the additionalTextEdits for the current file.
// Also check if we still have to apply other workspace edits and commands
// using a vscode command
const additionalTextEdits: lsp.TextEdit[] = [];
const commandTextEditors: lsp.WorkspaceEdit[] = [];

for (const tsAction of codeActions) {
const currentFileChanges =
tsAction.changes.filter(change => change.fileName === currentFilePath);
const otherWorkspaceFileChanges =
tsAction.changes.filter(change => change.fileName !== currentFilePath);

if (currentFileChanges.length > 0) {
// Apply all edits in the current file using `additionalTextEdits`
const additionalWorkspaceEdit =
tsFileTextChangesToLspWorkspaceEdit(currentFileChanges, getScriptInfo).changes;
if (additionalWorkspaceEdit !== undefined) {
for (const edit of Object.values(additionalWorkspaceEdit)) {
additionalTextEdits.push(...edit);
}
}
}

if (otherWorkspaceFileChanges.length > 0) {
commandTextEditors.push(
tsFileTextChangesToLspWorkspaceEdit(otherWorkspaceFileChanges, getScriptInfo),
);
}
}

let command: lsp.Command|undefined = undefined;
if (commandTextEditors.length > 0) {
// Create command that applies all edits not in the current file.
command = {
title: '',
command: 'angular.applyCompletionCodeAction',
arguments: [commandTextEditors],
};
}

return {
command,
additionalTextEdits: additionalTextEdits.length ? additionalTextEdits : undefined
};
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@
uuid "^8.3.2"
yargs "^17.0.0"

"@angular/language-service@18.1.0-next.0":
version "18.1.0-next.0"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.1.0-next.0.tgz#ad62863fd2172d494c2c464dad35a8b1ca47e8b3"
integrity sha512-9kMpU+P9KY0YK56GlR6csFq/8GCZUPcTkTGwbMoOFLJCBa/y/tho9Ikl7epupl1GjaYZraKqNUxH+5z4P0DzCg==
"@angular/language-service@18.1.0-next.2":
version "18.1.0-next.2"
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.1.0-next.2.tgz#f8e31a175ea3df6535f50e1bacf5038e83b5d6d4"
integrity sha512-d1c/rOmbmVxigzuENEdSKjEx+/tqSbuoQJ5iHUmof/rRQGub4fzFI2I3d2sVOJ4eP38/jifVMWGrX0MdrBbJAw==

"@assemblyscript/loader@^0.10.1":
version "0.10.1"
Expand Down

0 comments on commit 68f0d60

Please sign in to comment.