UNPKG

monaco-editor-core

Version:

A browser based code editor

225 lines (224 loc) • 9.68 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { quickSelect } from '../../../../base/common/arrays.js'; import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions } from '../../../../base/common/filters.js'; import { compareIgnoreCase } from '../../../../base/common/strings.js'; export class LineContext { constructor(leadingLineContent, characterCountDelta) { this.leadingLineContent = leadingLineContent; this.characterCountDelta = characterCountDelta; } } /** * Sorted, filtered completion view model * */ export class CompletionModel { constructor(items, column, lineContext, wordDistance, options, snippetSuggestions, fuzzyScoreOptions = FuzzyScoreOptions.default, clipboardText = undefined) { this.clipboardText = clipboardText; this._snippetCompareFn = CompletionModel._compareCompletionItems; this._items = items; this._column = column; this._wordDistance = wordDistance; this._options = options; this._refilterKind = 1 /* Refilter.All */; this._lineContext = lineContext; this._fuzzyScoreOptions = fuzzyScoreOptions; if (snippetSuggestions === 'top') { this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp; } else if (snippetSuggestions === 'bottom') { this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown; } } get lineContext() { return this._lineContext; } set lineContext(value) { if (this._lineContext.leadingLineContent !== value.leadingLineContent || this._lineContext.characterCountDelta !== value.characterCountDelta) { this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? 2 /* Refilter.Incr */ : 1 /* Refilter.All */; this._lineContext = value; } } get items() { this._ensureCachedState(); return this._filteredItems; } getItemsByProvider() { this._ensureCachedState(); return this._itemsByProvider; } getIncompleteProvider() { this._ensureCachedState(); const result = new Set(); for (const [provider, items] of this.getItemsByProvider()) { if (items.length > 0 && items[0].container.incomplete) { result.add(provider); } } return result; } get stats() { this._ensureCachedState(); return this._stats; } _ensureCachedState() { if (this._refilterKind !== 0 /* Refilter.Nothing */) { this._createCachedState(); } } _createCachedState() { this._itemsByProvider = new Map(); const labelLengths = []; const { leadingLineContent, characterCountDelta } = this._lineContext; let word = ''; let wordLow = ''; // incrementally filter less const source = this._refilterKind === 1 /* Refilter.All */ ? this._items : this._filteredItems; const target = []; // picks a score function based on the number of // items that we have to score/filter and based on the // user-configuration const scoreFn = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive; for (let i = 0; i < source.length; i++) { const item = source[i]; if (item.isInvalid) { continue; // SKIP invalid items } // keep all items by their provider const arr = this._itemsByProvider.get(item.provider); if (arr) { arr.push(item); } else { this._itemsByProvider.set(item.provider, [item]); } // 'word' is that remainder of the current line that we // filter and score against. In theory each suggestion uses a // different word, but in practice not - that's why we cache const overwriteBefore = item.position.column - item.editStart.column; const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column); if (word.length !== wordLen) { word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen); wordLow = word.toLowerCase(); } // remember the word against which this item was // scored item.word = word; if (wordLen === 0) { // when there is nothing to score against, don't // event try to do. Use a const rank and rely on // the fallback-sort using the initial sort order. // use a score of `-100` because that is out of the // bound of values `fuzzyScore` will return item.score = FuzzyScore.Default; } else { // skip word characters that are whitespace until // we have hit the replace range (overwriteBefore) let wordPos = 0; while (wordPos < overwriteBefore) { const ch = word.charCodeAt(wordPos); if (ch === 32 /* CharCode.Space */ || ch === 9 /* CharCode.Tab */) { wordPos += 1; } else { break; } } if (wordPos >= wordLen) { // the wordPos at which scoring starts is the whole word // and therefore the same rules as not having a word apply item.score = FuzzyScore.Default; } else if (typeof item.completion.filterText === 'string') { // when there is a `filterText` it must match the `word`. // if it matches we check with the label to compute highlights // and if that doesn't yield a result we have no highlights, // despite having the match const match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } if (compareIgnoreCase(item.completion.filterText, item.textLabel) === 0) { // filterText and label are actually the same -> use good highlights item.score = match; } else { // re-run the scorer on the label in the hope of a result BUT use the rank // of the filterText-match item.score = anyScore(word, wordLow, wordPos, item.textLabel, item.labelLow, 0); item.score[0] = match[0]; // use score from filterText } } else { // by default match `word` against the `label` const match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } item.score = match; } } item.idx = i; item.distance = this._wordDistance.distance(item.position, item.completion); target.push(item); // update stats labelLengths.push(item.textLabel.length); } this._filteredItems = target.sort(this._snippetCompareFn); this._refilterKind = 0 /* Refilter.Nothing */; this._stats = { pLabelLen: labelLengths.length ? quickSelect(labelLengths.length - .85, labelLengths, (a, b) => a - b) : 0 }; } static _compareCompletionItems(a, b) { if (a.score[0] > b.score[0]) { return -1; } else if (a.score[0] < b.score[0]) { return 1; } else if (a.distance < b.distance) { return -1; } else if (a.distance > b.distance) { return 1; } else if (a.idx < b.idx) { return -1; } else if (a.idx > b.idx) { return 1; } else { return 0; } } static _compareCompletionItemsSnippetsDown(a, b) { if (a.completion.kind !== b.completion.kind) { if (a.completion.kind === 27 /* CompletionItemKind.Snippet */) { return 1; } else if (b.completion.kind === 27 /* CompletionItemKind.Snippet */) { return -1; } } return CompletionModel._compareCompletionItems(a, b); } static _compareCompletionItemsSnippetsUp(a, b) { if (a.completion.kind !== b.completion.kind) { if (a.completion.kind === 27 /* CompletionItemKind.Snippet */) { return -1; } else if (b.completion.kind === 27 /* CompletionItemKind.Snippet */) { return 1; } } return CompletionModel._compareCompletionItems(a, b); } }