Skip to content
This repository was archived by the owner on Oct 16, 2020. It is now read-only.

Commit 8591294

Browse files
tomv564felixfbecker
authored andcommitted
feat(completion): support snippets (#323)
1 parent 54fb99e commit 8591294

File tree

2 files changed

+135
-7
lines changed

2 files changed

+135
-7
lines changed

src/test/typescript-service-helpers.ts

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as chai from 'chai';
22
import * as sinon from 'sinon';
33
import * as ts from 'typescript';
4-
import { CompletionItemKind, CompletionList, DiagnosticSeverity, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver';
4+
import { CompletionItemKind, CompletionList, DiagnosticSeverity, InsertTextFormat, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver';
55
import { Command, Diagnostic, Hover, Location, SignatureHelp, SymbolInformation, SymbolKind } from 'vscode-languageserver-types';
66
import { LanguageClient, RemoteLanguageClient } from '../lang-handler';
77
import { DependencyReference, PackageInformation, ReferenceInformation, TextDocumentContentParams, WorkspaceFilesParams } from '../request-type';
8-
import { SymbolLocationInformation } from '../request-type';
8+
import { ClientCapabilities, SymbolLocationInformation } from '../request-type';
99
import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service';
1010
import { observableFromIterable, toUnixPath, uri2path } from '../util';
1111
import chaiAsPromised = require('chai-as-promised');
@@ -16,6 +16,11 @@ import { IBeforeAndAfterContext, ISuiteCallbackContext, ITestCallbackContext } f
1616
chai.use(chaiAsPromised);
1717
const assert = chai.assert;
1818

19+
const DEFAULT_CAPABILITIES: ClientCapabilities = {
20+
xcontentProvider: true,
21+
xfilesProvider: true
22+
};
23+
1924
export interface TestContext {
2025

2126
/** TypeScript service under test */
@@ -31,7 +36,7 @@ export interface TestContext {
3136
* @param createService A factory that creates the TypeScript service. Allows to test subclasses of TypeScriptService
3237
* @param files A Map from URI to file content of files that should be available in the workspace
3338
*/
34-
export const initializeTypeScriptService = (createService: TypeScriptServiceFactory, rootUri: string, files: Map<string, string>) => async function (this: TestContext & IBeforeAndAfterContext): Promise<void> {
39+
export const initializeTypeScriptService = (createService: TypeScriptServiceFactory, rootUri: string, files: Map<string, string>, clientCapabilities: ClientCapabilities = DEFAULT_CAPABILITIES) => async function (this: TestContext & IBeforeAndAfterContext): Promise<void> {
3540

3641
// Stub client
3742
this.client = sinon.createStubInstance(RemoteLanguageClient);
@@ -56,10 +61,7 @@ export const initializeTypeScriptService = (createService: TypeScriptServiceFact
5661
await this.service.initialize({
5762
processId: process.pid,
5863
rootUri,
59-
capabilities: {
60-
xcontentProvider: true,
61-
xfilesProvider: true
62-
}
64+
capabilities: clientCapabilities || DEFAULT_CAPABILITIES
6365
}).toPromise();
6466
};
6567

@@ -2123,6 +2125,91 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
21232125

21242126
});
21252127

2128+
describe('textDocumentCompletion() with snippets', function (this: TestContext & ISuiteCallbackContext){
2129+
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
2130+
[rootUri + 'a.ts', [
2131+
'class A {',
2132+
' /** foo doc*/',
2133+
' foo() {}',
2134+
' /** bar doc*/',
2135+
' bar(num: number): number { return 1; }',
2136+
' /** baz doc*/',
2137+
' baz(num: number): string { return ""; }',
2138+
' /** qux doc*/',
2139+
' qux: number;',
2140+
'}',
2141+
'const a = new A();',
2142+
'a.'
2143+
].join('\n')]
2144+
]), {
2145+
textDocument: {
2146+
completion: {
2147+
completionItem: {
2148+
snippetSupport: true
2149+
}
2150+
}
2151+
},
2152+
...DEFAULT_CAPABILITIES
2153+
}));
2154+
2155+
afterEach(shutdownService);
2156+
2157+
it('should produce completions with snippets if supported', async function (this: TestContext & ITestCallbackContext) {
2158+
const result: CompletionList = await this.service.textDocumentCompletion({
2159+
textDocument: {
2160+
uri: rootUri + 'a.ts'
2161+
},
2162+
position: {
2163+
line: 11,
2164+
character: 2
2165+
}
2166+
}).reduce<Operation, CompletionList>(applyReducer, null as any).toPromise();
2167+
// * A snippet can define tab stops and placeholders with `$1`, `$2`
2168+
// * and `${3:foo}`. `$0` defines the final tab stop, it defaults to
2169+
// * the end of the snippet. Placeholders with equal identifiers are linked,
2170+
// * that is typing in one will update others too.
2171+
assert.equal(result.isIncomplete, false);
2172+
assert.sameDeepMembers(result.items, [
2173+
{
2174+
label: 'bar',
2175+
kind: CompletionItemKind.Method,
2176+
documentation: 'bar doc',
2177+
sortText: '0',
2178+
insertTextFormat: InsertTextFormat.Snippet,
2179+
insertText: 'bar(${1:num})',
2180+
detail: '(method) A.bar(num: number): number'
2181+
},
2182+
{
2183+
label: 'baz',
2184+
kind: CompletionItemKind.Method,
2185+
documentation: 'baz doc',
2186+
sortText: '0',
2187+
insertTextFormat: InsertTextFormat.Snippet,
2188+
insertText: 'baz(${1:num})',
2189+
detail: '(method) A.baz(num: number): string'
2190+
},
2191+
{
2192+
label: 'foo',
2193+
kind: CompletionItemKind.Method,
2194+
documentation: 'foo doc',
2195+
sortText: '0',
2196+
insertTextFormat: InsertTextFormat.Snippet,
2197+
insertText: 'foo()',
2198+
detail: '(method) A.foo(): void'
2199+
},
2200+
{
2201+
label: 'qux',
2202+
kind: CompletionItemKind.Property,
2203+
documentation: 'qux doc',
2204+
sortText: '0',
2205+
insertTextFormat: InsertTextFormat.Snippet,
2206+
insertText: 'qux',
2207+
detail: '(property) A.qux: number'
2208+
}
2209+
]);
2210+
});
2211+
});
2212+
21262213
describe('textDocumentCompletion()', function (this: TestContext & ISuiteCallbackContext) {
21272214
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
21282215
[rootUri + 'a.ts', [
@@ -2175,32 +2262,41 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
21752262
label: 'bar',
21762263
kind: CompletionItemKind.Method,
21772264
documentation: 'bar doc',
2265+
insertText: 'bar',
2266+
insertTextFormat: InsertTextFormat.PlainText,
21782267
sortText: '0',
21792268
detail: '(method) A.bar(): number'
21802269
},
21812270
{
21822271
label: 'baz',
21832272
kind: CompletionItemKind.Method,
21842273
documentation: 'baz doc',
2274+
insertText: 'baz',
2275+
insertTextFormat: InsertTextFormat.PlainText,
21852276
sortText: '0',
21862277
detail: '(method) A.baz(): string'
21872278
},
21882279
{
21892280
label: 'foo',
21902281
kind: CompletionItemKind.Method,
21912282
documentation: 'foo doc',
2283+
insertText: 'foo',
2284+
insertTextFormat: InsertTextFormat.PlainText,
21922285
sortText: '0',
21932286
detail: '(method) A.foo(): void'
21942287
},
21952288
{
21962289
label: 'qux',
21972290
kind: CompletionItemKind.Property,
21982291
documentation: 'qux doc',
2292+
insertText: 'qux',
2293+
insertTextFormat: InsertTextFormat.PlainText,
21992294
sortText: '0',
22002295
detail: '(property) A.qux: number'
22012296
}
22022297
]);
22032298
});
2299+
22042300
it('produces completions for imported symbols', async function (this: TestContext & ITestCallbackContext) {
22052301
const result: CompletionList = await this.service.textDocumentCompletion({
22062302
textDocument: {
@@ -2217,6 +2313,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
22172313
label: 'd',
22182314
kind: CompletionItemKind.Function,
22192315
documentation: 'd doc',
2316+
insertText: 'd',
2317+
insertTextFormat: InsertTextFormat.PlainText,
22202318
detail: 'function d(): void',
22212319
sortText: '0'
22222320
}]
@@ -2238,6 +2336,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
22382336
label: 'bar',
22392337
kind: CompletionItemKind.Interface,
22402338
documentation: 'bar doc',
2339+
insertText: 'bar',
2340+
insertTextFormat: InsertTextFormat.PlainText,
22412341
sortText: '0',
22422342
detail: 'interface foo.bar'
22432343
}]

src/typescript-service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
DocumentSymbolParams,
1919
ExecuteCommandParams,
2020
Hover,
21+
InsertTextFormat,
2122
Location,
2223
MarkedString,
2324
ParameterInformation,
@@ -173,6 +174,11 @@ export class TypeScriptService {
173174
}
174175
};
175176

177+
/**
178+
* Indicates if the client prefers completion results formatted as snippets.
179+
*/
180+
private supportsCompletionWithSnippets: boolean = false;
181+
176182
constructor(protected client: LanguageClient, protected options: TypeScriptServiceOptions = {}) {
177183
this.logger = new LSPLogger(client);
178184
}
@@ -200,6 +206,12 @@ export class TypeScriptService {
200206
if (params.rootUri || params.rootPath) {
201207
this.root = params.rootPath || uri2path(params.rootUri!);
202208
this.rootUri = params.rootUri || path2uri(params.rootPath!);
209+
210+
this.supportsCompletionWithSnippets = params.capabilities.textDocument &&
211+
params.capabilities.textDocument.completion &&
212+
params.capabilities.textDocument.completion.completionItem &&
213+
params.capabilities.textDocument.completion.completionItem.snippetSupport || false;
214+
203215
// The root URI always refers to a directory
204216
if (!this.rootUri.endsWith('/')) {
205217
this.rootUri += '/';
@@ -1002,6 +1014,7 @@ export class TypeScriptService {
10021014
return Observable.from(completions.entries)
10031015
.map(entry => {
10041016
const item: CompletionItem = { label: entry.name };
1017+
10051018
const kind = completionKinds[entry.kind];
10061019
if (kind) {
10071020
item.kind = kind;
@@ -1013,6 +1026,21 @@ export class TypeScriptService {
10131026
if (details) {
10141027
item.documentation = ts.displayPartsToString(details.documentation);
10151028
item.detail = ts.displayPartsToString(details.displayParts);
1029+
if (this.supportsCompletionWithSnippets) {
1030+
item.insertTextFormat = InsertTextFormat.Snippet;
1031+
if (entry.kind === 'property') {
1032+
item.insertText = details.name;
1033+
} else {
1034+
const parameters = details.displayParts
1035+
.filter(p => p.kind === 'parameterName')
1036+
.map((p, i) => '${' + `${i + 1}:${p.text}` + '}');
1037+
const paramString = parameters.join(', ');
1038+
item.insertText = details.name + `(${paramString})`;
1039+
}
1040+
} else {
1041+
item.insertTextFormat = InsertTextFormat.PlainText;
1042+
item.insertText = details.name;
1043+
}
10161044
}
10171045
return { op: 'add', path: '/items/-', value: item } as Operation;
10181046
})

0 commit comments

Comments
 (0)