From 1dfd5ba883ab6238d2b8a56a982d1405a0162ea7 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Tue, 7 Jan 2025 17:49:49 +0800 Subject: [PATCH] feat(audoedit): add the `accept` command callback --- .../analytics-logger/analytics-logger.ts | 15 +++- vscode/src/autoedits/autoedits-provider.ts | 1 + .../src/autoedits/renderer/inline-manager.ts | 36 +++----- vscode/src/autoedits/renderer/manager.ts | 89 +++++++++++++------ 4 files changed, 86 insertions(+), 55 deletions(-) diff --git a/vscode/src/autoedits/analytics-logger/analytics-logger.ts b/vscode/src/autoedits/analytics-logger/analytics-logger.ts index d0bb087ebc74..26d965de2179 100644 --- a/vscode/src/autoedits/analytics-logger/analytics-logger.ts +++ b/vscode/src/autoedits/analytics-logger/analytics-logger.ts @@ -21,6 +21,7 @@ import { upstreamHealthProvider } from '../../services/UpstreamHealthProvider' import { captureException, shouldErrorBeReported } from '../../services/sentry/sentry' import { splitSafeMetadata } from '../../services/telemetry-v2' import type { AutoeditsPrompt } from '../adapters/base' +import { autoeditsOutputChannelLogger } from '../output-channel-logger' import type { CodeToReplaceData } from '../prompt/prompt-utils' import type { DecorationInfo } from '../renderer/decorators/base' @@ -545,8 +546,6 @@ export class AutoeditAnalyticsLogger { if (result?.updatedRequest) { this.writeAutoeditRequestEvent('suggested', result.updatedRequest) this.writeAutoeditRequestEvent('accepted', result.updatedRequest) - - this.activeRequests.delete(result.updatedRequest.requestId) } } @@ -576,7 +575,6 @@ export class AutoeditAnalyticsLogger { if (result?.updatedRequest) { this.writeAutoeditRequestEvent('discarded', result.updatedRequest) - this.activeRequests.delete(result.updatedRequest.requestId) } } @@ -666,6 +664,17 @@ export class AutoeditAnalyticsLogger { action: AutoeditEventAction, params?: TelemetryEventParameters<{ [key: string]: number }, BillingProduct, BillingCategory> ): void { + autoeditsOutputChannelLogger.logDebug( + 'writeAutoeditEvent', + `${action} id: "${params?.interactionID ?? 'n/a'}"`, + { + verbose: { + latency: params?.metadata?.latency, + prediction: params?.privateMetadata?.prediction, + codeToRewrite: params?.privateMetadata?.codeToRewrite, + }, + } + ) telemetryRecorder.recordEvent('cody.autoedit', action, params) } diff --git a/vscode/src/autoedits/autoedits-provider.ts b/vscode/src/autoedits/autoedits-provider.ts index 697757cad136..124fa7c336ba 100644 --- a/vscode/src/autoedits/autoedits-provider.ts +++ b/vscode/src/autoedits/autoedits-provider.ts @@ -309,6 +309,7 @@ export class AutoeditsProvider implements vscode.InlineCompletionItemProvider, v const { inlineCompletionItems, updatedDecorationInfo, updatedPrediction } = this.rendererManager.tryMakeInlineCompletions({ + requestId, prediction, codeToReplaceData, document, diff --git a/vscode/src/autoedits/renderer/inline-manager.ts b/vscode/src/autoedits/renderer/inline-manager.ts index 79f474c7b6e0..012af2a5610f 100644 --- a/vscode/src/autoedits/renderer/inline-manager.ts +++ b/vscode/src/autoedits/renderer/inline-manager.ts @@ -4,9 +4,7 @@ import { isFileURI } from '@sourcegraph/cody-shared' import { completionMatchesSuffix } from '../../completions/is-completion-visible' import { getNewLineChar } from '../../completions/text-processing' -import { autoeditAnalyticsLogger } from '../analytics-logger' import { autoeditsOutputChannelLogger } from '../output-channel-logger' -import { areSameUriDocs } from '../utils' import type { AddedLineInfo, @@ -31,26 +29,6 @@ export class AutoEditsInlineRendererManager extends AutoEditsDefaultRendererManager implements AutoEditsRendererManager { - protected async acceptEdit(): Promise { - const editor = vscode.window.activeTextEditor - const { activeRequest } = this - if ( - !editor || - !activeRequest || - !areSameUriDocs(editor.document, this.activeRequest?.document) - ) { - return this.rejectEdit() - } - - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - await editor.edit(editBuilder => { - editBuilder.replace(activeRequest.codeToReplaceData.range, activeRequest.prediction) - }) - - await this.handleDidHideSuggestion() - autoeditAnalyticsLogger.markAsAccepted(activeRequest.requestId) - } - protected async onDidChangeTextEditorSelection( event: vscode.TextEditorSelectionChangeEvent ): Promise { @@ -59,11 +37,12 @@ export class AutoEditsInlineRendererManager // rendered as inline completion ghost text, which is hidden by default // whenever the cursor moves. if (isFileURI(event.textEditor.document.uri)) { - this.rejectEdit() + this.rejectActiveEdit() } } tryMakeInlineCompletions({ + requestId, prediction, document, position, @@ -94,7 +73,16 @@ export class AutoEditsInlineRendererManager new vscode.Range( document.lineAt(position).range.start, document.lineAt(position).range.end - ) + ), + { + title: 'Autoedit accepted', + command: 'cody.supersuggest.accept', + arguments: [ + { + requestId, + }, + ], + } ), ] diff --git a/vscode/src/autoedits/renderer/manager.ts b/vscode/src/autoedits/renderer/manager.ts index 82827ffc1fb4..e00a523f5243 100644 --- a/vscode/src/autoedits/renderer/manager.ts +++ b/vscode/src/autoedits/renderer/manager.ts @@ -15,6 +15,7 @@ import { import type { AutoEditsDecorator, DecorationInfo } from './decorators/base' export interface TryMakeInlineCompletionsArgs { + requestId: AutoeditRequestID prediction: string codeToReplaceData: CodeToReplaceData document: vscode.TextDocument @@ -83,8 +84,8 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager constructor(protected createDecorator: (editor: vscode.TextEditor) => AutoEditsDecorator) { this.disposables.push( - vscode.commands.registerCommand('cody.supersuggest.accept', () => this.acceptEdit()), - vscode.commands.registerCommand('cody.supersuggest.dismiss', () => this.rejectEdit()), + vscode.commands.registerCommand('cody.supersuggest.accept', () => this.acceptActiveEdit()), + vscode.commands.registerCommand('cody.supersuggest.dismiss', () => this.rejectActiveEdit()), vscode.workspace.onDidChangeTextDocument(event => this.onDidChangeTextDocument(event)), vscode.window.onDidChangeTextEditorSelection(event => this.onDidChangeTextEditorSelection(event) @@ -97,33 +98,46 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager } protected onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): void { - // Only dismiss if we have an active suggestion and the changed document matches - // else, we will falsely discard the suggestion on unrelated changes such as changes in output panel. - if (areSameUriDocs(event.document, this.activeRequest?.document)) { - this.rejectEdit() + if ( + // Only dismiss if there are inline decorations, as inline completion items rely on + // a native acceptance/rejection mechanism that we can't interfere with. + this.hasInlineDecorationOnly() && + // Only dismiss if we have an active suggestion and the changed document matches + // else, we will falsely discard the suggestion on unrelated changes such as changes in output panel. + areSameUriDocs(event.document, this.activeRequest?.document) + ) { + this.rejectActiveEdit() } } protected onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined): void { if (!editor || !areSameUriDocs(editor.document, this.activeRequest?.document)) { - this.rejectEdit() + this.rejectActiveEdit() } } protected onDidCloseTextDocument(document: vscode.TextDocument): void { if (areSameUriDocs(document, this.activeRequest?.document)) { - this.rejectEdit() + this.rejectActiveEdit() } } protected onDidChangeTextEditorSelection(event: vscode.TextEditorSelectionChangeEvent): void { if ( + // Only dismiss if there are inline decorations, as inline completion items rely on + // a native acceptance/rejection mechanism that we can't interfere with. + // + // For instance, the acceptance command is triggered only after the document changes. + // This means we can't depend on document or selection changes to handle cases where + // inline completion items are accepted because they get rejected before the + // acceptance callback is fired by VS Code. + this.hasInlineDecorationOnly() && this.activeRequest && areSameUriDocs(event.textEditor.document, this.activeRequest?.document) ) { const currentSelectionRange = event.selections.at(-1) if (!currentSelectionRange?.intersection(this.activeRequest.codeToReplaceData.range)) { - this.rejectEdit() + this.rejectActiveEdit() } } } @@ -142,8 +156,12 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager return this.activeRequestId !== null } + public hasInlineDecorationOnly(): boolean { + return !this.activeRequest?.inlineCompletionItems + } + public async handleDidShowSuggestion(requestId: AutoeditRequestID): Promise { - await this.rejectEdit() + await this.rejectActiveEdit() this.activeRequestId = requestId this.decorator = this.createDecorator(vscode.window.activeTextEditor!) @@ -163,43 +181,48 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager }, AUTOEDIT_VISIBLE_DELAY_MS) } - protected async handleDidHideSuggestion(): Promise { - if (this.activeRequest && this.decorator) { + protected async handleDidHideSuggestion(decorator: AutoEditsDecorator | null): Promise { + if (decorator) { + decorator.dispose() // Hide inline decorations - this.decorator.dispose() await vscode.commands.executeCommand('setContext', 'cody.supersuggest.active', false) - - // Hide inline completion provider item ghost text - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') } + // Hide inline completion provider item ghost text + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + this.activeRequestId = null this.decorator = null } - protected async acceptEdit(): Promise { + protected async acceptActiveEdit(): Promise { const editor = vscode.window.activeTextEditor - const { activeRequest } = this + const { activeRequest, decorator } = this if ( !editor || !activeRequest || !areSameUriDocs(editor.document, this.activeRequest?.document) ) { - return this.rejectEdit() + return this.rejectActiveEdit() } + await this.handleDidHideSuggestion(decorator) + autoeditAnalyticsLogger.markAsAccepted(activeRequest.requestId) + await editor.edit(editBuilder => { editBuilder.replace(activeRequest.codeToReplaceData.range, activeRequest.prediction) }) - - autoeditAnalyticsLogger.markAsAccepted(activeRequest.requestId) - await this.handleDidHideSuggestion() } - protected async rejectEdit(): Promise { - if (this.activeRequest) { - autoeditAnalyticsLogger.markAsRejected(this.activeRequest.requestId) - await this.handleDidHideSuggestion() + protected async rejectActiveEdit(): Promise { + const { activeRequest, decorator } = this + + if (decorator) { + await this.handleDidHideSuggestion(decorator) + } + + if (activeRequest) { + autoeditAnalyticsLogger.markAsRejected(activeRequest.requestId) } } @@ -209,6 +232,7 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager } tryMakeInlineCompletions({ + requestId, prediction, codeToReplaceData, document, @@ -247,7 +271,16 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager new vscode.Range( document.lineAt(position).range.start, document.lineAt(position).range.end - ) + ), + { + title: 'Autoedit accepted', + command: 'cody.supersuggest.accept', + arguments: [ + { + requestId, + }, + ], + } ) autoeditsOutputChannelLogger.logDebug( 'tryMakeInlineCompletions', @@ -273,7 +306,7 @@ export class AutoEditsDefaultRendererManager implements AutoEditsRendererManager } public dispose(): void { - this.rejectEdit() + this.rejectActiveEdit() for (const disposable of this.disposables) { disposable.dispose() }