monaco-editor-core
Version:
A browser based code editor
225 lines (224 loc) • 9.68 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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);
}
}