Path: blob/main/src/vs/editor/browser/services/inlineCompletionsService.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { WindowIntervalTimer } from '../../../base/browser/dom.js';6import { BugIndicatingError } from '../../../base/common/errors.js';7import { Emitter, Event } from '../../../base/common/event.js';8import { Disposable } from '../../../base/common/lifecycle.js';9import { localize, localize2 } from '../../../nls.js';10import { Action2 } from '../../../platform/actions/common/actions.js';11import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../platform/contextkey/common/contextkey.js';12import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';13import { createDecorator, ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';14import { IQuickInputService, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';15import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';16import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';1718export const IInlineCompletionsService = createDecorator<IInlineCompletionsService>('IInlineCompletionsService');1920export interface IInlineCompletionsService {21readonly _serviceBrand: undefined;2223onDidChangeIsSnoozing: Event<boolean>;2425/**26* Get the remaining time (in ms) for which inline completions should be snoozed,27* or 0 if not snoozed.28*/29readonly snoozeTimeLeft: number;3031/**32* Snooze inline completions for the specified duration. If already snoozed, extend the snooze time.33*/34snooze(durationMs?: number): void;3536/**37* Snooze inline completions for the specified duration. If already snoozed, overwrite the existing snooze time.38*/39setSnoozeDuration(durationMs: number): void;4041/**42* Check if inline completions are currently snoozed.43*/44isSnoozing(): boolean;4546/**47* Cancel the current snooze.48*/49cancelSnooze(): void;5051/**52* Report an inline completion.53*/54reportNewCompletion(requestUuid: string): void;55}5657const InlineCompletionsSnoozing = new RawContextKey<boolean>('inlineCompletions.snoozed', false, localize('inlineCompletions.snoozed', "Whether inline completions are currently snoozed"));5859export class InlineCompletionsService extends Disposable implements IInlineCompletionsService {60declare readonly _serviceBrand: undefined;6162private _onDidChangeIsSnoozing = this._register(new Emitter<boolean>());63readonly onDidChangeIsSnoozing: Event<boolean> = this._onDidChangeIsSnoozing.event;6465private static readonly SNOOZE_DURATION = 300_000; // 5 minutes6667private _snoozeTimeEnd: undefined | number = undefined;68get snoozeTimeLeft(): number {69if (this._snoozeTimeEnd === undefined) {70return 0;71}72return Math.max(0, this._snoozeTimeEnd - Date.now());73}7475private _timer: WindowIntervalTimer;7677constructor(78@IContextKeyService private _contextKeyService: IContextKeyService,79@ITelemetryService private _telemetryService: ITelemetryService,80) {81super();8283this._timer = this._register(new WindowIntervalTimer());8485const inlineCompletionsSnoozing = InlineCompletionsSnoozing.bindTo(this._contextKeyService);86this._register(this.onDidChangeIsSnoozing(() => inlineCompletionsSnoozing.set(this.isSnoozing())));87}8889snooze(durationMs: number = InlineCompletionsService.SNOOZE_DURATION): void {90this.setSnoozeDuration(durationMs + this.snoozeTimeLeft);91}9293setSnoozeDuration(durationMs: number): void {94if (durationMs < 0) {95throw new BugIndicatingError(`Invalid snooze duration: ${durationMs}. Duration must be non-negative.`);96}97if (durationMs === 0) {98this.cancelSnooze();99return;100}101102const wasSnoozing = this.isSnoozing();103const timeLeft = this.snoozeTimeLeft;104105this._snoozeTimeEnd = Date.now() + durationMs;106107if (!wasSnoozing) {108this._onDidChangeIsSnoozing.fire(true);109}110111this._timer.cancelAndSet(112() => {113if (!this.isSnoozing()) {114this._onDidChangeIsSnoozing.fire(false);115} else {116throw new BugIndicatingError('Snooze timer did not fire as expected');117}118},119this.snoozeTimeLeft + 1,120);121122this._reportSnooze(durationMs - timeLeft, durationMs);123}124125isSnoozing(): boolean {126return this.snoozeTimeLeft > 0;127}128129cancelSnooze(): void {130if (this.isSnoozing()) {131this._reportSnooze(-this.snoozeTimeLeft, 0);132this._snoozeTimeEnd = undefined;133this._timer.cancel();134this._onDidChangeIsSnoozing.fire(false);135}136}137138private _lastCompletionId: string | undefined;139private _recentCompletionIds: string[] = [];140reportNewCompletion(requestUuid: string): void {141this._lastCompletionId = requestUuid;142143this._recentCompletionIds.unshift(requestUuid);144if (this._recentCompletionIds.length > 5) {145this._recentCompletionIds.pop();146}147}148149private _reportSnooze(deltaMs: number, totalMs: number): void {150const deltaSeconds = Math.round(deltaMs / 1000);151const totalSeconds = Math.round(totalMs / 1000);152type WorkspaceStatsClassification = {153owner: 'benibenj';154comment: 'Snooze duration for inline completions';155deltaSeconds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration by which the snooze has changed, in seconds.' };156totalSeconds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The total duration for which inline completions are snoozed, in seconds.' };157lastCompletionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the last completion.' };158recentCompletionIds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The IDs of the recent completions.' };159};160type WorkspaceStatsEvent = {161deltaSeconds: number;162totalSeconds: number;163lastCompletionId: string | undefined;164recentCompletionIds: string[];165};166this._telemetryService.publicLog2<WorkspaceStatsEvent, WorkspaceStatsClassification>('inlineCompletions.snooze', {167deltaSeconds,168totalSeconds,169lastCompletionId: this._lastCompletionId,170recentCompletionIds: this._recentCompletionIds,171});172}173}174175registerSingleton(IInlineCompletionsService, InlineCompletionsService, InstantiationType.Delayed);176177const snoozeInlineSuggestId = 'editor.action.inlineSuggest.snooze';178const cancelSnoozeInlineSuggestId = 'editor.action.inlineSuggest.cancelSnooze';179const LAST_SNOOZE_DURATION_KEY = 'inlineCompletions.lastSnoozeDuration';180181export class SnoozeInlineCompletion extends Action2 {182public static ID = snoozeInlineSuggestId;183constructor() {184super({185id: SnoozeInlineCompletion.ID,186title: localize2('action.inlineSuggest.snooze', "Snooze Inline Suggestions"),187precondition: ContextKeyExpr.true(),188f1: true,189});190}191192public async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {193const quickInputService = accessor.get(IQuickInputService);194const inlineCompletionsService = accessor.get(IInlineCompletionsService);195const storageService = accessor.get(IStorageService);196197let durationMinutes: number | undefined;198if (args.length > 0 && typeof args[0] === 'number') {199durationMinutes = args[0];200}201202if (!durationMinutes) {203durationMinutes = await this.getDurationFromUser(quickInputService, storageService);204}205206if (durationMinutes) {207inlineCompletionsService.setSnoozeDuration(durationMinutes);208}209}210211private async getDurationFromUser(quickInputService: IQuickInputService, storageService: IStorageService): Promise<number | undefined> {212const lastSelectedDuration = storageService.getNumber(LAST_SNOOZE_DURATION_KEY, StorageScope.PROFILE, 300_000);213214const items: (IQuickPickItem & { value: number })[] = [215{ label: '1 minute', id: '1', value: 60_000 },216{ label: '5 minutes', id: '5', value: 300_000 },217{ label: '10 minutes', id: '10', value: 600_000 },218{ label: '15 minutes', id: '15', value: 900_000 },219{ label: '30 minutes', id: '30', value: 1_800_000 },220{ label: '60 minutes', id: '60', value: 3_600_000 }221];222223const picked = await quickInputService.pick(items, {224placeHolder: localize('snooze.placeholder', "Select snooze duration for Code completions and NES"),225activeItem: items.find(item => item.value === lastSelectedDuration),226});227228if (picked) {229storageService.store(LAST_SNOOZE_DURATION_KEY, picked.value, StorageScope.PROFILE, StorageTarget.USER);230return picked.value;231}232233return undefined;234}235}236237export class CancelSnoozeInlineCompletion extends Action2 {238public static ID = cancelSnoozeInlineSuggestId;239constructor() {240super({241id: CancelSnoozeInlineCompletion.ID,242title: localize2('action.inlineSuggest.cancelSnooze', "Cancel Snooze Inline Suggestions"),243precondition: InlineCompletionsSnoozing,244f1: true,245});246}247248public async run(accessor: ServicesAccessor): Promise<void> {249accessor.get(IInlineCompletionsService).cancelSnooze();250}251}252253254