UNPKG

monaco-editor

Version:
242 lines (241 loc) • 10.8 kB
/*--------------------------------------------------------------------------------------------- * 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; }