UNPKG

monaco-editor-core

Version:

A browser based code editor

399 lines (398 loc) • 17.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createFastDomNode } from '../../../../base/browser/fastDomNode.js'; import { ArrayQueue } from '../../../../base/common/arrays.js'; import './glyphMargin.css'; import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js'; import { ViewPart } from '../../view/viewPart.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; import { GlyphMarginLane } from '../../../common/model.js'; /** * Represents a decoration that should be shown along the lines from `startLineNumber` to `endLineNumber`. * This can end up producing multiple `LineDecorationToRender`. */ export class DecorationToRender { constructor(startLineNumber, endLineNumber, className, tooltip, zIndex) { this.startLineNumber = startLineNumber; this.endLineNumber = endLineNumber; this.className = className; this.tooltip = tooltip; this._decorationToRenderBrand = undefined; this.zIndex = zIndex ?? 0; } } /** * A decoration that should be shown along a line. */ export class LineDecorationToRender { constructor(className, zIndex, tooltip) { this.className = className; this.zIndex = zIndex; this.tooltip = tooltip; } } /** * Decorations to render on a visible line. */ export class VisibleLineDecorationsToRender { constructor() { this.decorations = []; } add(decoration) { this.decorations.push(decoration); } getDecorations() { return this.decorations; } } export class DedupOverlay extends DynamicViewOverlay { /** * Returns an array with an element for each visible line number. */ _render(visibleStartLineNumber, visibleEndLineNumber, decorations) { const output = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; output[lineIndex] = new VisibleLineDecorationsToRender(); } if (decorations.length === 0) { return output; } // Sort decorations by className, then by startLineNumber and then by endLineNumber decorations.sort((a, b) => { if (a.className === b.className) { if (a.startLineNumber === b.startLineNumber) { return a.endLineNumber - b.endLineNumber; } return a.startLineNumber - b.startLineNumber; } return (a.className < b.className ? -1 : 1); }); let prevClassName = null; let prevEndLineIndex = 0; for (let i = 0, len = decorations.length; i < len; i++) { const d = decorations[i]; const className = d.className; const zIndex = d.zIndex; let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber; const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber; if (prevClassName === className) { // Here we avoid rendering the same className multiple times on the same line startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex); prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex); } else { prevClassName = className; prevEndLineIndex = endLineIndex; } for (let i = startLineIndex; i <= prevEndLineIndex; i++) { output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip)); } } return output; } } export class GlyphMarginWidgets extends ViewPart { constructor(context) { super(context); this._widgets = {}; this._context = context; const options = this._context.configuration.options; const layoutInfo = options.get(146 /* EditorOption.layoutInfo */); this.domNode = createFastDomNode(document.createElement('div')); this.domNode.setClassName('glyph-margin-widgets'); this.domNode.setPosition('absolute'); this.domNode.setTop(0); this._lineHeight = options.get(67 /* EditorOption.lineHeight */); this._glyphMargin = options.get(57 /* EditorOption.glyphMargin */); this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; this._managedDomNodes = []; this._decorationGlyphsToRender = []; } dispose() { this._managedDomNodes = []; this._decorationGlyphsToRender = []; this._widgets = {}; super.dispose(); } getWidgets() { return Object.values(this._widgets); } // --- begin event handlers onConfigurationChanged(e) { const options = this._context.configuration.options; const layoutInfo = options.get(146 /* EditorOption.layoutInfo */); this._lineHeight = options.get(67 /* EditorOption.lineHeight */); this._glyphMargin = options.get(57 /* EditorOption.glyphMargin */); this._glyphMarginLeft = layoutInfo.glyphMarginLeft; this._glyphMarginWidth = layoutInfo.glyphMarginWidth; this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount; return true; } onDecorationsChanged(e) { return true; } onFlushed(e) { return true; } onLinesChanged(e) { return true; } onLinesDeleted(e) { return true; } onLinesInserted(e) { return true; } onScrollChanged(e) { return e.scrollTopChanged; } onZonesChanged(e) { return true; } // --- end event handlers // --- begin widget management addWidget(widget) { const domNode = createFastDomNode(widget.getDomNode()); this._widgets[widget.getId()] = { widget: widget, preference: widget.getPosition(), domNode: domNode, renderInfo: null }; domNode.setPosition('absolute'); domNode.setDisplay('none'); domNode.setAttribute('widgetId', widget.getId()); this.domNode.appendChild(domNode); this.setShouldRender(); } setWidgetPosition(widget, preference) { const myWidget = this._widgets[widget.getId()]; if (myWidget.preference.lane === preference.lane && myWidget.preference.zIndex === preference.zIndex && Range.equalsRange(myWidget.preference.range, preference.range)) { return false; } myWidget.preference = preference; this.setShouldRender(); return true; } removeWidget(widget) { const widgetId = widget.getId(); if (this._widgets[widgetId]) { const widgetData = this._widgets[widgetId]; const domNode = widgetData.domNode.domNode; delete this._widgets[widgetId]; domNode.remove(); this.setShouldRender(); } } // --- end widget management _collectDecorationBasedGlyphRenderRequest(ctx, requests) { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const decorations = ctx.getDecorationsInViewport(); for (const d of decorations) { const glyphMarginClassName = d.options.glyphMarginClassName; if (!glyphMarginClassName) { continue; } const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber); const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber); const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center; const zIndex = d.options.zIndex ?? 0; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 0)); const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(lane); requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, laneIndex, zIndex, glyphMarginClassName)); } } } _collectWidgetBasedGlyphRenderRequest(ctx, requests) { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; for (const widget of Object.values(this._widgets)) { const range = widget.preference.range; const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range)); if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) { // The widget is not in the viewport continue; } // The widget is in the viewport, find a good line for it const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber); const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(widgetLineNumber, 0)); const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(widget.preference.lane); requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, laneIndex, widget.preference.zIndex, widget)); } } _collectSortedGlyphRenderRequests(ctx) { const requests = []; this._collectDecorationBasedGlyphRenderRequest(ctx, requests); this._collectWidgetBasedGlyphRenderRequest(ctx, requests); // sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC // don't change this sort unless you understand `prepareRender` below. requests.sort((a, b) => { if (a.lineNumber === b.lineNumber) { if (a.laneIndex === b.laneIndex) { if (a.zIndex === b.zIndex) { if (b.type === a.type) { if (a.type === 0 /* GlyphRenderRequestType.Decoration */ && b.type === 0 /* GlyphRenderRequestType.Decoration */) { return (a.className < b.className ? -1 : 1); } return 0; } return b.type - a.type; } return b.zIndex - a.zIndex; } return a.laneIndex - b.laneIndex; } return a.lineNumber - b.lineNumber; }); return requests; } /** * Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`. */ prepareRender(ctx) { if (!this._glyphMargin) { this._decorationGlyphsToRender = []; return; } for (const widget of Object.values(this._widgets)) { widget.renderInfo = null; } const requests = new ArrayQueue(this._collectSortedGlyphRenderRequests(ctx)); const decorationGlyphsToRender = []; while (requests.length > 0) { const first = requests.peek(); if (!first) { // not possible break; } // Requests are sorted by lineNumber and lane, so we read all requests for this particular location const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.laneIndex === first.laneIndex); if (!requestsAtLocation || requestsAtLocation.length === 0) { // not possible break; } const winner = requestsAtLocation[0]; if (winner.type === 0 /* GlyphRenderRequestType.Decoration */) { // combine all decorations with the same z-index const classNames = []; // requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one for (const request of requestsAtLocation) { if (request.zIndex !== winner.zIndex || request.type !== winner.type) { break; } if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) { classNames.push(request.className); } } decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations } else { // widgets cannot be combined winner.widget.renderInfo = { lineNumber: winner.lineNumber, laneIndex: winner.laneIndex, }; } } this._decorationGlyphsToRender = decorationGlyphsToRender; } render(ctx) { if (!this._glyphMargin) { for (const widget of Object.values(this._widgets)) { widget.domNode.setDisplay('none'); } while (this._managedDomNodes.length > 0) { const domNode = this._managedDomNodes.pop(); domNode?.domNode.remove(); } return; } const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount)); // Render widgets for (const widget of Object.values(this._widgets)) { if (!widget.renderInfo) { // this widget is not visible widget.domNode.setDisplay('none'); } else { const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber]; const left = this._glyphMarginLeft + widget.renderInfo.laneIndex * this._lineHeight; widget.domNode.setDisplay('block'); widget.domNode.setTop(top); widget.domNode.setLeft(left); widget.domNode.setWidth(width); widget.domNode.setHeight(this._lineHeight); } } // Render decorations, reusing previous dom nodes as possible for (let i = 0; i < this._decorationGlyphsToRender.length; i++) { const dec = this._decorationGlyphsToRender[i]; const top = ctx.viewportData.relativeVerticalOffset[dec.lineNumber - ctx.viewportData.startLineNumber]; const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight; let domNode; if (i < this._managedDomNodes.length) { domNode = this._managedDomNodes[i]; } else { domNode = createFastDomNode(document.createElement('div')); this._managedDomNodes.push(domNode); this.domNode.appendChild(domNode); } domNode.setClassName(`cgmr codicon ` + dec.combinedClassName); domNode.setPosition(`absolute`); domNode.setTop(top); domNode.setLeft(left); domNode.setWidth(width); domNode.setHeight(this._lineHeight); } // remove extra dom nodes while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) { const domNode = this._managedDomNodes.pop(); domNode?.domNode.remove(); } } } /** * A request to render a decoration in the glyph margin at a certain location. */ class DecorationBasedGlyphRenderRequest { constructor(lineNumber, laneIndex, zIndex, className) { this.lineNumber = lineNumber; this.laneIndex = laneIndex; this.zIndex = zIndex; this.className = className; this.type = 0 /* GlyphRenderRequestType.Decoration */; } accept(combinedClassName) { return new DecorationBasedGlyph(this.lineNumber, this.laneIndex, combinedClassName); } } /** * A request to render a widget in the glyph margin at a certain location. */ class WidgetBasedGlyphRenderRequest { constructor(lineNumber, laneIndex, zIndex, widget) { this.lineNumber = lineNumber; this.laneIndex = laneIndex; this.zIndex = zIndex; this.widget = widget; this.type = 1 /* GlyphRenderRequestType.Widget */; } } class DecorationBasedGlyph { constructor(lineNumber, laneIndex, combinedClassName) { this.lineNumber = lineNumber; this.laneIndex = laneIndex; this.combinedClassName = combinedClassName; } }