/*--------------------------------------------------------------------------------------------- * 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); } }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { createCancelablePromise, RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { CoreEditingCommands } from '../../../browser/coreCommands.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Range } from '../../../common/core/range.js'; import { InlineCompletionTriggerKind } from '../../../common/languages.js'; import { BaseGhostTextWidgetModel, GhostTextReplacement } from './ghostText.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { inlineSuggestCommitId } from './consts.js'; import { inlineCompletionToGhostText } from './inlineCompletionToGhostText.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import { fixBracketsInLine } from '../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { SnippetParser } from '../../snippet/browser/snippetParser.js'; import { SnippetController2 } from '../../snippet/browser/snippetController2.js'; import { assertNever } from '../../../../base/common/types.js'; import { matchesSubString } from '../../../../base/common/filters.js'; import { getReadonlyEmptyArray } from './utils.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; let InlineCompletionsModel = class InlineCompletionsModel extends Disposable { constructor(editor, cache, commandService, languageConfigurationService, languageFeaturesService, debounceService, configurationService) { super(); this.editor = editor; this.cache = cache; this.commandService = commandService; this.languageConfigurationService = languageConfigurationService; this.languageFeaturesService = languageFeaturesService; this.debounceService = debounceService; this.onDidChangeEmitter = new Emitter(); this.onDidChange = this.onDidChangeEmitter.event; this.completionSession = this._register(new MutableDisposable()); this.active = false; this.disposed = false; this.debounceValue = this.debounceService.for(this.languageFeaturesService.inlineCompletionsProvider, 'InlineCompletionsDebounce', { min: 50, max: 50 }); this._register(commandService.onDidExecuteCommand((e) => { // These commands don't trigger onDidType. const commands = new Set([ CoreEditingCommands.Tab.id, CoreEditingCommands.DeleteLeft.id, CoreEditingCommands.DeleteRight.id, inlineSuggestCommitId, 'acceptSelectedSuggestion', ]); if (commands.has(e.commandId) && editor.hasTextFocus()) { this.handleUserInput(); } })); this._register(this.editor.onDidType((e) => { this.handleUserInput(); })); this._register(this.editor.onDidChangeCursorPosition((e) => { if (e.reason === 3 /* CursorChangeReason.Explicit */ || this.session && !this.session.isValid) { this.hide(); } })); this._register(toDisposable(() => { this.disposed = true; })); this._register(this.editor.onDidBlurEditorWidget(() => { // This is a hidden setting very useful for debugging if (configurationService.getValue('editor.inlineSuggest.hideOnBlur')) { return; } this.hide(); })); } handleUserInput() { if (this.session && !this.session.isValid) { this.hide(); } setTimeout(() => { if (this.disposed) { return; } // Wait for the cursor update that happens in the same iteration loop iteration this.startSessionIfTriggered(); }, 0); } get session() { return this.completionSession.value; } get ghostText() { var _a; return (_a = this.session) === null || _a === void 0 ? void 0 : _a.ghostText; } get minReservedLineCount() { return this.session ? this.session.minReservedLineCount : 0; } setExpanded(expanded) { var _a; (_a = this.session) === null || _a === void 0 ? void 0 : _a.setExpanded(expanded); } setActive(active) { var _a; this.active = active; if (active) { (_a = this.session) === null || _a === void 0 ? void 0 : _a.scheduleAutomaticUpdate(); } } startSessionIfTriggered() { const suggestOptions = this.editor.getOption(57 /* EditorOption.inlineSuggest */); if (!suggestOptions.enabled) { return; } if (this.session && this.session.isValid) { return; } this.trigger(InlineCompletionTriggerKind.Automatic); } trigger(triggerKind) { if (this.completionSession.value) { if (triggerKind === InlineCompletionTriggerKind.Explicit) { void this.completionSession.value.ensureUpdateWithExplicitContext(); } return; } this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService, this.cache, triggerKind, this.languageConfigurationService, this.languageFeaturesService.inlineCompletionsProvider, this.debounceValue); this.completionSession.value.takeOwnership(this.completionSession.value.onDidChange(() => { this.onDidChangeEmitter.fire(); })); } hide() { this.completionSession.clear(); this.onDidChangeEmitter.fire(); } commitCurrentSuggestion() { var _a; // Don't dispose the session, so that after committing, more suggestions are shown. (_a = this.session) === null || _a === void 0 ? void 0 : _a.commitCurrentCompletion(); } showNext() { var _a; (_a = this.session) === null || _a === void 0 ? void 0 : _a.showNextInlineCompletion(); } showPrevious() { var _a; (_a = this.session) === null || _a === void 0 ? void 0 : _a.showPreviousInlineCompletion(); } hasMultipleInlineCompletions() { var _a; return __awaiter(this, void 0, void 0, function* () { const result = yield ((_a = this.session) === null || _a === void 0 ? void 0 : _a.hasMultipleInlineCompletions()); return result !== undefined ? result : false; }); } }; InlineCompletionsModel = __decorate([ __param(2, ICommandService), __param(3, ILanguageConfigurationService), __param(4, ILanguageFeaturesService), __param(5, ILanguageFeatureDebounceService), __param(6, IConfigurationService) ], InlineCompletionsModel); export { InlineCompletionsModel }; export class InlineCompletionsSession extends BaseGhostTextWidgetModel { constructor(editor, triggerPosition, shouldUpdate, commandService, cache, initialTriggerKind, languageConfigurationService, registry, debounce) { super(editor); this.triggerPosition = triggerPosition; this.shouldUpdate = shouldUpdate; this.commandService = commandService; this.cache = cache; this.initialTriggerKind = initialTriggerKind; this.languageConfigurationService = languageConfigurationService; this.registry = registry; this.debounce = debounce; this.minReservedLineCount = 0; this.updateOperation = this._register(new MutableDisposable()); this.updateSoon = this._register(new RunOnceScheduler(() => { const triggerKind = this.initialTriggerKind; // All subsequent triggers are automatic. this.initialTriggerKind = InlineCompletionTriggerKind.Automatic; return this.update(triggerKind); }, 50)); this.filteredCompletions = []; //#region Selection // We use a semantic id to track the selection even if the cache changes. this.currentlySelectedCompletionId = undefined; let lastCompletionItem = undefined; this._register(this.onDidChange(() => { var _a; const currentCompletion = this.currentCompletion; if (currentCompletion && currentCompletion.sourceInlineCompletion !== lastCompletionItem) { lastCompletionItem = currentCompletion.sourceInlineCompletion; const provider = currentCompletion.sourceProvider; (_a = provider.handleItemDidShow) === null || _a === void 0 ? void 0 : _a.call(provider, currentCompletion.sourceInlineCompletions, lastCompletionItem); } })); this._register(toDisposable(() => { this.cache.clear(); })); this._register(this.editor.onDidChangeCursorPosition((e) => { var _a; if (e.reason === 3 /* CursorChangeReason.Explicit */) { return; } // Ghost text depends on the cursor position (_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.updateRanges(); if (this.cache.value) { this.updateFilteredInlineCompletions(); this.onDidChangeEmitter.fire(); } })); this._register(this.editor.onDidChangeModelContent((e) => { var _a; // Call this in case `onDidChangeModelContent` calls us first. (_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.updateRanges(); this.updateFilteredInlineCompletions(); this.scheduleAutomaticUpdate(); })); this._register(this.registry.onDidChange(() => { this.updateSoon.schedule(this.debounce.get(this.editor.getModel())); })); this.scheduleAutomaticUpdate(); } updateFilteredInlineCompletions() { if (!this.cache.value) { this.filteredCompletions = []; return; } const model = this.editor.getModel(); const cursorPosition = model.validatePosition(this.editor.getPosition()); this.filteredCompletions = this.cache.value.completions.filter(c => { const originalValue = model.getValueInRange(c.synchronizedRange).toLowerCase(); const filterText = c.inlineCompletion.filterText.toLowerCase(); const indent = model.getLineIndentColumn(c.synchronizedRange.startLineNumber); const cursorPosIndex = Math.max(0, cursorPosition.column - c.synchronizedRange.startColumn); let filterTextBefore = filterText.substring(0, cursorPosIndex); let filterTextAfter = filterText.substring(cursorPosIndex); let originalValueBefore = originalValue.substring(0, cursorPosIndex); let originalValueAfter = originalValue.substring(cursorPosIndex); if (c.synchronizedRange.startColumn <= indent) { // Remove indentation originalValueBefore = originalValueBefore.trimStart(); if (originalValueBefore.length === 0) { originalValueAfter = originalValueAfter.trimStart(); } filterTextBefore = filterTextBefore.trimStart(); if (filterTextBefore.length === 0) { filterTextAfter = filterTextAfter.trimStart(); } } return filterTextBefore.startsWith(originalValueBefore) && matchesSubString(originalValueAfter, filterTextAfter); }); } fixAndGetIndexOfCurrentSelection() { if (!this.currentlySelectedCompletionId || !this.cache.value) { return 0; } if (this.cache.value.completions.length === 0) { // don't reset the selection in this case return 0; } const idx = this.filteredCompletions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId); if (idx === -1) { // Reset the selection so that the selection does not jump back when it appears again this.currentlySelectedCompletionId = undefined; return 0; } return idx; } get currentCachedCompletion() { if (!this.cache.value) { return undefined; } return this.filteredCompletions[this.fixAndGetIndexOfCurrentSelection()]; } showNextInlineCompletion() { return __awaiter(this, void 0, void 0, function* () { yield this.ensureUpdateWithExplicitContext(); const completions = this.filteredCompletions || []; if (completions.length > 0) { const newIdx = (this.fixAndGetIndexOfCurrentSelection() + 1) % completions.length; this.currentlySelectedCompletionId = completions[newIdx].semanticId; } else { this.currentlySelectedCompletionId = undefined; } this.onDidChangeEmitter.fire(); }); } showPreviousInlineCompletion() { return __awaiter(this, void 0, void 0, function* () { yield this.ensureUpdateWithExplicitContext(); const completions = this.filteredCompletions || []; if (completions.length > 0) { const newIdx = (this.fixAndGetIndexOfCurrentSelection() + completions.length - 1) % completions.length; this.currentlySelectedCompletionId = completions[newIdx].semanticId; } else { this.currentlySelectedCompletionId = undefined; } this.onDidChangeEmitter.fire(); }); } ensureUpdateWithExplicitContext() { var _a; return __awaiter(this, void 0, void 0, function* () { if (this.updateOperation.value) { // Restart or wait for current update operation if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) { yield this.updateOperation.value.promise; } else { yield this.update(InlineCompletionTriggerKind.Explicit); } } else if (((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.triggerKind) !== InlineCompletionTriggerKind.Explicit) { // Refresh cache yield this.update(InlineCompletionTriggerKind.Explicit); } }); } hasMultipleInlineCompletions() { var _a; return __awaiter(this, void 0, void 0, function* () { yield this.ensureUpdateWithExplicitContext(); return (((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.completions.length) || 0) > 1; }); } //#endregion get ghostText() { const currentCompletion = this.currentCompletion; if (!currentCompletion) { return undefined; } const cursorPosition = this.editor.getPosition(); if (currentCompletion.range.getEndPosition().isBefore(cursorPosition)) { return undefined; } const mode = this.editor.getOptions().get(57 /* EditorOption.inlineSuggest */).mode; const ghostText = inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, cursorPosition); if (ghostText) { if (ghostText.isEmpty()) { return undefined; } return ghostText; } return new GhostTextReplacement(currentCompletion.range.startLineNumber, currentCompletion.range.startColumn, currentCompletion.range.endColumn - currentCompletion.range.startColumn, currentCompletion.insertText.split('\n'), 0); } get currentCompletion() { const completion = this.currentCachedCompletion; if (!completion) { return undefined; } return completion.toLiveInlineCompletion(); } get isValid() { return this.editor.getPosition().lineNumber === this.triggerPosition.lineNumber; } scheduleAutomaticUpdate() { // Since updateSoon debounces, starvation can happen. // To prevent stale cache, we clear the current update operation. this.updateOperation.clear(); this.updateSoon.schedule(this.debounce.get(this.editor.getModel())); } update(triggerKind) { return __awaiter(this, void 0, void 0, function* () { if (!this.shouldUpdate()) { return; } const position = this.editor.getPosition(); const startTime = new Date(); const promise = createCancelablePromise((token) => __awaiter(this, void 0, void 0, function* () { let result; try { result = yield provideInlineCompletions(this.registry, position, this.editor.getModel(), { triggerKind, selectedSuggestionInfo: undefined }, token, this.languageConfigurationService); const endTime = new Date(); this.debounce.update(this.editor.getModel(), endTime.getTime() - startTime.getTime()); } catch (e) { onUnexpectedError(e); return; } if (token.isCancellationRequested) { return; } this.cache.setValue(this.editor, result, triggerKind); this.updateFilteredInlineCompletions(); this.onDidChangeEmitter.fire(); })); const operation = new UpdateOperation(promise, triggerKind); this.updateOperation.value = operation; yield promise; if (this.updateOperation.value === operation) { this.updateOperation.clear(); } }); } takeOwnership(disposable) { this._register(disposable); } commitCurrentCompletion() { const ghostText = this.ghostText; if (!ghostText) { // No ghost text was shown for this completion. // Thus, we don't want to commit anything. return; } const completion = this.currentCompletion; if (completion) { this.commit(completion); } } commit(completion) { var _a; // Mark the cache as stale, but don't dispose it yet, // otherwise command args might get disposed. const cache = this.cache.clearAndLeak(); if (completion.snippetInfo) { this.editor.executeEdits('inlineSuggestion.accept', [ EditOperation.replaceMove(completion.range, ''), ...completion.additionalTextEdits ]); this.editor.setPosition(completion.snippetInfo.range.getStartPosition()); (_a = SnippetController2.get(this.editor)) === null || _a === void 0 ? void 0 : _a.insert(completion.snippetInfo.snippet); } else { this.editor.executeEdits('inlineSuggestion.accept', [ EditOperation.replaceMove(completion.range, completion.insertText), ...completion.additionalTextEdits ]); } if (completion.command) { this.commandService .executeCommand(completion.command.id, ...(completion.command.arguments || [])) .finally(() => { cache === null || cache === void 0 ? void 0 : cache.dispose(); }) .then(undefined, onUnexpectedExternalError); } else { cache === null || cache === void 0 ? void 0 : cache.dispose(); } this.onDidChangeEmitter.fire(); } get commands() { var _a; const lists = new Set(((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.completions.map(c => c.inlineCompletion.sourceInlineCompletions)) || []); return [...lists].flatMap(l => l.commands || []); } } export class UpdateOperation { constructor(promise, triggerKind) { this.promise = promise; this.triggerKind = triggerKind; } dispose() { this.promise.cancel(); } } /** * The cache keeps itself in sync with the editor. * It also owns the completions result and disposes it when the cache is diposed. */ export class SynchronizedInlineCompletionsCache extends Disposable { constructor(completionsSource, editor, onChange, triggerKind) { super(); this.editor = editor; this.onChange = onChange; this.triggerKind = triggerKind; this.isDisposing = false; const decorationIds = editor.changeDecorations((changeAccessor) => { return changeAccessor.deltaDecorations([], completionsSource.items.map(i => ({ range: i.range, options: { description: 'inline-completion-tracking-range' }, }))); }); this._register(toDisposable(() => { this.isDisposing = true; editor.removeDecorations(decorationIds); })); this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx])); this._register(editor.onDidChangeModelContent(() => { this.updateRanges(); })); this._register(completionsSource); } updateRanges() { if (this.isDisposing) { return; } let hasChanged = false; const model = this.editor.getModel(); for (const c of this.completions) { const newRange = model.getDecorationRange(c.decorationId); if (!newRange) { onUnexpectedError(new Error('Decoration has no range')); continue; } if (!c.synchronizedRange.equalsRange(newRange)) { hasChanged = true; c.synchronizedRange = newRange; } } if (hasChanged) { this.onChange(); } } } class CachedInlineCompletion { constructor(inlineCompletion, decorationId) { this.inlineCompletion = inlineCompletion; this.decorationId = decorationId; this.semanticId = JSON.stringify({ text: this.inlineCompletion.insertText, abbreviation: this.inlineCompletion.filterText, startLine: this.inlineCompletion.range.startLineNumber, startColumn: this.inlineCompletion.range.startColumn, command: this.inlineCompletion.command }); this.synchronizedRange = inlineCompletion.range; } toLiveInlineCompletion() { return { insertText: this.inlineCompletion.insertText, range: this.synchronizedRange, command: this.inlineCompletion.command, sourceProvider: this.inlineCompletion.sourceProvider, sourceInlineCompletions: this.inlineCompletion.sourceInlineCompletions, sourceInlineCompletion: this.inlineCompletion.sourceInlineCompletion, snippetInfo: this.inlineCompletion.snippetInfo, filterText: this.inlineCompletion.filterText, additionalTextEdits: this.inlineCompletion.additionalTextEdits, }; } } export function provideInlineCompletions(registry, position, model, context, token = CancellationToken.None, languageConfigurationService) { return __awaiter(this, void 0, void 0, function* () { const defaultReplaceRange = getDefaultRange(position, model); const providers = registry.all(model); const results = yield Promise.all(providers.map((provider) => __awaiter(this, void 0, void 0, function* () { const completions = yield Promise.resolve(provider.provideInlineCompletions(model, position, context, token)).catch(onUnexpectedExternalError); return ({ completions, provider, dispose: () => { if (completions) { provider.freeInlineCompletions(completions); } } }); }))); const itemsByHash = new Map(); for (const result of results) { const completions = result.completions; if (!completions) { continue; } for (const item of completions.items) { let range = item.range ? Range.lift(item.range) : defaultReplaceRange; if (range.startLineNumber !== range.endLineNumber) { // Ignore invalid ranges. continue; } let insertText; let snippetInfo; if (typeof item.insertText === 'string') { insertText = item.insertText; if (languageConfigurationService && item.completeBracketPairs) { insertText = closeBrackets(insertText, range.getStartPosition(), model, languageConfigurationService); // Modify range depending on if brackets are added or removed const diff = insertText.length - item.insertText.length; if (diff !== 0) { range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); } } snippetInfo = undefined; } else if ('snippet' in item.insertText) { const snippet = new SnippetParser().parse(item.insertText.snippet); insertText = snippet.toString(); snippetInfo = { snippet: item.insertText.snippet, range: range }; } else { assertNever(item.insertText); } const trackedItem = ({ insertText, snippetInfo, range, command: item.command, sourceProvider: result.provider, sourceInlineCompletions: completions, sourceInlineCompletion: item, filterText: item.filterText || insertText, additionalTextEdits: item.additionalTextEdits || getReadonlyEmptyArray() }); itemsByHash.set(JSON.stringify({ insertText, range: item.range }), trackedItem); } } return { items: [...itemsByHash.values()], dispose: () => { for (const result of results) { result.dispose(); } }, }; }); } 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; }