Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js';6import { onUnexpectedError } from '../../../../../base/common/errors.js';7import { Disposable } from '../../../../../base/common/lifecycle.js';8import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';9import { localize2 } from '../../../../../nls.js';10import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';11import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';12import { IsDevelopmentContext } from '../../../../../platform/contextkey/common/contextkeys.js';13import { IEditorService } from '../../../../services/editor/common/editorService.js';14import { getNotebookEditorFromEditorPane, INotebookViewCellsUpdateEvent, INotebookViewZone, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js';15import { NotebookCellListView } from '../view/notebookCellListView.js';16import { ICoordinatesConverter } from '../view/notebookRenderingCommon.js';17import { CellViewModel } from '../viewModel/notebookViewModelImpl.js';1819const invalidFunc = () => { throw new Error(`Invalid notebook view zone change accessor`); };2021interface IZoneWidget {22whitespaceId: string;23isInHiddenArea: boolean;24zone: INotebookViewZone;25domNode: FastDomNode<HTMLElement>;26}2728export class NotebookViewZones extends Disposable {29private _zones: { [key: string]: IZoneWidget };30public domNode: FastDomNode<HTMLElement>;3132constructor(private readonly listView: NotebookCellListView<CellViewModel>, private readonly coordinator: ICoordinatesConverter) {33super();34this.domNode = createFastDomNode(document.createElement('div'));35this.domNode.setClassName('view-zones');36this.domNode.setPosition('absolute');37this.domNode.setAttribute('role', 'presentation');38this.domNode.setAttribute('aria-hidden', 'true');39this.domNode.setWidth('100%');40this._zones = {};4142this.listView.containerDomNode.appendChild(this.domNode.domNode);43}4445changeViewZones(callback: (changeAccessor: INotebookViewZoneChangeAccessor) => void): boolean {46let zonesHaveChanged = false;47const changeAccessor: INotebookViewZoneChangeAccessor = {48addZone: (zone: INotebookViewZone): string => {49zonesHaveChanged = true;50return this._addZone(zone);51},52removeZone: (id: string): void => {53zonesHaveChanged = true;54// TODO: validate if zones have changed layout55this._removeZone(id);56},57layoutZone: (id: string): void => {58zonesHaveChanged = true;59// TODO: validate if zones have changed layout60this._layoutZone(id);61}62};6364safeInvoke1Arg(callback, changeAccessor);6566// Invalidate changeAccessor67changeAccessor.addZone = invalidFunc;68changeAccessor.removeZone = invalidFunc;69changeAccessor.layoutZone = invalidFunc;7071return zonesHaveChanged;72}7374getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null {75const zoneWidget = this._zones[viewZoneId];76if (!zoneWidget) {77return null;78}79const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId);80const height = zoneWidget.zone.heightInPx;81return { height: height, top: top };82}8384onCellsChanged(e: INotebookViewCellsUpdateEvent): void {85const splices = e.splices.slice().reverse();86splices.forEach(splice => {87const [start, deleted, newCells] = splice;88const fromIndex = start;89const toIndex = start + deleted;9091// 1, 2, 092// delete cell index 1 and 293// from index 1, to index 3 (exclusive): [1, 3)94// if we have whitespace afterModelPosition 3, which is after cell index 29596for (const id in this._zones) {97const zone = this._zones[id].zone;9899const cellBeforeWhitespaceIndex = zone.afterModelPosition - 1;100101if (cellBeforeWhitespaceIndex >= fromIndex && cellBeforeWhitespaceIndex < toIndex) {102// The cell this whitespace was after has been deleted103// => move whitespace to before first deleted cell104zone.afterModelPosition = fromIndex;105this._updateWhitespace(this._zones[id]);106} else if (cellBeforeWhitespaceIndex >= toIndex) {107// adjust afterModelPosition for all other cells108const insertLength = newCells.length;109const offset = insertLength - deleted;110zone.afterModelPosition += offset;111this._updateWhitespace(this._zones[id]);112}113}114});115}116117onHiddenRangesChange() {118for (const id in this._zones) {119this._updateWhitespace(this._zones[id]);120}121}122123private _updateWhitespace(zone: IZoneWidget) {124const whitespaceId = zone.whitespaceId;125const viewPosition = this.coordinator.convertModelIndexToViewIndex(zone.zone.afterModelPosition);126const isInHiddenArea = this._isInHiddenRanges(zone.zone);127zone.isInHiddenArea = isInHiddenArea;128this.listView.changeOneWhitespace(whitespaceId, viewPosition, isInHiddenArea ? 0 : zone.zone.heightInPx);129}130131layout() {132for (const id in this._zones) {133this._layoutZone(id);134}135}136137private _addZone(zone: INotebookViewZone): string {138const viewPosition = this.coordinator.convertModelIndexToViewIndex(zone.afterModelPosition);139const whitespaceId = this.listView.insertWhitespace(viewPosition, zone.heightInPx);140const isInHiddenArea = this._isInHiddenRanges(zone);141const myZone: IZoneWidget = {142whitespaceId: whitespaceId,143zone: zone,144domNode: createFastDomNode(zone.domNode),145isInHiddenArea: isInHiddenArea146};147148this._zones[whitespaceId] = myZone;149myZone.domNode.setPosition('absolute');150myZone.domNode.domNode.style.width = '100%';151myZone.domNode.setDisplay('none');152myZone.domNode.setAttribute('notebook-view-zone', whitespaceId);153this.domNode.appendChild(myZone.domNode);154return whitespaceId;155}156157private _removeZone(id: string): void {158this.listView.removeWhitespace(id);159const zoneWidget = this._zones[id];160if (zoneWidget) {161// safely remove the dom node from its parent162try {163this.domNode.removeChild(zoneWidget.domNode);164} catch {165// ignore the error166}167}168169delete this._zones[id];170}171172private _layoutZone(id: string): void {173const zoneWidget = this._zones[id];174if (!zoneWidget) {175return;176}177178this._updateWhitespace(this._zones[id]);179180const isInHiddenArea = this._isInHiddenRanges(zoneWidget.zone);181182if (isInHiddenArea) {183zoneWidget.domNode.setDisplay('none');184} else {185const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId);186zoneWidget.domNode.setTop(top);187zoneWidget.domNode.setDisplay('block');188zoneWidget.domNode.setHeight(zoneWidget.zone.heightInPx);189}190}191192private _isInHiddenRanges(zone: INotebookViewZone) {193// The view zone is between two cells (zone.afterModelPosition - 1, zone.afterModelPosition)194const afterIndex = zone.afterModelPosition;195196// In notebook, the first cell (markdown cell) in a folding range is always visible, so we need to check the cell after the notebook view zone197return !this.coordinator.modelIndexIsVisible(afterIndex);198199}200201override dispose(): void {202super.dispose();203this._zones = {};204}205}206207function safeInvoke1Arg(func: Function, arg1: any): void {208try {209func(arg1);210} catch (e) {211onUnexpectedError(e);212}213}214215class ToggleNotebookViewZoneDeveloperAction extends Action2 {216static viewZoneIds: string[] = [];217constructor() {218super({219id: 'notebook.developer.addViewZones',220title: localize2('workbench.notebook.developer.addViewZones', "Toggle Notebook View Zones"),221category: Categories.Developer,222precondition: IsDevelopmentContext,223f1: true224});225}226227async run(accessor: ServicesAccessor): Promise<void> {228const editorService = accessor.get(IEditorService);229const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);230231if (!editor) {232return;233}234235if (ToggleNotebookViewZoneDeveloperAction.viewZoneIds.length > 0) {236// remove all view zones237editor.changeViewZones(accessor => {238// remove all view zones in reverse order, to follow how we handle this in the prod code239ToggleNotebookViewZoneDeveloperAction.viewZoneIds.reverse().forEach(id => {240accessor.removeZone(id);241});242ToggleNotebookViewZoneDeveloperAction.viewZoneIds = [];243});244} else {245editor.changeViewZones(accessor => {246const cells = editor.getCellsInRange();247if (cells.length === 0) {248return;249}250251const viewZoneIds: string[] = [];252for (let i = 0; i < cells.length; i++) {253const domNode = document.createElement('div');254domNode.innerText = `View Zone ${i}`;255domNode.style.backgroundColor = 'rgba(0, 255, 0, 0.5)';256const viewZoneId = accessor.addZone({257afterModelPosition: i,258heightInPx: 200,259domNode: domNode,260});261viewZoneIds.push(viewZoneId);262}263ToggleNotebookViewZoneDeveloperAction.viewZoneIds = viewZoneIds;264});265}266}267}268269registerAction2(ToggleNotebookViewZoneDeveloperAction);270271272