diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 0d5f31ecb671a..ff09fc5bc7b14 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -865,12 +865,6 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: true, markdownDescription: localize('interactiveWindow.showExecutionHint', "Display a hint in the Interactive Window (REPL) input box to indicate how to execute code."), tags: ['replExecute'] - }, - [ReplEditorSettings.autoFocusAppendedCell]: { - type: 'string', - enum: ['auto', 'never', 'always'], - default: 'auto', - description: localize('interactive.autoFocusAppendedCell', "Control whether focus should automatically go to a newly appended cell in the REPL editor."), } } }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts index 5f4a75ebc831b..e7796663c9601 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveCommon.ts @@ -11,5 +11,4 @@ export const ReplEditorSettings = { interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell', executeWithShiftEnter: 'interactiveWindow.executeWithShiftEnter', showExecutionHint: 'interactiveWindow.showExecutionHint', - autoFocusAppendedCell: 'replEditor.autoFocusAppendedCell', }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts deleted file mode 100644 index 11a10ed5950c1..0000000000000 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts +++ /dev/null @@ -1,5 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index 65808dc57323c..3a2d0b1033a7a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -11,6 +11,7 @@ import { getNotebookEditorFromEditorPane } from './notebookBrowser.js'; import { NOTEBOOK_CELL_LIST_FOCUSED } from '../common/notebookContextKeys.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { InputFocusedContext } from '../../../../platform/contextkey/common/contextkeys.js'; +import { getOutputText } from './viewModel/outputHelper.js'; export class NotebookAccessibleView implements IAccessibleViewImplentation { readonly priority = 100; @@ -36,38 +37,7 @@ export function getAccessibleOutputProvider(editorService: IEditorService) { } const viewCell = notebookViewModel.viewCells[selections[0].start]; - let outputContent = ''; - const decoder = new TextDecoder(); - for (let i = 0; i < viewCell.outputsViewModels.length; i++) { - const outputViewModel = viewCell.outputsViewModels[i]; - const outputTextModel = viewCell.model.outputs[i]; - const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); - const mimeType = mimeTypes[pick].mimeType; - let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); - - if (!buffer || mimeType.startsWith('image')) { - buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); - } - - let text = `${mimeType}`; // default in case we can't get the text value for some reason. - if (buffer) { - const charLimit = 100_000; - text = decoder.decode(buffer.data.slice(0, charLimit).buffer); - - if (buffer.data.byteLength > charLimit) { - text = text + '...(truncated)'; - } - - if (mimeType.endsWith('error')) { - text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); - } - } - - const index = viewCell.outputsViewModels.length > 1 - ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` - : ''; - outputContent = outputContent.concat(`${index}${text}\n`); - } + const outputContent = getOutputText(notebookDocument, viewCell); if (!outputContent) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index a14d5f6ca4506..3880b5a3e4960 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -464,6 +464,7 @@ export interface INotebookViewModel { setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null; getSelections(): ICellRange[]; getCellIndex(cell: ICellViewModel): number; + getMostRecentlyExecutedCell(): ICellViewModel | undefined; deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[]; getFoldedLength(index: number): number; replaceOne(cell: ICellViewModel, range: Range, text: string): Promise; diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index 6541654dc3ed3..fb4f67329e30d 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts @@ -27,6 +27,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo private readonly _notebookListeners = new ResourceMap(); private readonly _cellListeners = new ResourceMap(); private readonly _lastFailedCells = new ResourceMap(); + private readonly _lastCompletedCellHandles = new ResourceMap(); private readonly _onDidChangeExecution = this._register(new Emitter()); onDidChangeExecution = this._onDidChangeExecution.event; @@ -48,6 +49,10 @@ export class NotebookExecutionStateService extends Disposable implements INotebo return failedCell?.visible ? failedCell.cellHandle : undefined; } + getLastCompletedCellForNotebook(notebook: URI): number | undefined { + return this._lastCompletedCellHandles.get(notebook); + } + forceCancelNotebookExecutions(notebookUri: URI): void { const notebookCellExecutions = this._executions.get(notebookUri); if (notebookCellExecutions) { @@ -119,6 +124,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo this._accessibilitySignalService.playSignal(AccessibilitySignal.notebookCellFailed); this._setLastFailedCell(notebookUri, cellHandle); } + this._lastCompletedCellHandles.set(notebookUri, cellHandle); } this._onDidChangeExecution.fire(new NotebookCellExecutionEvent(notebookUri, cellHandle)); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 09d6f75c5cfe6..8b9ca5099a031 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -199,7 +199,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IUndoRedoService private readonly _undoService: IUndoRedoService, @ITextModelService private readonly _textModelService: ITextModelService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, ) { super(); @@ -363,6 +363,11 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._selectionCollection.selections; } + getMostRecentlyExecutedCell(): ICellViewModel | undefined { + const handle = this.notebookExecutionStateService.getLastCompletedCellForNotebook(this._notebook.uri); + return handle !== undefined ? this.getCellByHandle(handle) : undefined; + } + setEditorFocus(focused: boolean) { this._focused = focused; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/outputHelper.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/outputHelper.ts new file mode 100644 index 0000000000000..caa2e8866a74b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/outputHelper.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; +import { ICellViewModel } from '../notebookBrowser.js'; + +export function getOutputText(notebook: NotebookTextModel, viewCell: ICellViewModel): string { + let outputContent = ''; + const decoder = new TextDecoder(); + for (let i = 0; i < viewCell.outputsViewModels.length; i++) { + const outputViewModel = viewCell.outputsViewModels[i]; + const outputTextModel = viewCell.model.outputs[i]; + const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebook, undefined); + const mimeType = mimeTypes[pick].mimeType; + let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); + + if (!buffer || mimeType.startsWith('image')) { + buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); + } + + let text = `${mimeType}`; // default in case we can't get the text value for some reason. + if (buffer) { + const charLimit = 100_000; + text = decoder.decode(buffer.data.slice(0, charLimit).buffer); + + if (buffer.data.byteLength > charLimit) { + text = text + '...(truncated)'; + } + + if (mimeType.endsWith('error')) { + text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); + } + } + + const index = viewCell.outputsViewModels.length > 1 + ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` + : ''; + outputContent = outputContent.concat(`${index}${text}\n`); + } + return outputContent.trim(); +} + +export const TEXT_BASED_MIMETYPES = [ + 'text/latex', + 'text/html', + 'application/vnd.code.notebook.error', + 'application/vnd.code.notebook.stdout', + 'application/x.notebook.stdout', + 'application/x.notebook.stream', + 'application/vnd.code.notebook.stderr', + 'application/x.notebook.stderr', + 'text/plain', + 'text/markdown', + 'application/json' +]; diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index b51293c2add28..8ea8fb79644ae 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -77,6 +77,7 @@ export interface INotebookExecutionStateService { getExecution(notebook: URI): INotebookExecution | undefined; createExecution(notebook: URI): INotebookExecution; getLastFailedCellForNotebook(notebook: URI): number | undefined; + getLastCompletedCellForNotebook(notebook: URI): number | undefined; } export interface INotebookCellExecution { diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index bbffb0d2a6856..0ea4f34561867 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -72,7 +72,7 @@ suite('NotebookTextModel', () => { assert.strictEqual(textModel.cells.length, 6); - assert.strictEqual(textModel.cells[1].getValue(), 'var e = 5;'); + assert.strictEqual(textModel.cells[1].getValue(), 'var e = ;'); assert.strictEqual(textModel.cells[2].getValue(), 'var f = 6;'); } ); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index de21fbf708ac3..bbca74e25cc3e 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -564,6 +564,9 @@ export class TestNotebookExecutionStateService implements INotebookExecutionStat getLastFailedCellForNotebook(notebook: URI): number | undefined { return; } + getLastCompletedCellForNotebook(notebook: URI): number | undefined { + return; + } getExecution(notebook: URI): INotebookExecution | undefined { return; } diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts index fb2e258f07ad2..0857f29c1ff06 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { alert } from '../../../../base/browser/ui/aria/aria.js'; import { Event } from '../../../../base/common/event.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -17,6 +18,7 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js'; import { localize2 } from '../../../../nls.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -44,6 +46,7 @@ import { INotebookEditorOptions } from '../../notebook/browser/notebookBrowser.j import { NotebookEditorWidget } from '../../notebook/browser/notebookEditorWidget.js'; import * as icons from '../../notebook/browser/notebookIcons.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; +import { getOutputText } from '../../notebook/browser/viewModel/outputHelper.js'; import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from '../../notebook/common/notebookCommon.js'; import { MOST_RECENT_REPL_EDITOR } from '../../notebook/common/notebookContextKeys.js'; import { NotebookEditorInputOptions } from '../../notebook/common/notebookEditorInput.js'; @@ -232,7 +235,7 @@ registerAction2(class extends Action2 { title: localize2('repl.focusLastReplOutput', 'Focus Most Recent REPL Execution'), category: 'REPL', keybinding: [{ - primary: KeyChord(KeyMod.Alt | KeyCode.End, KeyMod.Alt | KeyCode.End), + primary: KeyChord(KeyMod.Alt | KeyCode.Home, KeyMod.Alt | KeyCode.End), weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT }], menu: { @@ -282,6 +285,63 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'repl.readLastExecutedOutput', + title: localize2('repl.readMostRecentExecution', 'Read Most Recent Execution Output'), + category: 'REPL', + keybinding: [{ + primary: KeyChord(KeyMod.Alt | KeyCode.End, KeyMod.Alt | KeyCode.End), + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }], + menu: { + id: MenuId.CommandPalette, + when: MOST_RECENT_REPL_EDITOR, + }, + precondition: ContextKeyExpr.and(MOST_RECENT_REPL_EDITOR, CONTEXT_ACCESSIBILITY_MODE_ENABLED) + }); + } + + async run(accessor: ServicesAccessor, context?: UriComponents): Promise { + const editorService = accessor.get(IEditorService); + const editorControl = editorService.activeEditorPane?.getControl(); + const contextKeyService = accessor.get(IContextKeyService); + + let notebookEditor: NotebookEditorWidget | undefined; + if (editorControl && isReplEditorControl(editorControl)) { + notebookEditor = editorControl.notebookEditor; + } else { + const uriString = MOST_RECENT_REPL_EDITOR.getValue(contextKeyService); + const uri = uriString ? URI.parse(uriString) : undefined; + + if (!uri) { + return; + } + const replEditor = editorService.findEditors(uri)[0]; + + if (replEditor) { + const editor = await editorService.openEditor({ resource: uri, options: { preserveFocus: true } }, replEditor.groupId); + const editorControl = editor?.getControl(); + + if (editorControl && isReplEditorControl(editorControl)) { + notebookEditor = editorControl.notebookEditor; + } + } + } + + const viewModel = notebookEditor?.getViewModel(); + const notebook = notebookEditor?.textModel; + if (viewModel && notebook) { + const cell = viewModel.getMostRecentlyExecutedCell(); + if (cell) { + const text = getOutputText(notebook, cell); + alert(text); + } + } + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts index 1cbf7e5e5b64a..b953033a8ec3b 100644 --- a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts @@ -59,7 +59,6 @@ import { ReplInputHintContentWidget } from '../../interactive/browser/replInputH import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { localize } from '../../../../nls.js'; -import { NotebookViewModel } from '../../notebook/browser/viewModel/notebookViewModelImpl.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -543,29 +542,13 @@ export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { if (addedCells.length) { const viewModel = notebookWidget.viewModel; if (viewModel) { - this.handleAppend(notebookWidget, viewModel); + this._notebookWidgetService.updateReplContextKey(viewModel.notebookDocument.uri.toString()); break; } } } } - private handleAppend(notebookWidget: NotebookEditorWidget, viewModel: NotebookViewModel) { - this._notebookWidgetService.updateReplContextKey(viewModel.notebookDocument.uri.toString()); - const navigateToCell = this._configurationService.getValue(ReplEditorSettings.autoFocusAppendedCell); - if ((this._accessibilityService.isScreenReaderOptimized() && navigateToCell !== 'never') - || navigateToCell === 'always') { - - setTimeout(() => { - const lastCellIndex = viewModel.length - 1; - if (lastCellIndex >= 0) { - const cell = viewModel.viewCells[lastCellIndex]; - notebookWidget.focusNotebookCell(cell, 'container'); - } - }, 0); - } - } - override setOptions(options: INotebookEditorOptions | undefined): void { this._notebookWidget.value?.setOptions(options); super.setOptions(options);