Skip to content

Commit

Permalink
Implement GlyphMarginWidgets (#184375)
Browse files Browse the repository at this point in the history
* Implement GlyphMarginWidgets

---------

Co-authored-by: Alex Dima <alexdima@microsoft.com>
  • Loading branch information
joyceerhl and alexdima authored Jun 16, 2023
1 parent 19b9e08 commit 26a530f
Show file tree
Hide file tree
Showing 9 changed files with 591 additions and 159 deletions.
53 changes: 52 additions & 1 deletion src/vs/editor/browser/editorBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model';
import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, PositionAffinity, GlyphMarginLane } from 'vs/editor/common/model';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
Expand Down Expand Up @@ -251,6 +251,43 @@ export interface IOverlayWidget {
getPosition(): IOverlayWidgetPosition | null;
}

/**
* A glyph margin widget renders in the editor glyph margin.
*/
export interface IGlyphMarginWidget {
/**
* Get a unique identifier of the glyph widget.
*/
getId(): string;
/**
* Get the dom node of the glyph widget.
*/
getDomNode(): HTMLElement;
/**
* Get the placement of the glyph widget.
*/
getPosition(): IGlyphMarginWidgetPosition;
}

/**
* A position for rendering glyph margin widgets.
*/
export interface IGlyphMarginWidgetPosition {
/**
* The glyph margin lane where the widget should be shown.
*/
lane: GlyphMarginLane;
/**
* The priority order of the widget, used for determining which widget
* to render when there are multiple.
*/
zIndex: number;
/**
* The editor range that this widget applies to.
*/
range: IRange;
}

/**
* Type of hit element with the mouse in the editor.
*/
Expand Down Expand Up @@ -993,6 +1030,20 @@ export interface ICodeEditor extends editorCommon.IEditor {
*/
removeOverlayWidget(widget: IOverlayWidget): void;

/**
* Add a glyph margin widget. Widgets must have unique ids, otherwise they will be overwritten.
*/
addGlyphMarginWidget(widget: IGlyphMarginWidget): void;
/**
* Layout/Reposition a glyph margin widget. This is a ping to the editor to call widget.getPosition()
* and update appropriately.
*/
layoutGlyphMarginWidget(widget: IGlyphMarginWidget): void;
/**
* Remove a glyph margin widget.
*/
removeGlyphMarginWidget(widget: IGlyphMarginWidget): void;

/**
* Change the view zones. View zones are lost when a new model is attached to the editor.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ class DecorationCSSRules {
}

/**
* Build the CSS for decorations styled via `glpyhMarginClassName`.
* Build the CSS for decorations styled via `glyphMarginClassName`.
*/
private getCSSTextForModelDecorationGlyphMarginClassName(opts: IThemeDecorationRenderOptions | undefined): string {
if (!opts) {
Expand Down
106 changes: 103 additions & 3 deletions src/vs/editor/browser/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import * as dom from 'vs/base/browser/dom';
import { Selection } from 'vs/editor/common/core/selection';
import { Range } from 'vs/editor/common/core/range';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler';
import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler';
import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler';
import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser';
import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController';
import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents';
import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays';
Expand All @@ -20,7 +21,6 @@ import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/c
import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from 'vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight';
import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations';
import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar';
import { GlyphMarginOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides';
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines';
Expand Down Expand Up @@ -52,6 +52,8 @@ import { BlockDecorations } from 'vs/editor/browser/viewParts/blockDecorations/b
import { inputLatency } from 'vs/base/browser/performance';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace';
import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
import { GlyphMarginLane } from 'vs/editor/common/model';


export interface IContentWidgetData {
Expand All @@ -64,6 +66,11 @@ export interface IOverlayWidgetData {
position: IOverlayWidgetPosition | null;
}

export interface IGlyphMarginWidgetData {
widget: IGlyphMarginWidget;
position: IGlyphMarginWidgetPosition;
}

export class View extends ViewEventHandler {

private readonly _scrollbar: EditorScrollbar;
Expand All @@ -77,6 +84,7 @@ export class View extends ViewEventHandler {
private readonly _viewZones: ViewZones;
private readonly _contentWidgets: ViewContentWidgets;
private readonly _overlayWidgets: ViewOverlayWidgets;
private readonly _glyphMarginWidgets: GlyphMarginWidgets;
private readonly _viewCursors: ViewCursors;
private readonly _viewParts: ViewPart[];

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

// Actual mutable state
private _shouldRecomputeGlyphMarginLanes: boolean = false;
private _renderAnimationFrame: IDisposable | null;

constructor(
Expand Down Expand Up @@ -160,14 +169,18 @@ export class View extends ViewEventHandler {
const marginViewOverlays = new MarginViewOverlays(this._context);
this._viewParts.push(marginViewOverlays);
marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context));

// Glyph margin widgets
this._glyphMarginWidgets = new GlyphMarginWidgets(this._context);
this._viewParts.push(this._glyphMarginWidgets);

const margin = new Margin(this._context);
margin.getDomNode().appendChild(this._viewZones.marginDomNode);
margin.getDomNode().appendChild(marginViewOverlays.getDomNode());
margin.getDomNode().appendChild(this._glyphMarginWidgets.domNode);
this._viewParts.push(margin);

// Content widgets
Expand Down Expand Up @@ -226,10 +239,70 @@ export class View extends ViewEventHandler {
}

private _flushAccumulatedAndRenderNow(): void {
if (this._shouldRecomputeGlyphMarginLanes) {
this._shouldRecomputeGlyphMarginLanes = false;
this._context.configuration.setGlyphMarginDecorationLaneCount(this._computeGlyphMarginLaneCount());
}
inputLatency.onRenderStart();
this._renderNow();
}

private _computeGlyphMarginLaneCount(): number {
const model = this._context.viewModel.model;
type Glyph = { range: Range; lane: GlyphMarginLane };
let glyphs: Glyph[] = [];

// Add all margin decorations
glyphs = glyphs.concat(model.getAllMarginDecorations().map((decoration) => {
const lane = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Left;
return { range: decoration.range, lane };
}));

// Add all glyph margin widgets
glyphs = glyphs.concat(this._glyphMarginWidgets.getWidgets().map((widget) => {
const range = model.validateRange(widget.preference.range);
return { range, lane: widget.preference.lane };
}));

// Sorted by their start position
glyphs.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));

let leftDecRange: Range | null = null;
let rightDecRange: Range | null = null;
for (const decoration of glyphs) {

if (decoration.lane === GlyphMarginLane.Left && (!leftDecRange || Range.compareRangesUsingEnds(leftDecRange, decoration.range) < 0)) {
// assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane
leftDecRange = decoration.range;
}

if (decoration.lane === GlyphMarginLane.Right && (!rightDecRange || Range.compareRangesUsingEnds(rightDecRange, decoration.range) < 0)) {
// assign only if the range of `decoration` ends after, which means it has a higher chance to overlap with the other lane
rightDecRange = decoration.range;
}

if (leftDecRange && rightDecRange) {

if (leftDecRange.endLineNumber < rightDecRange.startLineNumber) {
// there's no chance for `leftDecRange` to ever intersect something going further
leftDecRange = null;
continue;
}

if (rightDecRange.endLineNumber < leftDecRange.startLineNumber) {
// there's no chance for `rightDecRange` to ever intersect something going further
rightDecRange = null;
continue;
}

// leftDecRange and rightDecRange are intersecting or touching => we need two lanes
return 2;
}
}

return 1;
}

private _createPointerHandlerHelper(): IPointerHandlerHelper {
return {
viewDomNode: this.domNode.domNode,
Expand Down Expand Up @@ -317,6 +390,12 @@ export class View extends ViewEventHandler {
this._selections = e.selections;
return false;
}
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
if (e.affectsGlyphMargin) {
this._shouldRecomputeGlyphMarginLanes = true;
}
return false;
}
public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
this.domNode.setClassName(this._getEditorClassName());
return false;
Expand Down Expand Up @@ -548,6 +627,27 @@ export class View extends ViewEventHandler {
this._scheduleRender();
}

public addGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
this._glyphMarginWidgets.addWidget(widgetData.widget);
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}

public layoutGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
const newPreference = widgetData.position;
const shouldRender = this._glyphMarginWidgets.setWidgetPosition(widgetData.widget, newPreference);
if (shouldRender) {
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}
}

public removeGlyphMarginWidget(widgetData: IGlyphMarginWidgetData): void {
this._glyphMarginWidgets.removeWidget(widgetData.widget);
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}

// --- END CodeEditor helpers

}
Expand Down
Loading

0 comments on commit 26a530f

Please sign in to comment.