monaco-editor-core
Version:
A browser based code editor
191 lines (190 loc) • 10.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { FuzzyScore } from '../../../../base/common/filters.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { Disposable, RefCountedDisposable } from '../../../../base/common/lifecycle.js';
import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';
import { Range } from '../../../common/core/range.js';
import { registerEditorFeature } from '../../../common/editorFeatures.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { CompletionModel, LineContext } from './completionModel.js';
import { CompletionOptions, provideSuggestionItems, QuickSuggestionsOptions } from './suggest.js';
import { ISuggestMemoryService } from './suggestMemory.js';
import { SuggestModel } from './suggestModel.js';
import { WordDistance } from './wordDistance.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
class SuggestInlineCompletion {
constructor(range, insertText, filterText, additionalTextEdits, command, completion) {
this.range = range;
this.insertText = insertText;
this.filterText = filterText;
this.additionalTextEdits = additionalTextEdits;
this.command = command;
this.completion = completion;
}
}
let InlineCompletionResults = class InlineCompletionResults extends RefCountedDisposable {
constructor(model, line, word, completionModel, completions, _suggestMemoryService) {
super(completions.disposable);
this.model = model;
this.line = line;
this.word = word;
this.completionModel = completionModel;
this._suggestMemoryService = _suggestMemoryService;
}
canBeReused(model, line, word) {
return this.model === model // same model
&& this.line === line
&& this.word.word.length > 0
&& this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word
&& this.completionModel.getIncompleteProvider().size === 0; // no incomplete results
}
get items() {
const result = [];
// Split items by preselected index. This ensures the memory-selected item shows first and that better/worst
// ranked items are before/after
const { items } = this.completionModel;
const selectedIndex = this._suggestMemoryService.select(this.model, { lineNumber: this.line, column: this.word.endColumn + this.completionModel.lineContext.characterCountDelta }, items);
const first = Iterable.slice(items, selectedIndex);
const second = Iterable.slice(items, 0, selectedIndex);
let resolveCount = 5;
for (const item of Iterable.concat(first, second)) {
if (item.score === FuzzyScore.Default) {
// skip items that have no overlap
continue;
}
const range = new Range(item.editStart.lineNumber, item.editStart.column, item.editInsertEnd.lineNumber, item.editInsertEnd.column + this.completionModel.lineContext.characterCountDelta // end PLUS character delta
);
const insertText = item.completion.insertTextRules && (item.completion.insertTextRules & 4 /* CompletionItemInsertTextRule.InsertAsSnippet */)
? { snippet: item.completion.insertText }
: item.completion.insertText;
result.push(new SuggestInlineCompletion(range, insertText, item.filterTextLow ?? item.labelLow, item.completion.additionalTextEdits, item.completion.command, item));
// resolve the first N suggestions eagerly
if (resolveCount-- >= 0) {
item.resolve(CancellationToken.None);
}
}
return result;
}
};
InlineCompletionResults = __decorate([
__param(5, ISuggestMemoryService)
], InlineCompletionResults);
let SuggestInlineCompletions = class SuggestInlineCompletions extends Disposable {
constructor(_languageFeatureService, _clipboardService, _suggestMemoryService, _editorService) {
super();
this._languageFeatureService = _languageFeatureService;
this._clipboardService = _clipboardService;
this._suggestMemoryService = _suggestMemoryService;
this._editorService = _editorService;
this._store.add(_languageFeatureService.inlineCompletionsProvider.register('*', this));
}
async provideInlineCompletions(model, position, context, token) {
if (context.selectedSuggestionInfo) {
return;
}
let editor;
for (const candidate of this._editorService.listCodeEditors()) {
if (candidate.getModel() === model) {
editor = candidate;
break;
}
}
if (!editor) {
return;
}
const config = editor.getOption(90 /* EditorOption.quickSuggestions */);
if (QuickSuggestionsOptions.isAllOff(config)) {
// quick suggest is off (for this model/language)
return;
}
model.tokenization.tokenizeIfCheap(position.lineNumber);
const lineTokens = model.tokenization.getLineTokens(position.lineNumber);
const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(position.column - 1 - 1, 0)));
if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'inline') {
// quick suggest is off (for this token)
return undefined;
}
// We consider non-empty leading words and trigger characters. The latter only
// when no word is being typed (word characters superseed trigger characters)
let wordInfo = model.getWordAtPosition(position);
let triggerCharacterInfo;
if (!wordInfo?.word) {
triggerCharacterInfo = this._getTriggerCharacterInfo(model, position);
}
if (!wordInfo?.word && !triggerCharacterInfo) {
// not at word, not a trigger character
return;
}
// ensure that we have word information and that we are at the end of a word
// otherwise we stop because we don't want to do quick suggestions inside words
if (!wordInfo) {
wordInfo = model.getWordUntilPosition(position);
}
if (wordInfo.endColumn !== position.column) {
return;
}
let result;
const leadingLineContents = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column));
if (!triggerCharacterInfo && this._lastResult?.canBeReused(model, position.lineNumber, wordInfo)) {
// reuse a previous result iff possible, only a refilter is needed
// TODO@jrieken this can be improved further and only incomplete results can be updated
// console.log(`REUSE with ${wordInfo.word}`);
const newLineContext = new LineContext(leadingLineContents, position.column - this._lastResult.word.endColumn);
this._lastResult.completionModel.lineContext = newLineContext;
this._lastResult.acquire();
result = this._lastResult;
}
else {
// refesh model is required
const completions = await provideSuggestionItems(this._languageFeatureService.completionProvider, model, position, new CompletionOptions(undefined, SuggestModel.createSuggestFilter(editor).itemKind, triggerCharacterInfo?.providers), triggerCharacterInfo && { triggerKind: 1 /* CompletionTriggerKind.TriggerCharacter */, triggerCharacter: triggerCharacterInfo.ch }, token);
let clipboardText;
if (completions.needsClipboard) {
clipboardText = await this._clipboardService.readText();
}
const completionModel = new CompletionModel(completions.items, position.column, new LineContext(leadingLineContents, 0), WordDistance.None, editor.getOption(119 /* EditorOption.suggest */), editor.getOption(113 /* EditorOption.snippetSuggestions */), { boostFullMatch: false, firstMatchCanBeWeak: false }, clipboardText);
result = new InlineCompletionResults(model, position.lineNumber, wordInfo, completionModel, completions, this._suggestMemoryService);
}
this._lastResult = result;
return result;
}
handleItemDidShow(_completions, item) {
item.completion.resolve(CancellationToken.None);
}
freeInlineCompletions(result) {
result.release();
}
_getTriggerCharacterInfo(model, position) {
const ch = model.getValueInRange(Range.fromPositions({ lineNumber: position.lineNumber, column: position.column - 1 }, position));
const providers = new Set();
for (const provider of this._languageFeatureService.completionProvider.all(model)) {
if (provider.triggerCharacters?.includes(ch)) {
providers.add(provider);
}
}
if (providers.size === 0) {
return undefined;
}
return { providers, ch };
}
};
SuggestInlineCompletions = __decorate([
__param(0, ILanguageFeaturesService),
__param(1, IClipboardService),
__param(2, ISuggestMemoryService),
__param(3, ICodeEditorService)
], SuggestInlineCompletions);
export { SuggestInlineCompletions };
registerEditorFeature(SuggestInlineCompletions);