Skip to content

Commit cdb66ca

Browse files
authored
Add snooze button for completions / NES (microsoft#253024)
* Add snooze button for completions / NES * add stub and make sure service is in correct layer
1 parent 6237869 commit cdb66ca

File tree

9 files changed

+325
-9
lines changed

9 files changed

+325
-9
lines changed

src/vs/base/browser/ui/button/button.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,9 @@ export class Button extends Disposable implements IButton {
333333
}
334334

335335
setTitle(title: string) {
336-
if (!this._hover && title !== '') {
336+
if (this.options.hoverDelegate?.showNativeHover) {
337+
this._element.title = title;
338+
} else if (!this._hover && title !== '') {
337339
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._element, title));
338340
} else if (this._hover) {
339341
this._hover.update(title);

src/vs/base/browser/ui/toggle/toggle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ICheckboxStyles {
4040
readonly checkboxDisabledBackground: string | undefined;
4141
readonly checkboxDisabledForeground: string | undefined;
4242
readonly size?: number;
43+
readonly hoverDelegate?: IHoverDelegate;
4344
}
4445

4546
export const unthemedToggleStyles = {
@@ -272,7 +273,7 @@ export class Checkbox extends Widget {
272273
constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) {
273274
super();
274275

275-
this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: Checkbox.CLASS_NAME, ...unthemedToggleStyles }));
276+
this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: Checkbox.CLASS_NAME, hoverDelegate: styles.hoverDelegate, ...unthemedToggleStyles }));
276277

277278
this.domNode = this.checkbox.domNode;
278279

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { WindowIntervalTimer } from '../../../base/browser/dom.js';
7+
import { BugIndicatingError } from '../../../base/common/errors.js';
8+
import { Emitter, Event } from '../../../base/common/event.js';
9+
import { Disposable } from '../../../base/common/lifecycle.js';
10+
import { localize, localize2 } from '../../../nls.js';
11+
import { Action2 } from '../../../platform/actions/common/actions.js';
12+
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../platform/contextkey/common/contextkey.js';
13+
import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';
14+
import { createDecorator, ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
15+
import { IQuickInputService, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';
16+
17+
export const IInlineCompletionsService = createDecorator<IInlineCompletionsService>('IInlineCompletionsService');
18+
19+
export interface IInlineCompletionsService {
20+
readonly _serviceBrand: undefined;
21+
22+
onDidChangeIsSnoozing: Event<boolean>;
23+
24+
/**
25+
* Get the remaining time (in ms) for which inline completions should be snoozed,
26+
* or 0 if not snoozed.
27+
*/
28+
readonly snoozeTimeLeft: number;
29+
30+
/**
31+
* Snooze inline completions for the specified duration. If already snoozed, extend the snooze time.
32+
*/
33+
snooze(durationMs?: number): void;
34+
35+
/**
36+
* Snooze inline completions for the specified duration. If already snoozed, overwrite the existing snooze time.
37+
*/
38+
setSnoozeDuration(durationMs: number): void;
39+
40+
/**
41+
* Check if inline completions are currently snoozed.
42+
*/
43+
isSnoozing(): boolean;
44+
45+
/**
46+
* Cancel the current snooze.
47+
*/
48+
cancelSnooze(): void;
49+
}
50+
51+
const InlineCompletionsSnoozing = new RawContextKey<boolean>('inlineCompletions.snoozed', false, localize('inlineCompletions.snoozed', "Whether inline completions are currently snoozed"));
52+
53+
export class InlineCompletionsService extends Disposable implements IInlineCompletionsService {
54+
declare readonly _serviceBrand: undefined;
55+
56+
private _onDidChangeIsSnoozing = this._register(new Emitter<boolean>());
57+
readonly onDidChangeIsSnoozing: Event<boolean> = this._onDidChangeIsSnoozing.event;
58+
59+
private static readonly SNOOZE_DURATION = 300_000; // 5 minutes
60+
61+
private _snoozeTimeEnd: undefined | number = undefined;
62+
get snoozeTimeLeft(): number {
63+
if (this._snoozeTimeEnd === undefined) {
64+
return 0;
65+
}
66+
return Math.max(0, this._snoozeTimeEnd - Date.now());
67+
}
68+
69+
private _timer: WindowIntervalTimer;
70+
71+
constructor(@IContextKeyService private _contextKeyService: IContextKeyService) {
72+
super();
73+
74+
this._timer = this._register(new WindowIntervalTimer());
75+
76+
const inlineCompletionsSnoozing = InlineCompletionsSnoozing.bindTo(this._contextKeyService);
77+
this._register(this.onDidChangeIsSnoozing(() => inlineCompletionsSnoozing.set(this.isSnoozing())));
78+
}
79+
80+
snooze(durationMs: number = InlineCompletionsService.SNOOZE_DURATION): void {
81+
this.setSnoozeDuration(durationMs + this.snoozeTimeLeft);
82+
}
83+
84+
setSnoozeDuration(durationMs: number): void {
85+
const wasSnoozing = this.isSnoozing();
86+
87+
if (this._snoozeTimeEnd === undefined) {
88+
this._snoozeTimeEnd = Date.now() + durationMs;
89+
} else if (this.snoozeTimeLeft > 0) {
90+
this._snoozeTimeEnd += durationMs;
91+
} else {
92+
this._snoozeTimeEnd = Date.now() + durationMs;
93+
}
94+
95+
const isSnoozing = this.isSnoozing();
96+
if (wasSnoozing !== isSnoozing) {
97+
this._onDidChangeIsSnoozing.fire(isSnoozing);
98+
}
99+
100+
if (isSnoozing) {
101+
this._timer.cancelAndSet(
102+
() => {
103+
if (!this.isSnoozing()) {
104+
this._onDidChangeIsSnoozing.fire(false);
105+
} else {
106+
throw new BugIndicatingError('Snooze timer did not fire as expected');
107+
}
108+
},
109+
this.snoozeTimeLeft + 1,
110+
);
111+
}
112+
}
113+
114+
isSnoozing(): boolean {
115+
return this.snoozeTimeLeft > 0;
116+
}
117+
118+
cancelSnooze(): void {
119+
if (this.isSnoozing()) {
120+
this._snoozeTimeEnd = undefined;
121+
this._timer.cancel();
122+
this._onDidChangeIsSnoozing.fire(false);
123+
}
124+
}
125+
}
126+
127+
registerSingleton(IInlineCompletionsService, InlineCompletionsService, InstantiationType.Delayed);
128+
129+
const snoozeInlineSuggestId = 'editor.action.inlineSuggest.snooze';
130+
const cancelSnoozeInlineSuggestId = 'editor.action.inlineSuggest.cancelSnooze';
131+
132+
export class SnoozeInlineCompletion extends Action2 {
133+
public static ID = snoozeInlineSuggestId;
134+
constructor() {
135+
super({
136+
id: SnoozeInlineCompletion.ID,
137+
title: localize2('action.inlineSuggest.snooze', "Snooze Inline Suggestions"),
138+
precondition: ContextKeyExpr.true(),
139+
f1: true,
140+
});
141+
}
142+
143+
public async run(accessor: ServicesAccessor): Promise<void> {
144+
const quickInputService = accessor.get(IQuickInputService);
145+
const inlineCompletionsService = accessor.get(IInlineCompletionsService);
146+
147+
const items: IQuickPickItem[] = [
148+
{ label: '5 minutes', id: '5', picked: true },
149+
{ label: '10 minutes', id: '10' },
150+
{ label: '15 minutes', id: '15' },
151+
{ label: '30 minutes', id: '30' },
152+
{ label: '60 minutes', id: '60' }
153+
];
154+
155+
const picked = await quickInputService.pick(items, {
156+
placeHolder: localize('snooze.placeholder', "Select snooze duration")
157+
});
158+
159+
if (picked) {
160+
const minutes = parseInt(picked.id!, 10);
161+
const durationMs = minutes * 60 * 1000;
162+
inlineCompletionsService.setSnoozeDuration(durationMs);
163+
}
164+
}
165+
}
166+
167+
export class CancelSnoozeInlineCompletion extends Action2 {
168+
public static ID = cancelSnoozeInlineSuggestId;
169+
constructor() {
170+
super({
171+
id: CancelSnoozeInlineCompletion.ID,
172+
title: localize2('action.inlineSuggest.cancelSnooze', "Cancel Snooze Inline Suggestions"),
173+
precondition: InlineCompletionsSnoozing,
174+
f1: true,
175+
});
176+
}
177+
178+
public async run(accessor: ServicesAccessor): Promise<void> {
179+
accessor.get(IInlineCompletionsService).cancelSnooze();
180+
}
181+
}

src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ObservableContextKeyService } from '../utils.js';
3737
import { InlineCompletionsView } from '../view/inlineCompletionsView.js';
3838
import { inlineSuggestCommitId } from './commandIds.js';
3939
import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js';
40+
import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js';
4041

4142
export class InlineCompletionsController extends Disposable {
4243
private static readonly _instances = new Set<InlineCompletionsController>();
@@ -95,6 +96,7 @@ export class InlineCompletionsController extends Disposable {
9596
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
9697
@IKeybindingService private readonly _keybindingService: IKeybindingService,
9798
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
99+
@IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService,
98100
) {
99101
super();
100102
this._editorObs = observableCodeEditor(this.editor);
@@ -110,7 +112,8 @@ export class InlineCompletionsController extends Disposable {
110112
this._contextKeyService.onDidChangeContext,
111113
() => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true
112114
);
113-
this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader)));
115+
const isSnoozing = observableFromEvent(this, this._inlineCompletionsService.onDidChangeIsSnoozing, () => this._inlineCompletionsService.isSnoozing());
116+
this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && !isSnoozing.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader)));
114117
this._debounceValue = this._debounceService.for(
115118
this._languageFeaturesService.inlineCompletionsProvider,
116119
'InlineCompletionsDebounce',

src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWor
1212
import { InlineCompletionsController } from './controller/inlineCompletionsController.js';
1313
import { InlineCompletionsHoverParticipant } from './hintsWidget/hoverParticipant.js';
1414
import { InlineCompletionsAccessibleView } from './inlineCompletionsAccessibleView.js';
15+
import { CancelSnoozeInlineCompletion, SnoozeInlineCompletion } from '../../../browser/services/inlineCompletionsService.js';
1516

1617
registerEditorContribution(InlineCompletionsController.ID, wrapInHotClass1(InlineCompletionsController.hot), EditorContributionInstantiation.Eventually);
1718

@@ -28,6 +29,8 @@ registerEditorAction(HideInlineCompletion);
2829
registerEditorAction(JumpToNextInlineEdit);
2930
registerAction2(ToggleAlwaysShowInlineSuggestionToolbar);
3031
registerEditorAction(DevExtractReproSample);
32+
registerAction2(SnoozeInlineCompletion);
33+
registerAction2(CancelSnoozeInlineCompletion);
3134

3235
HoverParticipantRegistry.register(InlineCompletionsHoverParticipant);
3336
AccessibleViewRegistry.register(new InlineCompletionsAccessibleView());

src/vs/editor/standalone/browser/standaloneServices.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../../common/services/languageFeatureDebounce.js';
1010
import '../../common/services/semanticTokensStylingService.js';
1111
import '../../common/services/languageFeaturesService.js';
1212
import '../../browser/services/hoverService/hoverService.js';
13+
import '../../browser/services/inlineCompletionsService.js';
1314

1415
import * as strings from '../../../base/common/strings.js';
1516
import * as dom from '../../../base/browser/dom.js';

src/vs/editor/test/browser/testCodeEditor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { IUndoRedoService } from '../../../platform/undoRedo/common/undoRedo.js'
6161
import { UndoRedoService } from '../../../platform/undoRedo/common/undoRedoService.js';
6262
import { ITreeSitterLibraryService } from '../../common/services/treeSitter/treeSitterLibraryService.js';
6363
import { TestTreeSitterLibraryService } from '../common/services/testTreeSitterLibraryService.js';
64+
import { IInlineCompletionsService, InlineCompletionsService } from '../../browser/services/inlineCompletionsService.js';
6465

6566
export interface ITestCodeEditor extends IActiveCodeEditor {
6667
getViewModel(): ViewModel | undefined;
@@ -222,6 +223,7 @@ export function createCodeEditorServices(disposables: Pick<DisposableStore, 'add
222223
define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService);
223224
define(ILanguageFeaturesService, LanguageFeaturesService);
224225
define(ITreeSitterLibraryService, TestTreeSitterLibraryService);
226+
define(IInlineCompletionsService, InlineCompletionsService);
225227

226228
const instantiationService = disposables.add(new TestInstantiationService(services, true));
227229
disposables.add(toDisposable(() => {

0 commit comments

Comments
 (0)