Skip to content

Commit 37740ec

Browse files
authored
add hold for speech command to inline chat which is for when a session already started (microsoft#202741)
* - cancel quick voice when loosing focus * add hold for speech command to inline chat which is for when a session already started
1 parent 18b601b commit 37740ec

File tree

3 files changed

+92
-42
lines changed

3 files changed

+92
-42
lines changed

src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChat.contribution.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
77
import { registerAction2 } from 'vs/platform/actions/common/actions';
88
import { CancelAction, InlineChatQuickVoice, StartAction, StopAction } from 'vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice';
9-
import * as StartSessionAction from './inlineChatActions';
9+
import { StartSessionAction, HoldToSpeak } from './inlineChatActions';
1010

1111
// start and hold for voice
1212

13-
registerAction2(StartSessionAction.StartSessionAction);
13+
registerAction2(StartSessionAction);
14+
registerAction2(HoldToSpeak);
1415

1516
// quick voice
1617

src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
1616
import { ICommandService } from 'vs/platform/commands/common/commands';
1717
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1818
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';
19+
import { CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
2020
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
21-
import { ISpeechService } from 'vs/workbench/contrib/speech/common/speechService';
21+
import { HasSpeechProvider, ISpeechService } from 'vs/workbench/contrib/speech/common/speechService';
22+
import { localize2 } from 'vs/nls';
2223

2324

2425
export class StartSessionAction extends EditorAction2 {
@@ -45,31 +46,11 @@ export class StartSessionAction extends EditorAction2 {
4546

4647
const configService = accessor.get(IConfigurationService);
4748
const speechService = accessor.get(ISpeechService);
48-
const keybindingService = accessor.get(IKeybindingService);
49-
const commandService = accessor.get(ICommandService);
5049

5150
if (configService.getValue<boolean>(InlineChatConfigKeys.HoldToSpeech) // enabled
5251
&& speechService.hasSpeechProvider // possible
5352
) {
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-
}, 250);
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-
}
53+
holdForSpeech(accessor, InlineChatController.get(editor), this.desc.id);
7354
}
7455

7556
let options: InlineChatRunOptions | undefined;
@@ -80,3 +61,50 @@ export class StartSessionAction extends EditorAction2 {
8061
InlineChatController.get(editor)?.run({ ...options });
8162
}
8263
}
64+
65+
export class HoldToSpeak extends AbstractInlineChatAction {
66+
67+
constructor() {
68+
super({
69+
id: 'inlineChat.holdForSpeech',
70+
precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE),
71+
title: localize2('holdForSpeech', "Hold for Speech"),
72+
keybinding: {
73+
when: EditorContextKeys.textInputFocus,
74+
weight: KeybindingWeight.WorkbenchContrib,
75+
primary: KeyMod.CtrlCmd | KeyCode.KeyI,
76+
},
77+
});
78+
}
79+
80+
override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: any[]): void {
81+
holdForSpeech(accessor, ctrl, this.desc.id);
82+
}
83+
}
84+
85+
function holdForSpeech(accessor: ServicesAccessor, ctrl: InlineChatController | null, commandId: string): void {
86+
const keybindingService = accessor.get(IKeybindingService);
87+
const commandService = accessor.get(ICommandService);
88+
if (!ctrl) {
89+
return;
90+
}
91+
const holdMode = keybindingService.enableKeybindingHoldMode(commandId);
92+
if (!holdMode) {
93+
return;
94+
}
95+
let listening = false;
96+
const handle = disposableTimeout(() => {
97+
// start VOICE input
98+
commandService.executeCommand(StartVoiceChatAction.ID);
99+
listening = true;
100+
}, 250);
101+
102+
holdMode.finally(() => {
103+
if (listening) {
104+
commandService.executeCommand(StopListeningAction.ID).finally(() => {
105+
ctrl!.acceptInput();
106+
});
107+
}
108+
handle.dispose();
109+
});
110+
}

src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatQuickVoice.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
1717
import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService';
1818
import { CancellationTokenSource } from 'vs/base/common/cancellation';
1919
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
20-
import { getWindow, h, reset, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
20+
import * as dom from 'vs/base/browser/dom';
2121
import { IDimension } from 'vs/editor/common/core/dimension';
2222
import { EditorOption } from 'vs/editor/common/config/editorOptions';
2323
import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions';
2424
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
25+
import { Emitter, Event } from 'vs/base/common/event';
26+
import { DisposableStore } from 'vs/base/common/lifecycle';
2527

2628
const CTX_QUICK_CHAT_IN_PROGRESS = new RawContextKey<boolean>('inlineChat.quickChatInProgress', false);
2729

@@ -107,17 +109,27 @@ class QuickVoiceWidget implements IContentWidget {
107109
readonly suppressMouseDown = true;
108110

109111
private readonly _domNode = document.createElement('div');
110-
private readonly _elements = h('.inline-chat-quick-voice@main', [
111-
h('span@mic'),
112-
h('span.message@message'),
112+
private readonly _elements = dom.h('.inline-chat-quick-voice@main', [
113+
dom.h('span@mic'),
114+
dom.h('span.message@message'),
113115
]);
114116

117+
private _focusTracker: dom.IFocusTracker | undefined;
118+
119+
private readonly _onDidBlur = new Emitter<void>();
120+
readonly onDidBlur: Event<void> = this._onDidBlur.event;
121+
115122
constructor(private readonly _editor: ICodeEditor) {
116123
this._domNode.appendChild(this._elements.root);
117124
this._domNode.style.zIndex = '1000';
118125
this._domNode.tabIndex = -1;
119126
this._domNode.style.outline = 'none';
120-
reset(this._elements.mic, renderIcon(Codicon.micFilled));
127+
dom.reset(this._elements.mic, renderIcon(Codicon.micFilled));
128+
}
129+
130+
dispose(): void {
131+
this._focusTracker?.dispose();
132+
this._onDidBlur.dispose();
121133
}
122134

123135
getId(): string {
@@ -150,24 +162,33 @@ class QuickVoiceWidget implements IContentWidget {
150162
return null;
151163
}
152164

165+
afterRender(): void {
166+
this._domNode.focus();
167+
this._focusTracker?.dispose();
168+
this._focusTracker = dom.trackFocus(this._domNode);
169+
this._focusTracker.onDidBlur(() => this._onDidBlur.fire());
170+
}
171+
153172
// ---
154173

155174
updateInput(input: string | undefined, isDefinite: boolean): void {
156175
this._elements.message.classList.toggle('preview', !isDefinite);
157176
this._elements.message.textContent = input ?? '';
158177
}
159178

160-
focus(): void {
161-
this._domNode.focus();
179+
show() {
180+
this._editor.addContentWidget(this);
162181
}
163182

164183
active(): void {
165184
this._elements.main.classList.add('recording');
166185
}
167186

168-
reset(): void {
187+
hide() {
169188
this._elements.main.classList.remove('recording');
170189
this.updateInput(undefined, true);
190+
this._editor.removeContentWidget(this);
191+
this._focusTracker?.dispose();
171192
}
172193
}
173194

@@ -179,6 +200,7 @@ export class InlineChatQuickVoice implements IEditorContribution {
179200
return editor.getContribution<InlineChatQuickVoice>(InlineChatQuickVoice.ID);
180201
}
181202

203+
private readonly _store = new DisposableStore();
182204
private readonly _ctxQuickChatInProgress: IContextKey<boolean>;
183205
private readonly _widget: QuickVoiceWidget;
184206
private _finishCallback?: (abort: boolean) => void;
@@ -188,24 +210,25 @@ export class InlineChatQuickVoice implements IEditorContribution {
188210
@ISpeechService private readonly _speechService: ISpeechService,
189211
@IContextKeyService contextKeyService: IContextKeyService,
190212
) {
191-
this._widget = new QuickVoiceWidget(this._editor);
213+
this._widget = this._store.add(new QuickVoiceWidget(this._editor));
214+
this._widget.onDidBlur(() => this._finishCallback?.(true), undefined, this._store);
192215
this._ctxQuickChatInProgress = CTX_QUICK_CHAT_IN_PROGRESS.bindTo(contextKeyService);
193216
}
194217

195218
dispose(): void {
196219
this._finishCallback?.(true);
220+
this._ctxQuickChatInProgress.reset();
221+
this._store.dispose();
197222
}
198223

199224
start() {
200225

226+
this._finishCallback?.(true);
227+
201228
const cts = new CancellationTokenSource();
202-
this._editor.addContentWidget(this._widget);
229+
this._widget.show();
203230
this._ctxQuickChatInProgress.set(true);
204231

205-
runAtThisOrScheduleAtNextAnimationFrame(getWindow(this._widget.getDomNode()), () => {
206-
this._widget.focus(); // requires RAF because...
207-
});
208-
209232
let message: string | undefined;
210233
const session = this._speechService.createSpeechToTextSession(cts.token);
211234
const listener = session.onDidChange(e => {
@@ -233,8 +256,7 @@ export class InlineChatQuickVoice implements IEditorContribution {
233256
const done = (abort: boolean) => {
234257
cts.dispose(true);
235258
listener.dispose();
236-
this._widget.reset();
237-
this._editor.removeContentWidget(this._widget);
259+
this._widget.hide();
238260
this._ctxQuickChatInProgress.reset();
239261

240262
if (!abort && message) {
@@ -252,5 +274,4 @@ export class InlineChatQuickVoice implements IEditorContribution {
252274
cancel(): void {
253275
this._finishCallback?.(true);
254276
}
255-
256277
}

0 commit comments

Comments
 (0)