Path: blob/main/src/vs/editor/browser/viewParts/viewZones/viewZones.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { FastDomNode, createFastDomNode } from '../../../../base/browser/fastDomNode.js';6import { onUnexpectedError } from '../../../../base/common/errors.js';7import { IViewZone, IViewZoneChangeAccessor } from '../../editorBrowser.js';8import { ViewPart } from '../../view/viewPart.js';9import { Position } from '../../../common/core/position.js';10import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js';11import { ViewContext } from '../../../common/viewModel/viewContext.js';12import * as viewEvents from '../../../common/viewEvents.js';13import { IEditorWhitespace, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../../../common/viewModel.js';14import { EditorOption } from '../../../common/config/editorOptions.js';1516interface IMyViewZone {17whitespaceId: string;18delegate: IViewZone;19isInHiddenArea: boolean;20isVisible: boolean;21domNode: FastDomNode<HTMLElement>;22marginDomNode: FastDomNode<HTMLElement> | null;23}2425interface IComputedViewZoneProps {26isInHiddenArea: boolean;27afterViewLineNumber: number;28heightInPx: number;29minWidthInPx: number;30}3132const invalidFunc = () => { throw new Error(`Invalid change accessor`); };3334/**35* A view zone is a rectangle that is a section that is inserted into the editor36* lines that can be used for various purposes such as showing a diffs, peeking37* an implementation, etc.38*/39export class ViewZones extends ViewPart {4041private _zones: { [id: string]: IMyViewZone };42private _lineHeight: number;43private _contentWidth: number;44private _contentLeft: number;4546public domNode: FastDomNode<HTMLElement>;4748public marginDomNode: FastDomNode<HTMLElement>;4950constructor(context: ViewContext) {51super(context);52const options = this._context.configuration.options;53const layoutInfo = options.get(EditorOption.layoutInfo);5455this._lineHeight = options.get(EditorOption.lineHeight);56this._contentWidth = layoutInfo.contentWidth;57this._contentLeft = layoutInfo.contentLeft;5859this.domNode = createFastDomNode(document.createElement('div'));60this.domNode.setClassName('view-zones');61this.domNode.setPosition('absolute');62this.domNode.setAttribute('role', 'presentation');63this.domNode.setAttribute('aria-hidden', 'true');6465this.marginDomNode = createFastDomNode(document.createElement('div'));66this.marginDomNode.setClassName('margin-view-zones');67this.marginDomNode.setPosition('absolute');68this.marginDomNode.setAttribute('role', 'presentation');69this.marginDomNode.setAttribute('aria-hidden', 'true');7071this._zones = {};72}7374public override dispose(): void {75super.dispose();76this._zones = {};77}7879// ---- begin view event handlers8081private _recomputeWhitespacesProps(): boolean {82const whitespaces = this._context.viewLayout.getWhitespaces();83const oldWhitespaces = new Map<string, IEditorWhitespace>();84for (const whitespace of whitespaces) {85oldWhitespaces.set(whitespace.id, whitespace);86}87let hadAChange = false;88this._context.viewModel.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => {89const keys = Object.keys(this._zones);90for (let i = 0, len = keys.length; i < len; i++) {91const id = keys[i];92const zone = this._zones[id];93const props = this._computeWhitespaceProps(zone.delegate);94zone.isInHiddenArea = props.isInHiddenArea;95const oldWhitespace = oldWhitespaces.get(id);96if (oldWhitespace && (oldWhitespace.afterLineNumber !== props.afterViewLineNumber || oldWhitespace.height !== props.heightInPx)) {97whitespaceAccessor.changeOneWhitespace(id, props.afterViewLineNumber, props.heightInPx);98this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);99hadAChange = true;100}101}102});103return hadAChange;104}105106public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {107const options = this._context.configuration.options;108const layoutInfo = options.get(EditorOption.layoutInfo);109110this._lineHeight = options.get(EditorOption.lineHeight);111this._contentWidth = layoutInfo.contentWidth;112this._contentLeft = layoutInfo.contentLeft;113114if (e.hasChanged(EditorOption.lineHeight)) {115this._recomputeWhitespacesProps();116}117118return true;119}120121public override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean {122return this._recomputeWhitespacesProps();123}124125public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {126return true;127}128129public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {130return e.scrollTopChanged || e.scrollWidthChanged;131}132133public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {134return true;135}136137public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {138return true;139}140141// ---- end view event handlers142143private _getZoneOrdinal(zone: IViewZone): number {144return zone.ordinal ?? zone.afterColumn ?? 10000;145}146147private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps {148if (zone.afterLineNumber === 0) {149return {150isInHiddenArea: false,151afterViewLineNumber: 0,152heightInPx: this._heightInPixels(zone),153minWidthInPx: this._minWidthInPixels(zone)154};155}156157let zoneAfterModelPosition: Position;158if (typeof zone.afterColumn !== 'undefined') {159zoneAfterModelPosition = this._context.viewModel.model.validatePosition({160lineNumber: zone.afterLineNumber,161column: zone.afterColumn162});163} else {164const validAfterLineNumber = this._context.viewModel.model.validatePosition({165lineNumber: zone.afterLineNumber,166column: 1167}).lineNumber;168169zoneAfterModelPosition = new Position(170validAfterLineNumber,171this._context.viewModel.model.getLineMaxColumn(validAfterLineNumber)172);173}174175let zoneBeforeModelPosition: Position;176if (zoneAfterModelPosition.column === this._context.viewModel.model.getLineMaxColumn(zoneAfterModelPosition.lineNumber)) {177zoneBeforeModelPosition = this._context.viewModel.model.validatePosition({178lineNumber: zoneAfterModelPosition.lineNumber + 1,179column: 1180});181} else {182zoneBeforeModelPosition = this._context.viewModel.model.validatePosition({183lineNumber: zoneAfterModelPosition.lineNumber,184column: zoneAfterModelPosition.column + 1185});186}187188const viewPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(zoneAfterModelPosition, zone.afterColumnAffinity, true);189const isVisible = zone.showInHiddenAreas || this._context.viewModel.coordinatesConverter.modelPositionIsVisible(zoneBeforeModelPosition);190return {191isInHiddenArea: !isVisible,192afterViewLineNumber: viewPosition.lineNumber,193heightInPx: (isVisible ? this._heightInPixels(zone) : 0),194minWidthInPx: this._minWidthInPixels(zone)195};196}197198public changeViewZones(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean {199let zonesHaveChanged = false;200201this._context.viewModel.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => {202203const changeAccessor: IViewZoneChangeAccessor = {204addZone: (zone: IViewZone): string => {205zonesHaveChanged = true;206return this._addZone(whitespaceAccessor, zone);207},208removeZone: (id: string): void => {209if (!id) {210return;211}212zonesHaveChanged = this._removeZone(whitespaceAccessor, id) || zonesHaveChanged;213},214layoutZone: (id: string): void => {215if (!id) {216return;217}218zonesHaveChanged = this._layoutZone(whitespaceAccessor, id) || zonesHaveChanged;219}220};221222safeInvoke1Arg(callback, changeAccessor);223224// Invalidate changeAccessor225changeAccessor.addZone = invalidFunc;226changeAccessor.removeZone = invalidFunc;227changeAccessor.layoutZone = invalidFunc;228});229230return zonesHaveChanged;231}232233private _addZone(whitespaceAccessor: IWhitespaceChangeAccessor, zone: IViewZone): string {234const props = this._computeWhitespaceProps(zone);235const whitespaceId = whitespaceAccessor.insertWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx);236237const myZone: IMyViewZone = {238whitespaceId: whitespaceId,239delegate: zone,240isInHiddenArea: props.isInHiddenArea,241isVisible: false,242domNode: createFastDomNode(zone.domNode),243marginDomNode: zone.marginDomNode ? createFastDomNode(zone.marginDomNode) : null244};245246this._safeCallOnComputedHeight(myZone.delegate, props.heightInPx);247248myZone.domNode.setPosition('absolute');249myZone.domNode.domNode.style.width = '100%';250myZone.domNode.setDisplay('none');251myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId);252this.domNode.appendChild(myZone.domNode);253254if (myZone.marginDomNode) {255myZone.marginDomNode.setPosition('absolute');256myZone.marginDomNode.domNode.style.width = '100%';257myZone.marginDomNode.setDisplay('none');258myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId);259this.marginDomNode.appendChild(myZone.marginDomNode);260}261262this._zones[myZone.whitespaceId] = myZone;263264265this.setShouldRender();266267return myZone.whitespaceId;268}269270private _removeZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean {271if (this._zones.hasOwnProperty(id)) {272const zone = this._zones[id];273delete this._zones[id];274whitespaceAccessor.removeWhitespace(zone.whitespaceId);275276zone.domNode.removeAttribute('monaco-visible-view-zone');277zone.domNode.removeAttribute('monaco-view-zone');278zone.domNode.domNode.remove();279280if (zone.marginDomNode) {281zone.marginDomNode.removeAttribute('monaco-visible-view-zone');282zone.marginDomNode.removeAttribute('monaco-view-zone');283zone.marginDomNode.domNode.remove();284}285286this.setShouldRender();287288return true;289}290return false;291}292293private _layoutZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean {294if (this._zones.hasOwnProperty(id)) {295const zone = this._zones[id];296const props = this._computeWhitespaceProps(zone.delegate);297zone.isInHiddenArea = props.isInHiddenArea;298// const newOrdinal = this._getZoneOrdinal(zone.delegate);299whitespaceAccessor.changeOneWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx);300// TODO@Alex: change `newOrdinal` too301302this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);303this.setShouldRender();304305return true;306}307return false;308}309310public shouldSuppressMouseDownOnViewZone(id: string): boolean {311if (this._zones.hasOwnProperty(id)) {312const zone = this._zones[id];313return Boolean(zone.delegate.suppressMouseDown);314}315return false;316}317318private _heightInPixels(zone: IViewZone): number {319if (typeof zone.heightInPx === 'number') {320return zone.heightInPx;321}322if (typeof zone.heightInLines === 'number') {323return this._lineHeight * zone.heightInLines;324}325return this._lineHeight;326}327328private _minWidthInPixels(zone: IViewZone): number {329if (typeof zone.minWidthInPx === 'number') {330return zone.minWidthInPx;331}332return 0;333}334335private _safeCallOnComputedHeight(zone: IViewZone, height: number): void {336if (typeof zone.onComputedHeight === 'function') {337try {338zone.onComputedHeight(height);339} catch (e) {340onUnexpectedError(e);341}342}343}344345private _safeCallOnDomNodeTop(zone: IViewZone, top: number): void {346if (typeof zone.onDomNodeTop === 'function') {347try {348zone.onDomNodeTop(top);349} catch (e) {350onUnexpectedError(e);351}352}353}354355public prepareRender(ctx: RenderingContext): void {356// Nothing to read357}358359public render(ctx: RestrictedRenderingContext): void {360const visibleWhitespaces = ctx.viewportData.whitespaceViewportData;361const visibleZones: { [id: string]: IViewWhitespaceViewportData } = {};362363let hasVisibleZone = false;364for (const visibleWhitespace of visibleWhitespaces) {365if (this._zones[visibleWhitespace.id].isInHiddenArea) {366continue;367}368visibleZones[visibleWhitespace.id] = visibleWhitespace;369hasVisibleZone = true;370}371372const keys = Object.keys(this._zones);373for (let i = 0, len = keys.length; i < len; i++) {374const id = keys[i];375const zone = this._zones[id];376377let newTop = 0;378let newHeight = 0;379let newDisplay = 'none';380if (visibleZones.hasOwnProperty(id)) {381newTop = visibleZones[id].verticalOffset - ctx.bigNumbersDelta;382newHeight = visibleZones[id].height;383newDisplay = 'block';384// zone is visible385if (!zone.isVisible) {386zone.domNode.setAttribute('monaco-visible-view-zone', 'true');387zone.isVisible = true;388}389this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(visibleZones[id].verticalOffset));390} else {391if (zone.isVisible) {392zone.domNode.removeAttribute('monaco-visible-view-zone');393zone.isVisible = false;394}395this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(-1000000));396}397zone.domNode.setTop(newTop);398zone.domNode.setHeight(newHeight);399zone.domNode.setDisplay(newDisplay);400401if (zone.marginDomNode) {402zone.marginDomNode.setTop(newTop);403zone.marginDomNode.setHeight(newHeight);404zone.marginDomNode.setDisplay(newDisplay);405}406}407408if (hasVisibleZone) {409this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));410this.marginDomNode.setWidth(this._contentLeft);411}412}413}414415function safeInvoke1Arg(func: Function, arg1: any): any {416try {417return func(arg1);418} catch (e) {419onUnexpectedError(e);420}421}422423424