diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index b41762c603a..308c453e528 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -904,8 +904,17 @@ export interface InlineCompletionsProvider): void; + /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ @@ -930,6 +939,22 @@ export interface InlineCompletionsProvider = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; // User did an explicit action to accept +} | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; // User did an explicit action to reject +} | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: TInlineCompletion; + userTypingDisagreed: boolean; +}; + export interface CodeAction { title: string; command?: Command; diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 481bae63854..593e5194573 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -420,6 +420,12 @@ export enum InlayHintKind { Parameter = 2 } +export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2 +} + /** * How an {@link InlineCompletionsProvider inline completion provider} was triggered. */ @@ -982,4 +988,4 @@ export enum WrappingIndent { * DeepIndent => wrapped lines get +2 indentation toward the parent. */ DeepIndent = 3 -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 37cd318679e..268069a8281 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -26,7 +26,7 @@ import { Selection } from '../../../../common/core/selection.js'; import { SingleTextEdit, TextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; -import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; +import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; @@ -86,17 +86,12 @@ export class InlineCompletionsModel extends Disposable { this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise)); - let lastItem: InlineCompletionItem | undefined = undefined; this._register(autorun(reader => { /** @description call handleItemDidShow */ const item = this.inlineCompletionState.read(reader); const completion = item?.inlineCompletion; - if (completion?.semanticId !== lastItem?.semanticId) { - lastItem = completion; - if (completion) { - const src = completion.source; - src.provider.handleItemDidShow?.(src.inlineSuggestions, completion.getSourceCompletion(), completion.insertText); - } + if (completion) { + this.handleInlineSuggestionShown(completion); } })); @@ -316,10 +311,8 @@ export class InlineCompletionsModel extends Disposable { subtransaction(tx, tx => { if (stopReason === 'explicitCancel') { const inlineCompletion = this.state.get()?.inlineCompletion; - const source = inlineCompletion?.source; - const sourceInlineCompletion = inlineCompletion?.getSourceCompletion(); - if (sourceInlineCompletion && source?.provider.handleRejection) { - source.provider.handleRejection(source.inlineSuggestions, sourceInlineCompletion); + if (inlineCompletion) { + inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected }); } } @@ -673,6 +666,8 @@ export class InlineCompletionsModel extends Disposable { return; } + completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted }); + if (completion.command) { // Make sure the completion list will not be disposed. completion.source.addRef(); @@ -821,18 +816,12 @@ export class InlineCompletionsModel extends Disposable { this._isAcceptingPartially = false; } - if (completion.source.provider.handlePartialAccept) { - const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); - // This assumes that the inline completion and the model use the same EOL style. - const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); - const acceptedLength = text.length; - completion.source.provider.handlePartialAccept( - completion.source.inlineSuggestions, - completion.getSourceCompletion(), - acceptedLength, - { kind, acceptedLength: acceptedLength, } - ); - } + const acceptedRange = Range.fromPositions(completion.range.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos)); + // This assumes that the inline completion and the model use the same EOL style. + const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF); + const acceptedLength = text.length; + completion.reportPartialAccept(acceptedLength, { kind, acceptedLength: acceptedLength }); + } finally { completion.source.removeRef(); } @@ -847,16 +836,10 @@ export class InlineCompletionsModel extends Disposable { const alreadyAcceptedLength = this.textModel.getValueInRange(augmentedCompletion.completion.range, EndOfLinePreference.LF).length; const acceptedLength = alreadyAcceptedLength + itemEdit.text.length; - const source = augmentedCompletion.completion.source; - source.provider.handlePartialAccept?.( - source.inlineSuggestions, - augmentedCompletion.completion.getSourceCompletion(), - itemEdit.text.length, - { - kind: PartialAcceptTriggerKind.Suggest, - acceptedLength, - } - ); + augmentedCompletion.completion.reportPartialAccept(itemEdit.text.length, { + kind: PartialAcceptTriggerKind.Suggest, + acceptedLength, + }); } public extractReproSample(): Repro { @@ -895,17 +878,8 @@ export class InlineCompletionsModel extends Disposable { }); } - public async handleInlineEditShown(inlineCompletion: InlineSuggestionItem): Promise { - if (inlineCompletion.didShow) { - return; - } - inlineCompletion.didShow = true; - - inlineCompletion.source.provider.handleItemDidShow?.(inlineCompletion.source.inlineSuggestions, inlineCompletion.getSourceCompletion(), inlineCompletion.insertText); - - if (inlineCompletion.shownCommand) { - await this._commandService.executeCommand(inlineCompletion.shownCommand.id, ...(inlineCompletion.shownCommand.arguments || [])); - } + public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem): Promise { + await inlineCompletion.reportInlineEditShown(this._commandService); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 90b454d605f..540678d1785 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -18,7 +18,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { OffsetEdit } from '../../../../common/core/offsetEdit.js'; import { Position } from '../../../../common/core/position.js'; -import { InlineCompletionContext, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReasonKind, InlineCompletionContext, InlineCompletionTriggerKind } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { OffsetEdits } from '../../../../common/model/textModelOffsetEdit.js'; @@ -344,17 +344,19 @@ class InlineCompletionsState extends Disposable { const items: InlineSuggestionItem[] = []; for (const item of update.completions) { - const oldItem = this._findByHash(item.hash); + const i = InlineSuggestionItem.create(item, textModel); + const oldItem = this._findByHash(i.hash); if (oldItem) { - items.push(item.withIdentity(oldItem.identity)); + items.push(i.withIdentity(oldItem.identity)); + oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() }); } else { - items.push(item); + items.push(i); } } if (itemToPreserve) { const item = this._findById(itemToPreserve); - if (item && !update.has(item) && item.canBeReused(textModel, request.position)) { + if (item && !update.has(item.getSingleTextEdit()) && item.canBeReused(textModel, request.position)) { items.unshift(item); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index 2deef447750..e7c695d1655 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -7,6 +7,7 @@ import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { matchesSubString } from '../../../../../base/common/filters.js'; import { observableSignal, IObservable } from '../../../../../base/common/observable.js'; import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; @@ -16,83 +17,108 @@ import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit, StringText, TextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; -import { InlineCompletions, InlineCompletionsProvider, InlineCompletion, InlineCompletionContext, InlineCompletionTriggerKind, Command, InlineCompletionWarning, InlineCompletionDisplayLocation } from '../../../../common/languages.js'; +import { InlineCompletion, InlineCompletionTriggerKind, Command, InlineCompletionWarning, InlineCompletionDisplayLocation, PartialAcceptInfo, InlineCompletionEndOfLifeReason } from '../../../../common/languages.js'; import { ITextModel, EndOfLinePreference } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; +import { InlineSuggestData, InlineSuggestionList, SnippetInfo } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem; +export namespace InlineSuggestionItem { + export function create( + data: InlineSuggestData, + textModel: ITextModel, + ): InlineSuggestionItem { + if (!data.isInlineEdit) { + return InlineCompletionItem.create(data, textModel); + } else { + return InlineEditItem.create(data, textModel); + } + } +} + abstract class InlineSuggestionItemBase { - public didShow = false; - constructor( - /** - * A reference to the original inline completion this inline completion has been constructed from. - * Used for event data to ensure referential equality. - */ - protected readonly _sourceInlineCompletion: InlineCompletion, - - /** - * A reference to the original inline completion list this inline completion has been constructed from. - * Used for event data to ensure referential equality. - */ - readonly source: InlineSuggestionList, - + protected readonly _data: InlineSuggestData, public readonly identity: InlineSuggestionIdentity, - protected readonly _context: InlineCompletionContext, ) { } - abstract getSingleTextEdit(): SingleTextEdit; + /** + * A reference to the original inline completion list this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ + public get source(): InlineSuggestionList { return this._data.source; } - abstract withEdit(userEdit: OffsetEdit, textModel: ITextModel): InlineSuggestionItem | undefined; - - abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem; - - public get isFromExplicitRequest(): boolean { return this._context.triggerKind === InlineCompletionTriggerKind.Explicit; } + public get isFromExplicitRequest(): boolean { return this._data.context.triggerKind === InlineCompletionTriggerKind.Explicit; } public get forwardStable(): boolean { return this.source.inlineSuggestions.enableForwardStability ?? false; } public get range(): Range { return this.getSingleTextEdit().range; } public get insertText(): string { return this.getSingleTextEdit().text; } public get semanticId(): string { return this.hash; } - /** @deprecated */ - public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; } - - get action(): Command | undefined { return this._sourceInlineCompletion.action; } - get command(): Command | undefined { return this._sourceInlineCompletion.command; } - get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } - get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } - get displayLocation(): InlineCompletionDisplayLocation | undefined { return this._sourceInlineCompletion.displayLocation; } - + public get action(): Command | undefined { return this._sourceInlineCompletion.action; } + public get command(): Command | undefined { return this._sourceInlineCompletion.command; } + public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } + public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } + public get displayLocation(): InlineCompletionDisplayLocation | undefined { return this._sourceInlineCompletion.displayLocation; } public get hash() { return JSON.stringify([ this.getSingleTextEdit().text, this.getSingleTextEdit().range.getStartPosition().toString() ]); } + /** @deprecated */ + public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; } + + /** + * A reference to the original inline completion this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ + private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; } + + + public abstract getSingleTextEdit(): SingleTextEdit; + + public abstract withEdit(userEdit: OffsetEdit, textModel: ITextModel): InlineSuggestionItem | undefined; + + public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem; public abstract canBeReused(model: ITextModel, position: Position): boolean; - addRef(): void { + + public addRef(): void { this.identity.addRef(); this.source.addRef(); } - removeRef(): void { + public removeRef(): void { this.identity.removeRef(); this.source.removeRef(); } - getSourceCompletion(): InlineCompletion { + public reportInlineEditShown(commandService: ICommandService) { + this._data.reportInlineEditShown(commandService, this.insertText); + } + + public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo) { + this._data.reportPartialAccept(acceptedCharacters, info); + } + + public reportEndOfLife(reason: InlineCompletionEndOfLifeReason): void { + this._data.reportEndOfLife(reason); + } + + public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void { + this._data.setEndOfLifeReason(reason); + } + + /** + * Avoid using this method. Instead introduce getters for the needed properties. + */ + public getSourceCompletion(): InlineCompletion { return this._sourceInlineCompletion; } } -export interface SnippetInfo { - snippet: string; - /* Could be different than the main range */ - range: Range; -} - export class InlineSuggestionIdentity { private static idCounter = 0; private readonly _onDispose = observableSignal(this); @@ -113,47 +139,16 @@ export class InlineSuggestionIdentity { } } -/** - * A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that - * computed them. - */ -export class InlineSuggestionList { - private refCount = 1; - constructor( - public readonly inlineSuggestions: InlineCompletions, - public readonly provider: InlineCompletionsProvider - ) { } - - addRef(): void { - this.refCount++; - } - - removeRef(): void { - this.refCount--; - if (this.refCount === 0) { - this.provider.freeInlineCompletions(this.inlineSuggestions); - } - } -} - export class InlineCompletionItem extends InlineSuggestionItemBase { public static create( - range: Range, - insertText: string, - snippetInfo: SnippetInfo | undefined, - additionalTextEdits: readonly ISingleEditOperation[], - - sourceInlineCompletion: InlineCompletion, - source: InlineSuggestionList, - - context: InlineCompletionContext, + data: InlineSuggestData, textModel: ITextModel, ): InlineCompletionItem { const identity = new InlineSuggestionIdentity(); - const textEdit = new SingleTextEdit(range, insertText); + const textEdit = new SingleTextEdit(data.range, data.insertText); const edit = getPositionOffsetTransformerFromTextModel(textModel).getSingleOffsetEdit(textEdit); - return new InlineCompletionItem(edit, textEdit, range, snippetInfo, additionalTextEdits, sourceInlineCompletion, source, identity, context); + return new InlineCompletionItem(edit, textEdit, data.range, data.snippetInfo, data.additionalTextEdits, data, identity); } public readonly isInlineEdit = false; @@ -165,15 +160,10 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { public readonly snippetInfo: SnippetInfo | undefined, public readonly additionalTextEdits: readonly ISingleEditOperation[], - - sourceInlineCompletion: InlineCompletion, - - source: InlineSuggestionList, - + data: InlineSuggestData, identity: InlineSuggestionIdentity, - _context: InlineCompletionContext ) { - super(sourceInlineCompletion, source, identity, _context); + super(data, identity); } override getSingleTextEdit(): SingleTextEdit { return this._textEdit; } @@ -185,10 +175,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this._originalRange, this.snippetInfo, this.additionalTextEdits, - this._sourceInlineCompletion, - this.source, + this._data, identity, - this._context ); } @@ -205,10 +193,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this._originalRange, this.snippetInfo, this.additionalTextEdits, - this._sourceInlineCompletion, - this.source, + this._data, this.identity, - this._context ); } @@ -264,16 +250,10 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { export class InlineEditItem extends InlineSuggestionItemBase { public static create( - range: Range, - insertText: string, - - sourceInlineCompletion: InlineCompletion, - source: InlineSuggestionList, - - context: InlineCompletionContext, + data: InlineSuggestData, textModel: ITextModel, ): InlineEditItem { - const offsetEdit = getOffsetEdit(textModel, range, insertText); + const offsetEdit = getOffsetEdit(textModel, data.range, data.insertText); const text = new TextModelText(textModel); const textEdit = TextEdit.fromOffsetEdit(offsetEdit, text); const singleTextEdit = textEdit.toSingle(text); @@ -284,7 +264,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { const replacedText = textModel.getValueInRange(replacedRange); return SingleUpdatedNextEdit.create(edit, replacedText); }); - return new InlineEditItem(offsetEdit, singleTextEdit, sourceInlineCompletion, source, identity, context, edits, false, textModel.getVersionId()); + return new InlineEditItem(offsetEdit, singleTextEdit, data, identity, edits, false, textModel.getVersionId()); } public readonly snippetInfo: SnippetInfo | undefined = undefined; @@ -295,17 +275,14 @@ export class InlineEditItem extends InlineSuggestionItemBase { private readonly _edit: OffsetEdit, private readonly _textEdit: SingleTextEdit, - sourceInlineCompletion: InlineCompletion, - - source: InlineSuggestionList, + data: InlineSuggestData, identity: InlineSuggestionIdentity, - _context: InlineCompletionContext, private readonly _edits: readonly SingleUpdatedNextEdit[], private readonly _lastChangePartOfInlineEdit = false, private readonly _inlineEditModelVersion: number, ) { - super(sourceInlineCompletion, source, identity, _context); + super(data, identity); } public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; } @@ -319,13 +296,11 @@ export class InlineEditItem extends InlineSuggestionItemBase { return new InlineEditItem( this._edit, this._textEdit, - this._sourceInlineCompletion, - this.source, + this._data, identity, - this._context, this._edits, this._lastChangePartOfInlineEdit, - this._inlineEditModelVersion + this._inlineEditModelVersion, ); } @@ -370,10 +345,8 @@ export class InlineEditItem extends InlineSuggestionItemBase { return new InlineEditItem( edit, textEdit, - this._sourceInlineCompletion, - this.source, + this._data, this.identity, - this._context, edits, lastChangePartOfInlineEdit, inlineEditModelVersion, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index c86c62988dc..032e132c5b6 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -10,19 +10,21 @@ import { onUnexpectedExternalError } from '../../../../../base/common/errors.js' import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { SetMap } from '../../../../../base/common/map.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; +import { SingleTextEdit } from '../../../../common/core/textEdit.js'; import { LanguageFeatureRegistry } from '../../../../common/languageFeatureRegistry.js'; -import { InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind, PartialAcceptInfo } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; import { SnippetParser, Text } from '../../../snippet/browser/snippetParser.js'; import { getReadonlyEmptyArray } from '../utils.js'; -import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem, InlineSuggestionList, SnippetInfo } from './inlineSuggestionItem.js'; export async function provideInlineCompletions( registry: LanguageFeatureRegistry, @@ -60,12 +62,11 @@ export async function provideInlineCompletions( } type Result = Promise; - const states = new Map(); - const seen = new Set(); function findPreferredProviderCircle( provider: InlineCompletionsProvider, - stack: InlineCompletionsProvider[] + stack: InlineCompletionsProvider[], + seen: Set, ): InlineCompletionsProvider[] | undefined { stack = [...stack, provider]; if (seen.has(provider)) { return stack; } @@ -74,7 +75,7 @@ export async function provideInlineCompletions( try { const preferred = getPreferredProviders(provider); for (const p of preferred) { - const c = findPreferredProviderCircle(p, stack); + const c = findPreferredProviderCircle(p, stack, seen); if (c) { return c; } } } finally { @@ -83,11 +84,11 @@ export async function provideInlineCompletions( return undefined; } - function queryProviderOrPreferredProvider(provider: InlineCompletionsProvider): Result { + function queryProviderOrPreferredProvider(provider: InlineCompletionsProvider, states: Map): Result { const state = states.get(provider); if (state) { return state; } - const circle = findPreferredProviderCircle(provider, []); + const circle = findPreferredProviderCircle(provider, [], new Set()); if (circle) { onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected.` + ` Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`)); @@ -100,7 +101,7 @@ export async function provideInlineCompletions( if (!circle) { const preferred = getPreferredProviders(provider); for (const p of preferred) { - const result = await queryProviderOrPreferredProvider(p); + const result = await queryProviderOrPreferredProvider(p, states); if (result && result.inlineSuggestions.items.length > 0) { // Skip provider return undefined; @@ -128,13 +129,18 @@ export async function provideInlineCompletions( } if (!result) { return undefined; } - const list = new InlineSuggestionList(result, provider); + const data: InlineSuggestData[] = []; + const list = new InlineSuggestionList(result, data, provider); + for (const item of result.items) { + data.push(createInlineCompletionItem(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid)); + } runWhenCancelled(token, () => list.removeRef()); return list; } - const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(queryProviderOrPreferredProvider)); + const states = new Map(); + const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(p => queryProviderOrPreferredProvider(p, states))); if (token.isCancellationRequested) { tokenSource.dispose(true); @@ -142,8 +148,8 @@ export async function provideInlineCompletions( return new InlineCompletionProviderResult([], new Set(), []); } - const result = await addRefAndCreateResult(contextWithUuid, inlineCompletionLists, defaultReplaceRange, model, languageConfigurationService); - tokenSource.dispose(true); // This disposes results that are not referenced. + const result = await addRefAndCreateResult(contextWithUuid, inlineCompletionLists, model); + tokenSource.dispose(true); // This disposes results that are not referenced by now. return result; } @@ -161,16 +167,13 @@ function runWhenCancelled(token: CancellationToken, callback: () => void): IDisp } } -// TODO: check cancellation token! async function addRefAndCreateResult( context: InlineCompletionContext, inlineCompletionLists: AsyncIterable<(InlineSuggestionList | undefined)>, - defaultReplaceRange: Range, model: ITextModel, - languageConfigurationService: ILanguageConfigurationService | undefined ): Promise { // for deduplication - const itemsByHash = new Map(); + const itemsByHash = new Map(); let shouldStop = false; const lists: InlineSuggestionList[] = []; @@ -178,27 +181,19 @@ async function addRefAndCreateResult( if (!completions) { continue; } completions.addRef(); lists.push(completions); - for (const item of completions.inlineSuggestions.items) { + for (const item of completions.inlineSuggestionsData) { if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) { continue; } if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) { continue; } - const inlineCompletionItem = createInlineCompletionItem( - item, - completions, - defaultReplaceRange, - model, - languageConfigurationService, - context, - ); - itemsByHash.set(inlineCompletionItem.hash, inlineCompletionItem); + itemsByHash.set(createHashFromSingleTextEdit(item.getSingleTextEdit()), item); // Stop after first visible inline completion if (!(item.isInlineEdit || item.showInlineEditMenu) && context.triggerKind === InlineCompletionTriggerKind.Automatic) { - const minifiedEdit = inlineCompletionItem.getSingleTextEdit().removeCommonPrefix(new TextModelText(model)); + const minifiedEdit = item.getSingleTextEdit().removeCommonPrefix(new TextModelText(model)); if (!minifiedEdit.isEmpty) { shouldStop = true; } @@ -219,13 +214,13 @@ export class InlineCompletionProviderResult implements IDisposable { /** * Free of duplicates. */ - public readonly completions: readonly InlineSuggestionItem[], + public readonly completions: readonly InlineSuggestData[], private readonly hashs: Set, private readonly providerResults: readonly InlineSuggestionList[], ) { } - public has(item: InlineSuggestionItem): boolean { - return this.hashs.has(item.hash); + public has(edit: SingleTextEdit): boolean { + return this.hashs.has(createHashFromSingleTextEdit(edit)); } // TODO: This is not complete as it does not take the textmodel into account @@ -241,6 +236,10 @@ export class InlineCompletionProviderResult implements IDisposable { } } +function createHashFromSingleTextEdit(edit: SingleTextEdit): string { + return JSON.stringify([edit.text, edit.range.getStartPosition().toString()]); +} + function createInlineCompletionItem( inlineCompletion: InlineCompletion, source: InlineSuggestionList, @@ -248,7 +247,7 @@ function createInlineCompletionItem( textModel: ITextModel, languageConfigurationService: ILanguageConfigurationService | undefined, context: InlineCompletionContext, -): InlineSuggestionItem { +): InlineSuggestData { let insertText: string; let snippetInfo: SnippetInfo | undefined; let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange; @@ -306,27 +305,127 @@ function createInlineCompletionItem( assertNever(inlineCompletion.insertText); } - if (inlineCompletion.isInlineEdit) { - return InlineEditItem.create( - range, - insertText, - inlineCompletion, - source, - context, - textModel, - ); - } else { - return InlineCompletionItem.create( - range, - insertText, - snippetInfo, - inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), - inlineCompletion, - source, - context, - textModel, + return new InlineSuggestData( + range, + insertText, + snippetInfo, + inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), + inlineCompletion, + source, + context, + inlineCompletion.isInlineEdit ?? false, + ); +} + +export class InlineSuggestData { + private _didShow = false; + private _didReportEndOfLife = false; + private _lastSetEndOfLifeReason: InlineCompletionEndOfLifeReason | undefined = undefined; + + constructor( + public readonly range: Range, + public readonly insertText: string, + public readonly snippetInfo: SnippetInfo | undefined, + public readonly additionalTextEdits: readonly ISingleEditOperation[], + + public readonly sourceInlineCompletion: InlineCompletion, + public readonly source: InlineSuggestionList, + public readonly context: InlineCompletionContext, + public readonly isInlineEdit: boolean, + ) { } + + public get showInlineEditMenu() { return this.sourceInlineCompletion.showInlineEditMenu ?? false; } + + public getSingleTextEdit() { + return new SingleTextEdit(this.range, this.insertText); + } + + public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string): Promise { + if (this._didShow) { + return; + } + this._didShow = true; + + this.source.provider.handleItemDidShow?.(this.source.inlineSuggestions, this.sourceInlineCompletion, updatedInsertText); + + if (this.sourceInlineCompletion.shownCommand) { + await commandService.executeCommand(this.sourceInlineCompletion.shownCommand.id, ...(this.sourceInlineCompletion.shownCommand.arguments || [])); + } + } + + public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo) { + this.source.provider.handlePartialAccept?.( + this.source.inlineSuggestions, + this.sourceInlineCompletion, + acceptedCharacters, + info ); } + + /** + * Sends the end of life event to the provider. + * If no reason is provided, the last set reason is used. + * If no reason was set, the default reason is used. + */ + public reportEndOfLife(reason?: InlineCompletionEndOfLifeReason): void { + if (this._didReportEndOfLife) { + return; + } + this._didReportEndOfLife = true; + + if (!reason) { + reason = this._lastSetEndOfLifeReason ?? { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; + } + + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && this.source.provider.handleRejection) { + this.source.provider.handleRejection(this.source.inlineSuggestions, this.sourceInlineCompletion); + } + + if (this.source.provider.handleEndOfLifetime) { + this.source.provider.handleEndOfLifetime(this.source.inlineSuggestions, this.sourceInlineCompletion, reason); + } + } + + /** + * Sets the end of life reason, but does not send the event to the provider yet. + */ + public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void { + this._lastSetEndOfLifeReason = reason; + } +} + +export interface SnippetInfo { + snippet: string; + /* Could be different than the main range */ + range: Range; +} + +/** + * A ref counted pointer to the computed `InlineCompletions` and the `InlineCompletionsProvider` that + * computed them. + */ +export class InlineSuggestionList { + private refCount = 1; + constructor( + public readonly inlineSuggestions: InlineCompletions, + public readonly inlineSuggestionsData: readonly InlineSuggestData[], + public readonly provider: InlineCompletionsProvider, + ) { } + + addRef(): void { + this.refCount++; + } + + removeRef(): void { + this.refCount--; + if (this.refCount === 0) { + for (const item of this.inlineSuggestionsData) { + // Fallback if it has not been called before + item.reportEndOfLife(); + } + this.provider.freeInlineCompletions(this.inlineSuggestions); + } + } } function getDefaultRange(position: Position, model: ITextModel): Range { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts index 5c7c7b2a6e0..62275ba537a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts @@ -52,7 +52,7 @@ export class InlineEditModel implements IInlineEditModel { } handleInlineEditShown() { - this._model.handleInlineEditShown(this.inlineEdit.inlineCompletion); + this._model.handleInlineSuggestionShown(this.inlineEdit.inlineCompletion); } } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 849e4dc42d7..2dc93ca30c6 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -812,6 +812,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { NewSymbolNameTriggerKind: standaloneEnums.NewSymbolNameTriggerKind, PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, HoverVerbosityAction: standaloneEnums.HoverVerbosityAction, + InlineCompletionEndOfLifeReasonKind: standaloneEnums.InlineCompletionEndOfLifeReasonKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 56091e7c09c..a8df403e1cc 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7352,7 +7352,15 @@ declare namespace monaco.languages { * @param acceptedCharacters Deprecated. Use `info.acceptedCharacters` instead. */ handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; + /** + * @deprecated Use `handleEndOfLifetime` instead. + */ handleRejection?(completions: T, item: T['items'][number]): void; + /** + * Is called when an inline completion item is no longer being used. + * Provides a reason of why it is not used anymore. + */ + handleEndOfLifetime?(completions: T, item: T['items'][number], reason: InlineCompletionEndOfLifeReason): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ @@ -7372,6 +7380,22 @@ declare namespace monaco.languages { toString?(): string; } + export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2 + } + + export type InlineCompletionEndOfLifeReason = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; + } | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; + } | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: TInlineCompletion; + userTypingDisagreed: boolean; + }; + export interface CodeAction { title: string; command?: Command; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 9aadd0d679a..482d6c6abb6 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -632,6 +632,22 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, + handleEndOfLifetime: async (completions, item, reason) => { + + function mapReason(reason: languages.InlineCompletionEndOfLifeReason, f: (reason: T1) => T2): languages.InlineCompletionEndOfLifeReason { + if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Ignored) { + return { + ...reason, + supersededBy: reason.supersededBy ? f(reason.supersededBy) : undefined, + }; + } + return reason; + } + + if (supportsHandleEvents) { + await this._proxy.$handleInlineCompletionEndOfLifetime(handle, completions.pid, item.idx, mapReason(reason, i => ({ pid: completions.pid, idx: i.idx }))); + } + }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { this._proxy.$freeInlineCompletionsList(handle, completions.pid); }, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 79a1112b410..1f6ac90b9f6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1784,6 +1784,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SpeechToTextStatus: extHostTypes.SpeechToTextStatus, TextToSpeechStatus: extHostTypes.TextToSpeechStatus, PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, + InlineCompletionEndOfLifeReasonKind: extHostTypes.InlineCompletionEndOfLifeReasonKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c37fb341e52..d8af1cbcc5f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2336,6 +2336,7 @@ export interface ExtHostLanguageFeaturesShape { $provideInlineEditsForRange(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; + $handleInlineCompletionEndOfLifetime(handle: number, pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void; $handleInlineCompletionRejection(handle: number, pid: number, idx: number): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index fce9a74ffb5..425f2d58ea2 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1350,6 +1350,7 @@ class InlineCompletionAdapter { && (typeof this._provider.handleDidShowCompletionItem === 'function' || typeof this._provider.handleDidPartiallyAcceptCompletionItem === 'function' || typeof this._provider.handleDidRejectCompletionItem === 'function' + || typeof this._provider.handleEndOfLifetime === 'function' ); } @@ -1559,6 +1560,16 @@ class InlineCompletionAdapter { } } + handleEndOfLifetime(pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void { + const completionItem = this._references.get(pid)?.items[idx]; + if (completionItem) { + if (this._provider.handleEndOfLifetime && this._isAdditionsProposedApiEnabled) { + const r = typeConvert.InlineCompletionEndOfLifeReason.to(reason, ref => this._references.get(ref.pid)?.items[ref.idx]); + this._provider.handleEndOfLifetime(completionItem, r); + } + } + } + handleRejection(pid: number, idx: number): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { @@ -2742,6 +2753,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } + $handleInlineCompletionEndOfLifetime(handle: number, pid: number, idx: number, reason: languages.InlineCompletionEndOfLifeReason<{ pid: number; idx: number }>): void { + this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + adapter.handleEndOfLifetime(pid, idx, reason); + }, undefined, undefined); + } + $handleInlineCompletionRejection(handle: number, pid: number, idx: number): void { this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.handleRejection(pid, idx); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 0af43d22a7b..b83ac1694b4 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3157,6 +3157,26 @@ export namespace PartialAcceptTriggerKind { } } +export namespace InlineCompletionEndOfLifeReason { + export function to(reason: languages.InlineCompletionEndOfLifeReason, convertFn: (item: T) => vscode.InlineCompletionItem | undefined): vscode.InlineCompletionEndOfLifeReason { + if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Ignored) { + const supersededBy = reason.supersededBy ? convertFn(reason.supersededBy) : undefined; + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Ignored, + supersededBy: supersededBy, + userTypingDisagreed: reason.userTypingDisagreed, + }; + } else if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Accepted) { + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Accepted, + }; + } + return { + kind: types.InlineCompletionEndOfLifeReasonKind.Rejected, + }; + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 350dbe37b57..d14292e920f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1866,6 +1866,12 @@ export enum PartialAcceptTriggerKind { Suggest = 3, } +export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2, +} + export enum ViewColumn { Active = -1, Beside = -2, diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 3052431737c..0c31199872b 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -79,10 +79,25 @@ declare module 'vscode' { handleDidShowCompletionItem?(completionItem: InlineCompletionItem, updatedInsertText: string): void; /** - * @param completionItem The completion item that was rejected. + * Is called when an inline completion item was accepted partially. + * @param info Additional info for the partial accepted trigger. + */ + // eslint-disable-next-line local/vscode-dts-provider-naming + handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + + /** + * Is called when an inline completion item is no longer being used. + * Provides a reason of why it is not used anymore. */ // eslint-disable-next-line local/vscode-dts-provider-naming - handleDidRejectCompletionItem?(completionItem: InlineCompletionItem): void; + handleEndOfLifetime?(completionItem: InlineCompletionItem, reason: InlineCompletionEndOfLifeReason): void; + + readonly debounceDelayMs?: number; + + // #region Deprecated methods + + /** @deprecated */ + provideInlineEditsForRange?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; /** * Is called when an inline completion item was accepted partially. @@ -93,17 +108,31 @@ declare module 'vscode' { handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, acceptedLength: number): void; /** - * Is called when an inline completion item was accepted partially. - * @param info Additional info for the partial accepted trigger. - */ + * @param completionItem The completion item that was rejected. + * @deprecated Use {@link handleEndOfLifetime} instead. + */ // eslint-disable-next-line local/vscode-dts-provider-naming - handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + handleDidRejectCompletionItem?(completionItem: InlineCompletionItem): void; - provideInlineEditsForRange?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; - - readonly debounceDelayMs?: number; + // #endregion } + export enum InlineCompletionEndOfLifeReasonKind { + Accepted = 0, + Rejected = 1, + Ignored = 2, + } + + export type InlineCompletionEndOfLifeReason = { + kind: InlineCompletionEndOfLifeReasonKind.Accepted; // User did an explicit action to accept + } | { + kind: InlineCompletionEndOfLifeReasonKind.Rejected; // User did an explicit action to reject + } | { + kind: InlineCompletionEndOfLifeReasonKind.Ignored; + supersededBy?: InlineCompletionItem; + userTypingDisagreed: boolean; + }; + export interface InlineCompletionContext { readonly userPrompt?: string;