Skip to content

Commit 26a530f

Browse files
joyceerhlalexdima
andauthored
Implement GlyphMarginWidgets (microsoft#184375)
* Implement GlyphMarginWidgets --------- Co-authored-by: Alex Dima <[email protected]>
1 parent 19b9e08 commit 26a530f

File tree

9 files changed

+591
-159
lines changed

9 files changed

+591
-159
lines changed

src/vs/editor/browser/editorBrowser.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
1212
import { IRange, Range } from 'vs/editor/common/core/range';
1313
import { Selection } from 'vs/editor/common/core/selection';
1414
import * as editorCommon from 'vs/editor/common/editorCommon';
15-
import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model';
15+
import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity, GlyphMarginLane } from 'vs/editor/common/model';
1616
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
1717
import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
1818
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
@@ -251,6 +251,43 @@ export interface IOverlayWidget {
251251
getPosition(): IOverlayWidgetPosition | null;
252252
}
253253

254+
/**
255+
* A glyph margin widget renders in the editor glyph margin.
256+
*/
257+
export interface IGlyphMarginWidget {
258+
/**
259+
* Get a unique identifier of the glyph widget.
260+
*/
261+
getId(): string;
262+
/**
263+
* Get the dom node of the glyph widget.
264+
*/
265+
getDomNode(): HTMLElement;
266+
/**
267+
* Get the placement of the glyph widget.
268+
*/
269+
getPosition(): IGlyphMarginWidgetPosition;
270+
}
271+
272+
/**
273+
* A position for rendering glyph margin widgets.
274+
*/
275+
export interface IGlyphMarginWidgetPosition {
276+
/**
277+
* The glyph margin lane where the widget should be shown.
278+
*/
279+
lane: GlyphMarginLane;
280+
/**
281+
* The priority order of the widget, used for determining which widget
282+
* to render when there are multiple.
283+
*/
284+
zIndex: number;
285+
/**
286+
* The editor range that this widget applies to.
287+
*/
288+
range: IRange;
289+
}
290+
254291
/**
255292
* Type of hit element with the mouse in the editor.
256293
*/
@@ -993,6 +1030,20 @@ export interface ICodeEditor extends editorCommon.IEditor {
9931030
*/
9941031
removeOverlayWidget(widget: IOverlayWidget): void;
9951032

1033+
/**
1034+
* Add a glyph margin widget. Widgets must have unique ids, otherwise they will be overwritten.
1035+
*/
1036+
addGlyphMarginWidget(widget: IGlyphMarginWidget): void;
1037+
/**
1038+
* Layout/Reposition a glyph margin widget. This is a ping to the editor to call widget.getPosition()
1039+
* and update appropriately.
1040+
*/
1041+
layoutGlyphMarginWidget(widget: IGlyphMarginWidget): void;
1042+
/**
1043+
* Remove a glyph margin widget.
1044+
*/
1045+
removeGlyphMarginWidget(widget: IGlyphMarginWidget): void;
1046+
9961047
/**
9971048
* Change the view zones. View zones are lost when a new model is attached to the editor.
9981049
*/

src/vs/editor/browser/services/abstractCodeEditorService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ class DecorationCSSRules {
786786
}
787787

788788
/**
789-
* Build the CSS for decorations styled via `glpyhMarginClassName`.
789+
* Build the CSS for decorations styled via `glyphMarginClassName`.
790790
*/
791791
private getCSSTextForModelDecorationGlyphMarginClassName(opts: IThemeDecorationRenderOptions | undefined): string {
792792
if (!opts) {

src/vs/editor/browser/view.ts

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
import * as dom from 'vs/base/browser/dom';
77
import { Selection } from 'vs/editor/common/core/selection';
8+
import { Range } from 'vs/editor/common/core/range';
89
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
910
import { onUnexpectedError } from 'vs/base/common/errors';
1011
import { IDisposable } from 'vs/base/common/lifecycle';
1112
import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler';
1213
import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler';
1314
import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler';
14-
import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser';
15+
import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser';
1516
import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController';
1617
import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents';
1718
import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays';
@@ -20,7 +21,6 @@ import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/c
2021
import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from 'vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight';
2122
import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations';
2223
import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar';
23-
import { GlyphMarginOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
2424
import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides';
2525
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
2626
import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines';
@@ -52,6 +52,8 @@ import { BlockDecorations } from 'vs/editor/browser/viewParts/blockDecorations/b
5252
import { inputLatency } from 'vs/base/browser/performance';
5353
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
5454
import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace';
55+
import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
56+
import { GlyphMarginLane } from 'vs/editor/common/model';
5557

5658

5759
export interface IContentWidgetData {
@@ -64,6 +66,11 @@ export interface IOverlayWidgetData {
6466
position: IOverlayWidgetPosition | null;
6567
}
6668

69+
export interface IGlyphMarginWidgetData {
70+
widget: IGlyphMarginWidget;
71+
position: IGlyphMarginWidgetPosition;
72+
}
73+
6774
export class View extends ViewEventHandler {
6875

6976
private readonly _scrollbar: EditorScrollbar;
@@ -77,6 +84,7 @@ export class View extends ViewEventHandler {
7784
private readonly _viewZones: ViewZones;
7885
private readonly _contentWidgets: ViewContentWidgets;
7986
private readonly _overlayWidgets: ViewOverlayWidgets;
87+
private readonly _glyphMarginWidgets: GlyphMarginWidgets;
8088
private readonly _viewCursors: ViewCursors;
8189
private readonly _viewParts: ViewPart[];
8290

@@ -89,6 +97,7 @@ export class View extends ViewEventHandler {
8997
private readonly _overflowGuardContainer: FastDomNode<HTMLElement>;
9098

9199
// Actual mutable state
100+
private _shouldRecomputeGlyphMarginLanes: boolean = false;
92101
private _renderAnimationFrame: IDisposable | null;
93102

94103
constructor(
@@ -160,14 +169,18 @@ export class View extends ViewEventHandler {
160169
const marginViewOverlays = new MarginViewOverlays(this._context);
161170
this._viewParts.push(marginViewOverlays);
162171
marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context));
163-
marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context));
164172
marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context));
165173
marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context));
166174
marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context));
167175

176+
// Glyph margin widgets
177+
this._glyphMarginWidgets = new GlyphMarginWidgets(this._context);
178+
this._viewParts.push(this._glyphMarginWidgets);
179+
168180
const margin = new Margin(this._context);
169181
margin.getDomNode().appendChild(this._viewZones.marginDomNode);
170182
margin.getDomNode().appendChild(marginViewOverlays.getDomNode());
183+
margin.getDomNode().appendChild(this._glyphMarginWidgets.domNode);
171184
this._viewParts.push(margin);
172185

173186
// Content widgets
@@ -226,10 +239,70 @@ export class View extends ViewEventHandler {
226239
}
227240

228241
private _flushAccumulatedAndRenderNow(): void {
242+
if (this._shouldRecomputeGlyphMarginLanes) {
243+
this._shouldRecomputeGlyphMarginLanes = false;
244+
this._context.configuration.setGlyphMarginDecorationLaneCount(this._computeGlyphMarginLaneCount());
245+
}
229246
inputLatency.onRenderStart();
230247
this._renderNow();
231248
}
232249

250+
private _computeGlyphMarginLaneCount(): number {
251+
const model = this._context.viewModel.model;
252+
type Glyph = { range: Range; lane: GlyphMarginLane };
253+
let glyphs: Glyph[] = [];
254+
255+
// Add all margin decorations
256+
glyphs = glyphs.concat(model.getAllMarginDecorations().map((decoration) => {
257+
const lane = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left;
258+
return { range: decoration.range, lane };
259+
}));
260+
261+
// Add all glyph margin widgets
262+
glyphs = glyphs.concat(this._glyphMarginWidgets.getWidgets().map((widget) => {
263+
const range = model.validateRange(widget.preference.range);
264+
return { range, lane: widget.preference.lane };
265+
}));
266+
267+
// Sorted by their start position
268+
glyphs.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
269+
270+
let leftDecRange: Range | null = null;
271+
let rightDecRange: Range | null = null;
272+
for (const decoration of glyphs) {
273+
274+
if (decoration.lane === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) {
275+
// assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane
276+
leftDecRange = decoration.range;
277+
}
278+
279+
if (decoration.lane === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) {
280+
// assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane
281+
rightDecRange = decoration.range;
282+
}
283+
284+
if (leftDecRange && rightDecRange) {
285+
286+
if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) {
287+
// there's no chance for `leftDecRange` to ever intersect something going further
288+
leftDecRange = null;
289+
continue;
290+
}
291+
292+
if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) {
293+
// there's no chance for `rightDecRange` to ever intersect something going further
294+
rightDecRange = null;
295+
continue;
296+
}
297+
298+
// leftDecRange and rightDecRange are intersecting or touching => we need two lanes
299+
return 2;
300+
}
301+
}
302+
303+
return 1;
304+
}
305+
233306
private _createPointerHandlerHelper(): IPointerHandlerHelper {
234307
return {
235308
viewDomNode: this.domNode.domNode,
@@ -317,6 +390,12 @@ export class View extends ViewEventHandler {
317390
this._selections = e.selections;
318391
return false;
319392
}
393+
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
394+
if (e.affectsGlyphMargin) {
395+
this._shouldRecomputeGlyphMarginLanes = true;
396+
}
397+
return false;
398+
}
320399
public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
321400
this.domNode.setClassName(this._getEditorClassName());
322401
return false;
@@ -548,6 +627,27 @@ export class View extends ViewEventHandler {
548627
this._scheduleRender();
549628
}
550629

630+
public addGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
631+
this._glyphMarginWidgets.addWidget(widgetData.widget);
632+
this._shouldRecomputeGlyphMarginLanes = true;
633+
this._scheduleRender();
634+
}
635+
636+
public layoutGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
637+
const newPreference = widgetData.position;
638+
const shouldRender = this._glyphMarginWidgets.setWidgetPosition(widgetData.widget, newPreference);
639+
if (shouldRender) {
640+
this._shouldRecomputeGlyphMarginLanes = true;
641+
this._scheduleRender();
642+
}
643+
}
644+
645+
public removeGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
646+
this._glyphMarginWidgets.removeWidget(widgetData.widget);
647+
this._shouldRecomputeGlyphMarginLanes = true;
648+
this._scheduleRender();
649+
}
650+
551651
// --- END CodeEditor helpers
552652

553653
}

0 commit comments

Comments
 (0)