Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js';
7
import { onUnexpectedError } from '../../../../../base/common/errors.js';
8
import { Disposable } from '../../../../../base/common/lifecycle.js';
9
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
10
import { localize2 } from '../../../../../nls.js';
11
import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js';
12
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
13
import { IsDevelopmentContext } from '../../../../../platform/contextkey/common/contextkeys.js';
14
import { IEditorService } from '../../../../services/editor/common/editorService.js';
15
import { getNotebookEditorFromEditorPane, INotebookViewCellsUpdateEvent, INotebookViewZone, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js';
16
import { NotebookCellListView } from '../view/notebookCellListView.js';
17
import { ICoordinatesConverter } from '../view/notebookRenderingCommon.js';
18
import { CellViewModel } from '../viewModel/notebookViewModelImpl.js';
19
20
const invalidFunc = () => { throw new Error(`Invalid notebook view zone change accessor`); };
21
22
interface IZoneWidget {
23
whitespaceId: string;
24
isInHiddenArea: boolean;
25
zone: INotebookViewZone;
26
domNode: FastDomNode<HTMLElement>;
27
}
28
29
export class NotebookViewZones extends Disposable {
30
private _zones: { [key: string]: IZoneWidget };
31
public domNode: FastDomNode<HTMLElement>;
32
33
constructor(private readonly listView: NotebookCellListView<CellViewModel>, private readonly coordinator: ICoordinatesConverter) {
34
super();
35
this.domNode = createFastDomNode(document.createElement('div'));
36
this.domNode.setClassName('view-zones');
37
this.domNode.setPosition('absolute');
38
this.domNode.setAttribute('role', 'presentation');
39
this.domNode.setAttribute('aria-hidden', 'true');
40
this.domNode.setWidth('100%');
41
this._zones = {};
42
43
this.listView.containerDomNode.appendChild(this.domNode.domNode);
44
}
45
46
changeViewZones(callback: (changeAccessor: INotebookViewZoneChangeAccessor) => void): boolean {
47
let zonesHaveChanged = false;
48
const changeAccessor: INotebookViewZoneChangeAccessor = {
49
addZone: (zone: INotebookViewZone): string => {
50
zonesHaveChanged = true;
51
return this._addZone(zone);
52
},
53
removeZone: (id: string): void => {
54
zonesHaveChanged = true;
55
// TODO: validate if zones have changed layout
56
this._removeZone(id);
57
},
58
layoutZone: (id: string): void => {
59
zonesHaveChanged = true;
60
// TODO: validate if zones have changed layout
61
this._layoutZone(id);
62
}
63
};
64
65
safeInvoke1Arg(callback, changeAccessor);
66
67
// Invalidate changeAccessor
68
changeAccessor.addZone = invalidFunc;
69
changeAccessor.removeZone = invalidFunc;
70
changeAccessor.layoutZone = invalidFunc;
71
72
return zonesHaveChanged;
73
}
74
75
getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null {
76
const zoneWidget = this._zones[viewZoneId];
77
if (!zoneWidget) {
78
return null;
79
}
80
const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId);
81
const height = zoneWidget.zone.heightInPx;
82
return { height: height, top: top };
83
}
84
85
onCellsChanged(e: INotebookViewCellsUpdateEvent): void {
86
const splices = e.splices.slice().reverse();
87
splices.forEach(splice => {
88
const [start, deleted, newCells] = splice;
89
const fromIndex = start;
90
const toIndex = start + deleted;
91
92
// 1, 2, 0
93
// delete cell index 1 and 2
94
// from index 1, to index 3 (exclusive): [1, 3)
95
// if we have whitespace afterModelPosition 3, which is after cell index 2
96
97
for (const id in this._zones) {
98
const zone = this._zones[id].zone;
99
100
const cellBeforeWhitespaceIndex = zone.afterModelPosition - 1;
101
102
if (cellBeforeWhitespaceIndex >= fromIndex && cellBeforeWhitespaceIndex < toIndex) {
103
// The cell this whitespace was after has been deleted
104
// => move whitespace to before first deleted cell
105
zone.afterModelPosition = fromIndex;
106
this._updateWhitespace(this._zones[id]);
107
} else if (cellBeforeWhitespaceIndex >= toIndex) {
108
// adjust afterModelPosition for all other cells
109
const insertLength = newCells.length;
110
const offset = insertLength - deleted;
111
zone.afterModelPosition += offset;
112
this._updateWhitespace(this._zones[id]);
113
}
114
}
115
});
116
}
117
118
onHiddenRangesChange() {
119
for (const id in this._zones) {
120
this._updateWhitespace(this._zones[id]);
121
}
122
}
123
124
private _updateWhitespace(zone: IZoneWidget) {
125
const whitespaceId = zone.whitespaceId;
126
const viewPosition = this.coordinator.convertModelIndexToViewIndex(zone.zone.afterModelPosition);
127
const isInHiddenArea = this._isInHiddenRanges(zone.zone);
128
zone.isInHiddenArea = isInHiddenArea;
129
this.listView.changeOneWhitespace(whitespaceId, viewPosition, isInHiddenArea ? 0 : zone.zone.heightInPx);
130
}
131
132
layout() {
133
for (const id in this._zones) {
134
this._layoutZone(id);
135
}
136
}
137
138
private _addZone(zone: INotebookViewZone): string {
139
const viewPosition = this.coordinator.convertModelIndexToViewIndex(zone.afterModelPosition);
140
const whitespaceId = this.listView.insertWhitespace(viewPosition, zone.heightInPx);
141
const isInHiddenArea = this._isInHiddenRanges(zone);
142
const myZone: IZoneWidget = {
143
whitespaceId: whitespaceId,
144
zone: zone,
145
domNode: createFastDomNode(zone.domNode),
146
isInHiddenArea: isInHiddenArea
147
};
148
149
this._zones[whitespaceId] = myZone;
150
myZone.domNode.setPosition('absolute');
151
myZone.domNode.domNode.style.width = '100%';
152
myZone.domNode.setDisplay('none');
153
myZone.domNode.setAttribute('notebook-view-zone', whitespaceId);
154
this.domNode.appendChild(myZone.domNode);
155
return whitespaceId;
156
}
157
158
private _removeZone(id: string): void {
159
this.listView.removeWhitespace(id);
160
const zoneWidget = this._zones[id];
161
if (zoneWidget) {
162
// safely remove the dom node from its parent
163
try {
164
this.domNode.removeChild(zoneWidget.domNode);
165
} catch {
166
// ignore the error
167
}
168
}
169
170
delete this._zones[id];
171
}
172
173
private _layoutZone(id: string): void {
174
const zoneWidget = this._zones[id];
175
if (!zoneWidget) {
176
return;
177
}
178
179
this._updateWhitespace(this._zones[id]);
180
181
const isInHiddenArea = this._isInHiddenRanges(zoneWidget.zone);
182
183
if (isInHiddenArea) {
184
zoneWidget.domNode.setDisplay('none');
185
} else {
186
const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId);
187
zoneWidget.domNode.setTop(top);
188
zoneWidget.domNode.setDisplay('block');
189
zoneWidget.domNode.setHeight(zoneWidget.zone.heightInPx);
190
}
191
}
192
193
private _isInHiddenRanges(zone: INotebookViewZone) {
194
// The view zone is between two cells (zone.afterModelPosition - 1, zone.afterModelPosition)
195
const afterIndex = zone.afterModelPosition;
196
197
// 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 zone
198
return !this.coordinator.modelIndexIsVisible(afterIndex);
199
200
}
201
202
override dispose(): void {
203
super.dispose();
204
this._zones = {};
205
}
206
}
207
208
function safeInvoke1Arg(func: Function, arg1: any): void {
209
try {
210
func(arg1);
211
} catch (e) {
212
onUnexpectedError(e);
213
}
214
}
215
216
class ToggleNotebookViewZoneDeveloperAction extends Action2 {
217
static viewZoneIds: string[] = [];
218
constructor() {
219
super({
220
id: 'notebook.developer.addViewZones',
221
title: localize2('workbench.notebook.developer.addViewZones', "Toggle Notebook View Zones"),
222
category: Categories.Developer,
223
precondition: IsDevelopmentContext,
224
f1: true
225
});
226
}
227
228
async run(accessor: ServicesAccessor): Promise<void> {
229
const editorService = accessor.get(IEditorService);
230
const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
231
232
if (!editor) {
233
return;
234
}
235
236
if (ToggleNotebookViewZoneDeveloperAction.viewZoneIds.length > 0) {
237
// remove all view zones
238
editor.changeViewZones(accessor => {
239
// remove all view zones in reverse order, to follow how we handle this in the prod code
240
ToggleNotebookViewZoneDeveloperAction.viewZoneIds.reverse().forEach(id => {
241
accessor.removeZone(id);
242
});
243
ToggleNotebookViewZoneDeveloperAction.viewZoneIds = [];
244
});
245
} else {
246
editor.changeViewZones(accessor => {
247
const cells = editor.getCellsInRange();
248
if (cells.length === 0) {
249
return;
250
}
251
252
const viewZoneIds: string[] = [];
253
for (let i = 0; i < cells.length; i++) {
254
const domNode = document.createElement('div');
255
domNode.innerText = `View Zone ${i}`;
256
domNode.style.backgroundColor = 'rgba(0, 255, 0, 0.5)';
257
const viewZoneId = accessor.addZone({
258
afterModelPosition: i,
259
heightInPx: 200,
260
domNode: domNode,
261
});
262
viewZoneIds.push(viewZoneId);
263
}
264
ToggleNotebookViewZoneDeveloperAction.viewZoneIds = viewZoneIds;
265
});
266
}
267
}
268
}
269
270
registerAction2(ToggleNotebookViewZoneDeveloperAction);
271
272