Skip to content

Commit

Permalink
feat: Add insert replace support for completions (#583)
Browse files Browse the repository at this point in the history
Co-authored-by: Rafał Chłodnicki <rchl2k@gmail.com>
  • Loading branch information
predragnikolic and rchl committed Sep 6, 2022
1 parent f872078 commit fdf9d11
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
13 changes: 9 additions & 4 deletions src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface ParameterListParts {
readonly hasOptionalParameters: boolean;
}

export function asCompletionItem(entry: tsp.CompletionEntry, file: string, position: lsp.Position, document: LspDocument, features: SupportedFeatures): lsp.CompletionItem | null {
export function asCompletionItem(entry: tsp.CompletionEntry, optionalReplacementSpan: tsp.TextSpan | undefined, file: string, position: lsp.Position, document: LspDocument, features: SupportedFeatures): lsp.CompletionItem | null {
const item: lsp.CompletionItem = {
label: entry.name,
...features.completionLabelDetails ? { labelDetails: entry.labelDetails } : {},
Expand Down Expand Up @@ -63,7 +63,8 @@ export function asCompletionItem(entry: tsp.CompletionEntry, file: string, posit
}

let insertText = entry.insertText;
let replacementRange = entry.replacementSpan && Range.fromTextSpan(entry.replacementSpan);
const replacementSpan = entry.replacementSpan || optionalReplacementSpan;
let replacementRange = replacementSpan && Range.fromTextSpan(replacementSpan);
// Make sure we only replace a single line at most
if (replacementRange && replacementRange.start.line !== replacementRange.end.line) {
replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line));
Expand Down Expand Up @@ -108,8 +109,12 @@ export function asCompletionItem(entry: tsp.CompletionEntry, file: string, posit
if (!insertText) {
insertText = item.label;
}

item.textEdit = lsp.TextEdit.replace(replacementRange, insertText);
if (features.completionInsertReplaceSupport) {
const insertRange = lsp.Range.create(replacementRange.start, position);
item.textEdit = lsp.InsertReplaceEdit.create(insertText, insertRange, replacementRange);
} else {
item.textEdit = lsp.TextEdit.replace(replacementRange, insertText);
}
} else {
item.insertText = insertText;
}
Expand Down
64 changes: 64 additions & 0 deletions src/lsp-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,70 @@ describe('completion', () => {
server.didCloseTextDocument({ textDocument: doc });
});

it('completions for clients that do not support insertReplaceSupport', async () => {
const doc = {
uri: uri('bar.ts'),
languageId: 'typescript',
version: 1,
text: `
class Foo {
getById() {};
}
const foo = new Foo()
foo.getById()
`,
};
server.didOpenTextDocument({ textDocument: doc });
const proposals = await server.completion({ textDocument: doc, position: positionAfter(doc, '.get') });
assert.isNotNull(proposals);
const completion = proposals!.items.find(completion => completion.label === 'getById');
assert.isDefined(completion);
assert.isDefined(completion!.textEdit);
assert.containsAllKeys(completion!.textEdit, ['newText', 'range']);
server.didCloseTextDocument({ textDocument: doc });
});

it('completions for clients that support insertReplaceSupport', async () => {
const clientCapabilitiesOverride: lsp.ClientCapabilities = {
textDocument: {
completion: {
completionItem: {
insertReplaceSupport: true,
},
},
},
};
const localServer = await createServer({
rootUri: null,
publishDiagnostics: () => {},
clientCapabilitiesOverride,
});
const doc = {
uri: uri('bar.ts'),
languageId: 'typescript',
version: 1,
text: `
class Foo {
getById() {};
}
const foo = new Foo()
foo.getById()
`,
};
localServer.didOpenTextDocument({ textDocument: doc });
const proposals = await localServer.completion({ textDocument: doc, position: positionAfter(doc, '.get') });
assert.isNotNull(proposals);
const completion = proposals!.items.find(completion => completion.label === 'getById');
assert.isDefined(completion);
assert.isDefined(completion!.textEdit);
assert.containsAllKeys(completion!.textEdit, ['newText', 'insert', 'replace']);
localServer.didCloseTextDocument({ textDocument: doc });
localServer.closeAll();
localServer.shutdown();
});

it('includes detail field with package name for auto-imports', async () => {
const doc = {
uri: uri('bar.ts'),
Expand Down
13 changes: 9 additions & 4 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ export class LspServer {
// Setup supported features.
const { textDocument } = clientCapabilities;
if (textDocument) {
const completionCapabilities = textDocument.completion;
this.features.codeActionDisabledSupport = textDocument.codeAction?.disabledSupport;
this.features.definitionLinkSupport = textDocument.definition?.linkSupport && typescriptVersion.version?.gte(API.v270);
const completionCapabilities = textDocument.completion;
this.features.completionInsertReplaceSupport = completionCapabilities?.completionItem?.insertReplaceSupport;
if (completionCapabilities?.completionItem) {
if (this.configurationManager.tsPreferences.useLabelDetailsInCompletionEntries
&& completionCapabilities.completionItem.labelDetailsSupport
Expand Down Expand Up @@ -657,18 +658,22 @@ export class LspServer {
triggerKind: params.context?.triggerKind,
}));
const { body } = result;
if (!body) {
return lsp.CompletionList.create();
}
const { entries, isIncomplete, optionalReplacementSpan } = body;
const completions: lsp.CompletionItem[] = [];
for (const entry of body?.entries ?? []) {
for (const entry of entries || []) {
if (entry.kind === 'warning') {
continue;
}
const completion = asCompletionItem(entry, file, params.position, document, this.features);
const completion = asCompletionItem(entry, optionalReplacementSpan, file, params.position, document, this.features);
if (!completion) {
continue;
}
completions.push(completion);
}
return lsp.CompletionList.create(completions, body?.isIncomplete);
return lsp.CompletionList.create(completions, isIncomplete);
} catch (error) {
if ((error as Error).message === 'No content available.') {
this.logger.info('No content was available for completion request');
Expand Down
3 changes: 2 additions & 1 deletion src/ts-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ export class DisplayPartKind {

export interface SupportedFeatures {
codeActionDisabledSupport?: boolean;
completionInsertReplaceSupport?: boolean;
completionLabelDetails?: boolean;
completionSnippets?: boolean;
diagnosticsTagSupport?: boolean;
definitionLinkSupport?: boolean;
diagnosticsTagSupport?: boolean;
}

export interface TypeScriptPlugin {
Expand Down

0 comments on commit fdf9d11

Please sign in to comment.