Skip to content

Commit

Permalink
SR command to read last REPL execution
Browse files Browse the repository at this point in the history
  • Loading branch information
amunger committed Nov 7, 2024
1 parent 793580e commit bbda1a6
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -865,12 +865,6 @@ Registry.as<IConfigurationRegistry>(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."),
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ export const ReplEditorSettings = {
interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell',
executeWithShiftEnter: 'interactiveWindow.executeWithShiftEnter',
showExecutionHint: 'interactiveWindow.showExecutionHint',
autoFocusAppendedCell: 'replEditor.autoFocusAppendedCell',
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo
private readonly _notebookListeners = new ResourceMap<NotebookExecutionListeners>();
private readonly _cellListeners = new ResourceMap<IDisposable>();
private readonly _lastFailedCells = new ResourceMap<IFailedCellInfo>();
private readonly _lastCompletedCellHandles = new ResourceMap<number>();

private readonly _onDidChangeExecution = this._register(new Emitter<ICellExecutionStateChangedEvent | IExecutionStateChangedEvent>());
onDidChangeExecution = this._onDidChangeExecution.event;
Expand All @@ -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) {
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
];
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;');
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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<void> {
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({
Expand Down
19 changes: 1 addition & 18 deletions src/vs/workbench/contrib/replNotebook/browser/replEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit bbda1a6

Please sign in to comment.