mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-22 04:40:30 -04:00
301 lines
8.9 KiB
TypeScript
301 lines
8.9 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
import { LRUCache, TernarySearchTree } from 'vs/base/common/map';
|
|
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
|
import { ITextModel } from 'vs/editor/common/model';
|
|
import { IPosition } from 'vs/editor/common/core/position';
|
|
import { CompletionItemKind, completionKindFromString } from 'vs/editor/common/modes';
|
|
import { Disposable } from 'vs/base/common/lifecycle';
|
|
import { RunOnceScheduler } from 'vs/base/common/async';
|
|
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
|
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
|
|
|
|
export abstract class Memory {
|
|
|
|
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
|
if (items.length === 0) {
|
|
return 0;
|
|
}
|
|
let topScore = items[0].score[0];
|
|
for (let i = 1; i < items.length; i++) {
|
|
const { score, completion: suggestion } = items[i];
|
|
if (score[0] !== topScore) {
|
|
// stop when leaving the group of top matches
|
|
break;
|
|
}
|
|
if (suggestion.preselect) {
|
|
// stop when seeing an auto-select-item
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
abstract memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
|
|
|
|
abstract toJSON(): object | undefined;
|
|
|
|
abstract fromJSON(data: object): void;
|
|
}
|
|
|
|
export class NoMemory extends Memory {
|
|
|
|
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
|
// no-op
|
|
}
|
|
|
|
toJSON() {
|
|
return undefined;
|
|
}
|
|
|
|
fromJSON() {
|
|
//
|
|
}
|
|
}
|
|
|
|
export interface MemItem {
|
|
type: string | CompletionItemKind;
|
|
insertText: string;
|
|
touch: number;
|
|
}
|
|
|
|
export class LRUMemory extends Memory {
|
|
|
|
private _cache = new LRUCache<string, MemItem>(300, 0.66);
|
|
private _seq = 0;
|
|
|
|
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
|
const { label } = item.completion;
|
|
const key = `${model.getLanguageIdentifier().language}/${label}`;
|
|
this._cache.set(key, {
|
|
touch: this._seq++,
|
|
type: item.completion.kind,
|
|
insertText: item.completion.insertText
|
|
});
|
|
}
|
|
|
|
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
|
|
|
if (items.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const lineSuffix = model.getLineContent(pos.lineNumber).substr(pos.column - 10, pos.column - 1);
|
|
if (/\s$/.test(lineSuffix)) {
|
|
return super.select(model, pos, items);
|
|
}
|
|
|
|
let topScore = items[0].score[0];
|
|
let indexPreselect = -1;
|
|
let indexRecency = -1;
|
|
let seq = -1;
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (items[i].score[0] !== topScore) {
|
|
// consider only top items
|
|
break;
|
|
}
|
|
const key = `${model.getLanguageIdentifier().language}/${items[i].completion.label}`;
|
|
const item = this._cache.peek(key);
|
|
if (item && item.touch > seq && item.type === items[i].completion.kind && item.insertText === items[i].completion.insertText) {
|
|
seq = item.touch;
|
|
indexRecency = i;
|
|
}
|
|
if (items[i].completion.preselect && indexPreselect === -1) {
|
|
// stop when seeing an auto-select-item
|
|
return indexPreselect = i;
|
|
}
|
|
}
|
|
if (indexRecency !== -1) {
|
|
return indexRecency;
|
|
} else if (indexPreselect !== -1) {
|
|
return indexPreselect;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
toJSON(): object {
|
|
let data: [string, MemItem][] = [];
|
|
this._cache.forEach((value, key) => {
|
|
data.push([key, value]);
|
|
});
|
|
return data;
|
|
}
|
|
|
|
fromJSON(data: [string, MemItem][]): void {
|
|
this._cache.clear();
|
|
let seq = 0;
|
|
for (const [key, value] of data) {
|
|
value.touch = seq;
|
|
value.type = typeof value.type === 'number' ? value.type : completionKindFromString(value.type);
|
|
this._cache.set(key, value);
|
|
}
|
|
this._seq = this._cache.size;
|
|
}
|
|
}
|
|
|
|
|
|
export class PrefixMemory extends Memory {
|
|
|
|
private _trie = TernarySearchTree.forStrings<MemItem>();
|
|
private _seq = 0;
|
|
|
|
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
|
const { word } = model.getWordUntilPosition(pos);
|
|
const key = `${model.getLanguageIdentifier().language}/${word}`;
|
|
this._trie.set(key, {
|
|
type: item.completion.kind,
|
|
insertText: item.completion.insertText,
|
|
touch: this._seq++
|
|
});
|
|
}
|
|
|
|
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
|
let { word } = model.getWordUntilPosition(pos);
|
|
if (!word) {
|
|
return super.select(model, pos, items);
|
|
}
|
|
let key = `${model.getLanguageIdentifier().language}/${word}`;
|
|
let item = this._trie.get(key);
|
|
if (!item) {
|
|
item = this._trie.findSubstr(key);
|
|
}
|
|
if (item) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
let { kind, insertText } = items[i].completion;
|
|
if (kind === item.type && insertText === item.insertText) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return super.select(model, pos, items);
|
|
}
|
|
|
|
toJSON(): object {
|
|
|
|
let entries: [string, MemItem][] = [];
|
|
this._trie.forEach((value, key) => entries.push([key, value]));
|
|
|
|
// sort by last recently used (touch), then
|
|
// take the top 200 item and normalize their
|
|
// touch
|
|
entries
|
|
.sort((a, b) => -(a[1].touch - b[1].touch))
|
|
.forEach((value, i) => value[1].touch = i);
|
|
|
|
return entries.slice(0, 200);
|
|
}
|
|
|
|
fromJSON(data: [string, MemItem][]): void {
|
|
this._trie.clear();
|
|
if (data.length > 0) {
|
|
this._seq = data[0][1].touch + 1;
|
|
for (const [key, value] of data) {
|
|
value.type = typeof value.type === 'number' ? value.type : completionKindFromString(value.type);
|
|
this._trie.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export type MemMode = 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix';
|
|
|
|
export class SuggestMemoryService extends Disposable implements ISuggestMemoryService {
|
|
|
|
readonly _serviceBrand: any;
|
|
|
|
private readonly _storagePrefix = 'suggest/memories';
|
|
|
|
private readonly _persistSoon: RunOnceScheduler;
|
|
private _mode!: MemMode;
|
|
private _shareMem!: boolean;
|
|
private _strategy!: Memory;
|
|
|
|
constructor(
|
|
@IStorageService private readonly _storageService: IStorageService,
|
|
@IConfigurationService private readonly _configService: IConfigurationService,
|
|
) {
|
|
super();
|
|
|
|
const update = () => {
|
|
const mode = this._configService.getValue<MemMode>('editor.suggestSelection');
|
|
const share = this._configService.getValue<boolean>('editor.suggest.shareSuggestSelections');
|
|
this._update(mode, share, false);
|
|
};
|
|
|
|
this._persistSoon = this._register(new RunOnceScheduler(() => this._saveState(), 500));
|
|
this._register(_storageService.onWillSaveState(e => {
|
|
if (e.reason === WillSaveStateReason.SHUTDOWN) {
|
|
this._saveState();
|
|
}
|
|
}));
|
|
|
|
this._register(this._configService.onDidChangeConfiguration(e => {
|
|
if (e.affectsConfiguration('editor.suggestSelection') || e.affectsConfiguration('editor.suggest.shareSuggestSelections')) {
|
|
update();
|
|
}
|
|
}));
|
|
this._register(this._storageService.onDidChangeStorage(e => {
|
|
if (e.scope === StorageScope.GLOBAL && e.key.indexOf(this._storagePrefix) === 0) {
|
|
if (!document.hasFocus()) {
|
|
// windows that aren't focused have to drop their current
|
|
// storage value and accept what's stored now
|
|
this._update(this._mode, this._shareMem, true);
|
|
}
|
|
}
|
|
}));
|
|
update();
|
|
}
|
|
|
|
private _update(mode: MemMode, shareMem: boolean, force: boolean): void {
|
|
if (!force && this._mode === mode && this._shareMem === shareMem) {
|
|
return;
|
|
}
|
|
this._shareMem = shareMem;
|
|
this._mode = mode;
|
|
this._strategy = mode === 'recentlyUsedByPrefix' ? new PrefixMemory() : mode === 'recentlyUsed' ? new LRUMemory() : new NoMemory();
|
|
|
|
try {
|
|
const scope = shareMem ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
|
|
const raw = this._storageService.get(`${this._storagePrefix}/${this._mode}`, scope);
|
|
if (raw) {
|
|
this._strategy.fromJSON(JSON.parse(raw));
|
|
}
|
|
} catch (e) {
|
|
// things can go wrong with JSON...
|
|
}
|
|
}
|
|
|
|
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void {
|
|
this._strategy.memorize(model, pos, item);
|
|
this._persistSoon.schedule();
|
|
}
|
|
|
|
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number {
|
|
return this._strategy.select(model, pos, items);
|
|
}
|
|
|
|
private _saveState() {
|
|
const raw = JSON.stringify(this._strategy);
|
|
const scope = this._shareMem ? StorageScope.GLOBAL : StorageScope.WORKSPACE;
|
|
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, scope);
|
|
}
|
|
}
|
|
|
|
|
|
export const ISuggestMemoryService = createDecorator<ISuggestMemoryService>('ISuggestMemories');
|
|
|
|
export interface ISuggestMemoryService {
|
|
_serviceBrand: any;
|
|
memorize(model: ITextModel, pos: IPosition, item: CompletionItem): void;
|
|
select(model: ITextModel, pos: IPosition, items: CompletionItem[]): number;
|
|
}
|
|
|
|
registerSingleton(ISuggestMemoryService, SuggestMemoryService, true);
|