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

'use strict';

8
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
9
import { indexOfIgnoreCase } from 'vs/base/common/strings';
10 11 12
import { IMatch, fuzzyContiguousFilter } from 'vs/base/common/filters';
import { ISuggestSupport } from 'vs/editor/common/modes';
import { ISuggestionItem } from './suggest';
13

14 15
export interface ICompletionItem extends ISuggestionItem {
	highlights?: IMatch[];
16 17
}

18
export interface ICompletionStats {
19 20 21 22 23 24
	suggestionCount: number;
	snippetCount: number;
	textCount: number;
	[name: string]: any;
}

25 26 27 28 29
export class LineContext {
	leadingLineContent: string;
	characterCountDelta: number;
}

30 31
export class CompletionModel {

32
	private _lineContext: LineContext;
33
	private _column: number;
34
	private _items: ICompletionItem[];
35

36
	private _filteredItems: ICompletionItem[];
37
	private _topScoreIdx: number;
J
Johannes Rieken 已提交
38
	private _isIncomplete: boolean;
39
	private _stats: ICompletionStats;
40

41
	constructor(items: ISuggestionItem[], column: number, lineContext: LineContext) {
42
		this._items = items;
43
		this._column = column;
44
		this._lineContext = lineContext;
J
Johannes Rieken 已提交
45 46
	}

47 48
	get lineContext(): LineContext {
		return this._lineContext;
49 50
	}

51
	set lineContext(value: LineContext) {
52 53 54
		if (this._lineContext.leadingLineContent !== value.leadingLineContent
			|| this._lineContext.characterCountDelta !== value.characterCountDelta) {

55
			this._lineContext = value;
56
			this._filteredItems = undefined;
57
		}
58
	}
59

60 61
	get items(): ICompletionItem[] {
		this._ensureCachedState();
62
		return this._filteredItems;
63
	}
64

65
	get topScoreIdx(): number {
66
		this._ensureCachedState();
67 68
		return this._topScoreIdx;
	}
69

J
Johannes Rieken 已提交
70
	get incomplete(): boolean {
71
		this._ensureCachedState();
J
Johannes Rieken 已提交
72 73 74 75 76 77 78 79
		return this._isIncomplete;
	}

	resolveIncompleteInfo(): { incomplete: ISuggestSupport[], complete: ISuggestionItem[] } {
		const incomplete: ISuggestSupport[] = [];
		const complete: ISuggestionItem[] = [];

		for (const item of this._items) {
80
			if (!item.container.incomplete) {
J
Johannes Rieken 已提交
81
				complete.push(item);
82 83
			} else if (incomplete.indexOf(item.support) < 0) {
				incomplete.push(item.support);
J
Johannes Rieken 已提交
84 85 86 87
			}
		}

		return { incomplete, complete };
88 89 90 91 92 93 94 95
	}

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

	private _ensureCachedState(): void {
96
		if (!this._filteredItems) {
97
			this._createCachedState();
98 99 100
		}
	}

101
	private _createCachedState(): void {
102
		this._filteredItems = [];
103
		this._topScoreIdx = -1;
J
Johannes Rieken 已提交
104
		this._isIncomplete = false;
105
		this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0 };
106

107
		const {leadingLineContent, characterCountDelta} = this._lineContext;
108 109 110
		let word = '';
		let topScore = -1;

111
		for (const item of this._items) {
112

113
			const {suggestion, container} = item;
114

115 116
			// collect those supports that signaled having
			// an incomplete result
J
Johannes Rieken 已提交
117
			this._isIncomplete = this._isIncomplete || container.incomplete;
118

119 120 121
			// 'word' is that remainder of the current line that we
			// filter and score against. In theory each suggestion uses a
			// differnet word, but in practice not - that's why we cache
122
			const wordLen = suggestion.overwriteBefore + characterCountDelta - (item.position.column - this._column);
123
			if (word.length !== wordLen) {
J
Johannes Rieken 已提交
124
				word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
125 126
			}

127 128 129
			let match = false;

			// compute highlights based on 'label'
130
			item.highlights = fuzzyContiguousFilter(word, suggestion.label);
131
			match = item.highlights !== null;
132 133

			// no match on label nor codeSnippet -> check on filterText
134
			if (!match && typeof suggestion.filterText === 'string') {
135
				if (!isFalsyOrEmpty(fuzzyContiguousFilter(word, suggestion.filterText))) {
136 137 138 139
					match = true;

					// try to compute highlights by stripping none-word
					// characters from the end of the string
140
					item.highlights = fuzzyContiguousFilter(word.replace(/^\W+|\W+$/, ''), suggestion.label);
141
				}
142
			}
143

144 145 146 147 148 149 150
			if (!match) {
				continue;
			}

			this._filteredItems.push(item);

			// compute score against word
J
Johannes Rieken 已提交
151
			const score = CompletionModel._scoreByHighlight(item, word);
152 153 154
			if (score > topScore) {
				topScore = score;
				this._topScoreIdx = this._filteredItems.length - 1;
155
			}
156 157 158

			// update stats
			this._stats.suggestionCount++;
159
			switch (suggestion.type) {
160 161 162
				case 'snippet': this._stats.snippetCount++; break;
				case 'text': this._stats.textCount++; break;
			}
163 164
		}
	}
165

166 167
	private static _base = 100;

J
Johannes Rieken 已提交
168
	private static _scoreByHighlight(item: ICompletionItem, currentWord: string): number {
169
		const {highlights, suggestion} = item;
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185

		if (isFalsyOrEmpty(highlights)) {
			return 0;
		}

		let caseSensitiveMatches = 0;
		let caseInsensitiveMatches = 0;
		let firstMatchStart = 0;

		const len = Math.min(CompletionModel._base, suggestion.label.length);
		let currentWordOffset = 0;

		for (let pos = 0, idx = 0; pos < len; pos++) {

			const highlight = highlights[idx];

J
Johannes Rieken 已提交
186
			if (pos === highlight.start) {
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
				// reached a highlight: find highlighted part
				// and count case-sensitive /case-insensitive matches
				const part = suggestion.label.substring(highlight.start, highlight.end);
				currentWordOffset = indexOfIgnoreCase(currentWord, part, currentWordOffset);
				if (currentWordOffset >= 0) {
					do {
						if (suggestion.label[pos] === currentWord[currentWordOffset]) {
							caseSensitiveMatches += 1;
						} else {
							caseInsensitiveMatches += 1;
						}
						pos += 1;
						currentWordOffset += 1;
					} while (pos < highlight.end);
				}

				// proceed with next highlight, store first start,
				// exit loop when no highlight is available
				if (idx === 0) {
					firstMatchStart = highlight.start;
				}
				idx += 1;
J
Johannes Rieken 已提交
209

210 211
				if (idx >= highlights.length) {
					break;
212
				}
213 214
			}
		}
215

J
Johannes Rieken 已提交
216
		// combine the 4 scoring values into one
217 218
		// value using base_100. Values further left
		// are more important
J
Johannes Rieken 已提交
219 220 221 222
		return (CompletionModel._base ** 3) * caseSensitiveMatches
			+ (CompletionModel._base ** 2) * caseInsensitiveMatches
			+ (CompletionModel._base ** 1) * (CompletionModel._base - firstMatchStart)
			+ (CompletionModel._base ** 0) * (CompletionModel._base - highlights.length);
223
	}
224
}