Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestMemory.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
7
import { RunOnceScheduler } from '../../../../base/common/async.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { LRUCache } from '../../../../base/common/map.js';
10
import { TernarySearchTree } from '../../../../base/common/ternarySearchTree.js';
11
import { IPosition } from '../../../common/core/position.js';
12
import { ITextModel } from '../../../common/model.js';
13
import { CompletionItemKind, CompletionItemKinds } from '../../../common/languages.js';
14
import { CompletionItem } from './suggest.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
17
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
18
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';
19
20
export abstract class Memory {
21
22
constructor(readonly name: MemMode) { }
23
24
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
25
if (items.length === 0) {
26
return 0;
27
}
28
const topScore = items[0].score[0];
29
for (let i = 0; i < items.length; i++) {
30
const { score, completion: suggestion } = items[i];
31
if (score[0] !== topScore) {
32
// stop when leaving the group of top matches
33
break;
34
}
35
if (suggestion.preselect) {
36
// stop when seeing an auto-select-item
37
return i;
38
}
39
}
40
return 0;
41
}
42
43
abstract memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
44
45
abstract toJSON(): object | undefined;
46
47
abstract fromJSON(data: object): void;
48
}
49
50
export class NoMemory extends Memory {
51
52
constructor() {
53
super('first');
54
}
55
56
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
57
// no-op
58
}
59
60
toJSON() {
61
return undefined;
62
}
63
64
fromJSON() {
65
//
66
}
67
}
68
69
export interface MemItem {
70
type: string | CompletionItemKind;
71
insertText: string;
72
touch: number;
73
}
74
75
export class LRUMemory extends Memory {
76
77
constructor() {
78
super('recentlyUsed');
79
}
80
81
private _cache = new LRUCache<string, MemItem>(300, 0.66);
82
private _seq = 0;
83
84
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
85
const key = `${model.getLanguageId()}/${item.textLabel}`;
86
this._cache.set(key, {
87
touch: this._seq++,
88
type: item.completion.kind,
89
insertText: item.completion.insertText
90
});
91
}
92
93
override select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
94
95
if (items.length === 0) {
96
return 0;
97
}
98
99
const lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1);
100
if (/\s$/.test(lineSuffix)) {
101
return super.select(model, pos, items);
102
}
103
104
const topScore = items[0].score[0];
105
let indexPreselect = -1;
106
let indexRecency = -1;
107
let seq = -1;
108
for (let i = 0; i < items.length; i++) {
109
if (items[i].score[0] !== topScore) {
110
// consider only top items
111
break;
112
}
113
const key = `${model.getLanguageId()}/${items[i].textLabel}`;
114
const item = this._cache.peek(key);
115
if (item && item.touch > seq && item.type === items[i].completion.kind && item.insertText === items[i].completion.insertText) {
116
seq = item.touch;
117
indexRecency = i;
118
}
119
if (items[i].completion.preselect && indexPreselect === -1) {
120
// stop when seeing an auto-select-item
121
return indexPreselect = i;
122
}
123
}
124
if (indexRecency !== -1) {
125
return indexRecency;
126
} else if (indexPreselect !== -1) {
127
return indexPreselect;
128
} else {
129
return 0;
130
}
131
}
132
133
toJSON(): object {
134
return this._cache.toJSON();
135
}
136
137
fromJSON(data: [string, MemItem][]): void {
138
this._cache.clear();
139
const seq = 0;
140
for (const [key, value] of data) {
141
value.touch = seq;
142
value.type = typeof value.type === 'number' ? value.type : CompletionItemKinds.fromString(value.type);
143
this._cache.set(key, value);
144
}
145
this._seq = this._cache.size;
146
}
147
}
148
149
150
export class PrefixMemory extends Memory {
151
152
constructor() {
153
super('recentlyUsedByPrefix');
154
}
155
156
private _trie = TernarySearchTree.forStrings<MemItem>();
157
private _seq = 0;
158
159
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
160
const { word } = model.getWordUntilPosition(pos);
161
const key = `${model.getLanguageId()}/${word}`;
162
this._trie.set(key, {
163
type: item.completion.kind,
164
insertText: item.completion.insertText,
165
touch: this._seq++
166
});
167
}
168
169
override select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
170
const { word } = model.getWordUntilPosition(pos);
171
if (!word) {
172
return super.select(model, pos, items);
173
}
174
const key = `${model.getLanguageId()}/${word}`;
175
let item = this._trie.get(key);
176
if (!item) {
177
item = this._trie.findSubstr(key);
178
}
179
if (item) {
180
for (let i = 0; i < items.length; i++) {
181
const { kind, insertText } = items[i].completion;
182
if (kind === item.type && insertText === item.insertText) {
183
return i;
184
}
185
}
186
}
187
return super.select(model, pos, items);
188
}
189
190
toJSON(): object {
191
192
const entries: [string, MemItem][] = [];
193
this._trie.forEach((value, key) => entries.push([key, value]));
194
195
// sort by last recently used (touch), then
196
// take the top 200 item and normalize their
197
// touch
198
entries
199
.sort((a, b) => -(a[1].touch - b[1].touch))
200
.forEach((value, i) => value[1].touch = i);
201
202
return entries.slice(0, 200);
203
}
204
205
fromJSON(data: [string, MemItem][]): void {
206
this._trie.clear();
207
if (data.length > 0) {
208
this._seq = data[0][1].touch + 1;
209
for (const [key, value] of data) {
210
value.type = typeof value.type === 'number' ? value.type : CompletionItemKinds.fromString(value.type);
211
this._trie.set(key, value);
212
}
213
}
214
}
215
}
216
217
export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';
218
219
export class SuggestMemoryService implements ISuggestMemoryService {
220
221
private static readonly _strategyCtors = new Map<MemMode, { new(): Memory }>([
222
['recentlyUsedByPrefix', PrefixMemory],
223
['recentlyUsed', LRUMemory],
224
['first', NoMemory]
225
]);
226
227
private static readonly _storagePrefix = 'suggest/memories';
228
229
readonly _serviceBrand: undefined;
230
231
232
private readonly _persistSoon: RunOnceScheduler;
233
private readonly _disposables = new DisposableStore();
234
235
private _strategy?: Memory;
236
237
constructor(
238
@IStorageService private readonly _storageService: IStorageService,
239
@IConfigurationService private readonly _configService: IConfigurationService,
240
) {
241
this._persistSoon = new RunOnceScheduler(() => this._saveState(), 500);
242
this._disposables.add(_storageService.onWillSaveState(e => {
243
if (e.reason === WillSaveStateReason.SHUTDOWN) {
244
this._saveState();
245
}
246
}));
247
}
248
249
dispose(): void {
250
this._disposables.dispose();
251
this._persistSoon.dispose();
252
}
253
254
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
255
this._withStrategy(model, pos).memorize(model, pos, item);
256
this._persistSoon.schedule();
257
}
258
259
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
260
return this._withStrategy(model, pos).select(model, pos, items);
261
}
262
263
private _withStrategy(model: ITextModel, pos: IPosition): Memory {
264
265
const mode = this._configService.getValue<MemMode>('editor.suggestSelection', {
266
overrideIdentifier: model.getLanguageIdAtPosition(pos.lineNumber, pos.column),
267
resource: model.uri
268
});
269
270
if (this._strategy?.name !== mode) {
271
272
this._saveState();
273
const ctor = SuggestMemoryService._strategyCtors.get(mode) || NoMemory;
274
this._strategy = new ctor();
275
276
try {
277
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
278
const scope = share ? StorageScope.PROFILE : StorageScope.WORKSPACE;
279
const raw = this._storageService.get(`${SuggestMemoryService._storagePrefix}/${mode}`, scope);
280
if (raw) {
281
this._strategy.fromJSON(JSON.parse(raw));
282
}
283
} catch (e) {
284
// things can go wrong with JSON...
285
}
286
}
287
288
return this._strategy;
289
}
290
291
private _saveState() {
292
if (this._strategy) {
293
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
294
const scope = share ? StorageScope.PROFILE : StorageScope.WORKSPACE;
295
const raw = JSON.stringify(this._strategy);
296
this._storageService.store(`${SuggestMemoryService._storagePrefix}/${this._strategy.name}`, raw, scope, StorageTarget.MACHINE);
297
}
298
}
299
}
300
301
302
export const ISuggestMemoryService = createDecorator<ISuggestMemoryService>('ISuggestMemories');
303
304
export interface ISuggestMemoryService {
305
readonly _serviceBrand: undefined;
306
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
307
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number;
308
}
309
310
registerSingleton(ISuggestMemoryService, SuggestMemoryService, InstantiationType.Delayed);
311
312