Files
vscode/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts
2022-02-16 15:43:37 +00:00

752 lines
30 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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!./media/interactive';
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorBackground, editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput';
import { CodeCellLayoutChangeEvent, IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService';
import { cellEditorBackground, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController';
import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon';
import { ComplexNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { NotebookCellExecutionState, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IAction } from 'vs/base/common/actions';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion';
import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover';
import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
const DECORATION_KEY = 'interactiveInputDecoration';
const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState';
const enum ScrollingState {
Initial = 0,
StickyToBottom = 1
}
const INPUT_CELL_VERTICAL_PADDING = 8;
const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10;
const INPUT_EDITOR_PADDING = 8;
export interface InteractiveEditorViewState {
readonly notebook?: INotebookEditorViewState;
readonly input?: ICodeEditorViewState | null;
}
export interface InteractiveEditorOptions extends ITextEditorOptions {
readonly viewState?: InteractiveEditorViewState;
}
export class InteractiveEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.interactive';
#rootElement!: HTMLElement;
#styleElement!: HTMLStyleElement;
#notebookEditorContainer!: HTMLElement;
#notebookWidget: IBorrowValue<NotebookEditorWidget> = { value: undefined };
#inputCellContainer!: HTMLElement;
#inputFocusIndicator!: HTMLElement;
#inputRunButtonContainer!: HTMLElement;
#inputEditorContainer!: HTMLElement;
#codeEditorWidget!: CodeEditorWidget;
// #inputLineCount = 1;
#notebookWidgetService: INotebookEditorService;
#instantiationService: IInstantiationService;
#languageService: ILanguageService;
#contextKeyService: IContextKeyService;
#notebookKernelService: INotebookKernelService;
#keybindingService: IKeybindingService;
#menuService: IMenuService;
#contextMenuService: IContextMenuService;
#editorGroupService: IEditorGroupsService;
#notebookExecutionStateService: INotebookExecutionStateService;
#widgetDisposableStore: DisposableStore = this._register(new DisposableStore());
#dimension?: DOM.Dimension;
#notebookOptions: NotebookOptions;
#editorMemento: IEditorMemento<InteractiveEditorViewState>;
#groupListener = this._register(new DisposableStore());
#onDidFocusWidget = this._register(new Emitter<void>());
override get onDidFocus(): Event<void> { return this.#onDidFocusWidget.event; }
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService,
@INotebookEditorService notebookWidgetService: INotebookEditorService,
@IContextKeyService contextKeyService: IContextKeyService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@INotebookKernelService notebookKernelService: INotebookKernelService,
@ILanguageService languageService: ILanguageService,
@IKeybindingService keybindingService: IKeybindingService,
@IConfigurationService configurationService: IConfigurationService,
@IMenuService menuService: IMenuService,
@IContextMenuService contextMenuService: IContextMenuService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService
) {
super(
InteractiveEditor.ID,
telemetryService,
themeService,
storageService
);
this.#instantiationService = instantiationService;
this.#notebookWidgetService = notebookWidgetService;
this.#contextKeyService = contextKeyService;
this.#notebookKernelService = notebookKernelService;
this.#languageService = languageService;
this.#keybindingService = keybindingService;
this.#menuService = menuService;
this.#contextMenuService = contextMenuService;
this.#editorGroupService = editorGroupService;
this.#notebookExecutionStateService = notebookExecutionStateService;
this.#notebookOptions = new NotebookOptions(configurationService, notebookExecutionStateService, { cellToolbarInteraction: 'hover', globalToolbar: true, defaultCellCollapseConfig: { codeCell: { inputCollapsed: true } } });
this.#editorMemento = this.getEditorMemento<InteractiveEditorViewState>(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY);
codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {});
this._register(this.#keybindingService.onDidUpdateKeybindings(this.#updateInputDecoration, this));
}
private get _inputCellContainerHeight() {
return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2;
}
private get _inputCellEditorHeight() {
return 19 + INPUT_EDITOR_PADDING * 2;
}
protected createEditor(parent: HTMLElement): void {
this.#rootElement = DOM.append(parent, DOM.$('.interactive-editor'));
this.#rootElement.style.position = 'relative';
this.#notebookEditorContainer = DOM.append(this.#rootElement, DOM.$('.notebook-editor-container'));
this.#inputCellContainer = DOM.append(this.#rootElement, DOM.$('.input-cell-container'));
this.#inputCellContainer.style.position = 'absolute';
this.#inputCellContainer.style.height = `${this._inputCellContainerHeight}px`;
this.#inputFocusIndicator = DOM.append(this.#inputCellContainer, DOM.$('.input-focus-indicator'));
this.#inputRunButtonContainer = DOM.append(this.#inputCellContainer, DOM.$('.run-button-container'));
this.#setupRunButtonToolbar(this.#inputRunButtonContainer);
this.#inputEditorContainer = DOM.append(this.#inputCellContainer, DOM.$('.input-editor-container'));
this.#createLayoutStyles();
}
#setupRunButtonToolbar(runButtonContainer: HTMLElement) {
const menu = this._register(this.#menuService.createMenu(MenuId.InteractiveInputExecute, this.#contextKeyService));
const toolbar = this._register(new ToolBar(runButtonContainer, this.#contextMenuService, {
getKeyBinding: action => this.#keybindingService.lookupKeybinding(action.id),
actionViewItemProvider: action => {
return createActionViewItem(this.#instantiationService, action);
},
renderDropdownAsChildElement: true
}));
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };
createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result);
toolbar.setActions([...primary, ...secondary]);
}
#createLayoutStyles(): void {
this.#styleElement = DOM.createStyleSheet(this.#rootElement);
const styleSheets: string[] = [];
const {
focusIndicator,
codeCellLeftMargin,
cellRunGutter
} = this.#notebookOptions.getLayoutConfiguration();
const leftMargin = codeCellLeftMargin + cellRunGutter;
styleSheets.push(`
.interactive-editor .input-cell-container {
padding: ${INPUT_CELL_VERTICAL_PADDING}px ${INPUT_CELL_HORIZONTAL_PADDING_RIGHT}px ${INPUT_CELL_VERTICAL_PADDING}px ${leftMargin}px;
}
`);
if (focusIndicator === 'gutter') {
styleSheets.push(`
.interactive-editor .input-cell-container:focus-within .input-focus-indicator::before {
border-color: var(--notebook-focused-cell-border-color) !important;
}
.interactive-editor .input-focus-indicator::before {
border-color: var(--notebook-inactive-focused-cell-border-color) !important;
}
.interactive-editor .input-cell-container .input-focus-indicator {
display: block;
top: ${INPUT_CELL_VERTICAL_PADDING}px;
}
.interactive-editor .input-cell-container {
border-top: 1px solid var(--notebook-inactive-focused-cell-border-color);
}
`);
} else {
// border
styleSheets.push(`
.interactive-editor .input-cell-container {
border-top: 1px solid var(--notebook-inactive-focused-cell-border-color);
}
.interactive-editor .input-cell-container .input-focus-indicator {
display: none;
}
`);
}
styleSheets.push(`
.interactive-editor .input-cell-container .run-button-container {
width: ${cellRunGutter}px;
left: ${codeCellLeftMargin}px;
margin-top: ${INPUT_EDITOR_PADDING - 2}px;
}
`);
this.#styleElement.textContent = styleSheets.join('\n');
}
override saveState(): void {
this._saveEditorViewState(this.input);
super.saveState();
}
override getViewState(): InteractiveEditorViewState | undefined {
const input = this.input;
if (!(input instanceof InteractiveEditorInput)) {
return undefined;
}
this._saveEditorViewState(input);
return this._loadNotebookEditorViewState(input);
}
private _saveEditorViewState(input: EditorInput | undefined): void {
if (this.group && this.#notebookWidget.value && input instanceof InteractiveEditorInput) {
if (this.#notebookWidget.value.isDisposed) {
return;
}
const state = this.#notebookWidget.value.getEditorViewState();
const editorState = this.#codeEditorWidget.saveViewState();
this.#editorMemento.saveEditorState(this.group, input.notebookEditorInput.resource, {
notebook: state,
input: editorState
});
}
}
private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined {
let result: InteractiveEditorViewState | undefined;
if (this.group) {
result = this.#editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource);
}
if (result) {
return result;
}
// when we don't have a view state for the group/input-tuple then we try to use an existing
// editor for the same resource.
for (const group of this.#editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
if (group.activeEditorPane !== this && group.activeEditorPane === this && group.activeEditor?.matches(input)) {
const notebook = this.#notebookWidget.value?.getEditorViewState();
const input = this.#codeEditorWidget.saveViewState();
return {
notebook,
input
};
}
}
return;
}
override async setInput(input: InteractiveEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
const group = this.group!;
const notebookInput = input.notebookEditorInput;
// there currently is a widget which we still own so
// we need to hide it before getting a new widget
if (this.#notebookWidget.value) {
this.#notebookWidget.value.onWillHide();
}
if (this.#codeEditorWidget) {
this.#codeEditorWidget.dispose();
}
this.#widgetDisposableStore.clear();
this.#notebookWidget = <IBorrowValue<NotebookEditorWidget>>this.#instantiationService.invokeFunction(this.#notebookWidgetService.retrieveWidget, group, notebookInput, {
isEmbedded: true,
isReadOnly: true,
contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([
ExecutionStateCellStatusBarContrib.id,
TimerCellStatusBarContrib.id
]),
menuIds: {
notebookToolbar: MenuId.InteractiveToolbar,
cellTitleToolbar: MenuId.InteractiveCellTitle,
cellInsertToolbar: MenuId.NotebookCellBetween,
cellTopInsertToolbar: MenuId.NotebookCellListTop,
cellExecuteToolbar: MenuId.InteractiveCellExecute,
cellExecutePrimary: undefined
},
cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([
SelectionClipboardContributionID,
ContextMenuController.ID,
ModesHoverController.ID,
MarkerController.ID
]),
options: this.#notebookOptions
});
this.#codeEditorWidget = this.#instantiationService.createInstance(CodeEditorWidget, this.#inputEditorContainer, {
...getSimpleEditorOptions(),
...{
glyphMargin: true,
padding: {
top: INPUT_EDITOR_PADDING,
bottom: INPUT_EDITOR_PADDING
},
hover: {
enabled: true
}
}
}, {
...{
isSimpleWidget: false,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
MenuPreventer.ID,
SelectionClipboardContributionID,
ContextMenuController.ID,
SuggestController.ID,
SnippetController2.ID,
TabCompletionController.ID,
ModesHoverController.ID,
MarkerController.ID
])
}
});
if (this.#dimension) {
this.#notebookEditorContainer.style.height = `${this.#dimension.height - this._inputCellContainerHeight}px`;
this.#notebookWidget.value!.layout(this.#dimension.with(this.#dimension.width, this.#dimension.height - this._inputCellContainerHeight), this.#notebookEditorContainer);
const {
codeCellLeftMargin,
cellRunGutter
} = this.#notebookOptions.getLayoutConfiguration();
const leftMargin = codeCellLeftMargin + cellRunGutter;
const maxHeight = Math.min(this.#dimension.height / 2, this._inputCellEditorHeight);
this.#codeEditorWidget.layout(this.#validateDimension(this.#dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight));
this.#inputFocusIndicator.style.height = `${this._inputCellEditorHeight}px`;
this.#inputCellContainer.style.top = `${this.#dimension.height - this._inputCellContainerHeight}px`;
this.#inputCellContainer.style.width = `${this.#dimension.width}px`;
}
await super.setInput(input, options, context, token);
const model = await input.resolve();
if (model === null) {
throw new Error('?');
}
this.#notebookWidget.value?.setParentContextKeyService(this.#contextKeyService);
const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input);
await this.#notebookWidget.value!.setModel(model.notebook, viewState?.notebook);
model.notebook.setCellCollapseDefault(this.#notebookOptions.getCellCollapseDefault());
this.#notebookWidget.value!.setOptions({
isReadOnly: true
});
this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire()));
this.#widgetDisposableStore.add(model.notebook.onDidChangeContent(() => {
(model as ComplexNotebookEditorModel).setDirty(false);
}));
this.#widgetDisposableStore.add(this.#notebookOptions.onDidChangeOptions(e => {
if (e.compactView || e.focusIndicator) {
// update the styling
this.#styleElement?.remove();
this.#createLayoutStyles();
}
if (this.#dimension && this.isVisible()) {
this.layout(this.#dimension);
}
if (e.interactiveWindowCollapseCodeCells) {
model.notebook.setCellCollapseDefault(this.#notebookOptions.getCellCollapseDefault());
}
}));
const editorModel = await input.resolveInput(this.#notebookWidget.value?.activeKernel?.supportedLanguages[0] ?? PLAINTEXT_LANGUAGE_ID);
this.#codeEditorWidget.setModel(editorModel);
if (viewState?.input) {
this.#codeEditorWidget.restoreViewState(viewState.input);
}
this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidFocusEditorWidget(() => this.#onDidFocusWidget.fire()));
this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidContentSizeChange(e => {
if (!e.contentHeightChanged) {
return;
}
if (this.#dimension) {
this.#layoutWidgets(this.#dimension);
}
}));
this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeNotebookAffinity(this.#syncWithKernel, this));
this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeSelectedNotebooks(this.#syncWithKernel, this));
this.#widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => {
if (this.isVisible()) {
this.#updateInputDecoration();
}
}));
this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeModelContent(() => {
if (this.isVisible()) {
this.#updateInputDecoration();
}
}));
if (this.#notebookWidget.value?.hasModel()) {
this.#registerExecutionScrollListener(this.#notebookWidget.value);
}
const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this.#contextKeyService);
if (input.resource && input.historyService.has(input.resource)) {
cursorAtBoundaryContext.set('top');
} else {
cursorAtBoundaryContext.set('none');
}
this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeCursorPosition(({ position }) => {
const viewModel = this.#codeEditorWidget._getViewModel()!;
const lastLineNumber = viewModel.getLineCount();
const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1;
const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position);
const firstLine = viewPosition.lineNumber === 1 && viewPosition.column === 1;
const lastLine = viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol;
if (firstLine) {
if (lastLine) {
cursorAtBoundaryContext.set('both');
} else {
cursorAtBoundaryContext.set('top');
}
} else {
if (lastLine) {
cursorAtBoundaryContext.set('bottom');
} else {
cursorAtBoundaryContext.set('none');
}
}
}));
this.#widgetDisposableStore.add(editorModel.onDidChangeContent(() => {
const value = editorModel!.getValue();
if (this.input?.resource && value !== '') {
(this.input as InteractiveEditorInput).historyService.replaceLast(this.input.resource, value);
}
}));
this.#syncWithKernel();
}
#lastCell: ICellViewModel | undefined = undefined;
#lastCellDisposable = new DisposableStore();
#state: ScrollingState = ScrollingState.Initial;
#cellAtBottom(widget: IActiveNotebookEditorDelegate, cell: ICellViewModel): boolean {
const visibleRanges = widget.visibleRanges;
const cellIndex = widget.getCellIndex(cell);
if (cellIndex === Math.max(...visibleRanges.map(range => range.end))) {
return true;
}
return false;
}
/**
* - Init state: 0
* - Will cell insertion: check if the last cell is at the bottom, false, stay 0
* if true, state 1 (ready for auto reveal)
* - receive a scroll event (scroll even already happened). If the last cell is at bottom, false, 0, true, state 1
* - height change of the last cell, if state 0, do nothing, if state 1, scroll the last cell fully into view
*/
#registerExecutionScrollListener(widget: NotebookEditorWidget & IActiveNotebookEditorDelegate) {
this.#widgetDisposableStore.add(widget.textModel.onWillAddRemoveCells(e => {
const lastViewCell = widget.cellAt(widget.getLength() - 1);
// check if the last cell is at the bottom
if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) {
this.#state = ScrollingState.StickyToBottom;
} else {
this.#state = ScrollingState.Initial;
}
}));
this.#widgetDisposableStore.add(widget.onDidScroll(() => {
const lastViewCell = widget.cellAt(widget.getLength() - 1);
// check if the last cell is at the bottom
if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) {
this.#state = ScrollingState.StickyToBottom;
} else {
this.#state = ScrollingState.Initial;
}
}));
this.#widgetDisposableStore.add(widget.textModel.onDidChangeContent(e => {
for (let i = 0; i < e.rawEvents.length; i++) {
const event = e.rawEvents[i];
if (event.kind === NotebookCellsChangeType.ModelChange && this.#notebookWidget.value?.hasModel()) {
const lastViewCell = this.#notebookWidget.value.cellAt(this.#notebookWidget.value.getLength() - 1);
if (lastViewCell !== this.#lastCell) {
this.#lastCellDisposable.clear();
this.#lastCell = lastViewCell;
this.#registerListenerForCell();
}
}
}
}));
}
#registerListenerForCell() {
if (!this.#lastCell) {
return;
}
this.#lastCellDisposable.add(this.#lastCell.onDidChangeLayout((e) => {
if (e.totalHeight === undefined) {
// not cell height change
return;
}
if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value!.isScrolledToBottom()) {
return;
}
if (this.#state !== ScrollingState.StickyToBottom) {
return;
}
if (this.#lastCell) {
const runState = this.#notebookExecutionStateService.getCellExecution(this.#lastCell.uri)?.state;
if (runState === NotebookCellExecutionState.Executing) {
return;
}
}
// scroll to bottom
// postpone to next tick as the list view might not process the output height change yet
// e.g., when we register this listener later than the list view
this.#lastCellDisposable.add(DOM.scheduleAtNextAnimationFrame(() => {
if (this.#state === ScrollingState.StickyToBottom) {
this.#notebookWidget.value!.scrollToBottom();
}
}));
}));
}
#syncWithKernel() {
const notebook = this.#notebookWidget.value?.textModel;
const textModel = this.#codeEditorWidget.getModel();
if (notebook && textModel) {
const info = this.#notebookKernelService.getMatchingKernel(notebook);
const selectedOrSuggested = info.selected ?? info.suggestions[0];
if (selectedOrSuggested) {
const language = selectedOrSuggested.supportedLanguages[0];
const newMode = language ? this.#languageService.createById(language).languageId : PLAINTEXT_LANGUAGE_ID;
textModel.setMode(newMode);
NOTEBOOK_KERNEL.bindTo(this.#contextKeyService).set(selectedOrSuggested.id);
}
}
this.#updateInputDecoration();
}
layout(dimension: DOM.Dimension): void {
this.#rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600);
this.#rootElement.classList.toggle('narrow-width', dimension.width < 600);
this.#dimension = dimension;
if (!this.#notebookWidget.value) {
return;
}
this.#notebookEditorContainer.style.height = `${this.#dimension.height - this._inputCellContainerHeight}px`;
this.#layoutWidgets(dimension);
}
#layoutWidgets(dimension: DOM.Dimension) {
const contentHeight = this.#codeEditorWidget.hasModel() ? this.#codeEditorWidget.getContentHeight() : this._inputCellEditorHeight;
const maxHeight = Math.min(dimension.height / 2, contentHeight);
const {
codeCellLeftMargin,
cellRunGutter
} = this.#notebookOptions.getLayoutConfiguration();
const leftMargin = codeCellLeftMargin + cellRunGutter;
const inputCellContainerHeight = maxHeight + INPUT_CELL_VERTICAL_PADDING * 2;
this.#notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`;
this.#notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this.#notebookEditorContainer);
this.#codeEditorWidget.layout(this.#validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight));
this.#inputFocusIndicator.style.height = `${contentHeight}px`;
this.#inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`;
this.#inputCellContainer.style.width = `${dimension.width}px`;
}
#validateDimension(width: number, height: number) {
return new DOM.Dimension(Math.max(0, width), Math.max(0, height));
}
#updateInputDecoration(): void {
if (!this.#codeEditorWidget) {
return;
}
if (!this.#codeEditorWidget.hasModel()) {
return;
}
const model = this.#codeEditorWidget.getModel();
const decorations: IDecorationOptions[] = [];
if (model?.getValueLength() === 0) {
const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4);
const languageId = model.getLanguageId();
const keybinding = this.#keybindingService.lookupKeybinding('interactive.execute', this.#contextKeyService)?.getLabel();
const text = nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding ?? 'ctrl+enter');
decorations.push({
range: {
startLineNumber: 0,
endLineNumber: 0,
startColumn: 0,
endColumn: 1
},
renderOptions: {
after: {
contentText: text,
color: transparentForeground ? transparentForeground.toString() : undefined
}
}
});
}
this.#codeEditorWidget.setDecorations('interactive-decoration', DECORATION_KEY, decorations);
}
override focus() {
this.#codeEditorWidget.focus();
}
override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
super.setEditorVisible(visible, group);
if (group) {
this.#groupListener.clear();
this.#groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)));
}
if (!visible) {
this._saveEditorViewState(this.input);
if (this.input && this.#notebookWidget.value) {
this.#notebookWidget.value.onWillHide();
}
}
}
override clearInput() {
if (this.#notebookWidget.value) {
this._saveEditorViewState(this.input);
this.#notebookWidget.value.onWillHide();
}
if (this.#codeEditorWidget) {
this.#codeEditorWidget.dispose();
}
this.#notebookWidget = { value: undefined };
this.#widgetDisposableStore.clear();
super.clearInput();
}
override getControl(): { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } {
return {
notebookEditor: this.#notebookWidget.value,
codeEditor: this.#codeEditorWidget
};
}
}
registerThemingParticipant((theme, collector) => {
collector.addRule(`
.interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor {
outline: solid 1px var(--notebook-focused-cell-border-color);
}
.interactive-editor .input-cell-container .input-editor-container .monaco-editor {
outline: solid 1px var(--notebook-inactive-focused-cell-border-color);
}
.interactive-editor .input-cell-container .input-focus-indicator {
top: ${INPUT_CELL_VERTICAL_PADDING}px;
}
`);
const editorBackgroundColor = theme.getColor(cellEditorBackground) ?? theme.getColor(editorBackground);
if (editorBackgroundColor) {
collector.addRule(`.interactive-editor .input-cell-container .monaco-editor-background,
.interactive-editor .input-cell-container .margin-view-overlays {
background: ${editorBackgroundColor};
}`);
}
});