Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/viewZones/viewZones.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 { IViewZone, IViewZoneChangeAccessor } from '../../editorBrowser.js';
9
import { ViewPart } from '../../view/viewPart.js';
10
import { Position } from '../../../common/core/position.js';
11
import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';
12
import { ViewContext } from '../../../common/viewModel/viewContext.js';
13
import * as viewEvents from '../../../common/viewEvents.js';
14
import { IEditorWhitespace, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../../../common/viewModel.js';
15
import { EditorOption } from '../../../common/config/editorOptions.js';
16
17
interface IMyViewZone {
18
whitespaceId: string;
19
delegate: IViewZone;
20
isInHiddenArea: boolean;
21
isVisible: boolean;
22
domNode: FastDomNode<HTMLElement>;
23
marginDomNode: FastDomNode<HTMLElement> | null;
24
}
25
26
interface IComputedViewZoneProps {
27
isInHiddenArea: boolean;
28
afterViewLineNumber: number;
29
heightInPx: number;
30
minWidthInPx: number;
31
}
32
33
const invalidFunc = () => { throw new Error(`Invalid change accessor`); };
34
35
/**
36
* A view zone is a rectangle that is a section that is inserted into the editor
37
* lines that can be used for various purposes such as showing a diffs, peeking
38
* an implementation, etc.
39
*/
40
export class ViewZones extends ViewPart {
41
42
private _zones: { [id: string]: IMyViewZone };
43
private _lineHeight: number;
44
private _contentWidth: number;
45
private _contentLeft: number;
46
47
public domNode: FastDomNode<HTMLElement>;
48
49
public marginDomNode: FastDomNode<HTMLElement>;
50
51
constructor(context: ViewContext) {
52
super(context);
53
const options = this._context.configuration.options;
54
const layoutInfo = options.get(EditorOption.layoutInfo);
55
56
this._lineHeight = options.get(EditorOption.lineHeight);
57
this._contentWidth = layoutInfo.contentWidth;
58
this._contentLeft = layoutInfo.contentLeft;
59
60
this.domNode = createFastDomNode(document.createElement('div'));
61
this.domNode.setClassName('view-zones');
62
this.domNode.setPosition('absolute');
63
this.domNode.setAttribute('role', 'presentation');
64
this.domNode.setAttribute('aria-hidden', 'true');
65
66
this.marginDomNode = createFastDomNode(document.createElement('div'));
67
this.marginDomNode.setClassName('margin-view-zones');
68
this.marginDomNode.setPosition('absolute');
69
this.marginDomNode.setAttribute('role', 'presentation');
70
this.marginDomNode.setAttribute('aria-hidden', 'true');
71
72
this._zones = {};
73
}
74
75
public override dispose(): void {
76
super.dispose();
77
this._zones = {};
78
}
79
80
// ---- begin view event handlers
81
82
private _recomputeWhitespacesProps(): boolean {
83
const whitespaces = this._context.viewLayout.getWhitespaces();
84
const oldWhitespaces = new Map<string, IEditorWhitespace>();
85
for (const whitespace of whitespaces) {
86
oldWhitespaces.set(whitespace.id, whitespace);
87
}
88
let hadAChange = false;
89
this._context.viewModel.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => {
90
const keys = Object.keys(this._zones);
91
for (let i = 0, len = keys.length; i < len; i++) {
92
const id = keys[i];
93
const zone = this._zones[id];
94
const props = this._computeWhitespaceProps(zone.delegate);
95
zone.isInHiddenArea = props.isInHiddenArea;
96
const oldWhitespace = oldWhitespaces.get(id);
97
if (oldWhitespace && (oldWhitespace.afterLineNumber !== props.afterViewLineNumber || oldWhitespace.height !== props.heightInPx)) {
98
whitespaceAccessor.changeOneWhitespace(id, props.afterViewLineNumber, props.heightInPx);
99
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
100
hadAChange = true;
101
}
102
}
103
});
104
return hadAChange;
105
}
106
107
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
108
const options = this._context.configuration.options;
109
const layoutInfo = options.get(EditorOption.layoutInfo);
110
111
this._lineHeight = options.get(EditorOption.lineHeight);
112
this._contentWidth = layoutInfo.contentWidth;
113
this._contentLeft = layoutInfo.contentLeft;
114
115
if (e.hasChanged(EditorOption.lineHeight)) {
116
this._recomputeWhitespacesProps();
117
}
118
119
return true;
120
}
121
122
public override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean {
123
return this._recomputeWhitespacesProps();
124
}
125
126
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
127
return true;
128
}
129
130
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
131
return e.scrollTopChanged || e.scrollWidthChanged;
132
}
133
134
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
135
return true;
136
}
137
138
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
139
return true;
140
}
141
142
// ---- end view event handlers
143
144
private _getZoneOrdinal(zone: IViewZone): number {
145
return zone.ordinal ?? zone.afterColumn ?? 10000;
146
}
147
148
private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps {
149
if (zone.afterLineNumber === 0) {
150
return {
151
isInHiddenArea: false,
152
afterViewLineNumber: 0,
153
heightInPx: this._heightInPixels(zone),
154
minWidthInPx: this._minWidthInPixels(zone)
155
};
156
}
157
158
let zoneAfterModelPosition: Position;
159
if (typeof zone.afterColumn !== 'undefined') {
160
zoneAfterModelPosition = this._context.viewModel.model.validatePosition({
161
lineNumber: zone.afterLineNumber,
162
column: zone.afterColumn
163
});
164
} else {
165
const validAfterLineNumber = this._context.viewModel.model.validatePosition({
166
lineNumber: zone.afterLineNumber,
167
column: 1
168
}).lineNumber;
169
170
zoneAfterModelPosition = new Position(
171
validAfterLineNumber,
172
this._context.viewModel.model.getLineMaxColumn(validAfterLineNumber)
173
);
174
}
175
176
let zoneBeforeModelPosition: Position;
177
if (zoneAfterModelPosition.column === this._context.viewModel.model.getLineMaxColumn(zoneAfterModelPosition.lineNumber)) {
178
zoneBeforeModelPosition = this._context.viewModel.model.validatePosition({
179
lineNumber: zoneAfterModelPosition.lineNumber + 1,
180
column: 1
181
});
182
} else {
183
zoneBeforeModelPosition = this._context.viewModel.model.validatePosition({
184
lineNumber: zoneAfterModelPosition.lineNumber,
185
column: zoneAfterModelPosition.column + 1
186
});
187
}
188
189
const viewPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(zoneAfterModelPosition, zone.afterColumnAffinity, true);
190
const isVisible = zone.showInHiddenAreas || this._context.viewModel.coordinatesConverter.modelPositionIsVisible(zoneBeforeModelPosition);
191
return {
192
isInHiddenArea: !isVisible,
193
afterViewLineNumber: viewPosition.lineNumber,
194
heightInPx: (isVisible ? this._heightInPixels(zone) : 0),
195
minWidthInPx: this._minWidthInPixels(zone)
196
};
197
}
198
199
public changeViewZones(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean {
200
let zonesHaveChanged = false;
201
202
this._context.viewModel.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => {
203
204
const changeAccessor: IViewZoneChangeAccessor = {
205
addZone: (zone: IViewZone): string => {
206
zonesHaveChanged = true;
207
return this._addZone(whitespaceAccessor, zone);
208
},
209
removeZone: (id: string): void => {
210
if (!id) {
211
return;
212
}
213
zonesHaveChanged = this._removeZone(whitespaceAccessor, id) || zonesHaveChanged;
214
},
215
layoutZone: (id: string): void => {
216
if (!id) {
217
return;
218
}
219
zonesHaveChanged = this._layoutZone(whitespaceAccessor, id) || zonesHaveChanged;
220
}
221
};
222
223
safeInvoke1Arg(callback, changeAccessor);
224
225
// Invalidate changeAccessor
226
changeAccessor.addZone = invalidFunc;
227
changeAccessor.removeZone = invalidFunc;
228
changeAccessor.layoutZone = invalidFunc;
229
});
230
231
return zonesHaveChanged;
232
}
233
234
private _addZone(whitespaceAccessor: IWhitespaceChangeAccessor, zone: IViewZone): string {
235
const props = this._computeWhitespaceProps(zone);
236
const whitespaceId = whitespaceAccessor.insertWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx);
237
238
const myZone: IMyViewZone = {
239
whitespaceId: whitespaceId,
240
delegate: zone,
241
isInHiddenArea: props.isInHiddenArea,
242
isVisible: false,
243
domNode: createFastDomNode(zone.domNode),
244
marginDomNode: zone.marginDomNode ? createFastDomNode(zone.marginDomNode) : null
245
};
246
247
this._safeCallOnComputedHeight(myZone.delegate, props.heightInPx);
248
249
myZone.domNode.setPosition('absolute');
250
myZone.domNode.domNode.style.width = '100%';
251
myZone.domNode.setDisplay('none');
252
myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId);
253
this.domNode.appendChild(myZone.domNode);
254
255
if (myZone.marginDomNode) {
256
myZone.marginDomNode.setPosition('absolute');
257
myZone.marginDomNode.domNode.style.width = '100%';
258
myZone.marginDomNode.setDisplay('none');
259
myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId);
260
this.marginDomNode.appendChild(myZone.marginDomNode);
261
}
262
263
this._zones[myZone.whitespaceId] = myZone;
264
265
266
this.setShouldRender();
267
268
return myZone.whitespaceId;
269
}
270
271
private _removeZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean {
272
if (this._zones.hasOwnProperty(id)) {
273
const zone = this._zones[id];
274
delete this._zones[id];
275
whitespaceAccessor.removeWhitespace(zone.whitespaceId);
276
277
zone.domNode.removeAttribute('monaco-visible-view-zone');
278
zone.domNode.removeAttribute('monaco-view-zone');
279
zone.domNode.domNode.remove();
280
281
if (zone.marginDomNode) {
282
zone.marginDomNode.removeAttribute('monaco-visible-view-zone');
283
zone.marginDomNode.removeAttribute('monaco-view-zone');
284
zone.marginDomNode.domNode.remove();
285
}
286
287
this.setShouldRender();
288
289
return true;
290
}
291
return false;
292
}
293
294
private _layoutZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean {
295
if (this._zones.hasOwnProperty(id)) {
296
const zone = this._zones[id];
297
const props = this._computeWhitespaceProps(zone.delegate);
298
zone.isInHiddenArea = props.isInHiddenArea;
299
// const newOrdinal = this._getZoneOrdinal(zone.delegate);
300
whitespaceAccessor.changeOneWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx);
301
// TODO@Alex: change `newOrdinal` too
302
303
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
304
this.setShouldRender();
305
306
return true;
307
}
308
return false;
309
}
310
311
public shouldSuppressMouseDownOnViewZone(id: string): boolean {
312
if (this._zones.hasOwnProperty(id)) {
313
const zone = this._zones[id];
314
return Boolean(zone.delegate.suppressMouseDown);
315
}
316
return false;
317
}
318
319
private _heightInPixels(zone: IViewZone): number {
320
if (typeof zone.heightInPx === 'number') {
321
return zone.heightInPx;
322
}
323
if (typeof zone.heightInLines === 'number') {
324
return this._lineHeight * zone.heightInLines;
325
}
326
return this._lineHeight;
327
}
328
329
private _minWidthInPixels(zone: IViewZone): number {
330
if (typeof zone.minWidthInPx === 'number') {
331
return zone.minWidthInPx;
332
}
333
return 0;
334
}
335
336
private _safeCallOnComputedHeight(zone: IViewZone, height: number): void {
337
if (typeof zone.onComputedHeight === 'function') {
338
try {
339
zone.onComputedHeight(height);
340
} catch (e) {
341
onUnexpectedError(e);
342
}
343
}
344
}
345
346
private _safeCallOnDomNodeTop(zone: IViewZone, top: number): void {
347
if (typeof zone.onDomNodeTop === 'function') {
348
try {
349
zone.onDomNodeTop(top);
350
} catch (e) {
351
onUnexpectedError(e);
352
}
353
}
354
}
355
356
public prepareRender(ctx: RenderingContext): void {
357
// Nothing to read
358
}
359
360
public render(ctx: RestrictedRenderingContext): void {
361
const visibleWhitespaces = ctx.viewportData.whitespaceViewportData;
362
const visibleZones: { [id: string]: IViewWhitespaceViewportData } = {};
363
364
let hasVisibleZone = false;
365
for (const visibleWhitespace of visibleWhitespaces) {
366
if (this._zones[visibleWhitespace.id].isInHiddenArea) {
367
continue;
368
}
369
visibleZones[visibleWhitespace.id] = visibleWhitespace;
370
hasVisibleZone = true;
371
}
372
373
const keys = Object.keys(this._zones);
374
for (let i = 0, len = keys.length; i < len; i++) {
375
const id = keys[i];
376
const zone = this._zones[id];
377
378
let newTop = 0;
379
let newHeight = 0;
380
let newDisplay = 'none';
381
if (visibleZones.hasOwnProperty(id)) {
382
newTop = visibleZones[id].verticalOffset - ctx.bigNumbersDelta;
383
newHeight = visibleZones[id].height;
384
newDisplay = 'block';
385
// zone is visible
386
if (!zone.isVisible) {
387
zone.domNode.setAttribute('monaco-visible-view-zone', 'true');
388
zone.isVisible = true;
389
}
390
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(visibleZones[id].verticalOffset));
391
} else {
392
if (zone.isVisible) {
393
zone.domNode.removeAttribute('monaco-visible-view-zone');
394
zone.isVisible = false;
395
}
396
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(-1000000));
397
}
398
zone.domNode.setTop(newTop);
399
zone.domNode.setHeight(newHeight);
400
zone.domNode.setDisplay(newDisplay);
401
402
if (zone.marginDomNode) {
403
zone.marginDomNode.setTop(newTop);
404
zone.marginDomNode.setHeight(newHeight);
405
zone.marginDomNode.setDisplay(newDisplay);
406
}
407
}
408
409
if (hasVisibleZone) {
410
this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));
411
this.marginDomNode.setWidth(this._contentLeft);
412
}
413
}
414
}
415
416
function safeInvoke1Arg(func: Function, arg1: any): any {
417
try {
418
return func(arg1);
419
} catch (e) {
420
onUnexpectedError(e);
421
}
422
}
423
424