Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GlyphMarginWidgets #184375

Merged
merged 8 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 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,47 @@ export interface IOverlayWidget {
getPosition(): IOverlayWidgetPosition | null;
}

/**
* A glyph margin widget renders in the editor glyph margin.
*/
export interface IGlyphMarginWidget {
/**
* The class name to apply to the glyph widget.
*/
className: string;
joyceerhl marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 +1034,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