Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.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 { ArrayQueue } from '../../../../base/common/arrays.js';
8
import './glyphMargin.css';
9
import { IGlyphMarginWidget, IGlyphMarginWidgetPosition } from '../../editorBrowser.js';
10
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
11
import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';
12
import { ViewPart } from '../../view/viewPart.js';
13
import { EditorOption } from '../../../common/config/editorOptions.js';
14
import { Position } from '../../../common/core/position.js';
15
import { Range } from '../../../common/core/range.js';
16
import { GlyphMarginLane } from '../../../common/model.js';
17
import * as viewEvents from '../../../common/viewEvents.js';
18
import { ViewContext } from '../../../common/viewModel/viewContext.js';
19
20
/**
21
* Represents a decoration that should be shown along the lines from `startLineNumber` to `endLineNumber`.
22
* This can end up producing multiple `LineDecorationToRender`.
23
*/
24
export class DecorationToRender {
25
public readonly _decorationToRenderBrand: void = undefined;
26
27
public readonly zIndex: number;
28
29
constructor(
30
public readonly startLineNumber: number,
31
public readonly endLineNumber: number,
32
public readonly className: string,
33
public readonly tooltip: string | null,
34
zIndex: number | undefined,
35
) {
36
this.zIndex = zIndex ?? 0;
37
}
38
}
39
40
/**
41
* A decoration that should be shown along a line.
42
*/
43
export class LineDecorationToRender {
44
constructor(
45
public readonly className: string,
46
public readonly zIndex: number,
47
public readonly tooltip: string | null,
48
) { }
49
}
50
51
/**
52
* Decorations to render on a visible line.
53
*/
54
export class VisibleLineDecorationsToRender {
55
56
private readonly decorations: LineDecorationToRender[] = [];
57
58
public add(decoration: LineDecorationToRender) {
59
this.decorations.push(decoration);
60
}
61
62
public getDecorations(): LineDecorationToRender[] {
63
return this.decorations;
64
}
65
}
66
67
export abstract class DedupOverlay extends DynamicViewOverlay {
68
69
/**
70
* Returns an array with an element for each visible line number.
71
*/
72
protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): VisibleLineDecorationsToRender[] {
73
74
const output: VisibleLineDecorationsToRender[] = [];
75
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
76
const lineIndex = lineNumber - visibleStartLineNumber;
77
output[lineIndex] = new VisibleLineDecorationsToRender();
78
}
79
80
if (decorations.length === 0) {
81
return output;
82
}
83
84
// Sort decorations by className, then by startLineNumber and then by endLineNumber
85
decorations.sort((a, b) => {
86
if (a.className === b.className) {
87
if (a.startLineNumber === b.startLineNumber) {
88
return a.endLineNumber - b.endLineNumber;
89
}
90
return a.startLineNumber - b.startLineNumber;
91
}
92
return (a.className < b.className ? -1 : 1);
93
});
94
95
let prevClassName: string | null = null;
96
let prevEndLineIndex = 0;
97
for (let i = 0, len = decorations.length; i < len; i++) {
98
const d = decorations[i];
99
const className = d.className;
100
const zIndex = d.zIndex;
101
let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;
102
const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;
103
104
if (prevClassName === className) {
105
// Here we avoid rendering the same className multiple times on the same line
106
startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex);
107
prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex);
108
} else {
109
prevClassName = className;
110
prevEndLineIndex = endLineIndex;
111
}
112
113
for (let i = startLineIndex; i <= prevEndLineIndex; i++) {
114
output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip));
115
}
116
}
117
118
return output;
119
}
120
}
121
122
export class GlyphMarginWidgets extends ViewPart {
123
124
public domNode: FastDomNode<HTMLElement>;
125
126
private _lineHeight: number;
127
private _glyphMargin: boolean;
128
private _glyphMarginLeft: number;
129
private _glyphMarginWidth: number;
130
private _glyphMarginDecorationLaneCount: number;
131
132
private _managedDomNodes: FastDomNode<HTMLElement>[];
133
private _decorationGlyphsToRender: DecorationBasedGlyph[];
134
135
private _widgets: { [key: string]: IWidgetData } = {};
136
137
constructor(context: ViewContext) {
138
super(context);
139
this._context = context;
140
141
const options = this._context.configuration.options;
142
const layoutInfo = options.get(EditorOption.layoutInfo);
143
144
this.domNode = createFastDomNode(document.createElement('div'));
145
this.domNode.setClassName('glyph-margin-widgets');
146
this.domNode.setPosition('absolute');
147
this.domNode.setTop(0);
148
149
this._lineHeight = options.get(EditorOption.lineHeight);
150
this._glyphMargin = options.get(EditorOption.glyphMargin);
151
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
152
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
153
this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;
154
this._managedDomNodes = [];
155
this._decorationGlyphsToRender = [];
156
}
157
158
public override dispose(): void {
159
this._managedDomNodes = [];
160
this._decorationGlyphsToRender = [];
161
this._widgets = {};
162
super.dispose();
163
}
164
165
public getWidgets(): IWidgetData[] {
166
return Object.values(this._widgets);
167
}
168
169
// --- begin event handlers
170
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
171
const options = this._context.configuration.options;
172
const layoutInfo = options.get(EditorOption.layoutInfo);
173
174
this._lineHeight = options.get(EditorOption.lineHeight);
175
this._glyphMargin = options.get(EditorOption.glyphMargin);
176
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
177
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
178
this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;
179
return true;
180
}
181
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
182
return true;
183
}
184
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
185
return true;
186
}
187
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
188
return true;
189
}
190
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
191
return true;
192
}
193
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
194
return true;
195
}
196
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
197
return e.scrollTopChanged;
198
}
199
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
200
return true;
201
}
202
203
// --- end event handlers
204
205
// --- begin widget management
206
207
public addWidget(widget: IGlyphMarginWidget): void {
208
const domNode = createFastDomNode(widget.getDomNode());
209
210
this._widgets[widget.getId()] = {
211
widget: widget,
212
preference: widget.getPosition(),
213
domNode: domNode,
214
renderInfo: null
215
};
216
217
domNode.setPosition('absolute');
218
domNode.setDisplay('none');
219
domNode.setAttribute('widgetId', widget.getId());
220
this.domNode.appendChild(domNode);
221
222
this.setShouldRender();
223
}
224
225
public setWidgetPosition(widget: IGlyphMarginWidget, preference: IGlyphMarginWidgetPosition): boolean {
226
const myWidget = this._widgets[widget.getId()];
227
if (myWidget.preference.lane === preference.lane
228
&& myWidget.preference.zIndex === preference.zIndex
229
&& Range.equalsRange(myWidget.preference.range, preference.range)) {
230
return false;
231
}
232
233
myWidget.preference = preference;
234
this.setShouldRender();
235
236
return true;
237
}
238
239
public removeWidget(widget: IGlyphMarginWidget): void {
240
const widgetId = widget.getId();
241
if (this._widgets[widgetId]) {
242
const widgetData = this._widgets[widgetId];
243
const domNode = widgetData.domNode.domNode;
244
delete this._widgets[widgetId];
245
246
domNode.remove();
247
this.setShouldRender();
248
}
249
}
250
251
// --- end widget management
252
253
private _collectDecorationBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {
254
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
255
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
256
const decorations = ctx.getDecorationsInViewport();
257
258
for (const d of decorations) {
259
const glyphMarginClassName = d.options.glyphMarginClassName;
260
if (!glyphMarginClassName) {
261
continue;
262
}
263
264
const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);
265
const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);
266
const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center;
267
const zIndex = d.options.zIndex ?? 0;
268
269
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
270
const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 0));
271
const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(lane);
272
requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, laneIndex, zIndex, glyphMarginClassName));
273
}
274
}
275
}
276
277
private _collectWidgetBasedGlyphRenderRequest(ctx: RenderingContext, requests: GlyphRenderRequest[]): void {
278
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
279
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
280
281
for (const widget of Object.values(this._widgets)) {
282
const range = widget.preference.range;
283
const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range));
284
if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) {
285
// The widget is not in the viewport
286
continue;
287
}
288
289
// The widget is in the viewport, find a good line for it
290
const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber);
291
const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(widgetLineNumber, 0));
292
const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(widget.preference.lane);
293
requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, laneIndex, widget.preference.zIndex, widget));
294
}
295
}
296
297
private _collectSortedGlyphRenderRequests(ctx: RenderingContext): GlyphRenderRequest[] {
298
299
const requests: GlyphRenderRequest[] = [];
300
301
this._collectDecorationBasedGlyphRenderRequest(ctx, requests);
302
this._collectWidgetBasedGlyphRenderRequest(ctx, requests);
303
304
// sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC
305
// don't change this sort unless you understand `prepareRender` below.
306
requests.sort((a, b) => {
307
if (a.lineNumber === b.lineNumber) {
308
if (a.laneIndex === b.laneIndex) {
309
if (a.zIndex === b.zIndex) {
310
if (b.type === a.type) {
311
if (a.type === GlyphRenderRequestType.Decoration && b.type === GlyphRenderRequestType.Decoration) {
312
return (a.className < b.className ? -1 : 1);
313
}
314
return 0;
315
}
316
return b.type - a.type;
317
}
318
return b.zIndex - a.zIndex;
319
}
320
return a.laneIndex - b.laneIndex;
321
}
322
return a.lineNumber - b.lineNumber;
323
});
324
325
return requests;
326
}
327
328
/**
329
* Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`.
330
*/
331
public prepareRender(ctx: RenderingContext): void {
332
if (!this._glyphMargin) {
333
this._decorationGlyphsToRender = [];
334
return;
335
}
336
337
for (const widget of Object.values(this._widgets)) {
338
widget.renderInfo = null;
339
}
340
341
const requests = new ArrayQueue<GlyphRenderRequest>(this._collectSortedGlyphRenderRequests(ctx));
342
const decorationGlyphsToRender: DecorationBasedGlyph[] = [];
343
while (requests.length > 0) {
344
const first = requests.peek();
345
if (!first) {
346
// not possible
347
break;
348
}
349
350
// Requests are sorted by lineNumber and lane, so we read all requests for this particular location
351
const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.laneIndex === first.laneIndex);
352
if (!requestsAtLocation || requestsAtLocation.length === 0) {
353
// not possible
354
break;
355
}
356
357
const winner = requestsAtLocation[0];
358
if (winner.type === GlyphRenderRequestType.Decoration) {
359
// combine all decorations with the same z-index
360
361
const classNames: string[] = [];
362
// requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one
363
for (const request of requestsAtLocation) {
364
if (request.zIndex !== winner.zIndex || request.type !== winner.type) {
365
break;
366
}
367
if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) {
368
classNames.push(request.className);
369
}
370
}
371
372
decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations
373
} else {
374
// widgets cannot be combined
375
winner.widget.renderInfo = {
376
lineNumber: winner.lineNumber,
377
laneIndex: winner.laneIndex,
378
};
379
}
380
}
381
this._decorationGlyphsToRender = decorationGlyphsToRender;
382
}
383
384
public render(ctx: RestrictedRenderingContext): void {
385
if (!this._glyphMargin) {
386
for (const widget of Object.values(this._widgets)) {
387
widget.domNode.setDisplay('none');
388
}
389
while (this._managedDomNodes.length > 0) {
390
const domNode = this._managedDomNodes.pop();
391
domNode?.domNode.remove();
392
}
393
return;
394
}
395
396
const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount));
397
398
// Render widgets
399
for (const widget of Object.values(this._widgets)) {
400
if (!widget.renderInfo) {
401
// this widget is not visible
402
widget.domNode.setDisplay('none');
403
} else {
404
const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber];
405
const left = this._glyphMarginLeft + widget.renderInfo.laneIndex * this._lineHeight;
406
407
widget.domNode.setDisplay('block');
408
widget.domNode.setTop(top);
409
widget.domNode.setLeft(left);
410
widget.domNode.setWidth(width);
411
widget.domNode.setHeight(this._lineHeight);
412
}
413
}
414
415
// Render decorations, reusing previous dom nodes as possible
416
for (let i = 0; i < this._decorationGlyphsToRender.length; i++) {
417
const dec = this._decorationGlyphsToRender[i];
418
const decLineNumber = dec.lineNumber;
419
const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber];
420
const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight;
421
422
let domNode: FastDomNode<HTMLElement>;
423
if (i < this._managedDomNodes.length) {
424
domNode = this._managedDomNodes[i];
425
} else {
426
domNode = createFastDomNode(document.createElement('div'));
427
this._managedDomNodes.push(domNode);
428
this.domNode.appendChild(domNode);
429
}
430
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber);
431
432
domNode.setClassName(`cgmr codicon ` + dec.combinedClassName);
433
domNode.setPosition(`absolute`);
434
domNode.setTop(top);
435
domNode.setLeft(left);
436
domNode.setWidth(width);
437
domNode.setHeight(lineHeight);
438
}
439
440
// remove extra dom nodes
441
while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) {
442
const domNode = this._managedDomNodes.pop();
443
domNode?.domNode.remove();
444
}
445
}
446
}
447
448
export interface IWidgetData {
449
widget: IGlyphMarginWidget;
450
preference: IGlyphMarginWidgetPosition;
451
domNode: FastDomNode<HTMLElement>;
452
/**
453
* it will contain the location where to render the widget
454
* or null if the widget is not visible
455
*/
456
renderInfo: IRenderInfo | null;
457
}
458
459
export interface IRenderInfo {
460
lineNumber: number;
461
laneIndex: number;
462
}
463
464
const enum GlyphRenderRequestType {
465
Decoration = 0,
466
Widget = 1
467
}
468
469
/**
470
* A request to render a decoration in the glyph margin at a certain location.
471
*/
472
class DecorationBasedGlyphRenderRequest {
473
public readonly type = GlyphRenderRequestType.Decoration;
474
475
constructor(
476
public readonly lineNumber: number,
477
public readonly laneIndex: number,
478
public readonly zIndex: number,
479
public readonly className: string,
480
) { }
481
482
accept(combinedClassName: string): DecorationBasedGlyph {
483
return new DecorationBasedGlyph(this.lineNumber, this.laneIndex, combinedClassName);
484
}
485
}
486
487
/**
488
* A request to render a widget in the glyph margin at a certain location.
489
*/
490
class WidgetBasedGlyphRenderRequest {
491
public readonly type = GlyphRenderRequestType.Widget;
492
493
constructor(
494
public readonly lineNumber: number,
495
public readonly laneIndex: number,
496
public readonly zIndex: number,
497
public readonly widget: IWidgetData,
498
) { }
499
}
500
501
type GlyphRenderRequest = DecorationBasedGlyphRenderRequest | WidgetBasedGlyphRenderRequest;
502
503
class DecorationBasedGlyph {
504
constructor(
505
public readonly lineNumber: number,
506
public readonly laneIndex: number,
507
public readonly combinedClassName: string
508
) { }
509
}
510
511