diff --git a/src/vs/editor/contrib/suggest/browser/completionModel.ts b/src/vs/editor/contrib/suggest/browser/completionModel.ts index 5e3641fbdc99b3d1f817ffd4b232222353aee37a..f7d56a9b3967d6d6b2e08507aadd4d81e500e3dc 100644 --- a/src/vs/editor/contrib/suggest/browser/completionModel.ts +++ b/src/vs/editor/contrib/suggest/browser/completionModel.ts @@ -14,137 +14,95 @@ import {ISuggestResult, ISuggestSupport, ISuggestion, ISuggestionFilter} from 'v import {DefaultFilter, IMatch} from 'vs/editor/common/modes/modesFilters'; import {ISuggestResult2} from '../common/suggest'; -function completionItemCompare(item: CompletionItem, otherItem: CompletionItem): number { - const suggestion = item.suggestion; - const otherSuggestion = otherItem.suggestion; - - if (typeof suggestion.sortText === 'string' && typeof otherSuggestion.sortText === 'string') { - const one = suggestion.sortText.toLowerCase(); - const other = otherSuggestion.sortText.toLowerCase(); - - if (one < other) { - return -1; - } else if (one > other) { - return 1; - } - } - - return suggestion.label.toLowerCase() < otherSuggestion.label.toLowerCase() ? -1 : 1; -} - export class CompletionItem { suggestion: ISuggestion; highlights: IMatch[]; - support: ISuggestSupport; container: ISuggestResult; - constructor(public group: CompletionGroup, suggestion: ISuggestion, container: ISuggestResult2) { - this.support = container.support; + private _filter: ISuggestionFilter; + private _support: ISuggestSupport; + + constructor(suggestion: ISuggestion, container: ISuggestResult2) { + this._support = container.support; this.suggestion = suggestion; this.container = container; + this._filter = container.support && container.support.getFilter() || DefaultFilter; } resolveDetails(resource: URI, position: IPosition): TPromise { - if (!this.support || typeof this.support.getSuggestionDetails !== 'function') { + if (!this._support || typeof this._support.getSuggestionDetails !== 'function') { return TPromise.as(this.suggestion); } - return this.support.getSuggestionDetails(resource, position, this.suggestion); + return this._support.getSuggestionDetails(resource, position, this.suggestion); } updateDetails(value: ISuggestion): void { this.suggestion = assign(this.suggestion, value); } -} - -export class CompletionGroup { - - private _items: CompletionItem[]; - private cache: CompletionItem[]; - private cacheCurrentWord: string; - filter: ISuggestionFilter; - constructor(public model: CompletionModel, raw: ISuggestResult2[]) { - - this._items = raw.reduce((items, result) => { - return items.concat( - result.suggestions - .map(suggestion => new CompletionItem(this, suggestion, result)) - ); - }, []).sort(completionItemCompare); - - this.filter = DefaultFilter; - - if (this._items.length > 0) { - const [first] = this._items; - - if (first.support) { - this.filter = first.support.getFilter && first.support.getFilter() || this.filter; - } - } + updateHighlights(word: string): boolean { + this.highlights = this._filter(word, this.suggestion); + return !isFalsyOrEmpty(this.highlights); } - getItems(currentWord: string): CompletionItem[] { - if (currentWord === this.cacheCurrentWord) { - return this.cache; - } + static compare(item: CompletionItem, otherItem: CompletionItem): number { + const suggestion = item.suggestion; + const otherSuggestion = otherItem.suggestion; - let set: CompletionItem[]; + if (typeof suggestion.sortText === 'string' && typeof otherSuggestion.sortText === 'string') { + const one = suggestion.sortText.toLowerCase(); + const other = otherSuggestion.sortText.toLowerCase(); - // try to narrow down when possible, instead of always filtering everything - if (this.cacheCurrentWord && currentWord.substr(0, this.cacheCurrentWord.length) === this.cacheCurrentWord) { - set = this.cache; - } else { - set = this._items; - } - - const highlights = set.map(item => this.filter(currentWord, item.suggestion)); - const count = highlights.filter(h => !isFalsyOrEmpty(h)).length; - - if (count === 0) { - return []; + if (one < other) { + return -1; + } else if (one > other) { + return 1; + } } - this.cacheCurrentWord = currentWord; - this.cache = set - .map((item, index) => assign(item, { highlights: highlights[index] })) - .filter(item => !isFalsyOrEmpty(item.highlights)); - - return this.cache; - } - - invalidateCache(): void { - this.cacheCurrentWord = null; + return suggestion.label.toLowerCase() < otherSuggestion.label.toLowerCase() ? -1 : 1; } } export class CompletionModel { - private groups: CompletionGroup[]; - private cache: CompletionItem[]; - private cacheCurrentWord: string; + private _currentWord: string; + private _items: CompletionItem[] = []; + private _filteredItems: CompletionItem[] = undefined; - constructor(public raw: ISuggestResult2[][], public currentWord: string) { + constructor(public raw: ISuggestResult2[], currentWord: string) { + this._currentWord = currentWord; + for (let container of raw) { + for (let suggestion of container.suggestions) { + this._items.push(new CompletionItem(suggestion, container)); + } + } + this._items.sort(CompletionItem.compare); + } - this.groups = raw - .filter(s => !!s) - .map(suggestResults => new CompletionGroup(this, suggestResults)); + get currentWord(): string { + return this._currentWord; } - get items(): CompletionItem[] { - if (this.cacheCurrentWord === this.currentWord) { - return this.cache; + set currentWord(value: string) { + if (this._currentWord !== value) { + this._filteredItems = undefined; + this._currentWord = value; } + } - const result = this.groups.reduce((r, groups) => r.concat(groups.getItems(this.currentWord)), []); - - // let's only cache stuff that actually has results - if (result.length > 0) { - this.cache = result; - this.cacheCurrentWord = this.currentWord; + get items(): CompletionItem[] { + if (!this._filteredItems) { + this._filteredItems = []; + for (let item of this._items) { + if (item.updateHighlights(this.currentWord)) { + this._filteredItems.push(item); + } + } } - - return result; + return this._filteredItems; } + } \ No newline at end of file diff --git a/src/vs/editor/contrib/suggest/browser/suggestModel.ts b/src/vs/editor/contrib/suggest/browser/suggestModel.ts index 78849b38d2f324733eaf92379ae2d09a0f2cf984..a9482a46bbadc001ba8bd955b9954c2b6989731b 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestModel.ts @@ -171,7 +171,7 @@ export class SuggestModel implements IDisposable { private requestPromise: TPromise; private context: Context; - private raw: ISuggestResult2[][]; + private raw: ISuggestResult2[]; private completionModel: CompletionModel; private incomplete: boolean; @@ -319,7 +319,7 @@ export class SuggestModel implements IDisposable { } this.raw = all; - this.incomplete = all.reduce((r, s) => r || s.reduce((r, s) => r || s.incomplete, false), false); + this.incomplete = all.some(result => result.incomplete); this.onNewContext(new Context(this.editor, auto)); }).then(null, onUnexpectedError); diff --git a/src/vs/editor/contrib/suggest/common/suggest.ts b/src/vs/editor/contrib/suggest/common/suggest.ts index 2a39fdf048c485fd28d9c46cb63d850c527c686e..930caee104ed253d8477afccfbf5d458a4a547a3 100644 --- a/src/vs/editor/contrib/suggest/common/suggest.ts +++ b/src/vs/editor/contrib/suggest/common/suggest.ts @@ -5,6 +5,7 @@ 'use strict'; import {sequence} from 'vs/base/common/async'; +import {isFalsyOrEmpty} from 'vs/base/common/arrays'; import {illegalArgument, onUnexpectedError} from 'vs/base/common/errors'; import {TPromise} from 'vs/base/common/winjs.base'; import {IModel, IPosition} from 'vs/editor/common/editorCommon'; @@ -23,37 +24,34 @@ export interface ISuggestResult2 extends ISuggestResult { support?: ISuggestSupport; } -export function suggest(model: IModel, position: IPosition, triggerCharacter: string, groups?: ISuggestSupport[][]): TPromise { +export function suggest(model: IModel, position: IPosition, triggerCharacter: string, groups?: ISuggestSupport[][]): TPromise { if (!groups) { groups = SuggestRegistry.orderedGroups(model); } const resource = model.getAssociatedResource(); - const suggestions: ISuggestResult[][] = []; + const result: ISuggestResult2[] = []; const factory = groups.map((supports, index) => { return () => { // stop as soon as a group produced a result - if (suggestions.length > 0) { + if (result.length > 0) { return; } // for each support in the group ask for suggestions - const promises = supports.map(support => { + return TPromise.join(supports.map(support => { return support.suggest(resource, position, triggerCharacter).then(values => { if (!values) { return; } - const result: ISuggestResult2[] = []; for (let suggestResult of values) { - if (!suggestResult - || !Array.isArray(suggestResult.suggestions) - || suggestResult.suggestions.length === 0) { + if (!suggestResult || isFalsyOrEmpty(suggestResult.suggestions)) { continue; } @@ -65,30 +63,16 @@ export function suggest(model: IModel, position: IPosition, triggerCharacter: st }); } - return result; - }, onUnexpectedError); - }); - - return TPromise.join(promises).then(values => { - for (let value of values) { - if (Array.isArray(value) && value.length > 0) { - suggestions.push(value); - } - } - }); + })); }; }); return sequence(factory).then(() => { // add snippets to the first group const snippets = SnippetsRegistry.getSnippets(model, position); - if (suggestions.length === 0) { - suggestions.push([snippets]); - } else { - suggestions[0].push(snippets); - } - return suggestions; + result.push(snippets); + return result; }); } diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 7b4810a9138d2976a41119cbc9f667819ab3206b..c664de175e7031b76e3f157a022c5f7718030472 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -273,17 +273,15 @@ class ExtHostApiCommands { position: position && typeConverters.fromPosition(position), triggerCharacter }; - return this._commands.executeCommand('_executeCompletionItemProvider', args).then(value => { + return this._commands.executeCommand('_executeCompletionItemProvider', args).then(value => { if (value) { let items: types.CompletionItem[] = []; let incomplete: boolean; - for (let group of value) { - for (let suggestions of group) { - incomplete = suggestions.incomplete || incomplete; - for (let suggestion of suggestions.suggestions) { - const item = typeConverters.Suggest.to(suggestions, position, suggestion); - items.push(item); - } + for (let suggestions of value) { + incomplete = suggestions.incomplete || incomplete; + for (let suggestion of suggestions.suggestions) { + const item = typeConverters.Suggest.to(suggestions, position, suggestion); + items.push(item); } } return new types.CompletionList(items, incomplete); diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index 2572955c1b234c381e0e57abad7b4a613068706b..0dba0c35683c616b52aaee55be1e53ad47d88cfc 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -825,8 +825,8 @@ suite('ExtHostLanguageFeatures', function() { threadService.sync().then(() => { suggest(model, { lineNumber: 1, column: 1 }, ',').then(value => { - assert.equal(value.length, 1); - let [[first]] = value; + assert.ok(value.length >= 1); // check for min because snippets and others contribute + let [first] = value; assert.equal(first.suggestions.length, 1) assert.equal(first.suggestions[0].codeSnippet, 'testing2') done(); @@ -850,8 +850,8 @@ suite('ExtHostLanguageFeatures', function() { threadService.sync().then(() => { suggest(model, { lineNumber: 1, column: 1 }, ',').then(value => { - assert.equal(value.length, 1); - let [[first]] = value; + assert.ok(value.length >= 1); + let [first] = value; assert.equal(first.suggestions.length, 1) assert.equal(first.suggestions[0].codeSnippet, 'weak-selector') done(); @@ -876,8 +876,8 @@ suite('ExtHostLanguageFeatures', function() { threadService.sync().then(() => { suggest(model, { lineNumber: 1, column: 1 }, ',').then(value => { - assert.equal(value.length, 2); - let [[first], [second]] = value; + assert.ok(value.length >= 2); + let [first, second] = value; assert.equal(first.suggestions.length, 1) assert.equal(first.suggestions[0].codeSnippet, 'strong-2') // last wins assert.equal(second.suggestions[0].codeSnippet, 'strong-1') @@ -905,8 +905,7 @@ suite('ExtHostLanguageFeatures', function() { threadService.sync().then(() => { suggest(model, { lineNumber: 1, column: 1 }, ',').then(value => { - assert.equal(value.length, 1); - assert.equal(value[0][0].incomplete, undefined); + assert.equal(value[0].incomplete, undefined); done(); }); }); @@ -923,8 +922,7 @@ suite('ExtHostLanguageFeatures', function() { return threadService.sync().then(() => { suggest(model, { lineNumber: 1, column: 1 }, ',').then(value => { - assert.equal(value.length, 1); - assert.equal(value[0][0].incomplete, true); + assert.equal(value[0].incomplete, true); }); }); });