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

Snippet support for autocompletion #323

Merged
merged 9 commits into from
Aug 3, 2017
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
114 changes: 107 additions & 7 deletions src/test/typescript-service-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as chai from 'chai';
import * as sinon from 'sinon';
import * as ts from 'typescript';
import { CompletionItemKind, CompletionList, DiagnosticSeverity, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver';
import { CompletionItemKind, CompletionList, DiagnosticSeverity, InsertTextFormat, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver';
import { Command, Diagnostic, Hover, Location, SignatureHelp, SymbolInformation, SymbolKind } from 'vscode-languageserver-types';
import { LanguageClient, RemoteLanguageClient } from '../lang-handler';
import { DependencyReference, PackageInformation, ReferenceInformation, TextDocumentContentParams, WorkspaceFilesParams } from '../request-type';
import { SymbolLocationInformation } from '../request-type';
import { ClientCapabilities, SymbolLocationInformation } from '../request-type';
import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service';
import { observableFromIterable, toUnixPath, uri2path } from '../util';
import chaiAsPromised = require('chai-as-promised');
Expand All @@ -16,6 +16,11 @@ import { IBeforeAndAfterContext, ISuiteCallbackContext, ITestCallbackContext } f
chai.use(chaiAsPromised);
const assert = chai.assert;

const DEFAULT_CAPABILITIES: ClientCapabilities = {
xcontentProvider: true,
xfilesProvider: true
};

export interface TestContext {

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

// Stub client
this.client = sinon.createStubInstance(RemoteLanguageClient);
Expand All @@ -56,10 +61,7 @@ export const initializeTypeScriptService = (createService: TypeScriptServiceFact
await this.service.initialize({
processId: process.pid,
rootUri,
capabilities: {
xcontentProvider: true,
xfilesProvider: true
}
capabilities: clientCapabilities || DEFAULT_CAPABILITIES
}).toPromise();
};

Expand Down Expand Up @@ -2123,6 +2125,91 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor

});

describe('textDocumentCompletion() with snippets', function (this: TestContext & ISuiteCallbackContext){
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
[rootUri + 'a.ts', [
'class A {',
' /** foo doc*/',
' foo() {}',
' /** bar doc*/',
' bar(num: number): number { return 1; }',
' /** baz doc*/',
' baz(num: number): string { return ""; }',
' /** qux doc*/',
' qux: number;',
'}',
'const a = new A();',
'a.'
].join('\n')]
]), {
textDocument: {
completion: {
completionItem: {
snippetSupport: true
}
}
},
...DEFAULT_CAPABILITIES
}));

afterEach(shutdownService);

it('should produce completions with snippets if supported', async function (this: TestContext & ITestCallbackContext) {
const result: CompletionList = await this.service.textDocumentCompletion({
textDocument: {
uri: rootUri + 'a.ts'
},
position: {
line: 11,
character: 2
}
}).reduce<Operation, CompletionList>(applyReducer, null as any).toPromise();
// * A snippet can define tab stops and placeholders with `$1`, `$2`
// * and `${3:foo}`. `$0` defines the final tab stop, it defaults to
// * the end of the snippet. Placeholders with equal identifiers are linked,
// * that is typing in one will update others too.
assert.equal(result.isIncomplete, false);
assert.sameDeepMembers(result.items, [
{
label: 'bar',
kind: CompletionItemKind.Method,
documentation: 'bar doc',
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'bar(${1:num})',
detail: '(method) A.bar(num: number): number'
},
{
label: 'baz',
kind: CompletionItemKind.Method,
documentation: 'baz doc',
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'baz(${1:num})',
detail: '(method) A.baz(num: number): string'
},
{
label: 'foo',
kind: CompletionItemKind.Method,
documentation: 'foo doc',
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'foo()',
detail: '(method) A.foo(): void'
},
{
label: 'qux',
kind: CompletionItemKind.Property,
documentation: 'qux doc',
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'qux',
detail: '(property) A.qux: number'
}
]);
});
});

describe('textDocumentCompletion()', function (this: TestContext & ISuiteCallbackContext) {
beforeEach(initializeTypeScriptService(createService, rootUri, new Map([
[rootUri + 'a.ts', [
Expand Down Expand Up @@ -2175,32 +2262,41 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
label: 'bar',
kind: CompletionItemKind.Method,
documentation: 'bar doc',
insertText: 'bar',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.bar(): number'
},
{
label: 'baz',
kind: CompletionItemKind.Method,
documentation: 'baz doc',
insertText: 'baz',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.baz(): string'
},
{
label: 'foo',
kind: CompletionItemKind.Method,
documentation: 'foo doc',
insertText: 'foo',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.foo(): void'
},
{
label: 'qux',
kind: CompletionItemKind.Property,
documentation: 'qux doc',
insertText: 'qux',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(property) A.qux: number'
}
]);
});

it('produces completions for imported symbols', async function (this: TestContext & ITestCallbackContext) {
const result: CompletionList = await this.service.textDocumentCompletion({
textDocument: {
Expand All @@ -2217,6 +2313,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
label: 'd',
kind: CompletionItemKind.Function,
documentation: 'd doc',
insertText: 'd',
insertTextFormat: InsertTextFormat.PlainText,
detail: 'function d(): void',
sortText: '0'
}]
Expand All @@ -2238,6 +2336,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
label: 'bar',
kind: CompletionItemKind.Interface,
documentation: 'bar doc',
insertText: 'bar',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: 'interface foo.bar'
}]
Expand Down
28 changes: 28 additions & 0 deletions src/typescript-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DocumentSymbolParams,
ExecuteCommandParams,
Hover,
InsertTextFormat,
Location,
MarkedString,
ParameterInformation,
Expand Down Expand Up @@ -173,6 +174,11 @@ export class TypeScriptService {
}
};

/**
* Indicates if the client prefers completion results formatted as snippets.
*/
private supportsCompletionWithSnippets: boolean = false;

constructor(protected client: LanguageClient, protected options: TypeScriptServiceOptions = {}) {
this.logger = new LSPLogger(client);
}
Expand Down Expand Up @@ -200,6 +206,12 @@ export class TypeScriptService {
if (params.rootUri || params.rootPath) {
this.root = params.rootPath || uri2path(params.rootUri!);
this.rootUri = params.rootUri || path2uri(params.rootPath!);

this.supportsCompletionWithSnippets = params.capabilities.textDocument &&
params.capabilities.textDocument.completion &&
params.capabilities.textDocument.completion.completionItem &&
params.capabilities.textDocument.completion.completionItem.snippetSupport || false;

// The root URI always refers to a directory
if (!this.rootUri.endsWith('/')) {
this.rootUri += '/';
Expand Down Expand Up @@ -1002,6 +1014,7 @@ export class TypeScriptService {
return Observable.from(completions.entries)
.map(entry => {
const item: CompletionItem = { label: entry.name };

const kind = completionKinds[entry.kind];
if (kind) {
item.kind = kind;
Expand All @@ -1013,6 +1026,21 @@ export class TypeScriptService {
if (details) {
item.documentation = ts.displayPartsToString(details.documentation);
item.detail = ts.displayPartsToString(details.displayParts);
if (this.supportsCompletionWithSnippets) {
item.insertTextFormat = InsertTextFormat.Snippet;
if (entry.kind === 'property') {
item.insertText = details.name;
} else {
const parameters = details.displayParts
.filter(p => p.kind === 'parameterName')
.map((p, i) => '${' + `${i + 1}:${p.text}` + '}');
const paramString = parameters.join(', ');
item.insertText = details.name + `(${paramString})`;
}
} else {
item.insertTextFormat = InsertTextFormat.PlainText;
item.insertText = details.name;
}
}
return { op: 'add', path: '/items/-', value: item } as Operation;
})
Expand Down