/*--------------------------------------------------------------------------------------------- * 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/notebook'; import 'vs/css!./media/notebookCellEditorHint'; import 'vs/css!./media/notebookCellInsertToolbar'; import 'vs/css!./media/notebookCellStatusBar'; import 'vs/css!./media/notebookCellTitleToolbar'; import 'vs/css!./media/notebookFocusIndicator'; import 'vs/css!./media/notebookToolbar'; import 'vs/css!./media/notebookDnd'; import 'vs/css!./media/notebookFolding'; import 'vs/css!./media/notebookCellOutput'; import 'vs/css!./media/notebookEditorStickyScroll'; import 'vs/css!./media/notebookKernelActionViewItem'; import 'vs/css!./media/notebookOutline'; import { PixelRatio } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { DeferredPromise, runWhenIdle, SequencerByKey } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { setTimeout0 } from 'vs/base/common/platform'; import { extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { contrastBorder, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_PANE_BACKGROUND, PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealSyncType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; import { NotebookCellStateChangedEvent, NotebookLayoutChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys'; import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; import { ListViewInfoAccessor, NotebookCellList, NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; import { CodeCellRenderer, MarkupCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { IAckOutputHeight, IMarkupCellInitialization } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages'; import { CodeCellViewModel, outputDisplayLimit } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookEditorWorkbenchToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys'; import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler'; import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellEditType, CellKind, INotebookSearchOptions, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_OUPTUT_INPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions, OutputInnerContainerTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookPerfMarks } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { BaseCellEditorOptions } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions'; import { FloatingEditorClickMenu } from 'vs/workbench/browser/codeeditor'; import { IDimension } from 'vs/editor/common/core/dimension'; import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { Schemas } from 'vs/base/common/network'; import { DropIntoEditorController } from 'vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController'; import { CopyPasteController } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController'; import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll'; import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; const $ = DOM.$; export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOptions { // We inlined the id to avoid loading comment contrib in tests const skipContributions = [ 'editor.contrib.review', FloatingEditorClickMenu.ID, 'editor.contrib.dirtydiff', 'editor.contrib.testingOutputPeek', 'editor.contrib.testingDecorations', 'store.contrib.stickyScrollController', 'editor.contrib.findController', 'editor.contrib.emptyTextEditorHint' ]; const contributions = EditorExtensionsRegistry.getEditorContributions().filter(c => skipContributions.indexOf(c.id) === -1); return { menuIds: { notebookToolbar: MenuId.NotebookToolbar, cellTitleToolbar: MenuId.NotebookCellTitle, cellDeleteToolbar: MenuId.NotebookCellDelete, cellInsertToolbar: MenuId.NotebookCellBetween, cellTopInsertToolbar: MenuId.NotebookCellListTop, cellExecuteToolbar: MenuId.NotebookCellExecute, cellExecutePrimary: MenuId.NotebookCellExecutePrimary, }, cellEditorContributions: contributions }; } export class NotebookEditorWidget extends Disposable implements INotebookEditorDelegate, INotebookEditor { //#region Eventing private readonly _onDidChangeCellState = this._register(new Emitter()); readonly onDidChangeCellState = this._onDidChangeCellState.event; private readonly _onDidChangeViewCells = this._register(new Emitter()); readonly onDidChangeViewCells: Event = this._onDidChangeViewCells.event; private readonly _onWillChangeModel = this._register(new Emitter()); readonly onWillChangeModel: Event = this._onWillChangeModel.event; private readonly _onDidChangeModel = this._register(new Emitter()); readonly onDidChangeModel: Event = this._onDidChangeModel.event; private readonly _onDidAttachViewModel = this._register(new Emitter()); readonly onDidAttachViewModel: Event = this._onDidAttachViewModel.event; private readonly _onDidChangeOptions = this._register(new Emitter()); readonly onDidChangeOptions: Event = this._onDidChangeOptions.event; private readonly _onDidChangeDecorations = this._register(new Emitter()); readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; private readonly _onDidScroll = this._register(new Emitter()); readonly onDidScroll: Event = this._onDidScroll.event; private readonly _onDidChangeActiveCell = this._register(new Emitter()); readonly onDidChangeActiveCell: Event = this._onDidChangeActiveCell.event; private readonly _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; private readonly _onDidChangeVisibleRanges = this._register(new Emitter()); readonly onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; private readonly _onDidFocusEmitter = this._register(new Emitter()); readonly onDidFocusWidget = this._onDidFocusEmitter.event; private readonly _onDidBlurEmitter = this._register(new Emitter()); readonly onDidBlurWidget = this._onDidBlurEmitter.event; private readonly _onDidChangeActiveEditor = this._register(new Emitter()); readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; private readonly _onDidChangeActiveKernel = this._register(new Emitter()); readonly onDidChangeActiveKernel: Event = this._onDidChangeActiveKernel.event; private readonly _onMouseUp: Emitter = this._register(new Emitter()); readonly onMouseUp: Event = this._onMouseUp.event; private readonly _onMouseDown: Emitter = this._register(new Emitter()); readonly onMouseDown: Event = this._onMouseDown.event; private readonly _onDidReceiveMessage = this._register(new Emitter()); readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; private readonly _onDidRenderOutput = this._register(new Emitter()); private readonly onDidRenderOutput = this._onDidRenderOutput.event; private readonly _onDidRemoveOutput = this._register(new Emitter()); private readonly onDidRemoveOutput = this._onDidRemoveOutput.event; private readonly _onDidResizeOutputEmitter = this._register(new Emitter()); readonly onDidResizeOutput = this._onDidResizeOutputEmitter.event; //#endregion private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; private _notebookTopToolbar!: NotebookEditorWorkbenchToolbar; private _notebookStickyScrollContainer!: HTMLElement; private _notebookStickyScroll!: NotebookStickyScroll; private _notebookOverviewRulerContainer!: HTMLElement; private _notebookOverviewRuler!: NotebookOverviewRuler; private _body!: HTMLElement; private _styleElement!: HTMLStyleElement; private _overflowContainer!: HTMLElement; private _webview: BackLayerWebView | null = null; private _webviewResolvePromise: Promise | null> | null = null; private _webviewTransparentCover: HTMLElement | null = null; private _listDelegate: NotebookCellListDelegate | null = null; private _list!: INotebookCellList; private _listViewInfoAccessor!: ListViewInfoAccessor; private _dndController: CellDragAndDropController | null = null; private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); private _viewContext: ViewContext; private _notebookViewModel: NotebookViewModel | undefined; private _localStore: DisposableStore = this._register(new DisposableStore()); private _localCellStateListeners: DisposableStore[] = []; private _fontInfo: FontInfo | undefined; private _dimension?: DOM.Dimension; private _position?: DOM.IDomPosition; private _shadowElement?: HTMLElement; private _shadowElementViewInfo: { height: number; width: number; top: number; left: number } | null = null; private readonly _editorFocus: IContextKey; private readonly _outputFocus: IContextKey; private readonly _editorEditable: IContextKey; private readonly _cursorNavMode: IContextKey; private readonly _outputInputFocus: IContextKey; protected readonly _contributions = new Map(); private _scrollBeyondLastLine: boolean; private readonly _insetModifyQueueByOutputId = new SequencerByKey(); private _cellContextKeyManager: CellContextKeyManager | null = null; private readonly _uuid = generateUuid(); private _focusTracker!: DOM.IFocusTracker; private _webviewFocused: boolean = false; private _isVisible = false; get isVisible() { return this._isVisible; } private _isDisposed: boolean = false; get isDisposed() { return this._isDisposed; } set viewModel(newModel: NotebookViewModel | undefined) { this._onWillChangeModel.fire(this._notebookViewModel?.notebookDocument); this._notebookViewModel = newModel; this._onDidChangeModel.fire(newModel?.notebookDocument); } get viewModel() { return this._notebookViewModel; } get textModel() { return this._notebookViewModel?.notebookDocument; } get isReadOnly() { return this._notebookViewModel?.options.isReadOnly ?? false; } get activeCodeEditor(): ICodeEditor | undefined { if (this._isDisposed) { return; } const [focused] = this._list.getFocusedElements(); return this._renderedEditors.get(focused); } get codeEditors(): [ICellViewModel, ICodeEditor][] { return [...this._renderedEditors]; } get visibleRanges() { return this._list.visibleRanges || []; } private _baseCellEditorOptions = new Map(); readonly isEmbedded: boolean; private _readOnly: boolean; public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; private readonly _notebookOptions: NotebookOptions; public readonly _notebookOutline: NotebookCellOutlineProvider; private _currentProgress: IProgressRunner | undefined; get notebookOptions() { return this._notebookOptions; } constructor( readonly creationOptions: INotebookEditorCreationOptions, dimension: DOM.Dimension | undefined, @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService editorGroupsService: IEditorGroupsService, @INotebookRendererMessagingService private readonly notebookRendererMessaging: INotebookRendererMessagingService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @ILayoutService private readonly layoutService: ILayoutService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @ITelemetryService private readonly telemetryService: ITelemetryService, @INotebookExecutionService private readonly notebookExecutionService: INotebookExecutionService, @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService readonly logService: INotebookLoggingService, @IKeybindingService readonly keybindingService: IKeybindingService ) { super(); this._dimension = dimension; this.isEmbedded = creationOptions.isEmbedded ?? false; this._readOnly = creationOptions.isReadOnly ?? false; this._notebookOptions = creationOptions.options ?? new NotebookOptions(this.configurationService, notebookExecutionStateService, this._readOnly); this._register(this._notebookOptions); this._viewContext = new ViewContext( this._notebookOptions, new NotebookEventDispatcher(), language => this.getBaseCellEditorOptions(language)); this._register(this._viewContext.eventDispatcher.onDidChangeCellState(e => { this._onDidChangeCellState.fire(e); })); this._overlayContainer = document.createElement('div'); this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer); this.instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); this._register(_notebookService.onDidChangeOutputRenderers(() => { this._updateOutputRenderers(); })); this._register(this.instantiationService.createInstance(NotebookEditorContextKeys, this)); this._notebookOutline = this._register(this.instantiationService.createInstance(NotebookCellOutlineProvider, this, OutlineTarget.QuickPick)); this._register(notebookKernelService.onDidChangeSelectedNotebooks(e => { if (isEqual(e.notebook, this.viewModel?.uri)) { this._loadKernelPreloads(); this._onDidChangeActiveKernel.fire(); } })); this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { this._scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); if (this._dimension && this._isVisible) { this.layout(this._dimension); } } })); this._register(this._notebookOptions.onDidChangeOptions(e => { if (e.cellStatusBarVisibility || e.cellToolbarLocation || e.cellToolbarInteraction) { this._updateForNotebookConfiguration(); } if (e.fontFamily) { this._generateFontInfo(); } if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.markupFontSize || e.fontFamily || e.insertToolbarAlignment || e.outputFontSize || e.outputLineHeight || e.outputFontFamily || e.outputWordWrap || e.outputScrolling ) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions({ ...this.notebookOptions.computeWebviewOptions(), fontFamily: this._generateFontFamily() }); } if (this._dimension && this._isVisible) { this.layout(this._dimension); } })); this._register(editorGroupsService.onDidScroll(e => { if (!this._shadowElement || !this._isVisible) { return; } this.updateShadowElement(this._shadowElement, this._dimension); this.layoutContainerOverShadowElement(this._dimension, this._position); })); this.notebookEditorService.addNotebookEditor(this); const id = generateUuid(); this._overlayContainer.id = `notebook-${id}`; this._overlayContainer.className = 'notebookOverlay'; this._overlayContainer.classList.add('notebook-editor'); this._overlayContainer.style.visibility = 'hidden'; this.layoutService.container.appendChild(this._overlayContainer); this._createBody(this._overlayContainer); this._generateFontInfo(); this._isVisible = true; this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.scopedContextKeyService); this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.scopedContextKeyService); this._outputInputFocus = NOTEBOOK_OUPTUT_INPUT_FOCUSED.bindTo(this.scopedContextKeyService); this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService); this._cursorNavMode = NOTEBOOK_CURSOR_NAVIGATION_MODE.bindTo(this.scopedContextKeyService); this._editorEditable.set(!creationOptions.isReadOnly); let contributions: INotebookEditorContributionDescription[]; if (Array.isArray(this.creationOptions.contributions)) { contributions = this.creationOptions.contributions; } else { contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); } for (const desc of contributions) { let contribution: INotebookEditorContribution | undefined; try { contribution = this.instantiationService.createInstance(desc.ctor, this); } catch (err) { onUnexpectedError(err); } if (contribution) { if (!this._contributions.has(desc.id)) { this._contributions.set(desc.id, contribution); } else { contribution.dispose(); throw new Error(`DUPLICATE notebook editor contribution: '${desc.id}'`); } } } this._updateForNotebookConfiguration(); } private _debugFlag: boolean = false; private _debug(...args: any[]) { if (!this._debugFlag) { return; } notebookDebug(...args); } /** * EditorId */ public getId(): string { return this._uuid; } getViewModel(): NotebookViewModel | undefined { return this.viewModel; } getLength() { return this.viewModel?.length ?? 0; } getSelections() { return this.viewModel?.getSelections() ?? []; } setSelections(selections: ICellRange[]) { if (!this.viewModel) { return; } const focus = this.viewModel.getFocus(); this.viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: focus, selections: selections }); } getFocus() { return this.viewModel?.getFocus() ?? { start: 0, end: 0 }; } setFocus(focus: ICellRange) { if (!this.viewModel) { return; } const selections = this.viewModel.getSelections(); this.viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: focus, selections: selections }); } getSelectionViewModels(): ICellViewModel[] { if (!this.viewModel) { return []; } const cellsSet = new Set(); return this.viewModel.getSelections().map(range => this.viewModel!.viewCells.slice(range.start, range.end)).reduce((a, b) => { b.forEach(cell => { if (!cellsSet.has(cell.handle)) { cellsSet.add(cell.handle); a.push(cell); } }); return a; }, [] as ICellViewModel[]); } hasModel(): this is IActiveNotebookEditorDelegate { return !!this._notebookViewModel; } showProgress(): void { this._currentProgress = this.editorProgressService.show(true); } hideProgress(): void { if (this._currentProgress) { this._currentProgress.done(); this._currentProgress = undefined; } } //#region Editor Core getBaseCellEditorOptions(language: string): IBaseCellEditorOptions { const existingOptions = this._baseCellEditorOptions.get(language); if (existingOptions) { return existingOptions; } else { const options = new BaseCellEditorOptions(this, this.notebookOptions, this.configurationService, language); this._baseCellEditorOptions.set(language, options); return options; } } private _updateForNotebookConfiguration() { if (!this._overlayContainer) { return; } this._overlayContainer.classList.remove('cell-title-toolbar-left'); this._overlayContainer.classList.remove('cell-title-toolbar-right'); this._overlayContainer.classList.remove('cell-title-toolbar-hidden'); const cellToolbarLocation = this._notebookOptions.computeCellToolbarLocation(this.viewModel?.viewType); this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); const cellToolbarInteraction = this._notebookOptions.getLayoutConfiguration().cellToolbarInteraction; let cellToolbarInteractionState = 'hover'; this._overlayContainer.classList.remove('cell-toolbar-hover'); this._overlayContainer.classList.remove('cell-toolbar-click'); if (cellToolbarInteraction === 'hover' || cellToolbarInteraction === 'click') { cellToolbarInteractionState = cellToolbarInteraction; } this._overlayContainer.classList.add(`cell-toolbar-${cellToolbarInteractionState}`); } private _generateFontInfo(): void { const editorOptions = this.configurationService.getValue('editor'); this._fontInfo = FontMeasurements.readFontInfo(BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.value)); } private _createBody(parent: HTMLElement): void { this._notebookTopToolbarContainer = document.createElement('div'); this._notebookTopToolbarContainer.classList.add('notebook-toolbar-container'); this._notebookTopToolbarContainer.style.display = 'none'; DOM.append(parent, this._notebookTopToolbarContainer); this._notebookStickyScrollContainer = document.createElement('div'); this._notebookStickyScrollContainer.classList.add('notebook-sticky-scroll-container'); DOM.append(parent, this._notebookStickyScrollContainer); this._body = document.createElement('div'); DOM.append(parent, this._body); this._body.classList.add('cell-list-container'); this._createLayoutStyles(); this._createCellList(); this._notebookOverviewRulerContainer = document.createElement('div'); this._notebookOverviewRulerContainer.classList.add('notebook-overview-ruler-container'); this._list.scrollableElement.appendChild(this._notebookOverviewRulerContainer); this._registerNotebookOverviewRuler(); this._overflowContainer = document.createElement('div'); this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); } private _generateFontFamily() { return this._fontInfo?.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; } private _createLayoutStyles(): void { this._styleElement = DOM.createStyleSheet(this._body); const { cellRightMargin, cellTopMargin, cellRunGutter, cellBottomMargin, codeCellLeftMargin, markdownCellGutter, markdownCellLeftMargin, markdownCellBottomMargin, markdownCellTopMargin, collapsedIndicatorHeight, compactView, focusIndicator, insertToolbarPosition, insertToolbarAlignment, fontSize, outputFontSize, focusIndicatorLeftMargin, focusIndicatorGap } = this._notebookOptions.getLayoutConfiguration(); const { bottomToolbarGap, bottomToolbarHeight } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); const styleSheets: string[] = []; if (!this._fontInfo) { this._generateFontInfo(); } const fontFamily = this._generateFontFamily(); styleSheets.push(` .notebook-editor { --notebook-cell-output-font-size: ${outputFontSize}px; --notebook-cell-input-preview-font-size: ${fontSize}px; --notebook-cell-input-preview-font-family: ${fontFamily}; } `); if (compactView) { styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); } else { styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); } // focus indicator if (focusIndicator === 'border') { styleSheets.push(` .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { content: ""; position: absolute; width: 100%; height: 1px; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { content: ""; position: absolute; width: 1px; height: 100%; z-index: 10; } /* top border */ .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { border-top: 1px solid transparent; } /* left border */ .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { border-left: 1px solid transparent; } /* bottom border */ .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { border-bottom: 1px solid transparent; } /* right border */ .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { border-right: 1px solid transparent; } `); // left and right border margins styleSheets.push(` .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) }`); } else { styleSheets.push(` .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator { border-left: 3px solid transparent; border-radius: 4px; width: 0px; margin-left: ${focusIndicatorLeftMargin}px; border-color: var(--vscode-notebook-inactiveFocusedCellBorder) !important; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-left .codeOutput-focus-indicator-container, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-output-hover .cell-focus-indicator-left .codeOutput-focus-indicator-container, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .markdown-cell-hover .cell-focus-indicator-left .codeOutput-focus-indicator-container, .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row:hover .cell-focus-indicator-left .codeOutput-focus-indicator-container { display: block; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator-container:hover .codeOutput-focus-indicator { border-left: 5px solid transparent; margin-left: ${focusIndicatorLeftMargin - 1}px; } `); styleSheets.push(` .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-inner-container.cell-output-focus .cell-focus-indicator-left .codeOutput-focus-indicator, .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container .cell-focus-indicator-left .codeOutput-focus-indicator { border-color: var(--vscode-notebook-focusedCellBorder) !important; } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-inner-container .cell-focus-indicator-left .output-focus-indicator { margin-top: ${focusIndicatorGap}px; } `); } // between cell insert toolbar if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`); } else { styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`); } if (insertToolbarAlignment === 'left') { styleSheets.push(` .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { margin-right: 0px !important; }`); styleSheets.push(` .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { padding: 0px !important; justify-content: center; border-radius: 4px; }`); styleSheets.push(` .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { align-items: flex-start; justify-content: left; margin: 0 16px 0 ${8 + codeCellLeftMargin}px; }`); styleSheets.push(` .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, .notebookOverlay .cell-bottom-toolbar-container .action-item { border: 0px; }`); } styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container.webview-backed-markdown-cell { padding: 0; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .webview-backed-markdown-cell.markdown-cell-edit-mode .cell.code { padding-bottom: ${markdownCellBottomMargin}px; padding-top: ${markdownCellTopMargin}px; }`); styleSheets.push(`.notebookOverlay .output { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .output { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); // comment styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-comment-container { left: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-comment-container { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); // output collapse button styleSheets.push(`.monaco-workbench .notebookOverlay .output .output-collapse-container .expandButton { left: -${cellRunGutter}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay .output .output-collapse-container .expandButton { position: absolute; width: ${cellRunGutter}px; padding: 6px 0px; }`); // show more container styleSheets.push(`.notebookOverlay .output-show-more-container { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .output-show-more-container { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${cellRunGutter}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { left: ${(markdownCellGutter - 20) / 2 + markdownCellLeftMargin}px; }`); styleSheets.push(`.notebookOverlay > .cell-list-container .notebook-folded-hint { left: ${markdownCellGutter + markdownCellLeftMargin + 8}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row :not(.webview-backed-markdown-cell) .cell-focus-indicator-top { height: ${cellTopMargin}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${bottomToolbarGap}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left { width: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${codeCellLeftMargin}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator.cell-focus-indicator-right { width: ${cellRightMargin}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { height: ${cellBottomMargin}px; }`); styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); styleSheets.push(` .notebookOverlay .monaco-list .monaco-list-row:has(+ .monaco-list-row.selected) .cell-focus-indicator-bottom { height: ${bottomToolbarGap + cellBottomMargin}px; } `); styleSheets.push(` .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .input-collapse-container .cell-collapse-preview { line-height: ${collapsedIndicatorHeight}px; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .input-collapse-container .cell-collapse-preview .monaco-tokenized-source { max-height: ${collapsedIndicatorHeight}px; } `); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar { height: ${bottomToolbarHeight}px }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container .monaco-toolbar { height: ${bottomToolbarHeight}px }`); // cell toolbar styleSheets.push(`.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { right: ${cellRightMargin + 26}px; } .monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { left: ${codeCellLeftMargin + cellRunGutter + 16}px; } .monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { display: none; }`); // cell output innert container styleSheets.push(` .monaco-workbench .notebookOverlay .output > div.foreground.output-inner-container { padding: ${OutputInnerContainerTopPadding}px 8px; } .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .output-collapse-container { padding: ${OutputInnerContainerTopPadding}px 8px; } `); this._styleElement.textContent = styleSheets.join('\n'); } private _createCellList(): void { this._body.classList.add('cell-list-container'); this._dndController = this._register(new CellDragAndDropController(this, this._body)); const getScopedContextKeyService = (container: HTMLElement) => this._list.contextKeyService.createScoped(container); const renderers = [ this.instantiationService.createInstance(CodeCellRenderer, this, this._renderedEditors, this._dndController, getScopedContextKeyService), this.instantiationService.createInstance(MarkupCellRenderer, this, this._dndController, this._renderedEditors, getScopedContextKeyService), ]; renderers.forEach(renderer => { this._register(renderer); }); this._listDelegate = this.instantiationService.createInstance(NotebookCellListDelegate); this._register(this._listDelegate); const createNotebookAriaLabel = () => { const keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); if (this.configurationService.getValue(AccessibilityVerbositySettingId.Notebook)) { return keybinding ? nls.localize('notebookTreeAriaLabelHelp', "Notebook\nUse {0} for accessibility help", keybinding) : nls.localize('notebookTreeAriaLabelHelpNoKb', "Notebook\nRun the Open Accessibility Help command for more information", keybinding); } return nls.localize('notebookTreeAriaLabel', "Notebook"); }; this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', this._body, this._viewContext, this._listDelegate, renderers, this.scopedContextKeyService, { setRowLineHeight: false, setRowHeight: false, supportDynamicHeights: true, horizontalScrolling: false, keyboardSupport: false, mouseSupport: true, multipleSelectionSupport: true, selectionNavigation: true, typeNavigationEnabled: true, paddingTop: this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType), paddingBottom: 0, transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', initialSize: this._dimension, styleController: (_suffix: string) => { return this._list; }, overrideStyles: { listBackground: notebookEditorBackground, listActiveSelectionBackground: notebookEditorBackground, listActiveSelectionForeground: foreground, listFocusAndSelectionBackground: notebookEditorBackground, listFocusAndSelectionForeground: foreground, listFocusBackground: notebookEditorBackground, listFocusForeground: foreground, listHoverForeground: foreground, listHoverBackground: notebookEditorBackground, listHoverOutline: focusBorder, listFocusOutline: focusBorder, listInactiveSelectionBackground: notebookEditorBackground, listInactiveSelectionForeground: foreground, listInactiveFocusBackground: notebookEditorBackground, listInactiveFocusOutline: notebookEditorBackground, }, accessibilityProvider: { getAriaLabel: (element: CellViewModel) => { if (!this.viewModel) { return ''; } const index = this.viewModel.getCellIndex(element); if (index >= 0) { return `Cell ${index}, ${element.cellKind === CellKind.Markup ? 'markdown' : 'code'} cell`; } return ''; }, getWidgetAriaLabel: createNotebookAriaLabel }, }, ); this._dndController.setList(this._list); // create Webview this._register(this._list); this._listViewInfoAccessor = new ListViewInfoAccessor(this._list); this._register(this._listViewInfoAccessor); this._register(combinedDisposable(...renderers)); // top cell toolbar this._listTopCellToolbar = this._register(this.instantiationService.createInstance(ListTopCellToolbar, this, this.scopedContextKeyService, this._list.rowsContainer)); // transparent cover this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); this._webviewTransparentCover.style.display = 'none'; this._register(DOM.addStandardDisposableGenericMouseDownListener(this._overlayContainer, (e: StandardMouseEvent) => { if (e.target.classList.contains('slider') && this._webviewTransparentCover) { this._webviewTransparentCover.style.display = 'block'; } })); this._register(DOM.addStandardDisposableGenericMouseUpListener(this._overlayContainer, () => { if (this._webviewTransparentCover) { // no matter when this._webviewTransparentCover.style.display = 'none'; } })); this._register(this._list.onMouseDown(e => { if (e.element) { this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); } })); this._register(this._list.onMouseUp(e => { if (e.element) { this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); } })); this._register(this._list.onDidChangeFocus(_e => { this._onDidChangeActiveEditor.fire(this); this._onDidChangeActiveCell.fire(); this._cursorNavMode.set(false); })); this._register(this._list.onContextMenu(e => { this.showListContextMenu(e); })); this._register(this._list.onDidChangeVisibleRanges(() => { this._onDidChangeVisibleRanges.fire(); })); this._register(this._list.onDidScroll((e) => { this._onDidScroll.fire(); if (e.scrollTop !== e.oldScrollTop) { this.clearActiveCellWidgets(); } })); this._focusTracker = this._register(DOM.trackFocus(this.getDomNode())); this._register(this._focusTracker.onDidBlur(() => { this._editorFocus.set(false); this.viewModel?.setEditorFocus(false); this._onDidBlurEmitter.fire(); })); this._register(this._focusTracker.onDidFocus(() => { this._editorFocus.set(true); this.viewModel?.setEditorFocus(true); this._onDidFocusEmitter.fire(); })); this._registerNotebookActionsToolbar(); this._registerNotebookStickyScroll(); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Notebook)) { this._list.ariaLabel = createNotebookAriaLabel(); } })); } private showListContextMenu(e: IListContextMenuEvent) { this.contextMenuService.showContextMenu({ menuId: MenuId.NotebookCellTitle, contextKeyService: this.scopedContextKeyService, getAnchor: () => e.anchor }); } private _registerNotebookOverviewRuler() { this._notebookOverviewRuler = this._register(this.instantiationService.createInstance(NotebookOverviewRuler, this, this._notebookOverviewRulerContainer!)); } private _registerNotebookActionsToolbar() { this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorWorkbenchToolbar, this, this.scopedContextKeyService, this._notebookOptions, this._notebookTopToolbarContainer)); this._register(this._notebookTopToolbar.onDidChangeVisibility(() => { if (this._dimension && this._isVisible) { this.layout(this._dimension); } })); } private _registerNotebookStickyScroll() { this._notebookStickyScroll = this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._notebookOutline, this._list)); } private _updateOutputRenderers() { if (!this.viewModel || !this._webview) { return; } this._webview.updateOutputRenderers(); this.viewModel.viewCells.forEach(cell => { cell.outputsViewModels.forEach(output => { if (output.pickedMimeType?.rendererId === RENDERER_NOT_AVAILABLE) { output.resetRenderer(); } }); }); } getDomNode() { return this._overlayContainer; } getOverflowContainerDomNode() { return this._overflowContainer; } getInnerWebview(): IWebviewElement | undefined { return this._webview?.webview; } setEditorProgressService(editorProgressService: IEditorProgressService): void { this.editorProgressService = editorProgressService; } setParentContextKeyService(parentContextKeyService: IContextKeyService): void { this.scopedContextKeyService.updateParent(parentContextKeyService); } async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks): Promise { if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { const oldTopInsertToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); const oldBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); this._detachModel(); await this._attachModel(textModel, viewState, perf); const newTopInsertToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); const newBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); if (oldTopInsertToolbarHeight !== newTopInsertToolbarHeight || oldBottomToolbarDimensions.bottomToolbarGap !== newBottomToolbarDimensions.bottomToolbarGap || oldBottomToolbarDimensions.bottomToolbarHeight !== newBottomToolbarDimensions.bottomToolbarHeight) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions({ ...this.notebookOptions.computeWebviewOptions(), fontFamily: this._generateFontFamily() }); } type WorkbenchNotebookOpenClassification = { owner: 'rebornix'; comment: 'Identify the notebook editor view type'; scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'View type of the notebook editor' }; }; type WorkbenchNotebookOpenEvent = { scheme: string; ext: string; viewType: string; }; this.telemetryService.publicLog2('notebook/editorOpened', { scheme: textModel.uri.scheme, ext: extname(textModel.uri), viewType: textModel.viewType }); } else { this.restoreListViewState(viewState); } this._restoreSelectedKernel(viewState); // load preloads for matching kernel this._loadKernelPreloads(); // clear state this._dndController?.clearGlobalDragState(); this._localStore.add(this._list.onDidChangeFocus(() => { this.updateContextKeysOnFocusChange(); })); this.updateContextKeysOnFocusChange(); // render markdown top down on idle this._backgroundMarkdownRendering(); } private _backgroundMarkdownRenderRunning = false; private _backgroundMarkdownRendering() { if (this._backgroundMarkdownRenderRunning) { return; } this._backgroundMarkdownRenderRunning = true; runWhenIdle((deadline) => { this._backgroundMarkdownRenderingWithDeadline(deadline); }); } private _backgroundMarkdownRenderingWithDeadline(deadline: IdleDeadline) { const endTime = Date.now() + deadline.timeRemaining(); const execute = () => { try { this._backgroundMarkdownRenderRunning = true; if (this._isDisposed) { return; } if (!this.viewModel) { return; } const firstMarkupCell = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Markup && !this._webview?.markupPreviewMapping.has(cell.id) && !this.cellIsHidden(cell)) as MarkupCellViewModel | undefined; if (!firstMarkupCell) { return; } this.createMarkupPreview(firstMarkupCell); } finally { this._backgroundMarkdownRenderRunning = false; } if (Date.now() < endTime) { setTimeout0(execute); } else { this._backgroundMarkdownRendering(); } }; execute(); } private updateContextKeysOnFocusChange() { if (!this.viewModel) { return; } const focused = this._list.getFocusedElements()[0]; if (focused) { if (!this._cellContextKeyManager) { this._cellContextKeyManager = this._localStore.add(this.instantiationService.createInstance(CellContextKeyManager, this, focused as CellViewModel)); } this._cellContextKeyManager.updateForElement(focused as CellViewModel); } } async setOptions(options: INotebookEditorOptions | undefined) { if (options?.isReadOnly !== undefined) { this._readOnly = options?.isReadOnly; } if (!this.viewModel) { return; } this.viewModel.updateOptions({ isReadOnly: this._readOnly }); this.notebookOptions.updateOptions(this._readOnly); // reveal cell if editor options tell to do so const cellOptions = options?.cellOptions ?? this._parseIndexedCellOptions(options); if (cellOptions) { const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); const selection = cellOptions.options?.selection; if (selection) { cell.updateEditState(CellEditState.Editing, 'setOptions'); cell.focusMode = CellFocusMode.Editor; await this.revealRangeInCenterIfOutsideViewportAsync(cell, new Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber || selection.startLineNumber, selection.endColumn || selection.startColumn)); } else if (options?.cellRevealType === CellRevealType.NearTopIfOutsideViewport) { await this._list.revealCellAsync(cell, CellRevealType.NearTopIfOutsideViewport); } else { await this._list.revealCellAsync(cell, CellRevealType.CenterIfOutsideViewport); } const editor = this._renderedEditors.get(cell)!; if (editor) { if (cellOptions.options?.selection) { const { selection } = cellOptions.options; const editorSelection = new Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber || selection.startLineNumber, selection.endColumn || selection.startColumn); editor.setSelection(editorSelection); editor.revealPositionInCenterIfOutsideViewport({ lineNumber: selection.startLineNumber, column: selection.startColumn }); await this.revealRangeInCenterIfOutsideViewportAsync(cell, editorSelection); } if (!cellOptions.options?.preserveFocus) { editor.focus(); } } } } // select cells if options tell to do so // todo@rebornix https://github.com/microsoft/vscode/issues/118108 support selections not just focus // todo@rebornix support multipe selections if (options?.cellSelections) { const focusCellIndex = options.cellSelections[0].start; const focusedCell = this.viewModel.cellAt(focusCellIndex); if (focusedCell) { this.viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: focusCellIndex, end: focusCellIndex + 1 }, selections: options.cellSelections }); this.revealInCenterIfOutsideViewport(focusedCell); } } this._updateForOptions(); this._onDidChangeOptions.fire(); } private _parseIndexedCellOptions(options: INotebookEditorOptions | undefined) { if (options?.indexedCellOptions) { // convert index based selections const cell = this.cellAt(options.indexedCellOptions.index); if (cell) { return { resource: cell.uri, options: { selection: options.indexedCellOptions.selection, preserveFocus: false } }; } } return undefined; } private _detachModel() { this._localStore.clear(); dispose(this._localCellStateListeners); this._list.detachViewModel(); this.viewModel?.dispose(); // avoid event this.viewModel = undefined; this._webview?.dispose(); this._webview?.element.remove(); this._webview = null; this._list.clear(); } private _updateForOptions(): void { if (!this.viewModel) { return; } this._editorEditable.set(!this.viewModel.options.isReadOnly); this._overflowContainer.classList.toggle('notebook-editor-editable', !this.viewModel.options.isReadOnly); this.getDomNode().classList.toggle('notebook-editor-editable', !this.viewModel.options.isReadOnly); } private async _resolveWebview(): Promise | null> { if (!this.textModel) { return null; } if (this._webviewResolvePromise) { return this._webviewResolvePromise; } if (!this._webview) { this._ensureWebview(this.getId(), this.textModel.viewType, this.textModel.uri); } this._webviewResolvePromise = (async () => { if (!this._webview) { throw new Error('Notebook output webview object is not created successfully.'); } await this._webview.createWebview(); if (!this._webview.webview) { throw new Error('Notebook output webview element was not created successfully.'); } this._localStore.add(this._webview.webview.onDidBlur(() => { this._outputFocus.set(false); this._webviewFocused = false; this.updateEditorFocus(); this.updateCellFocusMode(); })); this._localStore.add(this._webview.webview.onDidFocus(() => { this._outputFocus.set(true); this.updateEditorFocus(); this._webviewFocused = true; })); this._localStore.add(this._webview.onMessage(e => { this._onDidReceiveMessage.fire(e); })); return this._webview; })(); return this._webviewResolvePromise; } private _ensureWebview(id: string, viewType: string, resource: URI) { if (this._webview) { return; } const that = this; this._webview = this.instantiationService.createInstance(BackLayerWebView, { get creationOptions() { return that.creationOptions; }, setScrollTop(scrollTop: number) { that._list.scrollTop = scrollTop; }, triggerScroll(event: IMouseWheelEvent) { that._list.triggerScrollFromMouseWheelEvent(event); }, getCellByInfo: that.getCellByInfo.bind(that), getCellById: that._getCellById.bind(that), toggleNotebookCellSelection: that._toggleNotebookCellSelection.bind(that), focusNotebookCell: that.focusNotebookCell.bind(that), focusNextNotebookCell: that.focusNextNotebookCell.bind(that), updateOutputHeight: that._updateOutputHeight.bind(that), scheduleOutputHeightAck: that._scheduleOutputHeightAck.bind(that), updateMarkupCellHeight: that._updateMarkupCellHeight.bind(that), setMarkupCellEditState: that._setMarkupCellEditState.bind(that), didStartDragMarkupCell: that._didStartDragMarkupCell.bind(that), didDragMarkupCell: that._didDragMarkupCell.bind(that), didDropMarkupCell: that._didDropMarkupCell.bind(that), didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that), didResizeOutput: that._didResizeOutput.bind(that), updatePerformanceMetadata: that._updatePerformanceMetadata.bind(that), didFocusOutputInputChange: that._didFocusOutputInputChange.bind(that), }, id, viewType, resource, { ...this._notebookOptions.computeWebviewOptions(), fontFamily: this._generateFontFamily() }, this.notebookRendererMessaging.getScoped(this._uuid)); this._webview.element.style.width = '100%'; // attach the webview container to the DOM tree first this._list.attachWebview(this._webview.element); } private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this._ensureWebview(this.getId(), textModel.viewType, textModel.uri); this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly }); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this.notebookOptions.updateOptions(this._readOnly); this._updateForOptions(); this._updateForNotebookConfiguration(); // restore view states, including contributions { // restore view state this.viewModel.restoreEditorViewState(viewState); // contribution state restore const contributionsState = viewState?.contributionsState || {}; for (const [id, contribution] of this._contributions) { if (typeof contribution.restoreViewState === 'function') { contribution.restoreViewState(contributionsState[id]); } } } this._localStore.add(this.viewModel.onDidChangeViewCells(e => { this._onDidChangeViewCells.fire(e); })); this._localStore.add(this.viewModel.onDidChangeSelection(() => { this._onDidChangeSelection.fire(); this.updateSelectedMarkdownPreviews(); })); this._localStore.add(this._list.onWillScroll(e => { if (this._webview?.isResolved()) { this._webviewTransparentCover!.style.transform = `translateY(${e.scrollTop})`; } })); let hasPendingChangeContentHeight = false; this._localStore.add(this._list.onDidChangeContentHeight(() => { if (hasPendingChangeContentHeight) { return; } hasPendingChangeContentHeight = true; this._localStore.add(DOM.scheduleAtNextAnimationFrame(() => { hasPendingChangeContentHeight = false; this._updateScrollHeight(); }, 100)); })); this._localStore.add(this._list.onDidRemoveOutputs(outputs => { outputs.forEach(output => this.removeInset(output)); })); this._localStore.add(this._list.onDidHideOutputs(outputs => { outputs.forEach(output => this.hideInset(output)); })); this._localStore.add(this._list.onDidRemoveCellsFromView(cells => { const hiddenCells: MarkupCellViewModel[] = []; const deletedCells: MarkupCellViewModel[] = []; for (const cell of cells) { if (cell.cellKind === CellKind.Markup) { const mdCell = cell as MarkupCellViewModel; if (this.viewModel?.viewCells.find(cell => cell.handle === mdCell.handle)) { // Cell has been folded but is still in model hiddenCells.push(mdCell); } else { // Cell was deleted deletedCells.push(mdCell); } } } this.hideMarkupPreviews(hiddenCells); this.deleteMarkupPreviews(deletedCells); })); // init rendering await this._warmupWithMarkdownRenderer(this.viewModel, viewState); perf?.mark('customMarkdownLoaded'); // model attached this._localCellStateListeners = this.viewModel.viewCells.map(cell => this._bindCellListener(cell)); this._lastCellWithEditorFocus = this.viewModel.viewCells.find(viewCell => this.getActiveCell() === viewCell && viewCell.focusMode === CellFocusMode.Editor) ?? null; this._localStore.add(this.viewModel.onDidChangeViewCells((e) => { if (this._isDisposed) { return; } // update cell listener [...e.splices].reverse().forEach(splice => { const [start, deleted, newCells] = splice; const deletedCells = this._localCellStateListeners.splice(start, deleted, ...newCells.map(cell => this._bindCellListener(cell))); dispose(deletedCells); }); if (e.splices.some(s => s[2].some(cell => cell.cellKind === CellKind.Markup))) { this._backgroundMarkdownRendering(); } })); if (this._dimension) { this._list.layout(this._dimension.height, this._dimension.width); } else { this._list.layout(); } this._dndController?.clearGlobalDragState(); // restore list state at last, it must be after list layout this.restoreListViewState(viewState); } private _bindCellListener(cell: ICellViewModel) { const store = new DisposableStore(); store.add(cell.onDidChangeLayout(e => { // e.totalHeight will be false it's not changed if (e.totalHeight || e.outerWidth) { this.layoutNotebookCell(cell, cell.layoutInfo.totalHeight, e.context); } })); if (cell.cellKind === CellKind.Code) { store.add((cell as CodeCellViewModel).onDidRemoveOutputs((outputs) => { outputs.forEach(output => this.removeInset(output)); })); } store.add((cell as CellViewModel).onDidChangeState(e => { if (e.inputCollapsedChanged && cell.isInputCollapsed && cell.cellKind === CellKind.Markup) { this.hideMarkupPreviews([(cell as MarkupCellViewModel)]); } if (e.outputCollapsedChanged && cell.isOutputCollapsed && cell.cellKind === CellKind.Code) { cell.outputsViewModels.forEach(output => this.hideInset(output)); } if (e.focusModeChanged) { this._validateCellFocusMode(cell); } })); return store; } private _lastCellWithEditorFocus: ICellViewModel | null = null; private _validateCellFocusMode(cell: ICellViewModel) { if (cell.focusMode !== CellFocusMode.Editor) { return; } if (this._lastCellWithEditorFocus && this._lastCellWithEditorFocus !== cell) { this._lastCellWithEditorFocus.focusMode = CellFocusMode.Container; } this._lastCellWithEditorFocus = cell; } private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { this.logService.debug('NotebookEditorWidget', 'warmup ' + this.viewModel?.uri.toString()); await this._resolveWebview(); this.logService.debug('NotebookEditorWidget', 'warmup - webview resolved'); // make sure that the webview is not visible otherwise users will see pre-rendered markdown cells in wrong position as the list view doesn't have a correct `top` offset yet this._webview!.element.style.visibility = 'hidden'; // warm up can take around 200ms to load markdown libraries, etc. await this._warmupViewportMarkdownCells(viewModel, viewState); this.logService.debug('NotebookEditorWidget', 'warmup - viewport warmed up'); // todo@rebornix @mjbvz, is this too complicated? /* now the webview is ready, and requests to render markdown are fast enough * we can start rendering the list view * render * - markdown cell -> request to webview to (10ms, basically just latency between UI and iframe) * - code cell -> render in place */ this._list.layout(0, 0); this._list.attachViewModel(viewModel); // now the list widget has a correct contentHeight/scrollHeight // setting scrollTop will work properly // after setting scroll top, the list view will update `top` of the scrollable element, e.g. `top: -584px` this._list.scrollTop = viewState?.scrollPosition?.top ?? 0; this._debug('finish initial viewport warmup and view state restore.'); this._webview!.element.style.visibility = 'visible'; this.logService.debug('NotebookEditorWidget', 'warmup - list view model attached, set to visible'); this._onDidAttachViewModel.fire(); } private async _warmupViewportMarkdownCells(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { if (viewState && viewState.cellTotalHeights) { const totalHeightCache = viewState.cellTotalHeights; const scrollTop = viewState.scrollPosition?.top ?? 0; const scrollBottom = scrollTop + Math.max(this._dimension?.height ?? 0, 1080); let offset = 0; const requests: [ICellViewModel, number][] = []; for (let i = 0; i < viewModel.length; i++) { const cell = viewModel.cellAt(i)!; const cellHeight = totalHeightCache[i] ?? 0; if (offset + cellHeight < scrollTop) { offset += cellHeight; continue; } if (cell.cellKind === CellKind.Markup) { requests.push([cell, offset]); } offset += cellHeight; if (offset > scrollBottom) { break; } } await this._webview!.initializeMarkup(requests.map(([model, offset]) => this.createMarkupCellInitialization(model, offset))); } else { const initRequests = viewModel.viewCells .filter(cell => cell.cellKind === CellKind.Markup) .slice(0, 5) .map(cell => this.createMarkupCellInitialization(cell, -10000)); await this._webview!.initializeMarkup(initRequests); // no cached view state so we are rendering the first viewport // after above async call, we already get init height for markdown cells, we can update their offset let offset = 0; const offsetUpdateRequests: { id: string; top: number }[] = []; const scrollBottom = Math.max(this._dimension?.height ?? 0, 1080); for (const cell of viewModel.viewCells) { if (cell.cellKind === CellKind.Markup) { offsetUpdateRequests.push({ id: cell.id, top: offset }); } offset += cell.getHeight(this.getLayoutInfo().fontInfo.lineHeight); if (offset > scrollBottom) { break; } } this._webview?.updateScrollTops([], offsetUpdateRequests); } } private createMarkupCellInitialization(model: ICellViewModel, offset: number): IMarkupCellInitialization { return ({ mime: model.mime, cellId: model.id, cellHandle: model.handle, content: model.getText(), offset: offset, visible: false, metadata: model.metadata, }); } restoreListViewState(viewState: INotebookEditorViewState | undefined): void { if (!this.viewModel) { return; } if (viewState?.scrollPosition !== undefined) { this._list.scrollTop = viewState!.scrollPosition.top; this._list.scrollLeft = viewState!.scrollPosition.left; } else { this._list.scrollTop = 0; this._list.scrollLeft = 0; } const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; if (focusIdx < this.viewModel.length) { const element = this.viewModel.cellAt(focusIdx); if (element) { this.viewModel?.updateSelectionsState({ kind: SelectionStateType.Handle, primary: element.handle, selections: [element.handle] }); } } else if (this._list.length > 0) { this.viewModel.updateSelectionsState({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }); } if (viewState?.editorFocused) { const cell = this.viewModel.cellAt(focusIdx); if (cell) { cell.focusMode = CellFocusMode.Editor; } } } private _restoreSelectedKernel(viewState: INotebookEditorViewState | undefined): void { if (viewState?.selectedKernelId && this.textModel) { const matching = this.notebookKernelService.getMatchingKernel(this.textModel); const kernel = matching.all.find(k => k.id === viewState.selectedKernelId); // Selected kernel may have already been picked prior to the view state loading // If so, don't overwrite it with the saved kernel. if (kernel && !matching.selected) { this.notebookKernelService.selectKernelForNotebook(kernel, this.textModel); } } } getEditorViewState(): INotebookEditorViewState { const state = this.viewModel?.getEditorViewState(); if (!state) { return { editingCells: {}, cellLineNumberStates: {}, editorViewStates: {}, collapsedInputCells: {}, collapsedOutputCells: {}, }; } if (this._list) { state.scrollPosition = { left: this._list.scrollLeft, top: this._list.scrollTop }; const cellHeights: { [key: number]: number } = {}; for (let i = 0; i < this.viewModel!.length; i++) { const elm = this.viewModel!.cellAt(i) as CellViewModel; cellHeights[i] = elm.layoutInfo.totalHeight; } state.cellTotalHeights = cellHeights; if (this.viewModel) { const focusRange = this.viewModel.getFocus(); const element = this.viewModel.cellAt(focusRange.start); if (element) { const itemDOM = this._list.domElementOfElement(element); const editorFocused = element.getEditState() === CellEditState.Editing && !!(document.activeElement && itemDOM && itemDOM.contains(document.activeElement)); state.editorFocused = editorFocused; state.focus = focusRange.start; } } } // Save contribution view states const contributionsState: { [key: string]: unknown } = {}; for (const [id, contribution] of this._contributions) { if (typeof contribution.saveViewState === 'function') { contributionsState[id] = contribution.saveViewState(); } } state.contributionsState = contributionsState; if (this.textModel?.uri.scheme === Schemas.untitled) { state.selectedKernelId = this.activeKernel?.id; } return state; } private _allowScrollBeyondLastLine() { return this._scrollBeyondLastLine && !this.isEmbedded; } layout(dimension: DOM.Dimension, shadowElement?: HTMLElement, position?: DOM.IDomPosition): void { if (!shadowElement && this._shadowElementViewInfo === null) { this._dimension = dimension; this._position = position; return; } if (dimension.width <= 0 || dimension.height <= 0) { this.onWillHide(); return; } if (shadowElement) { this.updateShadowElement(shadowElement, dimension, position); } if (this._shadowElementViewInfo && this._shadowElementViewInfo.width <= 0 && this._shadowElementViewInfo.height <= 0) { this.onWillHide(); return; } this._dimension = dimension; this._position = position; const newBodyHeight = Math.max(dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0), 0); DOM.size(this._body, dimension.width, newBodyHeight); const topInserToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType); const newCellListHeight = newBodyHeight; if (this._list.getRenderHeight() < newCellListHeight) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight }); this._list.layout(newCellListHeight, dimension.width); } else { // the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first. this._list.layout(newCellListHeight, dimension.width); this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight }); } this._overlayContainer.style.visibility = 'visible'; this._overlayContainer.style.display = 'block'; this._overlayContainer.style.position = 'absolute'; this._overlayContainer.style.overflow = 'hidden'; this.layoutContainerOverShadowElement(dimension, position); if (this._webviewTransparentCover) { this._webviewTransparentCover.style.height = `${dimension.height}px`; this._webviewTransparentCover.style.width = `${dimension.width}px`; } this._notebookTopToolbar.layout(this._dimension); this._notebookOverviewRuler.layout(); this._viewContext?.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } private updateShadowElement(shadowElement: HTMLElement, dimension?: IDimension, position?: DOM.IDomPosition) { this._shadowElement = shadowElement; if (dimension && position) { this._shadowElementViewInfo = { height: dimension.height, width: dimension.width, top: position.top, left: position.left, }; } else { // We have to recompute position and size ourselves (which is slow) const containerRect = shadowElement.getBoundingClientRect(); this._shadowElementViewInfo = { height: containerRect.height, width: containerRect.width, top: containerRect.top, left: containerRect.left }; } } private layoutContainerOverShadowElement(dimension?: DOM.Dimension, position?: DOM.IDomPosition): void { if (dimension && position) { this._overlayContainer.style.top = `${position.top}px`; this._overlayContainer.style.left = `${position.left}px`; this._overlayContainer.style.width = `${dimension.width}px`; this._overlayContainer.style.height = `${dimension.height}px`; return; } if (!this._shadowElementViewInfo) { return; } const elementContainerRect = this._overlayContainer.parentElement?.getBoundingClientRect(); this._overlayContainer.style.top = `${this._shadowElementViewInfo.top - (elementContainerRect?.top || 0)}px`; this._overlayContainer.style.left = `${this._shadowElementViewInfo.left - (elementContainerRect?.left || 0)}px`; this._overlayContainer.style.width = `${dimension ? dimension.width : this._shadowElementViewInfo.width}px`; this._overlayContainer.style.height = `${dimension ? dimension.height : this._shadowElementViewInfo.height}px`; } //#endregion //#region Focus tracker focus() { this._isVisible = true; this._editorFocus.set(true); if (this._webviewFocused) { this._webview?.focusWebview(); } else { if (this.viewModel) { const focusRange = this.viewModel.getFocus(); const element = this.viewModel.cellAt(focusRange.start); // The notebook editor doesn't have focus yet if (!this.hasEditorFocus()) { this.focusContainer(); // trigger editor to update as FocusTracker might not emit focus change event this.updateEditorFocus(); } if (element && element.focusMode === CellFocusMode.Editor) { element.updateEditState(CellEditState.Editing, 'editorWidget.focus'); element.focusMode = CellFocusMode.Editor; this.focusEditor(element); return; } } this._list.domFocus(); } if (this._currentProgress) { // The editor forces progress to hide when switching editors. So if progress should be visible, force it to show when the editor is focused. this.showProgress(); } } onShow() { this._isVisible = true; } private focusEditor(activeElement: CellViewModel): void { for (const [element, editor] of this._renderedEditors.entries()) { if (element === activeElement) { editor.focus(); return; } } } focusContainer() { if (this._webviewFocused) { this._webview?.focusWebview(); } else { this._list.focusContainer(); } } onWillHide() { this._isVisible = false; this._editorFocus.set(false); this._overlayContainer.style.visibility = 'hidden'; this._overlayContainer.style.left = '-50000px'; this._notebookTopToolbarContainer.style.display = 'none'; this.clearActiveCellWidgets(); } private clearActiveCellWidgets() { this._renderedEditors.forEach((editor, cell) => { if (this.getActiveCell() === cell && editor) { SuggestController.get(editor)?.cancelSuggestWidget(); DropIntoEditorController.get(editor)?.clearWidgets(); CopyPasteController.get(editor)?.clearWidgets(); } }); } private editorHasDomFocus(): boolean { return DOM.isAncestor(document.activeElement, this.getDomNode()); } updateEditorFocus() { // Note - focus going to the webview will fire 'blur', but the webview element will be // a descendent of the notebook editor root. this._focusTracker.refreshState(); const focused = this.editorHasDomFocus(); this._editorFocus.set(focused); this.viewModel?.setEditorFocus(focused); } updateCellFocusMode() { const activeCell = this.getActiveCell(); if (activeCell?.focusMode === CellFocusMode.Output && !this._webviewFocused) { // output previously has focus, but now it's blurred. activeCell.focusMode = CellFocusMode.Container; } } hasEditorFocus() { // _editorFocus is driven by the FocusTracker, which is only guaranteed to _eventually_ fire blur. // If we need to know whether we have focus at this instant, we need to check the DOM manually. this.updateEditorFocus(); return this.editorHasDomFocus(); } hasWebviewFocus() { return this._webviewFocused; } hasOutputTextSelection() { if (!this.hasEditorFocus()) { return false; } const windowSelection = window.getSelection(); if (windowSelection?.rangeCount !== 1) { return false; } const activeSelection = windowSelection.getRangeAt(0); if (activeSelection.startContainer === activeSelection.endContainer && activeSelection.endOffset - activeSelection.startOffset === 0) { return false; } let container: any = activeSelection.commonAncestorContainer; if (!this._body.contains(container)) { return false; } while (container && container !== this._body) { if ((container as HTMLElement).classList && (container as HTMLElement).classList.contains('output')) { return true; } container = container.parentNode; } return false; } _didFocusOutputInputChange(hasFocus: boolean) { this._outputInputFocus.set(hasFocus); } //#endregion //#region Editor Features focusElement(cell: ICellViewModel) { this.viewModel?.updateSelectionsState({ kind: SelectionStateType.Handle, primary: cell.handle, selections: [cell.handle] }); } get scrollTop() { return this._list.scrollTop; } getAbsoluteTopOfElement(cell: ICellViewModel) { return this._list.getCellViewScrollTop(cell); } scrollToBottom() { this._list.scrollToBottom(); } setScrollTop(scrollTop: number): void { this._list.scrollTop = scrollTop; } revealCellRangeInView(range: ICellRange) { return this._list.revealCellsInView(range); } revealInView(cell: ICellViewModel) { this._list.revealCell(cell, CellRevealSyncType.Default); } revealInViewAtTop(cell: ICellViewModel) { this._list.revealCell(cell, CellRevealSyncType.Top); } revealInCenter(cell: ICellViewModel) { this._list.revealCell(cell, CellRevealSyncType.Center); } revealInCenterIfOutsideViewport(cell: ICellViewModel) { this._list.revealCell(cell, CellRevealSyncType.CenterIfOutsideViewport); } async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Default); } async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise { return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Center); } async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise { return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.CenterIfOutsideViewport); } async revealRangeInViewAsync(cell: ICellViewModel, range: Selection | Range): Promise { return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Default); } async revealRangeInCenterAsync(cell: ICellViewModel, range: Selection | Range): Promise { return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Center); } async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Selection | Range): Promise { return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.CenterIfOutsideViewport); } async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise { return this._list.revealCellOffsetInCenterAsync(cell, offset); } getViewIndexByModelIndex(index: number): number { if (!this._listViewInfoAccessor) { return -1; } const cell = this.viewModel?.viewCells[index]; if (!cell) { return -1; } return this._listViewInfoAccessor.getViewIndex(cell); } getViewHeight(cell: ICellViewModel): number { if (!this._listViewInfoAccessor) { return -1; } return this._listViewInfoAccessor.getViewHeight(cell); } getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined { return this._listViewInfoAccessor.getCellRangeFromViewRange(startIndex, endIndex); } getCellsInRange(range?: ICellRange): ReadonlyArray { return this._listViewInfoAccessor.getCellsInRange(range); } setCellEditorSelection(cell: ICellViewModel, range: Range): void { this._list.setCellEditorSelection(cell, range); } setHiddenAreas(_ranges: ICellRange[]): boolean { return this._list.setHiddenAreas(_ranges, true); } getVisibleRangesPlusViewportAboveAndBelow(): ICellRange[] { return this._listViewInfoAccessor.getVisibleRangesPlusViewportAboveAndBelow(); } //#endregion //#region Decorations deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { const ret = this.viewModel?.deltaCellDecorations(oldDecorations, newDecorations) || []; this._onDidChangeDecorations.fire(); return ret; } deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]) { this._webview?.deltaCellContainerClassNames(cellId, added, removed); } changeModelDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null { return this.viewModel?.changeModelDecorations(callback) || null; } //#endregion //#region Kernel/Execution private async _loadKernelPreloads(): Promise { if (!this.hasModel()) { return; } const { selected } = this.notebookKernelService.getMatchingKernel(this.textModel); if (!this._webview?.isResolved()) { await this._resolveWebview(); } this._webview?.updateKernelPreloads(selected); } get activeKernel() { return this.textModel && this.notebookKernelService.getSelectedOrSuggestedKernel(this.textModel); } async cancelNotebookCells(cells?: Iterable): Promise { if (!this.viewModel || !this.hasModel()) { return; } if (!cells) { cells = this.viewModel.viewCells; } return this.notebookExecutionService.cancelNotebookCellHandles(this.textModel, Array.from(cells).map(cell => cell.handle)); } async executeNotebookCells(cells?: Iterable): Promise { if (!this.viewModel || !this.hasModel()) { this.logService.info('notebookEditorWidget', 'No NotebookViewModel, cannot execute cells'); return; } if (!cells) { cells = this.viewModel.viewCells; } return this.notebookExecutionService.executeNotebookCells(this.textModel, Array.from(cells).map(c => c.model), this.scopedContextKeyService); } //#endregion //#region Cell operations/layout API private _pendingLayouts: WeakMap | null = new WeakMap(); async layoutNotebookCell(cell: ICellViewModel, height: number, context?: CellLayoutContext): Promise { this._debug('layout cell', cell.handle, height); const viewIndex = this._list.getViewIndex(cell); if (viewIndex === undefined) { // the cell is hidden return; } if (this._pendingLayouts?.has(cell)) { this._pendingLayouts?.get(cell)!.dispose(); } const deferred = new DeferredPromise(); const doLayout = () => { if (this._isDisposed) { return; } if (!this.viewModel?.hasCell(cell)) { // Cell removed in the meantime? return; } if (this._list.elementHeight(cell) === height) { return; } this._pendingLayouts?.delete(cell); if (!this.hasEditorFocus()) { // Do not scroll inactive notebook // https://github.com/microsoft/vscode/issues/145340 const cellIndex = this.viewModel?.getCellIndex(cell); const visibleRanges = this.visibleRanges; if (cellIndex !== undefined && visibleRanges && visibleRanges.length && visibleRanges[0].start === cellIndex // cell is partially visible && this._list.scrollTop > this.getAbsoluteTopOfElement(cell) ) { return this._list.updateElementHeight2(cell, height, Math.min(cellIndex + 1, this.getLength() - 1)); } } this._list.updateElementHeight2(cell, height); deferred.complete(undefined); }; if (this._list.inRenderingTransaction) { const layoutDisposable = DOM.scheduleAtNextAnimationFrame(doLayout); this._pendingLayouts?.set(cell, toDisposable(() => { layoutDisposable.dispose(); deferred.complete(undefined); })); } else { doLayout(); } return deferred.p; } getActiveCell() { const elements = this._list.getFocusedElements(); if (elements && elements.length) { return elements[0]; } return undefined; } private _toggleNotebookCellSelection(selectedCell: ICellViewModel, selectFromPrevious: boolean): void { const currentSelections = this._list.getSelectedElements(); const isSelected = currentSelections.includes(selectedCell); const previousSelection = selectFromPrevious ? currentSelections[currentSelections.length - 1] ?? selectedCell : selectedCell; const selectedIndex = this._list.getViewIndex(selectedCell)!; const previousIndex = this._list.getViewIndex(previousSelection)!; const cellsInSelectionRange = this.getCellsInViewRange(selectedIndex, previousIndex); if (isSelected) { // Deselect this._list.selectElements(currentSelections.filter(current => !cellsInSelectionRange.includes(current))); } else { // Add to selection this.focusElement(selectedCell); this._list.selectElements([...currentSelections.filter(current => !cellsInSelectionRange.includes(current)), ...cellsInSelectionRange]); } } private getCellsInViewRange(fromInclusive: number, toInclusive: number): ICellViewModel[] { const selectedCellsInRange: ICellViewModel[] = []; for (let index = 0; index < this._list.length; ++index) { const cell = this._list.element(index); if (cell) { if ((index >= fromInclusive && index <= toInclusive) || (index >= toInclusive && index <= fromInclusive)) { selectedCellsInRange.push(cell); } } } return selectedCellsInRange; } async focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { if (this._isDisposed) { return; } if (focusItem === 'editor') { this.focusElement(cell); this._list.focusView(); cell.updateEditState(CellEditState.Editing, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Editor; if (!options?.skipReveal) { if (typeof options?.focusEditorLine === 'number') { this._cursorNavMode.set(true); await this.revealLineInViewAsync(cell, options.focusEditorLine); const editor = this._renderedEditors.get(cell)!; const focusEditorLine = options.focusEditorLine!; editor?.setSelection({ startLineNumber: focusEditorLine, startColumn: 1, endLineNumber: focusEditorLine, endColumn: 1 }); } else { const selectionsStartPosition = cell.getSelectionsStartPosition(); if (selectionsStartPosition?.length) { const firstSelectionPosition = selectionsStartPosition[0]; await this.revealRangeInCenterIfOutsideViewportAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); } else { this.revealInCenterIfOutsideViewport(cell); } } } } else if (focusItem === 'output') { this.focusElement(cell); if (!this.hasEditorFocus()) { this._list.focusView(); } if (!this._webview) { return; } const focusElementId = options?.outputId ?? cell.id; this._webview.focusOutput(focusElementId, options?.altOutputId, this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Output; if (!options?.skipReveal) { this.revealInCenterIfOutsideViewport(cell); } } else { const itemDOM = this._list.domElementOfElement(cell); if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { (document.activeElement as HTMLElement).blur(); } cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Container; this.focusElement(cell); if (!options?.skipReveal) { if (typeof options?.focusEditorLine === 'number') { this._cursorNavMode.set(true); this.revealInView(cell); } else if (options?.minimalScrolling) { this.revealInView(cell); } else { this.revealInCenterIfOutsideViewport(cell); } } this._list.focusView(); this.updateEditorFocus(); } } async focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { const idx = this.viewModel?.getCellIndex(cell); if (typeof idx !== 'number') { return; } const newCell = this.viewModel?.cellAt(idx + 1); if (!newCell) { return; } await this.focusNotebookCell(newCell, focusItem); } //#endregion //#region Find private async _warmupCell(viewCell: CodeCellViewModel) { if (viewCell.isOutputCollapsed) { return; } const outputs = viewCell.outputsViewModels; for (const output of outputs.slice(0, outputDisplayLimit)) { const [mimeTypes, pick] = output.resolveMimeTypes(this.textModel!, undefined); if (!mimeTypes.find(mimeType => mimeType.isTrusted) || mimeTypes.length === 0) { continue; } const pickedMimeTypeRenderer = mimeTypes[pick]; if (!pickedMimeTypeRenderer) { return; } const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); if (!renderer) { return; } const result: IInsetRenderOutput = { type: RenderOutputType.Extension, renderer, source: output, mimeType: pickedMimeTypeRenderer.mimeType }; const inset = this._webview?.insetMapping.get(result.source); if (!inset || !inset.initialized) { const p = new Promise(resolve => { this._register(Event.any(this.onDidRenderOutput, this.onDidRemoveOutput)(e => { if (e.model === result.source.model) { resolve(); } })); }); this.createOutput(viewCell, result, 0, false); await p; } else { // request to update its visibility this.createOutput(viewCell, result, 0, false); } return; } } private async _warmupAll(includeOutput: boolean) { if (!this.hasModel() || !this.viewModel) { return; } const cells = this.viewModel.viewCells; const requests = []; for (let i = 0; i < cells.length; i++) { if (cells[i].cellKind === CellKind.Markup && !this._webview!.markupPreviewMapping.has(cells[i].id)) { requests.push(this.createMarkupPreview(cells[i])); } } if (includeOutput && this._list) { for (let i = 0; i < this._list.length; i++) { const cell = this._list.element(i); if (cell?.cellKind === CellKind.Code) { requests.push(this._warmupCell((cell as CodeCellViewModel))); } } } return Promise.all(requests); } async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false, ownerID?: string): Promise { if (!this._notebookViewModel) { return []; } if (!ownerID) { ownerID = this.getId(); } const findMatches = this._notebookViewModel.find(query, options).filter(match => match.length > 0); if (!options.includeMarkupPreview && !options.includeOutput) { this._webview?.findStop(ownerID); return findMatches; } // search in webview enabled const matchMap: { [key: string]: CellFindMatchWithIndex } = {}; findMatches.forEach(match => { matchMap[match.cell.id] = match; }); if (this._webview) { // request all outputs to be rendered // measure perf const start = Date.now(); await this._warmupAll(!!options.includeOutput); const end = Date.now(); this.logService.debug('Find', `Warmup time: ${end - start}ms`); if (token.isCancellationRequested) { return []; } const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID }); if (token.isCancellationRequested) { return []; } // attach webview matches to model find matches webviewMatches.forEach(match => { const cell = this._notebookViewModel!.viewCells.find(cell => cell.id === match.cellId); if (!cell) { return; } if (match.type === 'preview') { // markup preview if (cell.getEditState() === CellEditState.Preview && !options.includeMarkupPreview) { return; } if (cell.getEditState() === CellEditState.Editing && options.includeMarkupInput) { return; } } else { if (!options.includeOutput) { // skip outputs if not included return; } } const exisitingMatch = matchMap[match.cellId]; if (exisitingMatch) { exisitingMatch.webviewMatches.push(match); } else { matchMap[match.cellId] = new CellFindMatchModel( this._notebookViewModel!.viewCells.find(cell => cell.id === match.cellId)!, this._notebookViewModel!.viewCells.findIndex(cell => cell.id === match.cellId)!, [], [match] ); } }); } const ret: CellFindMatchWithIndex[] = []; this._notebookViewModel.viewCells.forEach((cell, index) => { if (matchMap[cell.id]) { ret.push(new CellFindMatchModel(cell, index, matchMap[cell.id].contentMatches, matchMap[cell.id].webviewMatches)); } }); return ret; } async findHighlightCurrent(matchIndex: number, ownerID?: string): Promise { if (!this._webview) { return 0; } return this._webview?.findHighlightCurrent(matchIndex, ownerID ?? this.getId()); } async findUnHighlightCurrent(matchIndex: number, ownerID?: string): Promise { if (!this._webview) { return; } return this._webview?.findUnHighlightCurrent(matchIndex, ownerID ?? this.getId()); } findStop(ownerID?: string) { this._webview?.findStop(ownerID ?? this.getId()); } //#endregion //#region MISC getLayoutInfo(): NotebookLayoutInfo { if (!this._list) { throw new Error('Editor is not initalized successfully'); } if (!this._fontInfo) { this._generateFontInfo(); } return { width: this._dimension?.width ?? 0, height: this._dimension?.height ?? 0, scrollHeight: this._list?.getScrollHeight() ?? 0, fontInfo: this._fontInfo!, stickyHeight: this._notebookStickyScroll?.getCurrentStickyHeight() ?? 0 }; } async createMarkupPreview(cell: MarkupCellViewModel) { if (!this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } if (!this._webview || !this._list.webviewElement) { return; } if (!this.viewModel || !this._list.viewModel) { return; } if (this.viewModel.getCellIndex(cell) === -1) { return; } if (this.cellIsHidden(cell)) { return; } const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; const cellTop = this._list.getCellViewScrollTop(cell); await this._webview.showMarkupPreview({ mime: cell.mime, cellHandle: cell.handle, cellId: cell.id, content: cell.getText(), offset: cellTop + top, visible: true, metadata: cell.metadata, }); } private cellIsHidden(cell: ICellViewModel): boolean { const modelIndex = this.viewModel!.getCellIndex(cell); const foldedRanges = this.viewModel!.getHiddenRanges(); return foldedRanges.some(range => modelIndex >= range.start && modelIndex <= range.end); } async unhideMarkupPreviews(cells: readonly MarkupCellViewModel[]) { if (!this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } await this._webview?.unhideMarkupPreviews(cells.map(cell => cell.id)); } async hideMarkupPreviews(cells: readonly MarkupCellViewModel[]) { if (!this._webview || !cells.length) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } await this._webview?.hideMarkupPreviews(cells.map(cell => cell.id)); } async deleteMarkupPreviews(cells: readonly MarkupCellViewModel[]) { if (!this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } await this._webview?.deleteMarkupPreviews(cells.map(cell => cell.id)); } private async updateSelectedMarkdownPreviews(): Promise { if (!this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } const selectedCells = this.getSelectionViewModels().map(cell => cell.id); // Only show selection when there is more than 1 cell selected await this._webview?.updateMarkupPreviewSelections(selectedCells.length > 1 ? selectedCells : []); } async createOutput(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number, createWhenIdle: boolean): Promise { this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { if (this._isDisposed || !this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } if (!this._webview) { return; } if (!this._list.webviewElement) { return; } if (output.type === RenderOutputType.Extension) { this.notebookRendererMessaging.prepare(output.renderer.id); } const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; const cellTop = this._list.getCellViewScrollTop(cell) + top; const existingOutput = this._webview.insetMapping.get(output.source); if (!existingOutput || (!existingOutput.renderer && output.type === RenderOutputType.Extension) ) { if (createWhenIdle) { this._webview.requestCreateOutputWhenWebviewIdle({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri, executionId: cell.internalMetadata.executionId }, output, cellTop, offset); } else { this._webview.createOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri, executionId: cell.internalMetadata.executionId }, output, cellTop, offset); } } else if (existingOutput.renderer && output.type === RenderOutputType.Extension && existingOutput.renderer.id !== output.renderer.id) { // switch mimetype this._webview.removeInsets([output.source]); this._webview.createOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); } else if (existingOutput.versionId !== output.source.model.versionId) { this._webview.updateOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri, executionId: cell.internalMetadata.executionId }, output, cellTop, offset); } else { const outputIndex = cell.outputsViewModels.indexOf(output.source); const outputOffset = cell.getOutputOffset(outputIndex); this._webview.updateScrollTops([{ cell, output: output.source, cellTop, outputOffset, forceDisplay: !cell.isOutputCollapsed, }], []); } }); } async updateOutput(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { if (this._isDisposed || !this._webview) { return; } if (!this._webview.isResolved()) { await this._resolveWebview(); } if (!this._webview || !this._list.webviewElement) { return; } if (!this._webview.insetMapping.has(output.source)) { return this.createOutput(cell, output, offset, false); } if (output.type === RenderOutputType.Extension) { this.notebookRendererMessaging.prepare(output.renderer.id); } const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; const cellTop = this._list.getCellViewScrollTop(cell) + top; this._webview.updateOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); }); } async copyOutputImage(cellOutput: ICellOutputViewModel): Promise { this._webview?.copyImage(cellOutput); } removeInset(output: ICellOutputViewModel) { this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (this._isDisposed || !this._webview) { return; } if (this._webview?.isResolved()) { this._webview.removeInsets([output]); } this._onDidRemoveOutput.fire(output); }); } hideInset(output: ICellOutputViewModel) { this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (this._isDisposed || !this._webview) { return; } if (this._webview?.isResolved()) { this._webview.hideInset(output); } }); } //#region --- webview IPC ---- postMessage(message: any) { if (this._webview?.isResolved()) { this._webview.postKernelMessage(message); } } //#endregion addClassName(className: string) { this._overlayContainer.classList.add(className); } removeClassName(className: string) { this._overlayContainer.classList.remove(className); } cellAt(index: number): ICellViewModel | undefined { return this.viewModel?.cellAt(index); } getCellByInfo(cellInfo: ICommonCellInfo): ICellViewModel { const { cellHandle } = cellInfo; return this.viewModel?.viewCells.find(vc => vc.handle === cellHandle) as CodeCellViewModel; } getCellByHandle(handle: number): ICellViewModel | undefined { return this.viewModel?.getCellByHandle(handle); } getCellIndex(cell: ICellViewModel) { return this.viewModel?.getCellIndexByHandle(cell.handle); } getNextVisibleCellIndex(index: number): number | undefined { return this.viewModel?.getNextVisibleCellIndex(index); } getPreviousVisibleCellIndex(index: number): number | undefined { return this.viewModel?.getPreviousVisibleCellIndex(index); } private _updateScrollHeight() { if (this._isDisposed || !this._webview?.isResolved()) { return; } if (!this._list.webviewElement) { return; } const scrollHeight = this._list.scrollHeight; this._webview!.element.style.height = `${scrollHeight + NOTEBOOK_WEBVIEW_BOUNDARY * 2}px`; const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); const top = !!webviewTop ? (0 - webviewTop) : 0; const updateItems: IDisplayOutputLayoutUpdateRequest[] = []; const removedItems: ICellOutputViewModel[] = []; this._webview?.insetMapping.forEach((value, key) => { const cell = this.viewModel?.getCellByHandle(value.cellInfo.cellHandle); if (!cell || !(cell instanceof CodeCellViewModel)) { return; } this.viewModel?.viewCells.find(cell => cell.handle === value.cellInfo.cellHandle); const viewIndex = this._list.getViewIndex(cell); if (viewIndex === undefined) { return; } if (cell.outputsViewModels.indexOf(key) < 0) { // output is already gone removedItems.push(key); } const cellTop = this._list.getCellViewScrollTop(cell); const outputIndex = cell.outputsViewModels.indexOf(key); const outputOffset = cell.getOutputOffset(outputIndex); updateItems.push({ cell, output: key, cellTop: cellTop + top, outputOffset, forceDisplay: false, }); }); this._webview.removeInsets(removedItems); const markdownUpdateItems: { id: string; top: number }[] = []; for (const cellId of this._webview.markupPreviewMapping.keys()) { const cell = this.viewModel?.viewCells.find(cell => cell.id === cellId); if (cell) { const cellTop = this._list.getCellViewScrollTop(cell); // markdownUpdateItems.push({ id: cellId, top: cellTop }); markdownUpdateItems.push({ id: cellId, top: cellTop + top }); } } if (markdownUpdateItems.length || updateItems.length) { this._debug('_list.onDidChangeContentHeight/markdown', markdownUpdateItems); this._webview?.updateScrollTops(updateItems, markdownUpdateItems); } } //#endregion //#region BacklayerWebview delegate private _updateOutputHeight(cellInfo: ICommonCellInfo, output: ICellOutputViewModel, outputHeight: number, isInit: boolean, source?: string): void { const cell = this.viewModel?.viewCells.find(vc => vc.handle === cellInfo.cellHandle); if (cell && cell instanceof CodeCellViewModel) { const outputIndex = cell.outputsViewModels.indexOf(output); if (outputHeight !== 0) { cell.updateOutputMinHeight(0); } this._debug('update cell output', cell.handle, outputHeight); cell.updateOutputHeight(outputIndex, outputHeight, source); this.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); if (isInit) { this._onDidRenderOutput.fire(output); } } } private readonly _pendingOutputHeightAcks = new Map(); private _scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number) { const wasEmpty = this._pendingOutputHeightAcks.size === 0; this._pendingOutputHeightAcks.set(outputId, { cellId: cellInfo.cellId, outputId, height }); if (wasEmpty) { DOM.scheduleAtNextAnimationFrame(() => { this._debug('ack height'); this._updateScrollHeight(); this._webview?.ackHeight([...this._pendingOutputHeightAcks.values()]); this._pendingOutputHeightAcks.clear(); }, -1); // -1 priority because this depends on calls to layoutNotebookCell, and that may be called multiple times before this runs } } private _getCellById(cellId: string): ICellViewModel | undefined { return this.viewModel?.viewCells.find(vc => vc.id === cellId); } private _updateMarkupCellHeight(cellId: string, height: number, isInit: boolean) { const cell = this._getCellById(cellId); if (cell && cell instanceof MarkupCellViewModel) { const { bottomToolbarGap } = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType); this._debug('updateMarkdownCellHeight', cell.handle, height + bottomToolbarGap, isInit); cell.renderedMarkdownHeight = height; } } private _setMarkupCellEditState(cellId: string, editState: CellEditState): void { const cell = this._getCellById(cellId); if (cell instanceof MarkupCellViewModel) { this.revealInView(cell); cell.updateEditState(editState, 'setMarkdownCellEditState'); } } private _didStartDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void { const cell = this._getCellById(cellId); if (cell instanceof MarkupCellViewModel) { const webviewOffset = this._list.webviewElement ? -parseInt(this._list.webviewElement.domNode.style.top, 10) : 0; this._dndController?.startExplicitDrag(cell, event.dragOffsetY - webviewOffset); } } private _didDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void { const cell = this._getCellById(cellId); if (cell instanceof MarkupCellViewModel) { const webviewOffset = this._list.webviewElement ? -parseInt(this._list.webviewElement.domNode.style.top, 10) : 0; this._dndController?.explicitDrag(cell, event.dragOffsetY - webviewOffset); } } private _didDropMarkupCell(cellId: string, event: { dragOffsetY: number; ctrlKey: boolean; altKey: boolean }): void { const cell = this._getCellById(cellId); if (cell instanceof MarkupCellViewModel) { const webviewOffset = this._list.webviewElement ? -parseInt(this._list.webviewElement.domNode.style.top, 10) : 0; event.dragOffsetY -= webviewOffset; this._dndController?.explicitDrop(cell, event); } } private _didEndDragMarkupCell(cellId: string): void { const cell = this._getCellById(cellId); if (cell instanceof MarkupCellViewModel) { this._dndController?.endExplicitDrag(cell); } } private _didResizeOutput(cellId: string): void { const cell = this._getCellById(cellId); if (cell) { this._onDidResizeOutputEmitter.fire(cell); } } private _updatePerformanceMetadata(cellId: string, executionId: string, duration: number, rendererId: string): void { if (!this.hasModel()) { return; } const cell = this._getCellById(cellId); const cellIndex = !cell ? undefined : this.getCellIndex(cell); if (cell?.internalMetadata.executionId === executionId && cellIndex !== undefined) { const renderDurationMap = cell.internalMetadata.renderDuration || {}; renderDurationMap[rendererId] = (renderDurationMap[rendererId] ?? 0) + duration; this.textModel.applyEdits([ { editType: CellEditType.PartialInternalMetadata, index: cellIndex, internalMetadata: { executionId: executionId, renderDuration: renderDurationMap } } ], true, undefined, () => undefined, undefined, false); } } //#endregion //#region Editor Contributions getContribution(id: string): T { return (this._contributions.get(id) || null); } //#endregion override dispose() { this._isDisposed = true; // dispose webview first this._webview?.dispose(); this._webview = null; this.notebookEditorService.removeNotebookEditor(this); dispose(this._contributions.values()); this._contributions.clear(); this._localStore.clear(); dispose(this._localCellStateListeners); this._list.dispose(); this._listTopCellToolbar?.dispose(); this._overlayContainer.remove(); this.viewModel?.dispose(); this._renderedEditors.clear(); this._baseCellEditorOptions.forEach(v => v.dispose()); this._baseCellEditorOptions.clear(); this._notebookOverviewRulerContainer.remove(); super.dispose(); // unref this._webview = null; this._webviewResolvePromise = null; this._webviewTransparentCover = null; this._dndController = null; this._listTopCellToolbar = null; this._notebookViewModel = undefined; this._cellContextKeyManager = null; this._notebookTopToolbar = null!; this._list = null!; this._listViewInfoAccessor = null!; this._pendingLayouts = null; this._listDelegate = null; } toJSON(): { notebookUri: URI | undefined } { return { notebookUri: this.viewModel?.uri, }; } } registerZIndex(ZIndex.Base, 5, 'notebook-progress-bar',); registerZIndex(ZIndex.Base, 10, 'notebook-list-insertion-indicator'); registerZIndex(ZIndex.Base, 20, 'notebook-cell-editor-outline'); registerZIndex(ZIndex.Base, 25, 'notebook-scrollbar'); registerZIndex(ZIndex.Base, 26, 'notebook-cell-status'); registerZIndex(ZIndex.Base, 26, 'notebook-folding-indicator'); registerZIndex(ZIndex.Base, 27, 'notebook-output'); registerZIndex(ZIndex.Base, 28, 'notebook-cell-bottom-toolbar-container'); registerZIndex(ZIndex.Base, 29, 'notebook-run-button-container'); registerZIndex(ZIndex.Base, 29, 'notebook-input-collapse-condicon'); registerZIndex(ZIndex.Base, 30, 'notebook-cell-output-toolbar'); registerZIndex(ZIndex.Base, 31, 'notebook-sticky-scroll'); registerZIndex(ZIndex.Sash, 1, 'notebook-cell-expand-part-button'); registerZIndex(ZIndex.Sash, 2, 'notebook-cell-toolbar'); registerZIndex(ZIndex.Sash, 3, 'notebook-cell-toolbar-dropdown-active'); export const notebookCellBorder = registerColor('notebook.cellBorderColor', { dark: transparent(listInactiveSelectionBackground, 1), light: transparent(listInactiveSelectionBackground, 1), hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', { light: focusBorder, dark: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', { light: debugIconStartForeground, dark: debugIconStartForeground, hcDark: debugIconStartForeground, hcLight: debugIconStartForeground }, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); export const runningCellRulerDecorationColor = registerColor('notebookEditorOverviewRuler.runningCellForeground', { light: debugIconStartForeground, dark: debugIconStartForeground, hcDark: debugIconStartForeground, hcLight: debugIconStartForeground }, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', { light: errorForeground, dark: errorForeground, hcDark: errorForeground, hcLight: errorForeground }, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', { light: foreground, dark: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); // TODO@rebornix currently also used for toolbar border, if we keep all of this, pick a generic name export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparator', { dark: Color.fromHex('#808080').transparent(0.35), light: Color.fromHex('#808080').transparent(0.35), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('notebook.cellToolbarSeparator', "The color of the separator in the cell bottom toolbar")); export const focusedCellBackground = registerColor('notebook.focusedCellBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); export const selectedCellBackground = registerColor('notebook.selectedCellBackground', { dark: listInactiveSelectionBackground, light: listInactiveSelectionBackground, hcDark: null, hcLight: null }, nls.localize('selectedCellBackground', "The background color of a cell when the cell is selected.")); export const cellHoverBackground = registerColor('notebook.cellHoverBackground', { dark: transparent(focusedCellBackground, .5), light: transparent(focusedCellBackground, .7), hcDark: null, hcLight: null }, nls.localize('notebook.cellHoverBackground', "The background color of a cell when the cell is hovered.")); export const selectedCellBorder = registerColor('notebook.selectedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focused.")); export const inactiveSelectedCellBorder = registerColor('notebook.inactiveSelectedCellBorder', { dark: null, light: null, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('notebook.inactiveSelectedCellBorder', "The color of the cell's borders when multiple cells are selected.")); export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, hcDark: notebookCellBorder, hcLight: notebookCellBorder }, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), dark: new Color(new RGBA(255, 255, 255, 0.15)), hcDark: new Color(new RGBA(255, 255, 255, 0.15)), hcLight: new Color(new RGBA(0, 0, 0, 0.08)), }, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', { light: focusBorder, dark: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { dark: scrollbarSliderBackground, light: scrollbarSliderBackground, hcDark: scrollbarSliderBackground, hcLight: scrollbarSliderBackground }, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', { dark: scrollbarSliderHoverBackground, light: scrollbarSliderHoverBackground, hcDark: scrollbarSliderHoverBackground, hcLight: scrollbarSliderHoverBackground }, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', { dark: scrollbarSliderActiveBackground, light: scrollbarSliderActiveBackground, hcDark: scrollbarSliderActiveBackground, hcLight: scrollbarSliderActiveBackground }, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackground', { dark: Color.fromHex('#ffffff0b'), light: Color.fromHex('#fdff0033'), hcDark: null, hcLight: null }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); export const cellEditorBackground = registerColor('notebook.cellEditorBackground', { light: SIDE_BAR_BACKGROUND, dark: SIDE_BAR_BACKGROUND, hcDark: null, hcLight: null }, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); const notebookEditorBackground = registerColor('notebook.editorBackground', { light: EDITOR_PANE_BACKGROUND, dark: EDITOR_PANE_BACKGROUND, hcDark: null, hcLight: null }, nls.localize('notebook.editorBackground', "Notebook background color."));