Path: blob/main/src/vs/editor/contrib/suggest/browser/completionModel.ts
4797 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { quickSelect } from '../../../../base/common/arrays.js';6import { CharCode } from '../../../../base/common/charCode.js';7import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from '../../../../base/common/filters.js';8import { compareIgnoreCase } from '../../../../base/common/strings.js';9import { InternalSuggestOptions } from '../../../common/config/editorOptions.js';10import { CompletionItemKind, CompletionItemProvider } from '../../../common/languages.js';11import { WordDistance } from './wordDistance.js';12import { CompletionItem } from './suggest.js';1314type StrictCompletionItem = Required<CompletionItem>;1516export interface ICompletionStats {17pLabelLen: number;18}1920export class LineContext {21constructor(22readonly leadingLineContent: string,23readonly characterCountDelta: number,24) { }25}2627const enum Refilter {28Nothing = 0,29All = 1,30Incr = 231}3233/**34* Sorted, filtered completion view model35* */36export class CompletionModel {3738private readonly _items: CompletionItem[];39private readonly _column: number;40private readonly _wordDistance: WordDistance;41private readonly _options: InternalSuggestOptions;42private readonly _snippetCompareFn = CompletionModel._compareCompletionItems;43private readonly _fuzzyScoreOptions: FuzzyScoreOptions;4445private _lineContext: LineContext;46private _refilterKind: Refilter;47private _filteredItems?: StrictCompletionItem[];4849private _itemsByProvider?: Map<CompletionItemProvider, CompletionItem[]>;50private _stats?: ICompletionStats;5152constructor(53items: CompletionItem[],54column: number,55lineContext: LineContext,56wordDistance: WordDistance,57options: InternalSuggestOptions,58snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none',59fuzzyScoreOptions: FuzzyScoreOptions | undefined = FuzzyScoreOptions.default,60readonly clipboardText: string | undefined = undefined61) {62this._items = items;63this._column = column;64this._wordDistance = wordDistance;65this._options = options;66this._refilterKind = Refilter.All;67this._lineContext = lineContext;68this._fuzzyScoreOptions = fuzzyScoreOptions;6970if (snippetSuggestions === 'top') {71this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp;72} else if (snippetSuggestions === 'bottom') {73this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown;74}75}7677get lineContext(): LineContext {78return this._lineContext;79}8081set lineContext(value: LineContext) {82if (this._lineContext.leadingLineContent !== value.leadingLineContent83|| this._lineContext.characterCountDelta !== value.characterCountDelta84) {85this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? Refilter.Incr : Refilter.All;86this._lineContext = value;87}88}8990get items(): CompletionItem[] {91this._ensureCachedState();92return this._filteredItems!;93}9495getItemsByProvider(): ReadonlyMap<CompletionItemProvider, CompletionItem[]> {96this._ensureCachedState();97return this._itemsByProvider!;98}99100getIncompleteProvider(): Set<CompletionItemProvider> {101this._ensureCachedState();102const result = new Set<CompletionItemProvider>();103for (const [provider, items] of this.getItemsByProvider()) {104if (items.length > 0 && items[0].container.incomplete) {105result.add(provider);106}107}108return result;109}110111get stats(): ICompletionStats {112this._ensureCachedState();113return this._stats!;114}115116private _ensureCachedState(): void {117if (this._refilterKind !== Refilter.Nothing) {118this._createCachedState();119}120}121122private _createCachedState(): void {123124this._itemsByProvider = new Map();125126const labelLengths: number[] = [];127128const { leadingLineContent, characterCountDelta } = this._lineContext;129let word = '';130let wordLow = '';131132// incrementally filter less133const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems!;134const target: StrictCompletionItem[] = [];135136// picks a score function based on the number of137// items that we have to score/filter and based on the138// user-configuration139const scoreFn: FuzzyScorer = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;140141for (let i = 0; i < source.length; i++) {142143const item = source[i];144145if (item.isInvalid) {146continue; // SKIP invalid items147}148149// keep all items by their provider150const arr = this._itemsByProvider.get(item.provider);151if (arr) {152arr.push(item);153} else {154this._itemsByProvider.set(item.provider, [item]);155}156157// 'word' is that remainder of the current line that we158// filter and score against. In theory each suggestion uses a159// different word, but in practice not - that's why we cache160const overwriteBefore = item.position.column - item.editStart.column;161const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);162if (word.length !== wordLen) {163word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);164wordLow = word.toLowerCase();165}166167// remember the word against which this item was168// scored169item.word = word;170171if (wordLen === 0) {172// when there is nothing to score against, don't173// event try to do. Use a const rank and rely on174// the fallback-sort using the initial sort order.175// use a score of `-100` because that is out of the176// bound of values `fuzzyScore` will return177item.score = FuzzyScore.Default;178179} else {180// skip word characters that are whitespace until181// we have hit the replace range (overwriteBefore)182let wordPos = 0;183while (wordPos < overwriteBefore) {184const ch = word.charCodeAt(wordPos);185if (ch === CharCode.Space || ch === CharCode.Tab) {186wordPos += 1;187} else {188break;189}190}191192if (wordPos >= wordLen) {193// the wordPos at which scoring starts is the whole word194// and therefore the same rules as not having a word apply195item.score = FuzzyScore.Default;196197} else if (typeof item.completion.filterText === 'string') {198// when there is a `filterText` it must match the `word`.199// if it matches we check with the label to compute highlights200// and if that doesn't yield a result we have no highlights,201// despite having the match202const match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, this._fuzzyScoreOptions);203if (!match) {204continue; // NO match205}206if (compareIgnoreCase(item.completion.filterText, item.textLabel) === 0) {207// filterText and label are actually the same -> use good highlights208item.score = match;209} else {210// re-run the scorer on the label in the hope of a result BUT use the rank211// of the filterText-match212item.score = anyScore(word, wordLow, wordPos, item.textLabel, item.labelLow, 0);213item.score[0] = match[0]; // use score from filterText214}215216} else {217// by default match `word` against the `label`218const match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, this._fuzzyScoreOptions);219if (!match) {220continue; // NO match221}222item.score = match;223}224}225226item.idx = i;227item.distance = this._wordDistance.distance(item.position, item.completion);228target.push(item as StrictCompletionItem);229230// update stats231labelLengths.push(item.textLabel.length);232}233234this._filteredItems = target.sort(this._snippetCompareFn);235this._refilterKind = Refilter.Nothing;236this._stats = {237pLabelLen: labelLengths.length ?238quickSelect(labelLengths.length - .85, labelLengths, (a, b) => a - b)239: 0240};241}242243private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {244if (a.score[0] > b.score[0]) {245return -1;246} else if (a.score[0] < b.score[0]) {247return 1;248} else if (a.distance < b.distance) {249return -1;250} else if (a.distance > b.distance) {251return 1;252} else if (a.idx < b.idx) {253return -1;254} else if (a.idx > b.idx) {255return 1;256} else {257return 0;258}259}260261private static _compareCompletionItemsSnippetsDown(a: StrictCompletionItem, b: StrictCompletionItem): number {262if (a.completion.kind !== b.completion.kind) {263if (a.completion.kind === CompletionItemKind.Snippet) {264return 1;265} else if (b.completion.kind === CompletionItemKind.Snippet) {266return -1;267}268}269return CompletionModel._compareCompletionItems(a, b);270}271272private static _compareCompletionItemsSnippetsUp(a: StrictCompletionItem, b: StrictCompletionItem): number {273if (a.completion.kind !== b.completion.kind) {274if (a.completion.kind === CompletionItemKind.Snippet) {275return -1;276} else if (b.completion.kind === CompletionItemKind.Snippet) {277return 1;278}279}280return CompletionModel._compareCompletionItems(a, b);281}282}283284285