diff --git a/src/extension/completions-core/vscode-node/completionsServiceBridges.ts b/src/extension/completions-core/vscode-node/completionsServiceBridges.ts index 4f56d7ce7e..aa39055458 100644 --- a/src/extension/completions-core/vscode-node/completionsServiceBridges.ts +++ b/src/extension/completions-core/vscode-node/completionsServiceBridges.ts @@ -19,7 +19,6 @@ import { contextProviderMatch } from './extension/src/contextProviderMatch'; import { registerPanelSupport } from './extension/src/copilotPanel/common'; import { CopilotExtensionStatus, ICompletionsExtensionStatus } from './extension/src/extensionStatus'; import { extensionFileSystem } from './extension/src/fileSystem'; -import { registerGhostTextDependencies } from './extension/src/ghostText/ghostText'; import { exception } from './extension/src/inlineCompletion'; import { ModelPickerManager } from './extension/src/modelPicker'; import { CopilotStatusBar } from './extension/src/statusBar'; @@ -132,9 +131,6 @@ export function setup(serviceAccessor: ServicesAccessor, disposables: Disposable // CodeQuote needs to listen for the initial token notification event. disposables.add(serviceAccessor.get(ICompletionsCitationManager).register()); - // Send telemetry when ghost text is accepted - disposables.add(registerGhostTextDependencies(serviceAccessor)); - // Register to listen for changes to the active document to keep track // of last access time disposables.add(registerDocumentTracker(serviceAccessor)); diff --git a/src/extension/completions-core/vscode-node/extension/src/ghostText/ghostText.ts b/src/extension/completions-core/vscode-node/extension/src/ghostText/ghostText.ts index 6eb6797766..de445160df 100644 --- a/src/extension/completions-core/vscode-node/extension/src/ghostText/ghostText.ts +++ b/src/extension/completions-core/vscode-node/extension/src/ghostText/ghostText.ts @@ -5,21 +5,21 @@ import { CancellationToken, - commands, InlineCompletionContext, InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletionItem, - InlineCompletionItemProvider, InlineCompletionList, InlineCompletionTriggerKind, PartialAcceptInfo, Position, Range, TextDocument, - window, + window } from 'vscode'; -import { IInstantiationService, ServicesAccessor } from '../../../../../../util/vs/platform/instantiation/common/instantiation'; +import { ISurveyService } from '../../../../../../platform/survey/common/surveyService'; +import { assertNever } from '../../../../../../util/vs/base/common/assert'; +import { IInstantiationService } from '../../../../../../util/vs/platform/instantiation/common/instantiation'; import { createCorrelationId } from '../../../../../inlineEdits/common/correlationId'; import { CopilotCompletion } from '../../../lib/src/ghostText/copilotCompletion'; import { handleGhostTextPostInsert, handleGhostTextShown, handlePartialGhostTextPostInsert } from '../../../lib/src/ghostText/last'; @@ -27,14 +27,21 @@ import { GhostText } from '../../../lib/src/inlineCompletion'; import { telemetry } from '../../../lib/src/telemetry'; import { wrapDoc } from '../textDocumentManager'; -const postInsertCmdName = '_github.copilot.ghostTextPostInsert2'; +export interface GhostTextCompletionList extends InlineCompletionList { + items: GhostTextCompletionItem[]; +} + +export interface GhostTextCompletionItem extends InlineCompletionItem { + copilotCompletion: CopilotCompletion; +} -export class GhostTextProvider implements InlineCompletionItemProvider { +export class GhostTextProvider { private readonly ghostText: GhostText; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, + @ISurveyService private readonly _surveyService: ISurveyService, ) { this.ghostText = this.instantiationService.createInstance(GhostText); } @@ -44,7 +51,7 @@ export class GhostTextProvider implements InlineCompletionItemProvider { position: Position, context: InlineCompletionContext, token: CancellationToken - ): Promise { + ): Promise { const textDocument = wrapDoc(vscodeDoc); if (!textDocument) { return; @@ -69,60 +76,52 @@ export class GhostTextProvider implements InlineCompletionItemProvider { return; } - const items = rawCompletions.map(completion => { + const items: GhostTextCompletionItem[] = rawCompletions.map(completion => { const { start, end } = completion.range; const newRange = new Range(start.line, start.character, end.line, end.character); return { insertText: completion.insertText, range: newRange, - command: { - title: 'Completion Accepted', // Unused - command: postInsertCmdName, - arguments: [completion], - }, + copilotCompletion: completion, correlationId: createCorrelationId('completions'), - }; + } satisfies GhostTextCompletionItem; }); return { items }; } - handleDidShowCompletionItem(item: InlineCompletionItem) { - const cmp = item.command!.arguments![0] as CopilotCompletion; - this.instantiationService.invokeFunction(handleGhostTextShown, cmp); + handleDidShowCompletionItem(item: GhostTextCompletionItem) { + this.instantiationService.invokeFunction(handleGhostTextShown, item.copilotCompletion); } - handleDidPartiallyAcceptCompletionItem(item: InlineCompletionItem, info: number | PartialAcceptInfo) { + handleDidPartiallyAcceptCompletionItem(item: GhostTextCompletionItem, info: number | PartialAcceptInfo) { if (typeof info === 'number') { return; // deprecated API } - const cmp = item.command!.arguments![0] as CopilotCompletion; - this.instantiationService.invokeFunction(handlePartialGhostTextPostInsert, cmp, info.acceptedLength, info.kind); + this.instantiationService.invokeFunction(handlePartialGhostTextPostInsert, item.copilotCompletion, info.acceptedLength, info.kind); } - handleEndOfLifetime(completionItem: InlineCompletionItem, reason: InlineCompletionEndOfLifeReason) { - // Send telemetry event when a completion is explicitly dismissed - if (reason.kind !== InlineCompletionEndOfLifeReasonKind.Rejected) { - return; + async handleEndOfLifetime(completionItem: GhostTextCompletionItem, reason: InlineCompletionEndOfLifeReason) { + const copilotCompletion = completionItem.copilotCompletion; + switch (reason.kind) { + case InlineCompletionEndOfLifeReasonKind.Accepted: { + this.instantiationService.invokeFunction(handleGhostTextPostInsert, copilotCompletion); + this._surveyService.signalUsage('completions').catch(() => { + // Ignore errors from the survey command execution + }); + return; + } + case InlineCompletionEndOfLifeReasonKind.Rejected: { + this.instantiationService.invokeFunction(telemetry, 'ghostText.dismissed', copilotCompletion.telemetry); + return; + } + case InlineCompletionEndOfLifeReasonKind.Ignored: { + // @ulugbekna: no-op ? + return; + } + default: { + assertNever(reason); + } } - const cmp = completionItem.command?.arguments?.[0] as CopilotCompletion | undefined; - if (!cmp) { - return; - } - this.instantiationService.invokeFunction(telemetry, 'ghostText.dismissed', cmp.telemetry); } } - -/** Registers the commands necessary to use GhostTextProvider (but not GhostTextProvider itself) */ -export function registerGhostTextDependencies(accessor: ServicesAccessor) { - const instantiationService = accessor.get(IInstantiationService); - const postCmdHandler = commands.registerCommand(postInsertCmdName, async (e: CopilotCompletion) => { - instantiationService.invokeFunction(handleGhostTextPostInsert, e); - try { - await commands.executeCommand('github.copilot.survey.signalUsage', 'completions'); - } catch (e) { - // Ignore errors from the survey command execution - } - }); - return postCmdHandler; -} diff --git a/src/extension/completions-core/vscode-node/extension/src/inlineCompletion.ts b/src/extension/completions-core/vscode-node/extension/src/inlineCompletion.ts index 15a148f497..dd9ee898b8 100644 --- a/src/extension/completions-core/vscode-node/extension/src/inlineCompletion.ts +++ b/src/extension/completions-core/vscode-node/extension/src/inlineCompletion.ts @@ -7,14 +7,13 @@ import { CancellationToken, InlineCompletionContext, InlineCompletionEndOfLifeReason, - InlineCompletionItem, InlineCompletionItemProvider, InlineCompletionList, InlineCompletionTriggerKind, PartialAcceptInfo, Position, TextDocument, - workspace, + workspace } from 'vscode'; import { Disposable } from '../../../../../util/vs/base/common/lifecycle'; import { LineEdit } from '../../../../../util/vs/editor/common/core/edits/lineEdit'; @@ -32,7 +31,7 @@ import { Logger } from '../../lib/src/logger'; import { isCompletionEnabledForDocument } from './config'; import { CopilotCompletionFeedbackTracker, sendCompletionFeedbackCommand } from './copilotCompletionFeedbackTracker'; import { ICompletionsExtensionStatus } from './extensionStatus'; -import { GhostTextProvider } from './ghostText/ghostText'; +import { GhostTextCompletionItem, GhostTextCompletionList, GhostTextProvider } from './ghostText/ghostText'; const logger = new Logger('inlineCompletionItemProvider'); @@ -58,7 +57,7 @@ export function exception(accessor: ServicesAccessor, error: unknown, origin: st /** @public */ export class CopilotInlineCompletionItemProvider extends Disposable implements InlineCompletionItemProvider { private readonly copilotCompletionFeedbackTracker: CopilotCompletionFeedbackTracker; - private readonly ghostTextProvider: InlineCompletionItemProvider; + private readonly ghostTextProvider: GhostTextProvider; private readonly inlineEditLogger: InlineEditLogger; public onDidChange = undefined; @@ -80,7 +79,7 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I position: Position, context: InlineCompletionContext, token: CancellationToken - ): Promise { + ): Promise { const logContext = new GhostTextContext(doc.uri.toString(), doc.version, context); try { return await this._provideInlineCompletionItems(doc, position, context, logContext, token); @@ -98,7 +97,7 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I context: InlineCompletionContext, logContext: GhostTextContext, token: CancellationToken - ): Promise { + ): Promise { if (context.triggerKind === InlineCompletionTriggerKind.Automatic) { if (!this.instantiationService.invokeFunction(isCompletionEnabledForDocument, doc)) { return; @@ -139,34 +138,34 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I commands: [sendCompletionFeedbackCommand], }; } catch (e) { - this.instantiationService.invokeFunction(exception, e, '.provideInlineCompletionItems', logger); + this.instantiationService.invokeFunction(exception, e, '._provideInlineCompletionItems', logger); logContext.setError(e); } } - handleDidShowCompletionItem(item: InlineCompletionItem, updatedInsertText: string) { + handleDidShowCompletionItem(item: GhostTextCompletionItem) { try { this.copilotCompletionFeedbackTracker.trackItem(item); - return this.ghostTextProvider.handleDidShowCompletionItem?.(item, updatedInsertText); + return this.ghostTextProvider.handleDidShowCompletionItem(item); } catch (e) { - this.instantiationService.invokeFunction(exception, e, '.provideInlineCompletionItems', logger); + this.instantiationService.invokeFunction(exception, e, '.handleDidShowCompletionItem', logger); } } handleDidPartiallyAcceptCompletionItem( - item: InlineCompletionItem, - acceptedLengthOrInfo: number & PartialAcceptInfo + item: GhostTextCompletionItem, + acceptedLengthOrInfo: number | PartialAcceptInfo ) { try { - return this.ghostTextProvider.handleDidPartiallyAcceptCompletionItem?.(item, acceptedLengthOrInfo); + return this.ghostTextProvider.handleDidPartiallyAcceptCompletionItem(item, acceptedLengthOrInfo); } catch (e) { - this.instantiationService.invokeFunction(exception, e, '.provideInlineCompletionItems', logger); + this.instantiationService.invokeFunction(exception, e, '.handleDidPartiallyAcceptCompletionItem', logger); } } - handleEndOfLifetime(completionItem: InlineCompletionItem, reason: InlineCompletionEndOfLifeReason) { + handleEndOfLifetime(completionItem: GhostTextCompletionItem, reason: InlineCompletionEndOfLifeReason) { try { - return this.ghostTextProvider.handleEndOfLifetime?.(completionItem, reason); + return this.ghostTextProvider.handleEndOfLifetime(completionItem, reason); } catch (e) { this.instantiationService.invokeFunction(exception, e, '.handleEndOfLifetime', logger); } diff --git a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts index 39b72c6347..8c71552a28 100644 --- a/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts +++ b/src/extension/completions-core/vscode-node/lib/src/ghostText/ghostText.ts @@ -636,7 +636,7 @@ function getRemainingDebounceMs(accessor: ServicesAccessor, opts: GetGhostTextOp return Math.max(0, debounce - elapsed); } -function inlineCompletionRequestCancelled( +function isCompletionRequestCancelled( currentGhostText: ICompletionsCurrentGhostText, requestId: string, cancellationToken?: ICancellationToken @@ -668,7 +668,7 @@ async function getGhostTextWithoutAbortHandling( const currentGhostText = accessor.get(ICompletionsCurrentGhostText); const statusReporter = accessor.get(ICompletionsStatusReporter); - if (inlineCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { + if (isCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { return { type: 'abortedBeforeIssued', reason: 'cancelled before extractPrompt', @@ -758,7 +758,7 @@ async function getGhostTextWithoutAbortHandling( if (debounce > 0) { ghostTextLogger.debug(logTarget, `Debouncing ghost text request for ${debounce}ms`); await delay(debounce); - if (inlineCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { + if (isCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { return { type: 'abortedBeforeIssued', reason: 'cancelled after debounce', @@ -845,7 +845,7 @@ async function getGhostTextWithoutAbortHandling( const trimmedChoice = makeGhostAPIChoice(choice[0], { forceSingleLine }); choices = [[trimmedChoice], ResultType.Async]; } - if (inlineCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { + if (isCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { ghostTextLogger.debug(logTarget, 'Cancelled before requesting a new completion'); return { type: 'abortedBeforeIssued', @@ -988,7 +988,7 @@ async function getGhostTextWithoutAbortHandling( if (resultType !== ResultType.TypingAsSuggested && !ghostTextOptions.isCycling && remainingDelay > 0) { ghostTextLogger.debug(logTarget, `Waiting ${remainingDelay}ms before returning completion`); await delay(remainingDelay); - if (inlineCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { + if (isCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { ghostTextLogger.debug(logTarget, 'Cancelled after completions delay'); return { type: 'canceled', @@ -1038,7 +1038,7 @@ async function getGhostTextWithoutAbortHandling( `Produced ${results.length} results from ${resultTypeToString(resultType)} at ${telemetryData.measurements.foundOffset} offset` ); - if (inlineCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { + if (isCompletionRequestCancelled(currentGhostText, ourRequestId, cancellationToken)) { return { type: 'canceled', reason: 'after post processing completions', diff --git a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts index 80df2e83fb..540f70e082 100644 --- a/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts +++ b/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts @@ -33,6 +33,7 @@ import { StringText } from '../../../util/vs/editor/common/core/text/abstractTex import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { IExtensionContribution } from '../../common/contributions'; import { registerUnificationCommands } from '../../completions-core/vscode-node/completionsServiceBridges'; +import { GhostTextCompletionItem, GhostTextCompletionList } from '../../completions-core/vscode-node/extension/src/ghostText/ghostText'; import { CopilotInlineCompletionItemProvider } from '../../completions-core/vscode-node/extension/src/inlineCompletion'; import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService'; import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution'; @@ -227,16 +228,16 @@ export class JointCompletionsProviderContribution extends Disposable implements } type SingularCompletionItem = - | ({ source: 'completions' } & vscode.InlineCompletionItem) + | ({ source: 'completions' } & GhostTextCompletionItem) | ({ source: 'inlineEdits' } & NesCompletionItem) ; type SingularCompletionList = - | ({ source: 'completions' } & vscode.InlineCompletionList) + | ({ source: 'completions' } & GhostTextCompletionList) | ({ source: 'inlineEdits' } & NesCompletionList) ; -function toCompletionsList(list: vscode.InlineCompletionList): SingularCompletionList { +function toCompletionsList(list: GhostTextCompletionList): SingularCompletionList { return { ...list, items: list.items.map(item => ({ ...item, source: 'completions' })), source: 'completions' }; } @@ -573,7 +574,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple } private _invokeCompletionsProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, ct: CancellationToken, sw: StopWatch) { - let completionsP: Promise | undefined; + let completionsP: Promise | undefined; if (this._completionsProvider) { this._completionsRequestsInFlight.add(ct); const disp = ct.onCancellationRequested(() => this._completionsRequestsInFlight.delete(ct)); @@ -604,7 +605,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple } private async _returnCompletionsOrOtherwiseNES( - completionsP: Promise | undefined, + completionsP: Promise | undefined, nesP: Promise | undefined, docSnapshot: StringText, sw: StopWatch, @@ -644,7 +645,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple } private _returnCompletions( - completionsR: vscode.InlineCompletionList, + completionsR: GhostTextCompletionList, nesDisposeReason: vscode.InlineCompletionsDisposeReason, nesP: Promise | undefined, sw: StopWatch, @@ -661,7 +662,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple private _returnNES( nesR: NesCompletionList, completionsDisposeReason: vscode.InlineCompletionsDisposeReason, - completionsP: Promise | undefined, + completionsP: Promise | undefined, sw: StopWatch, tracer: ITracer, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, @@ -683,7 +684,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple private static retainOnlyMeaningfulEdits(docSnapshot: StringText, list: T): T { // meaningful = not noop - function isMeaningfulEdit(item: vscode.InlineCompletionItem): boolean { + function isMeaningfulEdit(item: T['items'][number]): boolean { if (item.range === undefined || // must be a completion with a side-effect, eg a command invocation or something typeof item.insertText !== 'string' // shouldn't happen ) { @@ -707,7 +708,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple public handleDidShowCompletionItem?(completionItem: SingularCompletionItem, updatedInsertText: string): void { switch (completionItem.source) { case 'completions': - this._completionsProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText); + this._completionsProvider?.handleDidShowCompletionItem?.(completionItem); break; case 'inlineEdits': this._inlineEditProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText); diff --git a/src/extension/survey/vscode-node/surveyCommands.ts b/src/extension/survey/vscode-node/surveyCommands.ts index bd5baa65c6..3f1a3838a3 100644 --- a/src/extension/survey/vscode-node/surveyCommands.ts +++ b/src/extension/survey/vscode-node/surveyCommands.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { ISurveyService } from '../../../platform/survey/common/surveyService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; -const SURVEY_SIGNAL_USAGE_ID = 'github.copilot.survey.signalUsage'; +export const SURVEY_SIGNAL_USAGE_ID = 'github.copilot.survey.signalUsage'; export class SurveyCommandContribution extends Disposable { constructor(@ISurveyService private readonly _surveyService: ISurveyService) {