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