mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-15 07:41:11 +01:00
because ghost text provider leaks and creating two of them results in a leak
614 lines
29 KiB
TypeScript
614 lines
29 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { join } from 'path';
|
|
import * as vscode from 'vscode';
|
|
import { IAuthenticationService, IExperimentationService } from '../../../lib/node/chatLibMain';
|
|
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
|
|
import { IEnvService } from '../../../platform/env/common/envService';
|
|
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
|
|
import { JointCompletionsProviderStrategy } from '../../../platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions';
|
|
import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext';
|
|
import { ObservableGit } from '../../../platform/inlineEdits/common/observableGit';
|
|
import { shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils';
|
|
import { NesHistoryContextProvider } from '../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';
|
|
import { ILogService } from '../../../platform/log/common/logService';
|
|
import * as errors from '../../../util/common/errors';
|
|
import { isNotebookCell } from '../../../util/common/notebooks';
|
|
import { createTracer, ITracer } from '../../../util/common/tracing';
|
|
import { coalesce } from '../../../util/vs/base/common/arrays';
|
|
import { assertNever, softAssert } from '../../../util/vs/base/common/assert';
|
|
import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async';
|
|
import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
|
|
import { Disposable } from '../../../util/vs/base/common/lifecycle';
|
|
import { autorun, derived, derivedDisposable, observableFromEvent } from '../../../util/vs/base/common/observable';
|
|
import { StopWatch } from '../../../util/vs/base/common/stopwatch';
|
|
import { URI } from '../../../util/vs/base/common/uri';
|
|
import { StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
|
|
import { Range } from '../../../util/vs/editor/common/core/range';
|
|
import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
|
|
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
|
|
import { IExtensionContribution } from '../../common/contributions';
|
|
import { registerUnificationCommands } from '../../completions-core/vscode-node/completionsServiceBridges';
|
|
import { CopilotInlineCompletionItemProvider } from '../../completions-core/vscode-node/extension/src/inlineCompletion';
|
|
import { ICopilotInlineCompletionItemProviderService } from '../../completions/common/copilotInlineCompletionItemProviderService';
|
|
import { CompletionsCoreContribution } from '../../completions/vscode-node/completionsCoreContribution';
|
|
import { unificationStateObservable } from '../../completions/vscode-node/completionsUnificationContribution';
|
|
import { TelemetrySender } from '../node/nextEditProviderTelemetry';
|
|
import { InlineEditDebugComponent, reportFeedbackCommandId } from './components/inlineEditDebugComponent';
|
|
import { LogContextRecorder } from './components/logContextRecorder';
|
|
import { DiagnosticsNextEditProvider } from './features/diagnosticsInlineEditProvider';
|
|
import { InlineCompletionProviderImpl, NesCompletionItem, NesCompletionList } from './inlineCompletionProvider';
|
|
import { InlineEditModel } from './inlineEditModel';
|
|
import { clearCacheCommandId, InlineEditProviderFeature, InlineEditProviderFeatureContribution, learnMoreCommandId, learnMoreLink, reportNotebookNESIssueCommandId } from './inlineEditProviderFeature';
|
|
import { InlineEditLogger } from './parts/inlineEditLogger';
|
|
import { VSCodeWorkspace } from './parts/vscodeWorkspace';
|
|
import { makeSettable } from './utils/observablesUtils';
|
|
|
|
export class JointCompletionsProviderContribution extends Disposable implements IExtensionContribution {
|
|
|
|
private readonly _inlineEditsProviderId = makeSettable(this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsProviderId, this._expService));
|
|
|
|
private readonly _hideInternalInterface = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsHideInternalInterface);
|
|
private readonly _enableDiagnosticsProvider = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.InlineEditsEnableDiagnosticsProvider, this._expService);
|
|
// FIXME@ulugbekna: re-enable when yieldTo is supported
|
|
// private readonly _yieldToCopilot = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsYieldToCopilot, this._expService);
|
|
private readonly _excludedProviders = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsExcludedProviders, this._expService).map(v => v ? v.split(',').map(v => v.trim()).filter(v => v !== '') : []);
|
|
private readonly _copilotToken = observableFromEvent(this, this._authenticationService.onDidAuthenticationChange, () => this._authenticationService.copilotToken);
|
|
|
|
public readonly inlineEditsEnabled = derived(this, (reader) => {
|
|
const copilotToken = this._copilotToken.read(reader);
|
|
if (copilotToken === undefined) {
|
|
return false;
|
|
}
|
|
if (copilotToken.isCompletionsQuotaExceeded) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
private readonly _internalActionsEnabled = derived(this, (reader) => {
|
|
return !!this._copilotToken.read(reader)?.isInternal && !this._hideInternalInterface.read(reader);
|
|
});
|
|
|
|
public readonly isInlineEditsLogFileEnabledObservable = this._configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsLogContextRecorderEnabled);
|
|
|
|
private readonly _workspace = derivedDisposable(this, _reader => {
|
|
return this._instantiationService.createInstance(VSCodeWorkspace);
|
|
});
|
|
|
|
|
|
constructor(
|
|
@IVSCodeExtensionContext private readonly _vscodeExtensionContext: IVSCodeExtensionContext,
|
|
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
|
@ICopilotInlineCompletionItemProviderService private readonly _copilotInlineCompletionItemProviderService: ICopilotInlineCompletionItemProviderService,
|
|
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
|
@IExperimentationService private readonly _expService: IExperimentationService,
|
|
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
|
|
@IEnvService private readonly _envService: IEnvService,
|
|
) {
|
|
super();
|
|
|
|
const useJointCompletionsProviderObs = _configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderEnabled, _expService);
|
|
|
|
this._register(autorun((reader) => { // FX
|
|
const useJointCompletionsProvider = useJointCompletionsProviderObs.read(reader);
|
|
if (!useJointCompletionsProvider) {
|
|
reader.store.add(_instantiationService.createInstance(InlineEditProviderFeatureContribution));
|
|
reader.store.add(_instantiationService.createInstance(CompletionsCoreContribution));
|
|
return;
|
|
}
|
|
|
|
const inlineEditFeature = _instantiationService.createInstance(InlineEditProviderFeature);
|
|
reader.store.add(inlineEditFeature.rolloutFeature());
|
|
inlineEditFeature.setContext();
|
|
|
|
const unificationState = unificationStateObservable(this);
|
|
|
|
reader.store.add(autorun((reader) => {
|
|
const unificationStateValue = unificationState.read(reader);
|
|
|
|
const excludes = this._excludedProviders.read(reader).slice();
|
|
|
|
let inlineEditProvider: InlineCompletionProviderImpl | undefined = undefined;
|
|
if (this.inlineEditsEnabled.read(reader)) {
|
|
const logger = reader.store.add(this._instantiationService.createInstance(InlineEditLogger));
|
|
|
|
const statelessProviderId = this._inlineEditsProviderId.read(reader);
|
|
|
|
const workspace = this._workspace.read(reader);
|
|
const git = reader.store.add(this._instantiationService.createInstance(ObservableGit));
|
|
const historyContextProvider = new NesHistoryContextProvider(workspace, git);
|
|
|
|
let diagnosticsProvider: DiagnosticsNextEditProvider | undefined = undefined;
|
|
if (this._enableDiagnosticsProvider.read(reader)) {
|
|
diagnosticsProvider = reader.store.add(this._instantiationService.createInstance(DiagnosticsNextEditProvider, workspace, git));
|
|
}
|
|
|
|
const model = reader.store.add(this._instantiationService.createInstance(InlineEditModel, statelessProviderId, workspace, historyContextProvider, diagnosticsProvider));
|
|
|
|
const recordingDirPath = join(this._vscodeExtensionContext.globalStorageUri.fsPath, 'logContextRecordings');
|
|
|
|
const isInlineEditLogFileEnabled = this.isInlineEditsLogFileEnabledObservable.read(reader);
|
|
|
|
let logContextRecorder: LogContextRecorder | undefined;
|
|
if (isInlineEditLogFileEnabled) {
|
|
logContextRecorder = reader.store.add(this._instantiationService.createInstance(LogContextRecorder, recordingDirPath, logger));
|
|
} else {
|
|
void LogContextRecorder.cleanupOldRecordings(recordingDirPath);
|
|
}
|
|
|
|
const inlineEditDebugComponent = reader.store.add(new InlineEditDebugComponent(this._internalActionsEnabled, this.inlineEditsEnabled, model.debugRecorder, this._inlineEditsProviderId));
|
|
|
|
const telemetrySender = reader.store.add(this._instantiationService.createInstance(TelemetrySender));
|
|
|
|
inlineEditProvider = this._instantiationService.createInstance(InlineCompletionProviderImpl, model, logger, logContextRecorder, inlineEditDebugComponent, telemetrySender);
|
|
|
|
reader.store.add(vscode.commands.registerCommand(learnMoreCommandId, () => {
|
|
this._envService.openExternal(URI.parse(learnMoreLink));
|
|
}));
|
|
|
|
reader.store.add(vscode.commands.registerCommand(clearCacheCommandId, () => {
|
|
model.nextEditProvider.clearCache();
|
|
}));
|
|
|
|
reader.store.add(vscode.commands.registerCommand(reportNotebookNESIssueCommandId, () => {
|
|
const activeNotebook = vscode.window.activeNotebookEditor;
|
|
const document = vscode.window.activeTextEditor?.document;
|
|
if (!activeNotebook || !document || !isNotebookCell(document.uri)) {
|
|
return;
|
|
}
|
|
const doc = model.workspace.getDocumentByTextDocument(document);
|
|
const selection = activeNotebook.selection;
|
|
if (!selection || !doc) {
|
|
return;
|
|
}
|
|
|
|
const logContext = new InlineEditRequestLogContext(doc.id.uri, document.version, undefined);
|
|
logContext.recordingBookmark = model.debugRecorder.createBookmark();
|
|
void vscode.commands.executeCommand(reportFeedbackCommandId, { logContext });
|
|
}));
|
|
}
|
|
|
|
let completionsProvider: CopilotInlineCompletionItemProvider | undefined;
|
|
{
|
|
const configEnabled = this._configurationService.getExperimentBasedConfigObservable<boolean>(ConfigKey.TeamInternal.InlineEditsEnableGhCompletionsProvider, this._expService).read(reader);
|
|
const extensionUnification = unificationStateValue?.extensionUnification ?? false;
|
|
|
|
// @ulugbekna: note that we don't want it if modelUnification is on
|
|
const modelUnification = unificationStateValue?.modelUnification ?? false;
|
|
if (!modelUnification || unificationStateValue?.codeUnification || extensionUnification || configEnabled || this._copilotToken.read(reader)?.isNoAuthUser) {
|
|
completionsProvider = this._copilotInlineCompletionItemProviderService.getOrCreateProvider() as CopilotInlineCompletionItemProvider;
|
|
}
|
|
|
|
void vscode.commands.executeCommand('setContext', 'github.copilot.extensionUnification.activated', extensionUnification);
|
|
|
|
if (extensionUnification && completionsProvider) {
|
|
const completionsInstaService = this._copilotInlineCompletionItemProviderService.getOrCreateInstantiationService();
|
|
reader.store.add(completionsInstaService.invokeFunction(registerUnificationCommands));
|
|
}
|
|
}
|
|
|
|
const singularProvider = reader.store.add(this._instantiationService.createInstance(JointCompletionsProvider, completionsProvider, inlineEditProvider));
|
|
|
|
if (unificationStateValue?.modelUnification) {
|
|
if (!excludes.includes('github.copilot')) {
|
|
excludes.push('github.copilot');
|
|
}
|
|
}
|
|
|
|
reader.store.add(vscode.languages.registerInlineCompletionItemProvider(
|
|
'*',
|
|
singularProvider,
|
|
{
|
|
displayName: inlineEditProvider?.displayName,
|
|
debounceDelayMs: 0, // set 0 debounce to ensure consistent delays/timings
|
|
groupId: 'nes',
|
|
excludes,
|
|
})
|
|
);
|
|
|
|
}));
|
|
}));
|
|
}
|
|
}
|
|
|
|
type SingularCompletionItem =
|
|
| ({ source: 'completions' } & vscode.InlineCompletionItem)
|
|
| ({ source: 'inlineEdits' } & NesCompletionItem)
|
|
;
|
|
|
|
type SingularCompletionList =
|
|
| ({ source: 'completions' } & vscode.InlineCompletionList)
|
|
| ({ source: 'inlineEdits' } & NesCompletionList)
|
|
;
|
|
|
|
function toCompletionsList(list: vscode.InlineCompletionList): SingularCompletionList {
|
|
return { ...list, items: list.items.map(item => ({ ...item, source: 'completions' })), source: 'completions' };
|
|
}
|
|
|
|
function toInlineEditsList(list: NesCompletionList): SingularCompletionList {
|
|
return { ...list, items: list.items.map(item => ({ ...item, source: 'inlineEdits' })), source: 'inlineEdits' };
|
|
}
|
|
|
|
type LastNesSuggestion = {
|
|
docUri: vscode.Uri;
|
|
docVersionId: number;
|
|
docWithNesEditApplied: StringText;
|
|
};
|
|
|
|
class JointCompletionsProvider extends Disposable implements vscode.InlineCompletionItemProvider {
|
|
|
|
public onDidChange?: vscode.Event<void> | undefined;
|
|
|
|
constructor(
|
|
private readonly _completionsProvider: CopilotInlineCompletionItemProvider | undefined,
|
|
private readonly _inlineEditProvider: InlineCompletionProviderImpl | undefined,
|
|
@IConfigurationService private readonly _configService: IConfigurationService,
|
|
@IExperimentationService private readonly _expService: IExperimentationService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
) {
|
|
super();
|
|
this.onDidChange = _inlineEditProvider?.onDidChange;
|
|
softAssert(
|
|
_completionsProvider?.onDidChange === undefined,
|
|
'CompletionsProvider does not implement onDidChange'
|
|
);
|
|
}
|
|
|
|
public async provideInlineCompletionItems(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken): Promise<SingularCompletionList | undefined> {
|
|
const tracer = createTracer(['JointCompletionsProvider', shortenOpportunityId(context.requestUuid), 'provideInlineCompletionItems'], (msg) => this._logService.trace(msg));
|
|
|
|
const strategy = this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsJointCompletionsProviderStrategy, this._expService);
|
|
|
|
switch (strategy) {
|
|
case JointCompletionsProviderStrategy.Regular:
|
|
return this.provideInlineCompletionItemsRegular(document, position, context, token, tracer);
|
|
default:
|
|
assertNever(strategy);
|
|
}
|
|
}
|
|
|
|
private lastNesSuggestion: null | LastNesSuggestion = null;
|
|
private provideInlineCompletionItemsInvocationCount = 0;
|
|
|
|
private async provideInlineCompletionItemsRegular(document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, token: vscode.CancellationToken, tracer: ITracer): Promise<SingularCompletionList | undefined> {
|
|
|
|
const invocationId = ++this.provideInlineCompletionItemsInvocationCount;
|
|
const completionsCts = new CancellationTokenSource(token);
|
|
const nesCts = new CancellationTokenSource(token);
|
|
|
|
let saveLastNesSuggestion: null | LastNesSuggestion = null;
|
|
try {
|
|
const docSnapshot = new StringText(document.getText());
|
|
const docVersionId = document.version;
|
|
|
|
if (this.lastNesSuggestion && this.lastNesSuggestion.docUri.toString() !== document.uri.toString()) {
|
|
tracer.trace('last NES suggestion is not for the current document, ignoring');
|
|
this.lastNesSuggestion = null;
|
|
}
|
|
|
|
const list = await this._provideInlineCompletionItemsRegular({ document, docSnapshot }, position, this.lastNesSuggestion, context, tracer, { coreToken: token, completionsCts, nesCts });
|
|
|
|
if (token.isCancellationRequested) {
|
|
return list;
|
|
}
|
|
|
|
if (!list || list.source !== 'inlineEdits' || list.items.length === 0) {
|
|
return list;
|
|
}
|
|
|
|
const firstItem = list.items[0];
|
|
if (!firstItem.range || typeof firstItem.insertText !== 'string') {
|
|
return list;
|
|
}
|
|
|
|
if (firstItem.uri && firstItem.uri.toString() !== document.uri.toString()) {
|
|
tracer.trace(`The NES suggestion is for a different document (${firstItem.uri.toString()} vs ${document.uri.toString()}), not saving as last NES suggestion`);
|
|
return list;
|
|
}
|
|
|
|
const applied = applyTextEdit(docSnapshot, firstItem.range, firstItem.insertText);
|
|
saveLastNesSuggestion = {
|
|
docUri: document.uri,
|
|
docVersionId,
|
|
docWithNesEditApplied: new StringText(applied),
|
|
};
|
|
|
|
return list;
|
|
} finally {
|
|
|
|
// Only save the last NES suggestion if this is the latest invocation
|
|
if (invocationId === this.provideInlineCompletionItemsInvocationCount) {
|
|
this.lastNesSuggestion = saveLastNesSuggestion;
|
|
if (this.lastNesSuggestion) {
|
|
tracer.trace(`Set the last NES suggestion for document ${this.lastNesSuggestion.docUri.toString()}`);
|
|
} else {
|
|
tracer.trace(`Cleared the last NES suggestion`);
|
|
}
|
|
} else {
|
|
tracer.trace(`Not setting the last NES suggestion because a newer invocation exists`);
|
|
}
|
|
|
|
completionsCts.dispose();
|
|
nesCts.dispose();
|
|
}
|
|
}
|
|
|
|
private async _provideInlineCompletionItemsRegular(
|
|
{ document, docSnapshot }: { document: vscode.TextDocument; docSnapshot: StringText },
|
|
position: vscode.Position,
|
|
lastNesSuggestion: null | LastNesSuggestion,
|
|
context: vscode.InlineCompletionContext,
|
|
tracer: ITracer,
|
|
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
|
|
): Promise<SingularCompletionList | undefined> {
|
|
|
|
const sw = new StopWatch();
|
|
|
|
if (this._completionsProvider === undefined && this._inlineEditProvider === undefined) {
|
|
tracer.returns('neither completions nor NES provider available');
|
|
return undefined;
|
|
}
|
|
|
|
tracer.trace('requesting completions and/or NES');
|
|
|
|
if (!lastNesSuggestion) {
|
|
// prefer completions unless there are none
|
|
tracer.trace(`no last NES suggestion to consider`);
|
|
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
|
|
const nesP = this._invokeNESProvider(tracer, document, position, context, tokens, sw);
|
|
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
|
|
}
|
|
|
|
tracer.trace(`last NES suggestion is for the current document, checking if it agrees with the current suggestion`);
|
|
|
|
const nesP = this._invokeNESProvider(tracer, document, position, context, tokens, sw);
|
|
if (!nesP) {
|
|
tracer.trace(`no NES provider`);
|
|
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
|
|
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
|
|
}
|
|
|
|
const NES_CACHE_WAIT_MS = 10;
|
|
// scoping the variables
|
|
{
|
|
tracer.trace(`giving the NES provider ${NES_CACHE_WAIT_MS}ms to return what it has in its cache`);
|
|
const fastNesResult = await raceCancellation(
|
|
raceTimeout(
|
|
nesP,
|
|
NES_CACHE_WAIT_MS
|
|
),
|
|
tokens.coreToken
|
|
);
|
|
|
|
// got nes quickly
|
|
if (fastNesResult && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (fastNesResult.items as NesCompletionItem[]).at(0))) {
|
|
tracer.trace('last NES suggestion agrees with the current suggestion, using NES');
|
|
const list: SingularCompletionList = toInlineEditsList(fastNesResult);
|
|
tracer.returns(`returning NES result in ${sw.elapsed()}ms`);
|
|
return list;
|
|
}
|
|
|
|
if (tokens.coreToken.isCancellationRequested) {
|
|
tracer.trace(`suggestions request was cancelled`);
|
|
void setEndOfLifeReason(this._completionsProvider, undefined, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
|
|
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
|
|
tokens.completionsCts.cancel();
|
|
tokens.nesCts.cancel();
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
tracer.trace(`the NES provider did not return in ${NES_CACHE_WAIT_MS}ms so we are triggering the completions provider too`);
|
|
const completionsP = this._invokeCompletionsProvider(tracer, document, position, context, tokens, sw);
|
|
|
|
const suggestionsList = await raceCancellation(
|
|
Promise.race(coalesce([
|
|
completionsP?.then(res => ({ type: 'completions' as const, res })),
|
|
nesP?.then(res => ({ type: 'nes' as const, res })),
|
|
])),
|
|
tokens.coreToken
|
|
);
|
|
|
|
// got cancelled
|
|
if (suggestionsList === undefined) {
|
|
tracer.trace(`suggestions request was cancelled`);
|
|
void setEndOfLifeReason(this._completionsProvider, completionsP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
|
|
void setEndOfLifeReason(this._inlineEditProvider, nesP, { kind: vscode.InlineCompletionsDisposeReasonKind.TokenCancellation });
|
|
tokens.completionsCts.cancel();
|
|
tokens.nesCts.cancel();
|
|
return undefined;
|
|
}
|
|
|
|
// got NES first
|
|
if (suggestionsList.type === 'nes' && suggestionsList.res && this.doesNesSuggestionAgree(docSnapshot, lastNesSuggestion.docWithNesEditApplied, (suggestionsList.res.items as NesCompletionItem[]).at(0))) {
|
|
tracer.trace('last NES suggestion agrees with the current suggestion, using NES');
|
|
return this._returnNES(suggestionsList.res, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, tracer, tokens);
|
|
}
|
|
|
|
tracer.trace('falling back to the default because completions came first or NES disagreed');
|
|
return this._returnCompletionsOrOtherwiseNES(completionsP, nesP, sw, tracer, tokens);
|
|
}
|
|
|
|
private _invokeNESProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) {
|
|
let nesP: Promise<NesCompletionList | undefined> | undefined;
|
|
if (this._inlineEditProvider) {
|
|
tracer.trace(`- requesting NES provideInlineCompletionItems`);
|
|
nesP = this._inlineEditProvider.provideInlineCompletionItems(document, position, context, tokens.nesCts.token);
|
|
nesP.then((nesR) => {
|
|
tracer.trace(`got NES response in ${sw.elapsed()}ms -- ${nesR === undefined ? 'undefined' : `with ${nesR.items.length} items`}`);
|
|
}).catch((e) => {
|
|
tracer.trace(`NES provider errored after ${sw.elapsed()}ms -- ${errors.toString(errors.fromUnknown(e))}`);
|
|
});
|
|
} else {
|
|
tracer.trace(`- no NES provider`);
|
|
nesP = undefined;
|
|
}
|
|
return nesP;
|
|
}
|
|
|
|
private _invokeCompletionsProvider(tracer: ITracer, document: vscode.TextDocument, position: vscode.Position, context: vscode.InlineCompletionContext, tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource }, sw: StopWatch) {
|
|
let completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined;
|
|
if (this._completionsProvider) {
|
|
tracer.trace(`- requesting completions provideInlineCompletionItems`);
|
|
completionsP = this._completionsProvider.provideInlineCompletionItems(document, position, context, tokens.completionsCts.token);
|
|
completionsP.then((completionsR) => {
|
|
tracer.trace(`got completions response in ${sw.elapsed()}ms -- ${completionsR === undefined ? 'undefined' : `with ${completionsR.items.length} items`}`);
|
|
}).catch((e) => {
|
|
tracer.trace(`completions provider errored after ${sw.elapsed()}ms -- ${errors.toString(errors.fromUnknown(e))}`);
|
|
});
|
|
} else {
|
|
tracer.trace(`- no completions provider`);
|
|
completionsP = undefined;
|
|
}
|
|
return completionsP;
|
|
}
|
|
|
|
private async _returnCompletionsOrOtherwiseNES(
|
|
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
|
|
nesP: Promise<NesCompletionList | undefined> | undefined,
|
|
sw: StopWatch,
|
|
tracer: ITracer,
|
|
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
|
|
) {
|
|
tracer.trace(`waiting for completions and/or NES responses`);
|
|
|
|
const completionsR = completionsP ? await completionsP : undefined;
|
|
tracer.trace(`completions response received`);
|
|
|
|
if (completionsR && completionsR.items.length > 0) {
|
|
tracer.trace(`using completions response, cancelling NES provider`);
|
|
return this._returnCompletions(completionsR, { kind: vscode.InlineCompletionsDisposeReasonKind.LostRace }, nesP, sw, tracer, tokens);
|
|
}
|
|
|
|
const nesR = nesP ? await nesP : undefined;
|
|
tracer.trace(`NES response received`);
|
|
|
|
if (nesR && nesR.items.length > 0) {
|
|
tracer.trace(`using NES response`);
|
|
return this._returnNES(nesR, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, completionsP, sw, tracer, tokens);
|
|
}
|
|
|
|
// return empty completions
|
|
return this._returnCompletions(completionsR ?? { items: [] }, { kind: vscode.InlineCompletionsDisposeReasonKind.NotTaken }, nesP, sw, tracer, tokens);
|
|
}
|
|
|
|
private _returnCompletions(
|
|
completionsR: vscode.InlineCompletionList,
|
|
nesDisposeReason: vscode.InlineCompletionsDisposeReason,
|
|
nesP: Promise<NesCompletionList | undefined> | undefined,
|
|
sw: StopWatch,
|
|
tracer: ITracer,
|
|
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
|
|
) {
|
|
void setEndOfLifeReason(this._inlineEditProvider, nesP, nesDisposeReason);
|
|
tokens.nesCts.cancel(); // cancel NES request if still pending
|
|
const list: SingularCompletionList = toCompletionsList(completionsR);
|
|
tracer.returns(`use completions response in ${sw.elapsed()}ms`);
|
|
return list;
|
|
}
|
|
|
|
private _returnNES(
|
|
nesR: NesCompletionList,
|
|
completionsDisposeReason: vscode.InlineCompletionsDisposeReason,
|
|
completionsP: Promise<vscode.InlineCompletionList | undefined> | undefined,
|
|
sw: StopWatch,
|
|
tracer: ITracer,
|
|
tokens: { coreToken: CancellationToken; completionsCts: CancellationTokenSource; nesCts: CancellationTokenSource },
|
|
) {
|
|
void setEndOfLifeReason(this._completionsProvider, completionsP, completionsDisposeReason);
|
|
tokens.completionsCts.cancel(); // cancel completions request if still pending
|
|
const list: SingularCompletionList = toInlineEditsList(nesR);
|
|
tracer.returns(`returning NES result in ${sw.elapsed()}ms`);
|
|
return list;
|
|
}
|
|
|
|
private doesNesSuggestionAgree(doc: StringText, docWithNesEditApplied: StringText, nesEdit: NesCompletionItem | undefined): boolean {
|
|
if (nesEdit === undefined || nesEdit.range === undefined || typeof nesEdit.insertText !== 'string') {
|
|
return false;
|
|
}
|
|
const applied = applyTextEdit(doc, nesEdit.range, nesEdit.insertText);
|
|
return applied === docWithNesEditApplied.getValue();
|
|
}
|
|
|
|
public handleDidShowCompletionItem?(completionItem: SingularCompletionItem, updatedInsertText: string): void {
|
|
switch (completionItem.source) {
|
|
case 'completions':
|
|
this._completionsProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);
|
|
break;
|
|
case 'inlineEdits':
|
|
this._inlineEditProvider?.handleDidShowCompletionItem?.(completionItem, updatedInsertText);
|
|
break;
|
|
default:
|
|
assertNever(completionItem);
|
|
}
|
|
}
|
|
|
|
public handleDidPartiallyAcceptCompletionItem?(completionItem: SingularCompletionItem, acceptedLength: number & vscode.PartialAcceptInfo): void {
|
|
switch (completionItem.source) {
|
|
case 'completions':
|
|
this._completionsProvider?.handleDidPartiallyAcceptCompletionItem?.(completionItem, acceptedLength);
|
|
break;
|
|
case 'inlineEdits':
|
|
softAssert(this._inlineEditProvider?.handleDidPartiallyAcceptCompletionItem === undefined, 'InlineEditProvider does not implement handleDidPartiallyAcceptCompletionItem');
|
|
break;
|
|
default:
|
|
assertNever(completionItem);
|
|
}
|
|
}
|
|
|
|
public handleEndOfLifetime?(completionItem: SingularCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {
|
|
switch (completionItem.source) {
|
|
case 'completions':
|
|
this._completionsProvider?.handleEndOfLifetime?.(completionItem, reason);
|
|
break;
|
|
case 'inlineEdits':
|
|
this._inlineEditProvider?.handleEndOfLifetime?.(completionItem, reason);
|
|
break;
|
|
default:
|
|
assertNever(completionItem);
|
|
}
|
|
}
|
|
|
|
public handleListEndOfLifetime?(list: SingularCompletionList, reason: vscode.InlineCompletionsDisposeReason): void {
|
|
switch (list.source) {
|
|
case 'completions':
|
|
softAssert(this._completionsProvider?.handleListEndOfLifetime === undefined, 'CompletionsProvider does not implement handleListEndOfLifetime');
|
|
break;
|
|
case 'inlineEdits':
|
|
this._inlineEditProvider?.handleListEndOfLifetime?.(list, reason);
|
|
break;
|
|
default:
|
|
assertNever(list);
|
|
}
|
|
}
|
|
|
|
// neither provider implements this deprecated method
|
|
public handleDidRejectCompletionItem = undefined;
|
|
}
|
|
|
|
function applyTextEdit(doc: StringText, range: vscode.Range, insertText: string): string {
|
|
const rangeOneBased = new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);
|
|
const offsetRange = doc.getTransformer().getOffsetRange(rangeOneBased);
|
|
const edit = new StringReplacement(offsetRange, insertText);
|
|
const bigEdit = edit.toEdit();
|
|
return bigEdit.apply(doc.getValue());
|
|
}
|
|
|
|
async function setEndOfLifeReason(provider: vscode.InlineCompletionItemProvider | undefined, promise: Promise<vscode.InlineCompletionList | undefined> | undefined, reason: vscode.InlineCompletionsDisposeReason) {
|
|
if (promise === undefined) {
|
|
return;
|
|
}
|
|
const result = await promise;
|
|
if (result === undefined) {
|
|
return;
|
|
}
|
|
for (const item of result.items) {
|
|
provider?.handleEndOfLifetime?.(item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false });
|
|
}
|
|
provider?.handleListEndOfLifetime?.(result, reason);
|
|
}
|