Skip to content

Commit d602cf9

Browse files
committed
fix(completions): fix completions for inputs / outputs (#1405)
* fix: add directive and event to completion kind type * fix(completions): fix completions for inputs / outputs The `TextEdit` is not meant to replace text to the right of the position. Instead, `InsesrtReplaceEdit` should be used. Reference discussion in LSP issue for examples on replacement vs insertion range for `InsertReplaceEdit`: microsoft/language-server-protocol#846 Fixes #1388 Note that the issue did not appear in the VE version of the extension because it was only providing completions as inserts (they had no `replacementSpan`). (cherry picked from commit c4310e2)
1 parent 6d8a926 commit d602cf9

File tree

5 files changed

+43
-9
lines changed

5 files changed

+43
-9
lines changed

integration/lsp/ivy_spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,27 @@ describe('Angular Ivy language server', () => {
234234
});
235235
});
236236

237+
describe('completions', () => {
238+
it('for events', async () => {
239+
openTextDocument(client, FOO_TEMPLATE, `<my-app ()></my-app>`);
240+
const languageServiceEnabled = await waitForNgcc(client);
241+
expect(languageServiceEnabled).toBeTrue();
242+
const response = await client.sendRequest(lsp.CompletionRequest.type, {
243+
textDocument: {
244+
uri: `file://${FOO_TEMPLATE}`,
245+
},
246+
position: {line: 0, character: 9},
247+
}) as lsp.CompletionItem[];
248+
const outputCompletion = response.find(i => i.label === '(appOutput)')!;
249+
expect(outputCompletion.kind).toEqual(lsp.CompletionItemKind.Property);
250+
expect((outputCompletion.textEdit as lsp.InsertReplaceEdit).insert)
251+
.toEqual({start: {line: 0, character: 8}, end: {line: 0, character: 9}});
252+
// replace range includes the closing )
253+
expect((outputCompletion.textEdit as lsp.InsertReplaceEdit).replace)
254+
.toEqual({start: {line: 0, character: 8}, end: {line: 0, character: 10}});
255+
});
256+
});
257+
237258
describe('renaming', () => {
238259
describe('from template files', () => {
239260
beforeEach(async () => {

integration/lsp/test_utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function initializeServer(client: MessageConnection): Promise<lsp.Initial
6666
});
6767
}
6868

69-
export function openTextDocument(client: MessageConnection, filePath: string) {
69+
export function openTextDocument(client: MessageConnection, filePath: string, newText?: string) {
7070
let languageId = 'unknown';
7171
if (filePath.endsWith('ts')) {
7272
languageId = 'typescript';
@@ -78,7 +78,7 @@ export function openTextDocument(client: MessageConnection, filePath: string) {
7878
uri: `file://${filePath}`,
7979
languageId,
8080
version: 1,
81-
text: fs.readFileSync(filePath, 'utf-8'),
81+
text: newText ?? fs.readFileSync(filePath, 'utf-8'),
8282
},
8383
});
8484
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import {Component} from '@angular/core';
1+
import {Component, EventEmitter, Input, Output} from '@angular/core';
22

33
@Component({
44
selector: 'my-app',
55
template: `<h1>Hello {{name}}</h1>`,
66
})
77
export class AppComponent {
88
name = 'Angular';
9+
@Input() appInput = '';
10+
@Output() appOutput = new EventEmitter<string>();
911
}

server/src/completion.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@
99
import * as ts from 'typescript/lib/tsserverlibrary';
1010
import * as lsp from 'vscode-languageserver';
1111

12-
import {tsTextSpanToLspRange} from './utils';
12+
import {lspPositionToTsPosition, tsTextSpanToLspRange} from './utils';
1313

1414
// TODO: Move this to `@angular/language-service`.
1515
enum CompletionKind {
1616
attribute = 'attribute',
1717
htmlAttribute = 'html attribute',
1818
property = 'property',
1919
component = 'component',
20+
directive = 'directive',
2021
element = 'element',
22+
event = 'event',
2123
key = 'key',
2224
method = 'method',
2325
pipe = 'pipe',
@@ -72,7 +74,9 @@ function ngCompletionKindToLspCompletionItemKind(kind: CompletionKind): lsp.Comp
7274
case CompletionKind.attribute:
7375
case CompletionKind.htmlAttribute:
7476
case CompletionKind.property:
77+
case CompletionKind.event:
7578
return lsp.CompletionItemKind.Property;
79+
case CompletionKind.directive:
7680
case CompletionKind.component:
7781
case CompletionKind.element:
7882
case CompletionKind.key:
@@ -114,9 +118,16 @@ export function tsCompletionEntryToLspCompletionItem(
114118
// from 'entry.name'. For example, a method name could be 'greet', but the
115119
// insertText is 'greet()'.
116120
const insertText = entry.insertText || entry.name;
117-
item.textEdit = entry.replacementSpan ?
118-
lsp.TextEdit.replace(tsTextSpanToLspRange(scriptInfo, entry.replacementSpan), insertText) :
119-
lsp.TextEdit.insert(position, insertText);
121+
if (entry.replacementSpan) {
122+
const replacementRange = tsTextSpanToLspRange(scriptInfo, entry.replacementSpan);
123+
const tsPosition = lspPositionToTsPosition(scriptInfo, position);
124+
const insertLength = tsPosition - entry.replacementSpan.start;
125+
const insertionRange =
126+
tsTextSpanToLspRange(scriptInfo, {...entry.replacementSpan, length: insertLength});
127+
item.textEdit = lsp.InsertReplaceEdit.create(insertText, insertionRange, replacementRange);
128+
} else {
129+
item.textEdit = lsp.TextEdit.insert(position, insertText);
130+
}
120131
item.data = {
121132
kind: 'ngCompletionOriginData',
122133
filePath: scriptInfo.fileName,

server/src/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@ export class Session {
941941
}
942942
const {kind, kindModifiers, textSpan, displayParts, documentation} = info;
943943
let desc = kindModifiers ? kindModifiers + ' ' : '';
944-
if (displayParts) {
944+
if (displayParts && displayParts.length > 0) {
945945
// displayParts does not contain info about kindModifiers
946946
// but displayParts does contain info about kind
947947
desc += displayParts.map(dp => dp.text).join('');
@@ -1006,7 +1006,7 @@ export class Session {
10061006

10071007
const {kind, kindModifiers, displayParts, documentation} = details;
10081008
let desc = kindModifiers ? kindModifiers + ' ' : '';
1009-
if (displayParts) {
1009+
if (displayParts && displayParts.length > 0) {
10101010
// displayParts does not contain info about kindModifiers
10111011
// but displayParts does contain info about kind
10121012
desc += displayParts.map(dp => dp.text).join('');

0 commit comments

Comments
 (0)