Skip to content

Commit

Permalink
Filter completions if the client isn't going to. (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
rictic authored Dec 5, 2017
1 parent cae4928 commit 583c6bd
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Supports getting all symbols in the workspace and in the document. At the
moment we just expose elements by tagname and Polymer 1.0 core features, as
other symbols should be well handled by other language services.
- Supports filtering autocompletions on the server side if the client does not.
Clients without autocompletion filtering support should send over
`capabilities.experimental['polymer-ide'].doesNotFilterCompletions` as `true`
in their client capabilities.

## 1.6.0 - 2017-11-21
- Generate Code Actions for fixable warnings and warnings with edit actions.
Expand Down
71 changes: 61 additions & 10 deletions src/language-server/auto-completer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import * as dom5 from 'dom5';
import * as fuzzaldrin from 'fuzzaldrin';
import {Analyzer, Document, Element, isPositionInsideRange, ParsedHtmlDocument, SourcePosition} from 'polymer-analyzer';
import {ClientCapabilities, CompletionItem, CompletionItemKind, CompletionList, IConnection, InsertTextFormat} from 'vscode-languageserver';
import {TextDocumentPositionParams} from 'vscode-languageserver-protocol';
Expand All @@ -30,7 +31,8 @@ import {Handler} from './util';
* give suggested completions at a given position.
*/
export default class AutoCompleter extends Handler {
private clientSupportsSnippets: boolean;
private readonly clientCannotFilter: boolean;
private readonly clientSupportsSnippets: boolean;
constructor(
protected connection: IConnection,
private converter: AnalyzerLSPConverter,
Expand All @@ -45,6 +47,13 @@ export default class AutoCompleter extends Handler {
this.capabilities.textDocument.completion.completionItem
.snippetSupport);

// Work around https://github.com/atom/atom-languageclient/issues/150
const ourExperimentalCapabilities = this.capabilities.experimental &&
this.capabilities.experimental['polymer-ide'];
this.clientCannotFilter = ourExperimentalCapabilities ?
!!ourExperimentalCapabilities.doesNotFilterCompletions :
false;

this.connection.onCompletion(async(request) => {
const result = await this.handleErrors(
this.autoComplete(request), {isIncomplete: true, items: []});
Expand All @@ -56,22 +65,26 @@ export default class AutoCompleter extends Handler {
Promise<CompletionList> {
const localPath =
this.converter.getWorkspacePathToFile(textPosition.textDocument);
const completions = await this.getTypeaheadCompletionsAtPosition(
localPath, this.converter.convertPosition(textPosition.position));
const document =
(await this.analyzer.analyze([localPath])).getDocument(localPath);
if (!(document instanceof Document)) {
return {isIncomplete: true, items: []};
}
const position = this.converter.convertPosition(textPosition.position);
const completions =
await this.getTypeaheadCompletionsAtPosition(document, position);
if (!completions) {
return {isIncomplete: false, items: []};
return {isIncomplete: true, items: []};
}
if (this.clientCannotFilter) {
return this.filterCompletions(completions, position, document);
}
return completions;
}

private async getTypeaheadCompletionsAtPosition(
localPath: string,
document: Document,
position: SourcePosition): Promise<CompletionList|undefined> {
const analysis = await this.analyzer.analyze([localPath]);
const document = analysis.getDocument(localPath);
if (!(document instanceof Document)) {
return;
}
const location =
await this.featureFinder.getAstAtPosition(document, position);
if (!location) {
Expand Down Expand Up @@ -323,6 +336,44 @@ export default class AutoCompleter extends Handler {
}
return item;
}

private filterCompletions(
completions: CompletionList, position: SourcePosition,
document: Document): CompletionList {
const leadingText = this.getLeadingIdentifier(position, document);
const filterableCompletions = completions.items.map(completion => {
return {
filterText: completion.filterText || completion.label,
completion
};
});
const items =
fuzzaldrin
.filter(filterableCompletions, leadingText, {key: 'filterText'})
.map(i => i.completion);
return {isIncomplete: true, items};
}

/**
* Gets the identifier that comes right before the given source position.
*
* So e.g. calling it at the end of "hello world" should return "world", but
* calling it with the line 0, character 4 should return "hell".
*/
private getLeadingIdentifier(position: SourcePosition, document: Document):
string {
const contents = document.parsedDocument.contents;
const endOfSpan = document.parsedDocument.sourcePositionToOffset(position);
let startOfSpan = endOfSpan;
while (true) {
const candidateChar = contents[startOfSpan - 1];
if (candidateChar === undefined || !candidateChar.match(/[a-zA-Z\-]/)) {
break;
}
startOfSpan--;
}
return contents.slice(startOfSpan, endOfSpan);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/test/auto-completer_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ suite('AutoCompleter', () => {
await client.openFile(indexFile, '<script>\n\n</script>\n' + indexContents);
const completions =
await client.getCompletions(indexFile, {line: 1, column: 0});
assert.deepEqual(completions, {isIncomplete: false, items: []});
assert.deepEqual(completions, {isIncomplete: true, items: []});
});

{
Expand Down

0 comments on commit 583c6bd

Please sign in to comment.