mirror of
https://github.com/Microsoft/vscode
synced 2024-10-04 02:14:06 +00:00
parent
95a3805aa7
commit
fc18e59421
|
@ -796,6 +796,8 @@
|
|||
"--vscode-inline-chat-expanded",
|
||||
"--vscode-inline-chat-quick-voice-height",
|
||||
"--vscode-inline-chat-quick-voice-width",
|
||||
"--vscode-editor-dictation-widget-height",
|
||||
"--vscode-editor-dictation-widget-width",
|
||||
"--vscode-interactive-session-foreground",
|
||||
"--vscode-interactive-result-editor-background-color",
|
||||
"--vscode-repl-font-family",
|
||||
|
|
|
@ -23,3 +23,4 @@ import './toggleWordWrap';
|
|||
import './emptyTextEditorHint/emptyTextEditorHint';
|
||||
import './workbenchReferenceSearch';
|
||||
import './editorLineNumberMenu';
|
||||
import './dictation/editorDictation';
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .editor-dictation-widget {
|
||||
background-color: var(--vscode-editor-background);
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 8px var(--vscode-widget-shadow);
|
||||
z-index: 1000;
|
||||
min-height: var(--vscode-editor-dictation-widget-height);
|
||||
line-height: var(--vscode-editor-dictation-widget-height);
|
||||
max-width: var(--vscode-editor-dictation-widget-width);
|
||||
}
|
||||
|
||||
.monaco-editor .editor-dictation-widget .codicon.codicon-mic-filled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.monaco-editor .editor-dictation-widget.recording .codicon.codicon-mic-filled {
|
||||
color: var(--vscode-activityBarBadge-background);
|
||||
animation: editor-dictation-animation 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes editor-dictation-animation {
|
||||
0% {
|
||||
color: var(--vscode-editorCursor-background);
|
||||
}
|
||||
|
||||
50% {
|
||||
color: var(--vscode-activityBarBadge-background);
|
||||
}
|
||||
|
||||
100% {
|
||||
color: var(--vscode-editorCursor-background);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./editorDictation';
|
||||
import { localize2 } from 'vs/nls';
|
||||
import { IDimension, h, reset } from 'vs/base/browser/dom';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService';
|
||||
import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
|
||||
const EDITOR_DICTATION_IN_PROGRESS = new RawContextKey<boolean>('editorDictation.inProgress', false);
|
||||
const VOICE_CATEGORY = localize2('voiceCategory', "Voice");
|
||||
|
||||
export class EditorDictationStartAction extends EditorAction2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.editorDictation.start',
|
||||
title: localize2('startDictation', "Start Dictation in Editor"),
|
||||
category: VOICE_CATEGORY,
|
||||
precondition: ContextKeyExpr.and(HasSpeechProvider, EDITOR_DICTATION_IN_PROGRESS.toNegated(), EditorContextKeys.readOnly.toNegated()),
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
|
||||
override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
const keybindingService = accessor.get(IKeybindingService);
|
||||
|
||||
const holdMode = keybindingService.enableKeybindingHoldMode(this.desc.id);
|
||||
if (holdMode) {
|
||||
let shouldCallStop = false;
|
||||
|
||||
const handle = setTimeout(() => {
|
||||
shouldCallStop = true;
|
||||
}, 500);
|
||||
|
||||
holdMode.finally(() => {
|
||||
clearTimeout(handle);
|
||||
|
||||
if (shouldCallStop) {
|
||||
EditorDictation.get(editor)?.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EditorDictation.get(editor)?.start();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorDictationStopAction extends EditorAction2 {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.editorDictation.stop',
|
||||
title: localize2('stopDictation', "Stop Dictation in Editor"),
|
||||
category: VOICE_CATEGORY,
|
||||
precondition: EDITOR_DICTATION_IN_PROGRESS,
|
||||
f1: true,
|
||||
keybinding: {
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 100
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
EditorDictation.get(editor)?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
export class DictationWidget extends Disposable implements IContentWidget {
|
||||
|
||||
readonly suppressMouseDown = true;
|
||||
readonly allowEditorOverflow = true;
|
||||
|
||||
private readonly domNode = document.createElement('div');
|
||||
private readonly elements = h('.editor-dictation-widget@main', [h('span@mic')]);
|
||||
|
||||
constructor(private readonly editor: ICodeEditor) {
|
||||
super();
|
||||
|
||||
this.domNode.appendChild(this.elements.root);
|
||||
this.domNode.style.zIndex = '1000';
|
||||
|
||||
reset(this.elements.mic, renderIcon(Codicon.micFilled));
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'editorDictation';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
if (!this.editor.hasModel()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = this.editor.getSelection();
|
||||
|
||||
return {
|
||||
position: selection.getPosition(),
|
||||
preference: [
|
||||
selection.getPosition().equals(selection.getStartPosition()) ? ContentWidgetPositionPreference.ABOVE : ContentWidgetPositionPreference.BELOW,
|
||||
ContentWidgetPositionPreference.EXACT
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
beforeRender(): IDimension | null {
|
||||
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
|
||||
const width = this.editor.getLayoutInfo().contentWidth * 0.7;
|
||||
|
||||
this.elements.main.style.setProperty('--vscode-editor-dictation-widget-height', `${lineHeight}px`);
|
||||
this.elements.main.style.setProperty('--vscode-editor-dictation-widget-width', `${width}px`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.editor.addContentWidget(this);
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
active(): void {
|
||||
this.elements.main.classList.add('recording');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elements.main.classList.remove('recording');
|
||||
this.editor.removeContentWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorDictation extends Disposable implements IEditorContribution {
|
||||
|
||||
static readonly ID = 'editorDictation';
|
||||
|
||||
static get(editor: ICodeEditor): EditorDictation | null {
|
||||
return editor.getContribution<EditorDictation>(EditorDictation.ID);
|
||||
}
|
||||
|
||||
private readonly widget = this._register(new DictationWidget(this.editor));
|
||||
private readonly editorDictationInProgress = EDITOR_DICTATION_IN_PROGRESS.bindTo(this.contextKeyService);
|
||||
|
||||
private sessionDisposables = this._register(new MutableDisposable());
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@ISpeechService private readonly speechService: ISpeechService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
start() {
|
||||
const disposables = new DisposableStore();
|
||||
this.sessionDisposables.value = disposables;
|
||||
|
||||
this.widget.show();
|
||||
disposables.add(toDisposable(() => this.widget.hide()));
|
||||
|
||||
this.editorDictationInProgress.set(true);
|
||||
disposables.add(toDisposable(() => this.editorDictationInProgress.reset()));
|
||||
|
||||
const collection = this.editor.createDecorationsCollection();
|
||||
disposables.add(toDisposable(() => collection.clear()));
|
||||
|
||||
let previewStart: Position | undefined = undefined;
|
||||
|
||||
let lastReplaceTextLength = 0;
|
||||
const replaceText = (text: string, isPreview: boolean) => {
|
||||
if (!previewStart) {
|
||||
previewStart = assertIsDefined(this.editor.getPosition());
|
||||
}
|
||||
|
||||
this.editor.executeEdits(EditorDictation.ID, [
|
||||
EditOperation.replace(Range.fromPositions(previewStart, previewStart.with(undefined, previewStart.column + lastReplaceTextLength)), text)
|
||||
], [
|
||||
Selection.fromPositions(new Position(previewStart.lineNumber, previewStart.column + text.length))
|
||||
]);
|
||||
|
||||
if (isPreview) {
|
||||
collection.set([
|
||||
{
|
||||
range: Range.fromPositions(previewStart, previewStart.with(undefined, previewStart.column + text.length)),
|
||||
options: {
|
||||
description: 'editor-dictation-preview',
|
||||
inlineClassName: 'ghost-text-decoration-preview'
|
||||
}
|
||||
}
|
||||
]);
|
||||
} else {
|
||||
collection.clear();
|
||||
}
|
||||
|
||||
lastReplaceTextLength = text.length;
|
||||
if (!isPreview) {
|
||||
previewStart = undefined;
|
||||
lastReplaceTextLength = 0;
|
||||
}
|
||||
|
||||
this.widget.layout();
|
||||
};
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
disposables.add(toDisposable(() => cts.dispose(true)));
|
||||
|
||||
const session = disposables.add(this.speechService.createSpeechToTextSession(cts.token));
|
||||
disposables.add(session.onDidChange(e => {
|
||||
if (cts.token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.status) {
|
||||
case SpeechToTextStatus.Started:
|
||||
this.widget.active();
|
||||
break;
|
||||
case SpeechToTextStatus.Stopped:
|
||||
disposables.dispose();
|
||||
break;
|
||||
case SpeechToTextStatus.Recognizing: {
|
||||
if (!e.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaceText(e.text, true);
|
||||
break;
|
||||
}
|
||||
case SpeechToTextStatus.Recognized: {
|
||||
if (!e.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaceText(`${e.text} `, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.sessionDisposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(EditorDictation.ID, EditorDictation, EditorContributionInstantiation.Lazy);
|
||||
registerAction2(EditorDictationStartAction);
|
||||
registerAction2(EditorDictationStopAction);
|
|
@ -267,6 +267,7 @@ export class InlineChatQuickVoice implements IEditorContribution {
|
|||
|
||||
const done = (abort: boolean) => {
|
||||
cts.dispose(true);
|
||||
session.dispose();
|
||||
listener.dispose();
|
||||
this._widget.hide();
|
||||
this._ctxQuickChatInProgress.reset();
|
||||
|
|
Loading…
Reference in a new issue