Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,8 @@ export async function startDictation(service: IChatSpeechToTextService, editor:
// prepared on first use (downloading/loading, which can take a while), show
// a "Preparing…/Downloading… X%" placeholder instead so the user knows why
// dictation has not started yet rather than staring at an idle editor. The
// placeholder must not appear during microphone acquisition. It remains
// visible until transcript text is inserted, and is restored to its
// previous value when the session ends.
// previous placeholder remains visible during microphone acquisition, then
// is restored when the session ends.
const previousPlaceholder = editor.getOption(EditorOption.placeholder);
const listeningPlaceholder = localize('chatStt.listening', "Listening…");
// The placeholder we last applied (listening or a preparing label), so we
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,27 @@ export function installRotatingChatPlaceholder(editor: ICodeEditor, options?: IR
const store = new DisposableStore();

const placeholderContribution = PlaceholderTextContribution.get(editor);
placeholderContribution?.setAnimateTransitions(true);
store.add(toDisposable(() => PlaceholderTextContribution.get(editor)?.setAnimateTransitions(false)));

const currentPlaceholder = editor.getOption(EditorOption.placeholder);
let expectedPlaceholder = currentPlaceholder;
let index = placeholders.indexOf(currentPlaceholder);
if (index === -1) {
index = Math.floor(Math.random() * placeholders.length);
}
store.add(disposableWindowInterval(getWindow(editor.getDomNode()), () => {
if (editor.getOption(EditorOption.placeholder) !== expectedPlaceholder) {
return;
}

index = (index + 1) % placeholders.length;
editor.updateOptions({ placeholder: placeholders[index] });
expectedPlaceholder = placeholders[index];
placeholderContribution?.setAnimateTransitions(true);
try {
editor.updateOptions({ placeholder: expectedPlaceholder });
} finally {
placeholderContribution?.setAnimateTransitions(false);
}
}, intervalMs));

return store;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { timeout } from '../../../../../base/common/async.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { EditorOption } from '../../../../../editor/common/config/editorOptions.js';
import { createTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js';
import { createTextModel } from '../../../../../editor/test/common/testTextModel.js';
import { installRotatingChatPlaceholder } from '../../browser/widget/input/chatInputPlaceholderRotation.js';

suite('ChatInputPlaceholderRotation', () => {

const store = ensureNoDisposablesAreLeakedInTestSuite();

test('does not overwrite a placeholder set by another feature', async () => {
const model = store.add(createTextModel(''));
const editor = store.add(createTestCodeEditor(model, { placeholder: 'First prompt' }));
store.add(installRotatingChatPlaceholder(editor, {
placeholders: ['First prompt', 'Second prompt'],
intervalMs: 5,
}));

editor.updateOptions({ placeholder: 'Listening\u2026' });
await timeout(20);

assert.strictEqual(editor.getOption(EditorOption.placeholder), 'Listening\u2026');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import assert from 'assert';
import { mainWindow } from '../../../../../base/browser/window.js';
import { Emitter } from '../../../../../base/common/event.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { EditorOption } from '../../../../../editor/common/config/editorOptions.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { createTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js';
import { createTextModel } from '../../../../../editor/test/common/testTextModel.js';
Expand All @@ -22,19 +23,25 @@ suite('DictationSession', () => {
const transcript = 'hello world';
const onDidUpdateTranscript = store.add(new Emitter<IChatDictationTranscript>());
const onDidChangeState = store.add(new Emitter<ChatSpeechToTextState>());
const onDidChangePreparingModel = store.add(new Emitter<boolean>());
let state = ChatSpeechToTextState.Idle;
let isPreparingModel = false;
let placeholderDuringStart: string | undefined;
const service: IChatSpeechToTextService = {
_serviceBrand: undefined,
onDidUpdateTranscript: onDidUpdateTranscript.event,
onDidChangeState: onDidChangeState.event,
onDidChangePreparingModel: store.add(new Emitter<boolean>()).event,
onDidChangePreparingModel: onDidChangePreparingModel.event,
onDidChangeModelDownloadProgress: store.add(new Emitter<void>()).event,
get state() { return state; },
get isConfigured() { return true; },
get isPreparingModel() { return false; },
get modelDownloadProgress() { return undefined; },
get currentBackend() { return 'mai' as const; },
get isPreparingModel() { return isPreparingModel; },
get modelDownloadProgress() { return 0.5; },
get currentBackend() { return 'local' as const; },
async start() {
placeholderDuringStart = editor.getOption(EditorOption.placeholder);
isPreparingModel = true;
onDidChangePreparingModel.fire(true);
state = ChatSpeechToTextState.Recording;
onDidChangeState.fire(state);
},
Expand All @@ -47,13 +54,29 @@ suite('DictationSession', () => {
logDictationAccuracy() { },
};
const model = store.add(createTextModel(''));
const editor = store.add(createTestCodeEditor(model));
const editor = store.add(createTestCodeEditor(model, { placeholder: 'Ask a question' }));

await startDictation(service, editor, mainWindow, new NullLogService());
const downloadingPlaceholder = editor.getOption(EditorOption.placeholder);
isPreparingModel = false;
onDidChangePreparingModel.fire(false);
const listeningPlaceholder = editor.getOption(EditorOption.placeholder);
onDidUpdateTranscript.fire({ text: transcript, finalizedText: '' });
editor.executeEdits('test', [{ range: new Range(1, 1, 1, transcript.length + 1), text: '' }]);
await stopDictation();

assert.strictEqual(editor.getValue(), '');
assert.deepStrictEqual({
placeholderDuringStart,
downloadingPlaceholder,
listeningPlaceholder,
placeholderAfterStop: editor.getOption(EditorOption.placeholder),
value: editor.getValue(),
}, {
placeholderDuringStart: 'Ask a question',
downloadingPlaceholder: 'Downloading speech-to-text model\u2026 50%',
listeningPlaceholder: 'Listening\u2026',
placeholderAfterStop: 'Ask a question',
value: '',
});
});
});
Loading