Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/suggest/browser/completionModel.ts
4797 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { quickSelect } from '../../../../base/common/arrays.js';
7
import { CharCode } from '../../../../base/common/charCode.js';
8
import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from '../../../../base/common/filters.js';
9
import { compareIgnoreCase } from '../../../../base/common/strings.js';
10
import { InternalSuggestOptions } from '../../../common/config/editorOptions.js';
11
import { CompletionItemKind, CompletionItemProvider } from '../../../common/languages.js';
12
import { WordDistance } from './wordDistance.js';
13
import { CompletionItem } from './suggest.js';
14
15
type StrictCompletionItem = Required<CompletionItem>;
16
17
export interface ICompletionStats {
18
pLabelLen: number;
19
}
20
21
export class LineContext {
22
constructor(
23
readonly leadingLineContent: string,
24
readonly characterCountDelta: number,
25
) { }
26
}
27
28
const enum Refilter {
29
Nothing = 0,
30
All = 1,
31
Incr = 2
32
}
33
34
/**
35
* Sorted, filtered completion view model
36
* */
37
export class CompletionModel {
38
39
private readonly _items: CompletionItem[];
40
private readonly _column: number;
41
private readonly _wordDistance: WordDistance;
42
private readonly _options: InternalSuggestOptions;
43
private readonly _snippetCompareFn = CompletionModel._compareCompletionItems;
44
private readonly _fuzzyScoreOptions: FuzzyScoreOptions;
45
46
private _lineContext: LineContext;
47
private _refilterKind: Refilter;
48
private _filteredItems?: StrictCompletionItem[];
49
50
private _itemsByProvider?: Map<CompletionItemProvider, CompletionItem[]>;
51
private _stats?: ICompletionStats;
52
53
constructor(
54
items: CompletionItem[],
55
column: number,
56
lineContext: LineContext,
57
wordDistance: WordDistance,
58
options: InternalSuggestOptions,
59
snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none',
60
fuzzyScoreOptions: FuzzyScoreOptions | undefined = FuzzyScoreOptions.default,
61
readonly clipboardText: string | undefined = undefined
62
) {
63
this._items = items;
64
this._column = column;
65
this._wordDistance = wordDistance;
66
this._options = options;
67
this._refilterKind = Refilter.All;
68
this._lineContext = lineContext;
69
this._fuzzyScoreOptions = fuzzyScoreOptions;
70
71
if (snippetSuggestions === 'top') {
72
this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp;
73
} else if (snippetSuggestions === 'bottom') {
74
this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown;
75
}
76
}
77
78
get lineContext(): LineContext {
79
return this._lineContext;
80
}
81
82
set lineContext(value: LineContext) {
83
if (this._lineContext.leadingLineContent !== value.leadingLineContent
84
|| this._lineContext.characterCountDelta !== value.characterCountDelta
85
) {
86
this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? Refilter.Incr : Refilter.All;
87
this._lineContext = value;
88
}
89
}
90
91
get items(): CompletionItem[] {
92
this._ensureCachedState();
93
return this._filteredItems!;
94
}
95
96
getItemsByProvider(): ReadonlyMap<CompletionItemProvider, CompletionItem[]> {
97
this._ensureCachedState();
98
return this._itemsByProvider!;
99
}
100
101
getIncompleteProvider(): Set<CompletionItemProvider> {
102
this._ensureCachedState();
103
const result = new Set<CompletionItemProvider>();
104
for (const [provider, items] of this.getItemsByProvider()) {
105
if (items.length > 0 && items[0].container.incomplete) {
106
result.add(provider);
107
}
108
}
109
return result;
110
}
111
112
get stats(): ICompletionStats {
113
this._ensureCachedState();
114
return this._stats!;
115
}
116
117
private _ensureCachedState(): void {
118
if (this._refilterKind !== Refilter.Nothing) {
119
this._createCachedState();
120
}
121
}
122
123
private _createCachedState(): void {
124
125
this._itemsByProvider = new Map();
126
127
const labelLengths: number[] = [];
128
129
const { leadingLineContent, characterCountDelta } = this._lineContext;
130
let word = '';
131
let wordLow = '';
132
133
// incrementally filter less
134
const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems!;
135
const target: StrictCompletionItem[] = [];
136
137
// picks a score function based on the number of
138
// items that we have to score/filter and based on the
139
// user-configuration
140
const scoreFn: FuzzyScorer = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
141
142
for (let i = 0; i < source.length; i++) {
143
144
const item = source[i];
145
146
if (item.isInvalid) {
147
continue; // SKIP invalid items
148
}
149
150
// keep all items by their provider
151
const arr = this._itemsByProvider.get(item.provider);
152
if (arr) {
153
arr.push(item);
154
} else {
155
this._itemsByProvider.set(item.provider, [item]);
156
}
157
158
// 'word' is that remainder of the current line that we
159
// filter and score against. In theory each suggestion uses a
160
// different word, but in practice not - that's why we cache
161
const overwriteBefore = item.position.column - item.editStart.column;
162
const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
163
if (word.length !== wordLen) {
164
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
165
wordLow = word.toLowerCase();
166
}
167
168
// remember the word against which this item was
169
// scored
170
item.word = word;
171
172
if (wordLen === 0) {
173
// when there is nothing to score against, don't
174
// event try to do. Use a const rank and rely on
175
// the fallback-sort using the initial sort order.
176
// use a score of `-100` because that is out of the
177
// bound of values `fuzzyScore` will return
178
item.score = FuzzyScore.Default;
179
180
} else {
181
// skip word characters that are whitespace until
182
// we have hit the replace range (overwriteBefore)
183
let wordPos = 0;
184
while (wordPos < overwriteBefore) {
185
const ch = word.charCodeAt(wordPos);
186
if (ch === CharCode.Space || ch === CharCode.Tab) {
187
wordPos += 1;
188
} else {
189
break;
190
}
191
}
192
193
if (wordPos >= wordLen) {
194
// the wordPos at which scoring starts is the whole word
195
// and therefore the same rules as not having a word apply
196
item.score = FuzzyScore.Default;
197
198
} else if (typeof item.completion.filterText === 'string') {
199
// when there is a `filterText` it must match the `word`.
200
// if it matches we check with the label to compute highlights
201
// and if that doesn't yield a result we have no highlights,
202
// despite having the match
203
const match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, this._fuzzyScoreOptions);
204
if (!match) {
205
continue; // NO match
206
}
207
if (compareIgnoreCase(item.completion.filterText, item.textLabel) === 0) {
208
// filterText and label are actually the same -> use good highlights
209
item.score = match;
210
} else {
211
// re-run the scorer on the label in the hope of a result BUT use the rank
212
// of the filterText-match
213
item.score = anyScore(word, wordLow, wordPos, item.textLabel, item.labelLow, 0);
214
item.score[0] = match[0]; // use score from filterText
215
}
216
217
} else {
218
// by default match `word` against the `label`
219
const match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, this._fuzzyScoreOptions);
220
if (!match) {
221
continue; // NO match
222
}
223
item.score = match;
224
}
225
}
226
227
item.idx = i;
228
item.distance = this._wordDistance.distance(item.position, item.completion);
229
target.push(item as StrictCompletionItem);
230
231
// update stats
232
labelLengths.push(item.textLabel.length);
233
}
234
235
this._filteredItems = target.sort(this._snippetCompareFn);
236
this._refilterKind = Refilter.Nothing;
237
this._stats = {
238
pLabelLen: labelLengths.length ?
239
quickSelect(labelLengths.length - .85, labelLengths, (a, b) => a - b)
240
: 0
241
};
242
}
243
244
private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {
245
if (a.score[0] > b.score[0]) {
246
return -1;
247
} else if (a.score[0] < b.score[0]) {
248
return 1;
249
} else if (a.distance < b.distance) {
250
return -1;
251
} else if (a.distance > b.distance) {
252
return 1;
253
} else if (a.idx < b.idx) {
254
return -1;
255
} else if (a.idx > b.idx) {
256
return 1;
257
} else {
258
return 0;
259
}
260
}
261
262
private static _compareCompletionItemsSnippetsDown(a: StrictCompletionItem, b: StrictCompletionItem): number {
263
if (a.completion.kind !== b.completion.kind) {
264
if (a.completion.kind === CompletionItemKind.Snippet) {
265
return 1;
266
} else if (b.completion.kind === CompletionItemKind.Snippet) {
267
return -1;
268
}
269
}
270
return CompletionModel._compareCompletionItems(a, b);
271
}
272
273
private static _compareCompletionItemsSnippetsUp(a: StrictCompletionItem, b: StrictCompletionItem): number {
274
if (a.completion.kind !== b.completion.kind) {
275
if (a.completion.kind === CompletionItemKind.Snippet) {
276
return -1;
277
} else if (b.completion.kind === CompletionItemKind.Snippet) {
278
return 1;
279
}
280
}
281
return CompletionModel._compareCompletionItems(a, b);
282
}
283
}
284
285