-
Notifications
You must be signed in to change notification settings - Fork 73
Switch to resolving expensive completionItem details #344
Changes from 2 commits
a74c79e
47b8bb0
afa33f9
ba9de9f
90a6515
7fc166e
5bfe836
9b2f9ea
f5913ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import { applyReducer, Operation } from 'fast-json-patch'; | |
import { IBeforeAndAfterContext, ISuiteCallbackContext, ITestCallbackContext } from 'mocha'; | ||
import * as sinon from 'sinon'; | ||
import * as ts from 'typescript'; | ||
import { CompletionItemKind, CompletionList, DiagnosticSeverity, InsertTextFormat, TextDocumentIdentifier, TextDocumentItem, WorkspaceEdit } from 'vscode-languageserver'; | ||
import { CompletionItem, 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'; | ||
|
@@ -2169,44 +2169,22 @@ 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, [ | ||
{ | ||
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' | ||
} | ||
]); | ||
|
||
const barItem = result.items.find(i => i.label === 'bar') as CompletionItem; | ||
const resolved: CompletionItem = await this.service | ||
.completionItemResolve(barItem) | ||
.reduce<Operation, CompletionItem>(applyReducer, null as any).toPromise(); | ||
|
||
assert.deepEqual(resolved, { | ||
label: 'bar', | ||
kind: CompletionItemKind.Method, | ||
documentation: 'bar doc', | ||
insertText: 'bar(${1:num})', | ||
insertTextFormat: InsertTextFormat.Snippet, | ||
sortText: '0', | ||
detail: '(method) A.bar(num: number): number' | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would better be a second test |
||
|
||
}); | ||
}); | ||
|
||
|
@@ -2259,42 +2237,77 @@ export function describeTypeScriptService(createService: TypeScriptServiceFactor | |
assert.equal(result.isIncomplete, false); | ||
assert.sameDeepMembers(result.items, [ | ||
{ | ||
data: { | ||
entryName: 'bar', | ||
offset: 188, | ||
uri: rootUri + 'a.ts' | ||
}, | ||
label: 'bar', | ||
kind: CompletionItemKind.Method, | ||
documentation: 'bar doc', | ||
insertText: 'bar', | ||
insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0', | ||
detail: '(method) A.bar(): number' | ||
// documentation: 'bar doc', | ||
// insertText: 'bar', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0' | ||
// detail: '(method) A.bar(): number' | ||
}, | ||
{ | ||
data: { | ||
entryName: 'baz', | ||
offset: 188, | ||
uri: rootUri + 'a.ts' | ||
}, | ||
label: 'baz', | ||
kind: CompletionItemKind.Method, | ||
documentation: 'baz doc', | ||
insertText: 'baz', | ||
insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0', | ||
detail: '(method) A.baz(): string' | ||
// documentation: 'baz doc', | ||
// insertText: 'baz', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0' | ||
// detail: '(method) A.baz(): string' | ||
}, | ||
{ | ||
data: { | ||
entryName: 'foo', | ||
offset: 188, | ||
uri: rootUri + 'a.ts' | ||
}, | ||
label: 'foo', | ||
kind: CompletionItemKind.Method, | ||
documentation: 'foo doc', | ||
insertText: 'foo', | ||
insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0', | ||
detail: '(method) A.foo(): void' | ||
// documentation: 'foo doc', | ||
// insertText: 'foo', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0' | ||
// detail: '(method) A.foo(): void' | ||
}, | ||
{ | ||
data: { | ||
entryName: 'qux', | ||
offset: 188, | ||
uri: rootUri + 'a.ts' | ||
}, | ||
label: 'qux', | ||
kind: CompletionItemKind.Property, | ||
documentation: 'qux doc', | ||
insertText: 'qux', | ||
insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0', | ||
detail: '(property) A.qux: number' | ||
// documentation: 'qux doc', | ||
// insertText: 'qux', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0' | ||
// detail: '(property) A.qux: number' | ||
} | ||
]); | ||
|
||
const barItem = result.items.find(i => i.label === 'bar') as CompletionItem; | ||
const resolved: CompletionItem = await this.service | ||
.completionItemResolve(barItem) | ||
.reduce<Operation, CompletionItem>(applyReducer, null as any).toPromise(); | ||
|
||
assert.deepEqual(resolved, { | ||
label: 'bar', | ||
kind: CompletionItemKind.Method, | ||
documentation: 'bar doc', | ||
insertText: 'bar', | ||
insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0', | ||
detail: '(method) A.bar(): number' | ||
}); | ||
}); | ||
|
||
it('produces completions for imported symbols', async function (this: TestContext & ITestCallbackContext) { | ||
|
@@ -2310,12 +2323,17 @@ 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', | ||
// documentation: 'd doc', | ||
// insertText: 'd', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
// detail: 'function d(): void', | ||
sortText: '0' | ||
}] | ||
}); | ||
|
@@ -2333,13 +2351,18 @@ 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' | ||
// documentation: 'bar doc', | ||
// insertText: 'bar', | ||
// insertTextFormat: InsertTextFormat.PlainText, | ||
sortText: '0' | ||
// detail: 'interface foo.bar' | ||
}] | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1022,33 +1022,64 @@ 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 | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
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; | ||
} | ||
delete item.data; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not needed. It would be nice to reduce payload and not have to include the field in the asserts on resolved completion items (I wonder if the tests should even care at all what is in the data field?). Could we set to |
||
} | ||
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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this assertion removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The snippets logic is only used when resolving completion items, and I thought it would be a lot of logic to resolve all the items from the initial response.
Now that I think of it, it's probably not that bad - I'll give it a try!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would leave this test as is (just change the objects) and add a separate test for the resolving