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

Switch to resolving expensive completionItem details #344

Merged
merged 9 commits into from
Sep 8, 2017
27 changes: 27 additions & 0 deletions src/request-type.ts
Original file line number Diff line number Diff line change
@@ -234,3 +234,30 @@ export interface PartialResultParams {
*/
patch: Operation[];
}

/**
* Restriction on vscode's CompletionItem interface
*/
export interface CompletionItem extends vscode.CompletionItem {
data?: CompletionItemData;
}

/**
* The necessary fields for a completion item details to be resolved by typescript
*/
export interface CompletionItemData {
/**
* The document from which the completion was requested
*/
uri: string;

/**
* The offset into the document at which the completion was requested
*/
offset: number;

/**
* The name field from typescript's returned completion entry
*/
entryName: string;
}
178 changes: 158 additions & 20 deletions src/test/typescript-service-helpers.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { CompletionItemKind, CompletionList, DiagnosticSeverity, InsertTextForma
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 { ClientCapabilities, SymbolLocationInformation } from '../request-type';
import { ClientCapabilities, CompletionItem, SymbolLocationInformation } from '../request-type';
import { TypeScriptService, TypeScriptServiceFactory } from '../typescript-service';
import { observableFromIterable, toUnixPath, uri2path } from '../util';

@@ -2154,7 +2154,62 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor

afterEach(shutdownService);

it('should produce completions with snippets if supported', async function (this: TestContext & ITestCallbackContext) {
it('should produce completions', 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();
assert.equal(result.isIncomplete, false);
assert.sameDeepMembers(result.items, [
{
label: 'bar',
kind: CompletionItemKind.Method,
sortText: '0',
data: {
entryName: 'bar',
offset: 210,
uri: rootUri + 'a.ts'
}
},
{
label: 'baz',
kind: CompletionItemKind.Method,
sortText: '0',
data: {
entryName: 'baz',
offset: 210,
uri: rootUri + 'a.ts'
}
},
{
label: 'foo',
kind: CompletionItemKind.Method,
sortText: '0',
data: {
entryName: 'foo',
offset: 210,
uri: rootUri + 'a.ts'
}
},
{
label: 'qux',
kind: CompletionItemKind.Property,
sortText: '0',
data: {
entryName: 'qux',
offset: 210,
uri: rootUri + 'a.ts'
}
}
]);
});

it('should resolve completions with snippets', async function (this: TestContext & ITestCallbackContext) {
const result: CompletionList = await this.service.textDocumentCompletion({
textDocument: {
uri: rootUri + 'a.ts'
@@ -2169,15 +2224,25 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
// * 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, [

const resolvedItems = await Observable.from(result.items)
.mergeMap(item => this.service
.completionItemResolve(item)
.reduce<Operation, CompletionItem>(applyReducer, null as any)
)
.toArray()
.toPromise();

assert.sameDeepMembers(resolvedItems, [
{
label: 'bar',
kind: CompletionItemKind.Method,
documentation: 'bar doc',
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'bar(${1:num})',
detail: '(method) A.bar(num: number): number'
detail: '(method) A.bar(num: number): number',
data: undefined
},
{
label: 'baz',
@@ -2186,7 +2251,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'baz(${1:num})',
detail: '(method) A.baz(num: number): string'
detail: '(method) A.baz(num: number): string',
data: undefined
},
{
label: 'foo',
@@ -2195,7 +2261,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'foo()',
detail: '(method) A.foo(): void'
detail: '(method) A.foo(): void',
data: undefined
},
{
label: 'qux',
@@ -2204,9 +2271,11 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
sortText: '0',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'qux',
detail: '(property) A.qux: number'
detail: '(property) A.qux: number',
data: undefined
}
]);

});
});

@@ -2258,14 +2327,77 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
}).reduce<Operation, CompletionList>(applyReducer, null as any).toPromise();
assert.equal(result.isIncomplete, false);
assert.sameDeepMembers(result.items, [
{
data: {
entryName: 'bar',
offset: 188,
uri: rootUri + 'a.ts'
},
label: 'bar',
kind: CompletionItemKind.Method,
sortText: '0'
},
{
data: {
entryName: 'baz',
offset: 188,
uri: rootUri + 'a.ts'
},
label: 'baz',
kind: CompletionItemKind.Method,
sortText: '0'
},
{
data: {
entryName: 'foo',
offset: 188,
uri: rootUri + 'a.ts'
},
label: 'foo',
kind: CompletionItemKind.Method,
sortText: '0'
},
{
data: {
entryName: 'qux',
offset: 188,
uri: rootUri + 'a.ts'
},
label: 'qux',
kind: CompletionItemKind.Property,
sortText: '0'
}
]);
});

it('resolves completions in the same file', 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();
assert.equal(result.isIncomplete, false);

const resolveItem = (item: CompletionItem) => this.service
.completionItemResolve(item)
.reduce<Operation, CompletionItem>(applyReducer, null as any).toPromise();

const resolvedItems = await Promise.all(result.items.map(resolveItem));

assert.sameDeepMembers(resolvedItems, [
{
label: 'bar',
kind: CompletionItemKind.Method,
documentation: 'bar doc',
insertText: 'bar',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.bar(): number'
detail: '(method) A.bar(): number',
data: undefined
},
{
label: 'baz',
@@ -2274,7 +2406,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
insertText: 'baz',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.baz(): string'
detail: '(method) A.baz(): string',
data: undefined
},
{
label: 'foo',
@@ -2283,7 +2416,8 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
insertText: 'foo',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(method) A.foo(): void'
detail: '(method) A.foo(): void',
data: undefined
},
{
label: 'qux',
@@ -2292,9 +2426,11 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
insertText: 'qux',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: '(property) A.qux: number'
detail: '(property) A.qux: number',
data: undefined
}
]);

});

it('produces completions for imported symbols', async function (this: TestContext & ITestCallbackContext) {
@@ -2310,12 +2446,13 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
assert.deepEqual(result, {
isIncomplete: false,
items: [{
data: {
entryName: 'd',
offset: 32,
uri: rootUri + 'uses-import.ts'
},
label: 'd',
kind: CompletionItemKind.Function,
documentation: 'd doc',
insertText: 'd',
insertTextFormat: InsertTextFormat.PlainText,
detail: 'function d(): void',
sortText: '0'
}]
});
@@ -2333,13 +2470,14 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor
assert.deepEqual(result, {
isIncomplete: false,
items: [{
data: {
entryName: 'bar',
offset: 51,
uri: rootUri + 'uses-reference.ts'
},
label: 'bar',
kind: CompletionItemKind.Interface,
documentation: 'bar doc',
insertText: 'bar',
insertTextFormat: InsertTextFormat.PlainText,
sortText: '0',
detail: 'interface foo.bar'
sortText: '0'
}]
});
});
76 changes: 55 additions & 21 deletions src/typescript-service.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ import * as url from 'url';
import {
CodeActionParams,
Command,
CompletionItem,
CompletionItemKind,
CompletionList,
DidChangeConfigurationParams,
@@ -44,6 +43,7 @@ import { InMemoryFileSystem, isTypeScriptLibrary } from './memfs';
import { extractDefinitelyTypedPackageName, extractNodeModulesPackageName, PackageJson, PackageManager } from './packages';
import { ProjectConfiguration, ProjectManager } from './project-manager';
import {
CompletionItem,
DependencyReference,
InitializeParams,
InitializeResult,
@@ -1022,33 +1022,67 @@ export class TypeScriptService {
if (entry.sortText) {
item.sortText = entry.sortText;
}
const details = configuration.getService().getCompletionEntryDetails(fileName, offset, entry.name);
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;
}
}

// context for future resolve requests:
item.data = {
uri,
offset,
entryName: entry.name
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you define an interface for this / add this to a custom CompletionItem interface in request-type?


return { op: 'add', path: '/items/-', value: item } as Operation;
})
.startWith({ op: 'add', path: '/isIncomplete', value: false } as Operation);
})
.startWith({ op: 'add', path: '', value: { isIncomplete: true, items: [] } as CompletionList } as Operation);
}

/**
* The completionItem/resolve request is used to fill in additional details from an incomplete
* CompletionItem returned from the textDocument/completions call.
*
* @return Observable of JSON Patches that build a `CompletionItem` result
*/
completionItemResolve(item: CompletionItem, span = new Span()): Observable<Operation> {
if (!item.data) {
throw new Error('Cannot resolve completion item without data');
}
const {uri, offset, entryName} = item.data;
const fileName: string = uri2path(uri);
return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span)
.toArray()
.map(() => {

const configuration = this.projectManager.getConfiguration(fileName);
configuration.ensureBasicFiles(span);

const details = configuration.getService().getCompletionEntryDetails(fileName, offset, entryName);
if (details) {
item.documentation = ts.displayPartsToString(details.documentation);
item.detail = ts.displayPartsToString(details.displayParts);
if (this.supportsCompletionWithSnippets) {
item.insertTextFormat = InsertTextFormat.Snippet;
if (details.kind === 'method' || details.kind === 'function') {
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.insertText = details.name;

}
} else {
item.insertTextFormat = InsertTextFormat.PlainText;
item.insertText = details.name;
}
item.data = undefined;
}
return item;
})
.map(completionItem => ({ op: 'add', path: '', value: completionItem }) as Operation);
}

/**
* The signature help request is sent from the client to the server to request signature
* information at a given cursor position.