Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,43 @@

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';
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);
}
Expand All @@ -44,7 +51,7 @@ export class GhostTextProvider implements InlineCompletionItemProvider {
position: Position,
context: InlineCompletionContext,
token: CancellationToken
): Promise<InlineCompletionList | undefined> {
): Promise<GhostTextCompletionList | undefined> {
const textDocument = wrapDoc(vscodeDoc);
if (!textDocument) {
return;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');

Expand All @@ -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;
Expand All @@ -80,7 +79,7 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I
position: Position,
context: InlineCompletionContext,
token: CancellationToken
): Promise<InlineCompletionList | undefined> {
): Promise<GhostTextCompletionList | undefined> {
const logContext = new GhostTextContext(doc.uri.toString(), doc.version, context);
try {
return await this._provideInlineCompletionItems(doc, position, context, logContext, token);
Expand All @@ -98,7 +97,7 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I
context: InlineCompletionContext,
logContext: GhostTextContext,
token: CancellationToken
): Promise<InlineCompletionList | undefined> {
): Promise<GhostTextCompletionList | undefined> {
if (context.triggerKind === InlineCompletionTriggerKind.Automatic) {
if (!this.instantiationService.invokeFunction(isCompletionEnabledForDocument, doc)) {
return;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' };
}

Expand Down Expand Up @@ -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<vscode.InlineCompletionList | undefined> | undefined;
let completionsP: Promise<GhostTextCompletionList | undefined> | undefined;
if (this._completionsProvider) {
this._completionsRequestsInFlight.add(ct);
const disp = ct.onCancellationRequested(() => this._completionsRequestsInFlight.delete(ct));
Expand Down Expand Up @@ -604,7 +605,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple
}

private async _returnCompletionsOrOtherwiseNES(
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
completionsP: Promise<GhostTextCompletionList | undefined> | undefined,
nesP: Promise<NesCompletionList | undefined> | undefined,
docSnapshot: StringText,
sw: StopWatch,
Expand Down Expand Up @@ -644,7 +645,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple
}

private _returnCompletions(
completionsR: vscode.InlineCompletionList,
completionsR: GhostTextCompletionList,
nesDisposeReason: vscode.InlineCompletionsDisposeReason,
nesP: Promise<NesCompletionList | undefined> | undefined,
sw: StopWatch,
Expand All @@ -661,7 +662,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple
private _returnNES(
nesR: NesCompletionList,
completionsDisposeReason: vscode.InlineCompletionsDisposeReason,
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
completionsP: Promise<GhostTextCompletionList | undefined> | undefined,
sw: StopWatch,
tracer: ITracer,
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
Expand All @@ -683,7 +684,7 @@ class JointCompletionsProvider extends Disposable implements vscode.InlineComple

private static retainOnlyMeaningfulEdits<T extends vscode.InlineCompletionList>(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
) {
Expand All @@ -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);
Expand Down
Loading