monaco-editor
Version:
A browser based code editor
242 lines (241 loc) • 10.8 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertNever } from '../../../../base/common/assert.js';
import { DeferredPromise } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { SetMap } from '../../../../base/common/map.js';
import { onUnexpectedExternalError } from '../../../../base/common/errors.js';
import { Range } from '../../../common/core/range.js';
import { fixBracketsInLine } from '../../../common/model/bracketPairsTextModelPart/fixBrackets.js';
import { getReadonlyEmptyArray } from './utils.js';
import { SnippetParser, Text } from '../../snippet/browser/snippetParser.js';
export async function provideInlineCompletions(registry, position, model, context, token = CancellationToken.None, languageConfigurationService) {
// Important: Don't use position after the await calls, as the model could have been changed in the meantime!
const defaultReplaceRange = getDefaultRange(position, model);
const providers = registry.all(model);
const multiMap = new SetMap();
for (const provider of providers) {
if (provider.groupId) {
multiMap.add(provider.groupId, provider);
}
}
function getPreferredProviders(provider) {
if (!provider.yieldsToGroupIds) {
return [];
}
const result = [];
for (const groupId of provider.yieldsToGroupIds || []) {
const providers = multiMap.get(groupId);
for (const p of providers) {
result.push(p);
}
}
return result;
}
const states = new Map();
const seen = new Set();
function findPreferredProviderCircle(provider, stack) {
stack = [...stack, provider];
if (seen.has(provider)) {
return stack;
}
seen.add(provider);
try {
const preferred = getPreferredProviders(provider);
for (const p of preferred) {
const c = findPreferredProviderCircle(p, stack);
if (c) {
return c;
}
}
}
finally {
seen.delete(provider);
}
return undefined;
}
function processProvider(provider) {
const state = states.get(provider);
if (state) {
return state;
}
const circle = findPreferredProviderCircle(provider, []);
if (circle) {
onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected. Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`));
}
const deferredPromise = new DeferredPromise();
states.set(provider, deferredPromise.p);
(async () => {
if (!circle) {
const preferred = getPreferredProviders(provider);
for (const p of preferred) {
const result = await processProvider(p);
if (result && result.items.length > 0) {
// Skip provider
return undefined;
}
}
}
try {
const completions = await provider.provideInlineCompletions(model, position, context, token);
return completions;
}
catch (e) {
onUnexpectedExternalError(e);
return undefined;
}
})().then(c => deferredPromise.complete(c), e => deferredPromise.error(e));
return deferredPromise.p;
}
const providerResults = await Promise.all(providers.map(async (provider) => ({ provider, completions: await processProvider(provider) })));
const itemsByHash = new Map();
const lists = [];
for (const result of providerResults) {
const completions = result.completions;
if (!completions) {
continue;
}
const list = new InlineCompletionList(completions, result.provider);
lists.push(list);
for (const item of completions.items) {
const inlineCompletionItem = InlineCompletionItem.from(item, list, defaultReplaceRange, model, languageConfigurationService);
itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem);
}
}
return new InlineCompletionProviderResult(Array.from(itemsByHash.values()), new Set(itemsByHash.keys()), lists);
}
export class InlineCompletionProviderResult {
constructor(
/**
* Free of duplicates.
*/
completions, hashs, providerResults) {
this.completions = completions;
this.hashs = hashs;
this.providerResults = providerResults;
}
has(item) {
return this.hashs.has(item.hash());
}
dispose() {
for (const result of this.providerResults) {
result.removeRef();
}
}
}
/**
* A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that
* computed them.
*/
export class InlineCompletionList {
constructor(inlineCompletions, provider) {
this.inlineCompletions = inlineCompletions;
this.provider = provider;
this.refCount = 1;
}
addRef() {
this.refCount++;
}
removeRef() {
this.refCount--;
if (this.refCount === 0) {
this.provider.freeInlineCompletions(this.inlineCompletions);
}
}
}
export class InlineCompletionItem {
static from(inlineCompletion, source, defaultReplaceRange, textModel, languageConfigurationService) {
let insertText;
let snippetInfo;
let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange;
if (typeof inlineCompletion.insertText === 'string') {
insertText = inlineCompletion.insertText;
if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
insertText = closeBrackets(insertText, range.getStartPosition(), textModel, languageConfigurationService);
// Modify range depending on if brackets are added or removed
const diff = insertText.length - inlineCompletion.insertText.length;
if (diff !== 0) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
}
}
snippetInfo = undefined;
}
else if ('snippet' in inlineCompletion.insertText) {
const preBracketCompletionLength = inlineCompletion.insertText.snippet.length;
if (languageConfigurationService && inlineCompletion.completeBracketPairs) {
inlineCompletion.insertText.snippet = closeBrackets(inlineCompletion.insertText.snippet, range.getStartPosition(), textModel, languageConfigurationService);
// Modify range depending on if brackets are added or removed
const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength;
if (diff !== 0) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff);
}
}
const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet);
if (snippet.children.length === 1 && snippet.children[0] instanceof Text) {
insertText = snippet.children[0].value;
snippetInfo = undefined;
}
else {
insertText = snippet.toString();
snippetInfo = {
snippet: inlineCompletion.insertText.snippet,
range: range
};
}
}
else {
assertNever(inlineCompletion.insertText);
}
return new InlineCompletionItem(insertText, inlineCompletion.command, range, insertText, snippetInfo, inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), inlineCompletion, source);
}
constructor(filterText, command, range, insertText, snippetInfo, additionalTextEdits,
/**
* A reference to the original inline completion this inline completion has been constructed from.
* Used for event data to ensure referential equality.
*/
sourceInlineCompletion,
/**
* A reference to the original inline completion list this inline completion has been constructed from.
* Used for event data to ensure referential equality.
*/
source) {
this.filterText = filterText;
this.command = command;
this.range = range;
this.insertText = insertText;
this.snippetInfo = snippetInfo;
this.additionalTextEdits = additionalTextEdits;
this.sourceInlineCompletion = sourceInlineCompletion;
this.source = source;
filterText = filterText.replace(/\r\n|\r/g, '\n');
insertText = filterText.replace(/\r\n|\r/g, '\n');
}
withRange(updatedRange) {
return new InlineCompletionItem(this.filterText, this.command, updatedRange, this.insertText, this.snippetInfo, this.additionalTextEdits, this.sourceInlineCompletion, this.source);
}
hash() {
return JSON.stringify({ insertText: this.insertText, range: this.range.toString() });
}
}
function getDefaultRange(position, model) {
const word = model.getWordAtPosition(position);
const maxColumn = model.getLineMaxColumn(position.lineNumber);
// By default, always replace up until the end of the current line.
// This default might be subject to change!
return word
? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn)
: Range.fromPositions(position, position.with(undefined, maxColumn));
}
function closeBrackets(text, position, model, languageConfigurationService) {
const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1);
const newLine = lineStart + text;
const newTokens = model.tokenization.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text);
const slicedTokens = newTokens === null || newTokens === void 0 ? void 0 : newTokens.sliceAndInflate(position.column - 1, newLine.length, 0);
if (!slicedTokens) {
return text;
}
const newText = fixBracketsInLine(slicedTokens, languageConfigurationService);
return newText;
}