Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate the import declaration for the completion item code ac… #2031

Merged
merged 1 commit into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}}
}]);
});
});
ivanwonder marked this conversation as resolved.
Show resolved Hide resolved
});

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/[email protected].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/[email protected].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
Loading