completionModel.ts 9.2 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore, anyScore } from 'vs/base/common/filters';
7
import { CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes';
8
import { CompletionItem } from './suggest';
9
import { InternalSuggestOptions, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions';
10
import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance';
11
import { CharCode } from 'vs/base/common/charCode';
12
import { compareIgnoreCase } from 'vs/base/common/strings';
13

14
type StrictCompletionItem = Required<CompletionItem>;
J
Johannes Rieken 已提交
15

K
kieferrm 已提交
16
/* __GDPR__FRAGMENT__
K
kieferrm 已提交
17
	"ICompletionStats" : {
K
kieferrm 已提交
18 19 20
		"suggestionCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"snippetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"textCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
K
kieferrm 已提交
21 22
	}
*/
K
kieferrm 已提交
23
// __GDPR__TODO__: This is a dynamically extensible structure which can not be declared statically.
24
export interface ICompletionStats {
25 26 27 28 29 30
	suggestionCount: number;
	snippetCount: number;
	textCount: number;
	[name: string]: any;
}

31 32 33 34 35
export class LineContext {
	leadingLineContent: string;
	characterCountDelta: number;
}

36 37 38 39 40 41
const enum Refilter {
	Nothing = 0,
	All = 1,
	Incr = 2
}

42 43
export class CompletionModel {

44
	private readonly _items: CompletionItem[];
45
	private readonly _column: number;
J
Johannes Rieken 已提交
46
	private readonly _wordDistance: WordDistance;
47
	private readonly _options: InternalSuggestOptions;
J
Johannes Rieken 已提交
48
	private readonly _snippetCompareFn = CompletionModel._compareCompletionItems;
49

J
Johannes Rieken 已提交
50
	private _lineContext: LineContext;
51
	private _refilterKind: Refilter;
52
	private _filteredItems: StrictCompletionItem[];
53
	private _isIncomplete: Set<CompletionItemProvider>;
54
	private _stats: ICompletionStats;
55

56
	constructor(
57
		items: CompletionItem[],
58 59
		column: number,
		lineContext: LineContext,
J
Johannes Rieken 已提交
60
		wordDistance: WordDistance,
61 62
		options: InternalSuggestOptions = EDITOR_DEFAULTS.contribInfo.suggest
	) {
63
		this._items = items;
64
		this._column = column;
J
Johannes Rieken 已提交
65
		this._wordDistance = wordDistance;
66
		this._options = options;
67
		this._refilterKind = Refilter.All;
68
		this._lineContext = lineContext;
J
Johannes Rieken 已提交
69

70
		if (options.snippets === 'top') {
J
Johannes Rieken 已提交
71
			this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp;
72
		} else if (options.snippets === 'bottom') {
J
Johannes Rieken 已提交
73 74
			this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsDown;
		}
J
Johannes Rieken 已提交
75 76
	}

77 78
	get lineContext(): LineContext {
		return this._lineContext;
79 80
	}

81
	set lineContext(value: LineContext) {
82
		if (this._lineContext.leadingLineContent !== value.leadingLineContent
83 84
			|| this._lineContext.characterCountDelta !== value.characterCountDelta
		) {
85
			this._refilterKind = this._lineContext.characterCountDelta < value.characterCountDelta && this._filteredItems ? Refilter.Incr : Refilter.All;
86
			this._lineContext = value;
87
		}
88
	}
89

90
	get items(): CompletionItem[] {
91
		this._ensureCachedState();
92
		return this._filteredItems;
93
	}
94

95
	get incomplete(): Set<CompletionItemProvider> {
96
		this._ensureCachedState();
J
Johannes Rieken 已提交
97 98 99
		return this._isIncomplete;
	}

100 101
	adopt(except: Set<CompletionItemProvider>): CompletionItem[] {
		let res = new Array<CompletionItem>();
J
Johannes Rieken 已提交
102
		for (let i = 0; i < this._items.length;) {
J
Johannes Rieken 已提交
103
			if (!except.has(this._items[i].provider)) {
J
Johannes Rieken 已提交
104
				res.push(this._items[i]);
J
Johannes Rieken 已提交
105

J
Johannes Rieken 已提交
106 107 108
				// unordered removed
				this._items[i] = this._items[this._items.length - 1];
				this._items.pop();
J
Johannes Rieken 已提交
109 110 111
			} else {
				// continue with next item
				i++;
J
Johannes Rieken 已提交
112 113
			}
		}
J
Johannes Rieken 已提交
114 115
		this._refilterKind = Refilter.All;
		return res;
116 117 118 119 120 121 122 123
	}

	get stats(): ICompletionStats {
		this._ensureCachedState();
		return this._stats;
	}

	private _ensureCachedState(): void {
124
		if (this._refilterKind !== Refilter.Nothing) {
125
			this._createCachedState();
126 127 128
		}
	}

129
	private _createCachedState(): void {
130

J
Johannes Rieken 已提交
131
		this._isIncomplete = new Set();
132
		this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0 };
133

J
Johannes Rieken 已提交
134
		const { leadingLineContent, characterCountDelta } = this._lineContext;
135
		let word = '';
136
		let wordLow = '';
137

138 139
		// incrementally filter less
		const source = this._refilterKind === Refilter.All ? this._items : this._filteredItems;
J
Johannes Rieken 已提交
140
		const target: StrictCompletionItem[] = [];
141

142
		// picks a score function based on the number of
143 144
		// items that we have to score/filter and based on the
		// user-configuration
145
		const scoreFn: FuzzyScorer = (!this._options.filterGraceful || source.length > 2000) ? fuzzyScore : fuzzyScoreGracefulAggressive;
146

147
		for (let i = 0; i < source.length; i++) {
148

149
			const item = source[i];
J
Johannes Rieken 已提交
150

151 152
			// collect those supports that signaled having
			// an incomplete result
153
			if (item.container.incomplete) {
J
Johannes Rieken 已提交
154
				this._isIncomplete.add(item.provider);
J
Johannes Rieken 已提交
155
			}
156

157 158
			// 'word' is that remainder of the current line that we
			// filter and score against. In theory each suggestion uses a
159
			// different word, but in practice not - that's why we cache
160
			const overwriteBefore = item.position.column - item.completion.range.startColumn;
161
			const wordLen = overwriteBefore + characterCountDelta - (item.position.column - this._column);
162
			if (word.length !== wordLen) {
J
Johannes Rieken 已提交
163
				word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
164
				wordLow = word.toLowerCase();
165 166
			}

167 168 169 170
			// remember the word against which this item was
			// scored
			item.word = word;

171 172 173 174 175 176
			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
177
				item.score = FuzzyScore.Default;
178

179 180 181 182 183 184 185 186 187 188 189
			} 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 === CharCode.Space || ch === CharCode.Tab) {
						wordPos += 1;
					} else {
						break;
					}
190
				}
191

192 193 194
				if (wordPos >= wordLen) {
					// the wordPos at which scoring starts is the whole word
					// and therefore the same rules as not having a word apply
195
					item.score = FuzzyScore.Default;
196

197
				} else if (typeof item.completion.filterText === 'string') {
198 199 200 201
					// 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
202
					let match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, false);
203
					if (!match) {
J
Johannes Rieken 已提交
204
						continue; // NO match
205
					}
206 207 208 209
					if (compareIgnoreCase(item.completion.filterText, item.completion.label) === 0) {
						// filterText and label are actually the same -> use good highlights
						item.score = match;
					} else {
210 211
						// re-run the scorer on the label in the hope of a result BUT use the rank
						// of the filterText-match
212
						item.score = anyScore(word, wordLow, wordPos, item.completion.label, item.labelLow, 0);
213 214
						item.score[0] = match[0]; // use score from filterText
					}
215

216
				} else {
217
					// by default match `word` against the `label`
218
					let match = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, false);
J
Johannes Rieken 已提交
219 220
					if (!match) {
						continue; // NO match
221
					}
222
					item.score = match;
223
				}
224
			}
225

226
			item.idx = i;
227
			item.distance = this._wordDistance.distance(item.position, item.completion);
J
Johannes Rieken 已提交
228
			target.push(item as StrictCompletionItem);
229

230 231
			// update stats
			this._stats.suggestionCount++;
232
			switch (item.completion.kind) {
233 234
				case CompletionItemKind.Snippet: this._stats.snippetCount++; break;
				case CompletionItemKind.Text: this._stats.textCount++; break;
235
			}
236
		}
237

238 239
		this._filteredItems = target.sort(this._snippetCompareFn);
		this._refilterKind = Refilter.Nothing;
240 241
	}

J
Johannes Rieken 已提交
242
	private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {
243
		if (a.score[0] > b.score[0]) {
244
			return -1;
245
		} else if (a.score[0] < b.score[0]) {
246
			return 1;
247 248 249 250
		} else if (a.distance < b.distance) {
			return -1;
		} else if (a.distance > b.distance) {
			return 1;
251 252 253 254 255 256 257
		} else if (a.idx < b.idx) {
			return -1;
		} else if (a.idx > b.idx) {
			return 1;
		} else {
			return 0;
		}
258
	}
J
Johannes Rieken 已提交
259

J
Johannes Rieken 已提交
260
	private static _compareCompletionItemsSnippetsDown(a: StrictCompletionItem, b: StrictCompletionItem): number {
261 262
		if (a.completion.kind !== b.completion.kind) {
			if (a.completion.kind === CompletionItemKind.Snippet) {
J
Johannes Rieken 已提交
263
				return 1;
264
			} else if (b.completion.kind === CompletionItemKind.Snippet) {
J
Johannes Rieken 已提交
265 266 267 268 269 270
				return -1;
			}
		}
		return CompletionModel._compareCompletionItems(a, b);
	}

J
Johannes Rieken 已提交
271
	private static _compareCompletionItemsSnippetsUp(a: StrictCompletionItem, b: StrictCompletionItem): number {
272 273
		if (a.completion.kind !== b.completion.kind) {
			if (a.completion.kind === CompletionItemKind.Snippet) {
J
Johannes Rieken 已提交
274
				return -1;
275
			} else if (b.completion.kind === CompletionItemKind.Snippet) {
J
Johannes Rieken 已提交
276 277 278 279 280
				return 1;
			}
		}
		return CompletionModel._compareCompletionItems(a, b);
	}
281
}