Skip to content

Commit bccefd3

Browse files
authored
fix(server): Only provide InsertReplaceEdit when the client supports it (#1452)
From the spec: >Most editors support two different operations when accepting a completion >item. One is to insert a completion text and the other is to replace an >existing text with a completion text. Since this can usually not be >predetermined by a server it can report both ranges. Clients need to >signal support for InsertReplaceEdits via the >textDocument.completion.completionItem.insertReplaceSupport client >capability property. Fixes #1451
1 parent a876c15 commit bccefd3

File tree

3 files changed

+31
-17
lines changed

3 files changed

+31
-17
lines changed

integration/lsp/ivy_spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,8 @@ describe('Angular Ivy language server', () => {
247247
}) as lsp.CompletionItem[];
248248
const outputCompletion = response.find(i => i.label === '(appOutput)')!;
249249
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)
250+
// // replace range includes the closing )
251+
expect((outputCompletion.textEdit as lsp.TextEdit).range)
254252
.toEqual({start: {line: 0, character: 8}, end: {line: 0, character: 10}});
255253
});
256254
});

server/src/completion.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ function ngCompletionKindToLspCompletionItemKind(kind: CompletionKind): lsp.Comp
103103
* @param scriptInfo
104104
*/
105105
export function tsCompletionEntryToLspCompletionItem(
106-
entry: ts.CompletionEntry, position: lsp.Position,
107-
scriptInfo: ts.server.ScriptInfo): lsp.CompletionItem {
106+
entry: ts.CompletionEntry, position: lsp.Position, scriptInfo: ts.server.ScriptInfo,
107+
insertReplaceSupport: boolean): lsp.CompletionItem {
108108
const item = lsp.CompletionItem.create(entry.name);
109109
// Even though `entry.kind` is typed as ts.ScriptElementKind, it's
110110
// really Angular's CompletionKind. This is because ts.ScriptElementKind does
@@ -118,20 +118,30 @@ export function tsCompletionEntryToLspCompletionItem(
118118
// from 'entry.name'. For example, a method name could be 'greet', but the
119119
// insertText is 'greet()'.
120120
const insertText = entry.insertText || entry.name;
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-
}
121+
item.textEdit = createTextEdit(scriptInfo, entry, position, insertText, insertReplaceSupport);
122+
131123
item.data = {
132124
kind: 'ngCompletionOriginData',
133125
filePath: scriptInfo.fileName,
134126
position,
135127
} as NgCompletionOriginData;
136128
return item;
137129
}
130+
131+
function createTextEdit(
132+
scriptInfo: ts.server.ScriptInfo, entry: ts.CompletionEntry, position: lsp.Position,
133+
insertText: string, insertReplaceSupport: boolean) {
134+
if (entry.replacementSpan === undefined) {
135+
return lsp.TextEdit.insert(position, insertText);
136+
} else if (insertReplaceSupport) {
137+
const replacementRange = tsTextSpanToLspRange(scriptInfo, entry.replacementSpan);
138+
const tsPosition = lspPositionToTsPosition(scriptInfo, position);
139+
const insertLength = tsPosition - entry.replacementSpan.start;
140+
const insertionRange =
141+
tsTextSpanToLspRange(scriptInfo, {...entry.replacementSpan, length: insertLength});
142+
return lsp.InsertReplaceEdit.create(insertText, insertionRange, replacementRange);
143+
} else {
144+
return lsp.TextEdit.replace(
145+
tsTextSpanToLspRange(scriptInfo, entry.replacementSpan), insertText);
146+
}
147+
}

server/src/session.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class Session {
6565
* disable renaming because we know that there are many cases where it will not work correctly.
6666
*/
6767
private renameDisabledProjects: WeakSet<ts.server.Project> = new WeakSet();
68+
private clientCapabilities: lsp.ClientCapabilities = {};
6869

6970
constructor(options: SessionOptions) {
7071
this.logger = options.logger;
@@ -576,6 +577,7 @@ export class Session {
576577
const serverOptions: ServerOptions = {
577578
logFile: this.logger.getLogFileName(),
578579
};
580+
this.clientCapabilities = params.capabilities;
579581
return {
580582
capabilities: {
581583
codeLensProvider: this.ivy ? {resolveProvider: true} : undefined,
@@ -969,8 +971,12 @@ export class Session {
969971
if (!completions) {
970972
return;
971973
}
974+
const clientSupportsInsertReplaceCompletion =
975+
this.clientCapabilities.textDocument?.completion?.completionItem?.insertReplaceSupport ??
976+
false;
972977
return completions.entries.map(
973-
(e) => tsCompletionEntryToLspCompletionItem(e, params.position, scriptInfo));
978+
(e) => tsCompletionEntryToLspCompletionItem(
979+
e, params.position, scriptInfo, clientSupportsInsertReplaceCompletion));
974980
}
975981

976982
private onCompletionResolve(item: lsp.CompletionItem): lsp.CompletionItem {

0 commit comments

Comments
 (0)