Skip to content

Commit 0692340

Browse files
authored
Merge pull request microsoft#201314 from microsoft/joh/bumpy-bonobo
first version of quick voice that starts inline chat with what you said
2 parents d0d2200 + ee6baba commit 0692340

File tree

14 files changed

+447
-46
lines changed

14 files changed

+447
-46
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,13 @@ export class StandaloneKeybindingService extends AbstractKeybindingService {
594594
public registerSchemaContribution(contribution: KeybindingsSchemaContribution): void {
595595
// noop
596596
}
597+
598+
/**
599+
* not yet supported
600+
*/
601+
public override enableKeybindingHoldMode(commandId: string): Promise<void> | undefined {
602+
return undefined;
603+
}
597604
}
598605

599606
class DomNodeListeners extends Disposable {

src/vs/platform/keybinding/common/abstractKeybindingService.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
5353
private _ignoreSingleModifiers: KeybindingModifierSet;
5454
private _currentSingleModifier: SingleModifierChord | null;
5555
private _currentSingleModifierClearTimeout: TimeoutTimer;
56+
protected _currentlyDispatchingCommandId: string | null;
5657

5758
protected _logging: boolean;
5859

@@ -75,6 +76,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
7576
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
7677
this._currentSingleModifier = null;
7778
this._currentSingleModifierClearTimeout = new TimeoutTimer();
79+
this._currentlyDispatchingCommandId = null;
7880
this._logging = false;
7981
}
8082

@@ -362,10 +364,15 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
362364
}
363365

364366
this._log(`+ Invoking command ${resolveResult.commandId}.`);
365-
if (typeof resolveResult.commandArgs === 'undefined') {
366-
this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err));
367-
} else {
368-
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
367+
this._currentlyDispatchingCommandId = resolveResult.commandId;
368+
try {
369+
if (typeof resolveResult.commandArgs === 'undefined') {
370+
this._commandService.executeCommand(resolveResult.commandId).then(undefined, err => this._notificationService.warn(err));
371+
} else {
372+
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
373+
}
374+
} finally {
375+
this._currentlyDispatchingCommandId = null;
369376
}
370377

371378
if (!HIGH_FREQ_COMMANDS.test(resolveResult.commandId)) {
@@ -378,6 +385,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
378385
}
379386
}
380387

388+
abstract enableKeybindingHoldMode(commandId: string): Promise<void> | undefined;
389+
381390
mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
382391
if (event.ctrlKey || event.metaKey) {
383392
// ignore ctrl/cmd-combination but not shift/alt-combinatios

src/vs/platform/keybinding/common/keybinding.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ export interface IKeybindingService {
6565
*/
6666
softDispatch(keyboardEvent: IKeyboardEvent, target: IContextKeyServiceTarget): ResolutionResult;
6767

68+
/**
69+
* Enable hold mode for this command. This is only possible if the command is current being dispatched, meaning
70+
* we are after its keydown and before is keyup event.
71+
*
72+
* @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled.
73+
*/
74+
enableKeybindingHoldMode(commandId: string): Promise<void> | undefined;
75+
6876
dispatchByUserSettingsLabel(userSettingsLabel: string, target: IContextKeyServiceTarget): void;
6977

7078
/**
@@ -100,4 +108,3 @@ export interface IKeybindingService {
100108
_dumpDebugInfo(): string;
101109
_dumpDebugInfoJSON(): string;
102110
}
103-

src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ suite('AbstractKeybindingService', () => {
9595
public registerSchemaContribution() {
9696
// noop
9797
}
98+
99+
public enableKeybindingHoldMode() {
100+
return undefined;
101+
}
98102
}
99103

100104
let createTestKeybindingService: (items: ResolvedKeybindingItem[], contextValue?: any) => TestKeybindingService = null!;

src/vs/platform/keybinding/test/common/mockKeybindingService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ export class MockKeybindingService implements IKeybindingService {
147147
return false;
148148
}
149149

150+
public enableKeybindingHoldMode(commandId: string): undefined {
151+
return undefined;
152+
}
153+
150154
public mightProducePrintableCharacter(e: IKeyboardEvent): boolean {
151155
return false;
152156
}

src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { registerAction2 } from 'vs/platform/actions/common/actions';
76
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
7+
import { registerAction2 } from 'vs/platform/actions/common/actions';
88
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
99
import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions';
1010
import { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
1111
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
1212
import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
13-
import { InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl';
14-
import { IInlineChatSessionService } from './inlineChatSessionService';
1513
import { Registry } from 'vs/platform/registry/common/platform';
1614
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
1715
import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook';
1816
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
19-
import { InlineChatAccessibleViewContribution } from './inlineChatAccessibleView';
2017
import { InlineChatSavingServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl';
21-
import { IInlineChatSavingService } from './inlineChatSavingService';
18+
import { InlineChatAccessibleViewContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView';
19+
import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService';
20+
import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService';
21+
import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl';
22+
23+
24+
// --- browser
2225

2326
registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed);
2427
registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed);
@@ -27,7 +30,6 @@ registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, Instant
2730
registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors
2831
registerEditorContribution(INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID, InlineChatActions.InlineAccessibilityHelpContribution, EditorContributionInstantiation.Eventually);
2932

30-
registerAction2(InlineChatActions.StartSessionAction);
3133
registerAction2(InlineChatActions.CloseAction);
3234
registerAction2(InlineChatActions.ConfigureInlineChatAction);
3335
// registerAction2(InlineChatActions.UnstashSessionAction);

src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Codicon } from 'vs/base/common/codicons';
7-
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
7+
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
88
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
99
import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
1010
import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
1111
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
12-
import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
12+
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
1313
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
1414
import { localize, localize2 } from 'vs/nls';
1515
import { IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions';
@@ -33,37 +33,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
3333

3434
CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start');
3535
export const LOCALIZED_START_INLINE_CHAT_STRING = localize('run', 'Start Inline Chat');
36-
const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.'));
37-
38-
export class StartSessionAction extends EditorAction2 {
39-
40-
constructor() {
41-
super({
42-
id: 'inlineChat.start',
43-
title: { value: LOCALIZED_START_INLINE_CHAT_STRING, original: 'Start Inline Chat' },
44-
category: AbstractInlineChatAction.category,
45-
f1: true,
46-
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable),
47-
keybinding: {
48-
when: EditorContextKeys.focus,
49-
weight: KeybindingWeight.WorkbenchContrib,
50-
primary: KeyMod.CtrlCmd | KeyCode.KeyI,
51-
secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI)],
52-
},
53-
icon: START_INLINE_CHAT
54-
});
55-
}
56-
57-
58-
override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) {
59-
let options: InlineChatRunOptions | undefined;
60-
const arg = _args[0];
61-
if (arg && InlineChatRunOptions.isInteractiveEditorOptions(arg)) {
62-
options = arg;
63-
}
64-
InlineChatController.get(editor)?.run({ ...options });
65-
}
66-
}
36+
export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.'));
6737

6838
export class UnstashSessionAction extends EditorAction2 {
6939
constructor() {
@@ -93,7 +63,7 @@ export class UnstashSessionAction extends EditorAction2 {
9363
}
9464
}
9565

96-
abstract class AbstractInlineChatAction extends EditorAction2 {
66+
export abstract class AbstractInlineChatAction extends EditorAction2 {
9767

9868
static readonly category = { value: localize('cat', 'Inline Chat'), original: 'Inline Chat' };
9969

src/vs/workbench/contrib/inlineChat/common/inlineChat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export const enum InlineChatConfigKeys {
202202
Mode = 'inlineChat.mode',
203203
FinishOnType = 'inlineChat.finishOnType',
204204
AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave',
205+
HoldToSpeech = 'inlineChat.holdToSpeech',
205206
}
206207

207208
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
@@ -227,6 +228,11 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
227228
description: localize('acceptedOrDiscardBeforeSave', "Whether pending inline chat sessions prevent saving."),
228229
default: true,
229230
type: 'boolean'
231+
},
232+
[InlineChatConfigKeys.HoldToSpeech]: {
233+
description: localize('holdToSpeech', "Whether holding the inline chat keybinding will automatically enable speech recognition."),
234+
default: true,
235+
type: 'boolean'
230236
}
231237
}
232238
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
7+
import { registerAction2 } from 'vs/platform/actions/common/actions';
8+
import { CancelAction, InlineChatQuickVoice, StartAction, StopAction } from 'vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice';
9+
import * as StartSessionAction from './inlineChatActions';
10+
11+
// start and hold for voice
12+
13+
registerAction2(StartSessionAction.StartSessionAction);
14+
15+
// quick voice
16+
17+
registerEditorContribution(InlineChatQuickVoice.ID, InlineChatQuickVoice, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors
18+
registerAction2(StartAction);
19+
registerAction2(StopAction);
20+
registerAction2(CancelAction);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
6+
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
7+
import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
8+
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
9+
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
10+
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
11+
import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
12+
import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions';
13+
import { LOCALIZED_START_INLINE_CHAT_STRING, START_INLINE_CHAT } from '../browser/inlineChatActions';
14+
import { disposableTimeout } from 'vs/base/common/async';
15+
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
16+
import { ICommandService } from 'vs/platform/commands/common/commands';
17+
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
18+
import { StartVoiceChatAction, StopListeningAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions';
19+
import { CTX_INLINE_CHAT_HAS_PROVIDER, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
20+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
21+
import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService';
22+
23+
24+
export class StartSessionAction extends EditorAction2 {
25+
26+
constructor() {
27+
super({
28+
id: 'inlineChat.start',
29+
title: { value: LOCALIZED_START_INLINE_CHAT_STRING, original: 'Start Inline Chat' },
30+
category: AbstractInlineChatAction.category,
31+
f1: true,
32+
precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable),
33+
keybinding: {
34+
when: EditorContextKeys.focus,
35+
weight: KeybindingWeight.WorkbenchContrib,
36+
primary: KeyMod.CtrlCmd | KeyCode.KeyI,
37+
secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI)],
38+
},
39+
icon: START_INLINE_CHAT
40+
});
41+
}
42+
43+
44+
override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) {
45+
46+
const configService = accessor.get(IConfigurationService);
47+
const speechService = accessor.get(ISpeechService);
48+
const keybindingService = accessor.get(IKeybindingService);
49+
const commandService = accessor.get(ICommandService);
50+
51+
if (configService.getValue<boolean>(InlineChatConfigKeys.HoldToSpeech) // enabled
52+
&& speechService.hasSpeechProvider // possible
53+
) {
54+
55+
const holdMode = keybindingService.enableKeybindingHoldMode(this.desc.id);
56+
if (holdMode) { // holding keys
57+
let listening = false;
58+
const handle = disposableTimeout(() => {
59+
// start VOICE input
60+
commandService.executeCommand(StartVoiceChatAction.ID);
61+
listening = true;
62+
}, 100);
63+
64+
holdMode.finally(() => {
65+
if (listening) {
66+
commandService.executeCommand(StopListeningAction.ID).finally(() => {
67+
InlineChatController.get(editor)?.acceptInput();
68+
});
69+
}
70+
handle.dispose();
71+
});
72+
}
73+
}
74+
75+
let options: InlineChatRunOptions | undefined;
76+
const arg = _args[0];
77+
if (arg && InlineChatRunOptions.isInteractiveEditorOptions(arg)) {
78+
options = arg;
79+
}
80+
InlineChatController.get(editor)?.run({ ...options });
81+
}
82+
}

0 commit comments

Comments
 (0)