Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestMemory.ts
4797 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { RunOnceScheduler } from '../../../../base/common/async.js';7import { DisposableStore } from '../../../../base/common/lifecycle.js';8import { LRUCache } from '../../../../base/common/map.js';9import { TernarySearchTree } from '../../../../base/common/ternarySearchTree.js';10import { IPosition } from '../../../common/core/position.js';11import { ITextModel } from '../../../common/model.js';12import { CompletionItemKind, CompletionItemKinds } from '../../../common/languages.js';13import { CompletionItem } from './suggest.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';16import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';17import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';1819export abstract class Memory {2021constructor(readonly name: MemMode) { }2223select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {24if (items.length === 0) {25return 0;26}27const topScore = items[0].score[0];28for (let i = 0; i < items.length; i++) {29const { score, completion: suggestion } = items[i];30if (score[0] !== topScore) {31// stop when leaving the group of top matches32break;33}34if (suggestion.preselect) {35// stop when seeing an auto-select-item36return i;37}38}39return 0;40}4142abstract memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;4344abstract toJSON(): object | undefined;4546abstract fromJSON(data: object): void;47}4849export class NoMemory extends Memory {5051constructor() {52super('first');53}5455memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {56// no-op57}5859toJSON() {60return undefined;61}6263fromJSON() {64//65}66}6768export interface MemItem {69type: string | CompletionItemKind;70insertText: string;71touch: number;72}7374export class LRUMemory extends Memory {7576constructor() {77super('recentlyUsed');78}7980private _cache = new LRUCache<string, MemItem>(300, 0.66);81private _seq = 0;8283memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {84const key = `${model.getLanguageId()}/${item.textLabel}`;85this._cache.set(key, {86touch: this._seq++,87type: item.completion.kind,88insertText: item.completion.insertText89});90}9192override select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {9394if (items.length === 0) {95return 0;96}9798const lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1);99if (/\s$/.test(lineSuffix)) {100return super.select(model, pos, items);101}102103const topScore = items[0].score[0];104let indexPreselect = -1;105let indexRecency = -1;106let seq = -1;107for (let i = 0; i < items.length; i++) {108if (items[i].score[0] !== topScore) {109// consider only top items110break;111}112const key = `${model.getLanguageId()}/${items[i].textLabel}`;113const item = this._cache.peek(key);114if (item && item.touch > seq && item.type === items[i].completion.kind && item.insertText === items[i].completion.insertText) {115seq = item.touch;116indexRecency = i;117}118if (items[i].completion.preselect && indexPreselect === -1) {119// stop when seeing an auto-select-item120return indexPreselect = i;121}122}123if (indexRecency !== -1) {124return indexRecency;125} else if (indexPreselect !== -1) {126return indexPreselect;127} else {128return 0;129}130}131132toJSON(): object {133return this._cache.toJSON();134}135136fromJSON(data: [string, MemItem][]): void {137this._cache.clear();138const seq = 0;139for (const [key, value] of data) {140value.touch = seq;141value.type = typeof value.type === 'number' ? value.type : CompletionItemKinds.fromString(value.type);142this._cache.set(key, value);143}144this._seq = this._cache.size;145}146}147148149export class PrefixMemory extends Memory {150151constructor() {152super('recentlyUsedByPrefix');153}154155private _trie = TernarySearchTree.forStrings<MemItem>();156private _seq = 0;157158memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {159const { word } = model.getWordUntilPosition(pos);160const key = `${model.getLanguageId()}/${word}`;161this._trie.set(key, {162type: item.completion.kind,163insertText: item.completion.insertText,164touch: this._seq++165});166}167168override select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {169const { word } = model.getWordUntilPosition(pos);170if (!word) {171return super.select(model, pos, items);172}173const key = `${model.getLanguageId()}/${word}`;174let item = this._trie.get(key);175if (!item) {176item = this._trie.findSubstr(key);177}178if (item) {179for (let i = 0; i < items.length; i++) {180const { kind, insertText } = items[i].completion;181if (kind === item.type && insertText === item.insertText) {182return i;183}184}185}186return super.select(model, pos, items);187}188189toJSON(): object {190191const entries: [string, MemItem][] = [];192this._trie.forEach((value, key) => entries.push([key, value]));193194// sort by last recently used (touch), then195// take the top 200 item and normalize their196// touch197entries198.sort((a, b) => -(a[1].touch - b[1].touch))199.forEach((value, i) => value[1].touch = i);200201return entries.slice(0, 200);202}203204fromJSON(data: [string, MemItem][]): void {205this._trie.clear();206if (data.length > 0) {207this._seq = data[0][1].touch + 1;208for (const [key, value] of data) {209value.type = typeof value.type === 'number' ? value.type : CompletionItemKinds.fromString(value.type);210this._trie.set(key, value);211}212}213}214}215216export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';217218export class SuggestMemoryService implements ISuggestMemoryService {219220private static readonly _strategyCtors = new Map<MemMode, { new(): Memory }>([221['recentlyUsedByPrefix', PrefixMemory],222['recentlyUsed', LRUMemory],223['first', NoMemory]224]);225226private static readonly _storagePrefix = 'suggest/memories';227228readonly _serviceBrand: undefined;229230231private readonly _persistSoon: RunOnceScheduler;232private readonly _disposables = new DisposableStore();233234private _strategy?: Memory;235236constructor(237@IStorageService private readonly _storageService: IStorageService,238@IConfigurationService private readonly _configService: IConfigurationService,239) {240this._persistSoon = new RunOnceScheduler(() => this._saveState(), 500);241this._disposables.add(_storageService.onWillSaveState(e => {242if (e.reason === WillSaveStateReason.SHUTDOWN) {243this._saveState();244}245}));246}247248dispose(): void {249this._disposables.dispose();250this._persistSoon.dispose();251}252253memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {254this._withStrategy(model, pos).memorize(model, pos, item);255this._persistSoon.schedule();256}257258select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {259return this._withStrategy(model, pos).select(model, pos, items);260}261262private _withStrategy(model: ITextModel, pos: IPosition): Memory {263264const mode = this._configService.getValue<MemMode>('editor.suggestSelection', {265overrideIdentifier: model.getLanguageIdAtPosition(pos.lineNumber, pos.column),266resource: model.uri267});268269if (this._strategy?.name !== mode) {270271this._saveState();272const ctor = SuggestMemoryService._strategyCtors.get(mode) || NoMemory;273this._strategy = new ctor();274275try {276const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');277const scope = share ? StorageScope.PROFILE : StorageScope.WORKSPACE;278const raw = this._storageService.get(`${SuggestMemoryService._storagePrefix}/${mode}`, scope);279if (raw) {280this._strategy.fromJSON(JSON.parse(raw));281}282} catch (e) {283// things can go wrong with JSON...284}285}286287return this._strategy;288}289290private _saveState() {291if (this._strategy) {292const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');293const scope = share ? StorageScope.PROFILE : StorageScope.WORKSPACE;294const raw = JSON.stringify(this._strategy);295this._storageService.store(`${SuggestMemoryService._storagePrefix}/${this._strategy.name}`, raw, scope, StorageTarget.MACHINE);296}297}298}299300301export const ISuggestMemoryService = createDecorator<ISuggestMemoryService>('ISuggestMemories');302303export interface ISuggestMemoryService {304readonly _serviceBrand: undefined;305memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;306select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number;307}308309registerSingleton(ISuggestMemoryService, SuggestMemoryService, InstantiationType.Delayed);310311312