mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
431
src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts
Normal file
431
src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { ContentWidgetPositionPreference, IContentWidget } from 'vs/editor/browser/editorBrowser';
|
||||
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { Position, IPosition } from 'vs/editor/common/core/position';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
class Coordinate {
|
||||
_coordinateBrand: void;
|
||||
|
||||
public readonly top: number;
|
||||
public readonly left: number;
|
||||
|
||||
constructor(top: number, left: number) {
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewContentWidgets extends ViewPart {
|
||||
|
||||
private _viewDomNode: FastDomNode<HTMLElement>;
|
||||
private _widgets: { [key: string]: Widget; };
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
public overflowingContentWidgetsDomNode: FastDomNode<HTMLElement>;
|
||||
|
||||
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>) {
|
||||
super(context);
|
||||
this._viewDomNode = viewDomNode;
|
||||
this._widgets = {};
|
||||
|
||||
this.domNode = createFastDomNode(document.createElement('div'));
|
||||
PartFingerprints.write(this.domNode, PartFingerprint.ContentWidgets);
|
||||
this.domNode.setClassName('contentWidgets');
|
||||
this.domNode.setPosition('absolute');
|
||||
this.domNode.setTop(0);
|
||||
|
||||
this.overflowingContentWidgetsDomNode = createFastDomNode(document.createElement('div'));
|
||||
PartFingerprints.write(this.overflowingContentWidgetsDomNode, PartFingerprint.OverflowingContentWidgets);
|
||||
this.overflowingContentWidgetsDomNode.setClassName('overflowingContentWidgets');
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._widgets = null;
|
||||
this.domNode = null;
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
let keys = Object.keys(this._widgets);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
const widgetId = keys[i];
|
||||
this._widgets[widgetId].onConfigurationChanged(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
// true for inline decorations that can end up relayouting text
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
public addWidget(_widget: IContentWidget): void {
|
||||
const myWidget = new Widget(this._context, this._viewDomNode, _widget);
|
||||
this._widgets[myWidget.id] = myWidget;
|
||||
|
||||
if (myWidget.allowEditorOverflow) {
|
||||
this.overflowingContentWidgetsDomNode.appendChild(myWidget.domNode);
|
||||
} else {
|
||||
this.domNode.appendChild(myWidget.domNode);
|
||||
}
|
||||
|
||||
this.setShouldRender();
|
||||
}
|
||||
|
||||
public setWidgetPosition(widget: IContentWidget, position: IPosition, preference: ContentWidgetPositionPreference[]): void {
|
||||
const myWidget = this._widgets[widget.getId()];
|
||||
myWidget.setPosition(position, preference);
|
||||
|
||||
this.setShouldRender();
|
||||
}
|
||||
|
||||
public removeWidget(widget: IContentWidget): void {
|
||||
const widgetId = widget.getId();
|
||||
if (this._widgets.hasOwnProperty(widgetId)) {
|
||||
const myWidget = this._widgets[widgetId];
|
||||
delete this._widgets[widgetId];
|
||||
|
||||
const domNode = myWidget.domNode.domNode;
|
||||
domNode.parentNode.removeChild(domNode);
|
||||
domNode.removeAttribute('monaco-visible-content-widget');
|
||||
|
||||
this.setShouldRender();
|
||||
}
|
||||
}
|
||||
|
||||
public shouldSuppressMouseDownOnWidget(widgetId: string): boolean {
|
||||
if (this._widgets.hasOwnProperty(widgetId)) {
|
||||
return this._widgets[widgetId].suppressMouseDown;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
let keys = Object.keys(this._widgets);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
const widgetId = keys[i];
|
||||
this._widgets[widgetId].prepareRender(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
let keys = Object.keys(this._widgets);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
const widgetId = keys[i];
|
||||
this._widgets[widgetId].render(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IBoxLayoutResult {
|
||||
aboveTop: number;
|
||||
fitsAbove: boolean;
|
||||
belowTop: number;
|
||||
fitsBelow: boolean;
|
||||
left: number;
|
||||
}
|
||||
|
||||
class Widget {
|
||||
private readonly _context: ViewContext;
|
||||
private readonly _viewDomNode: FastDomNode<HTMLElement>;
|
||||
private readonly _actual: IContentWidget;
|
||||
|
||||
public readonly domNode: FastDomNode<HTMLElement>;
|
||||
public readonly id: string;
|
||||
public readonly allowEditorOverflow: boolean;
|
||||
public readonly suppressMouseDown: boolean;
|
||||
|
||||
private _fixedOverflowWidgets: boolean;
|
||||
private _contentWidth: number;
|
||||
private _contentLeft: number;
|
||||
private _lineHeight: number;
|
||||
|
||||
private _position: IPosition;
|
||||
private _preference: ContentWidgetPositionPreference[];
|
||||
private _isVisible: boolean;
|
||||
private _renderData: Coordinate;
|
||||
|
||||
constructor(context: ViewContext, viewDomNode: FastDomNode<HTMLElement>, actual: IContentWidget) {
|
||||
this._context = context;
|
||||
this._viewDomNode = viewDomNode;
|
||||
this._actual = actual;
|
||||
this.domNode = createFastDomNode(this._actual.getDomNode());
|
||||
|
||||
this.id = this._actual.getId();
|
||||
this.allowEditorOverflow = this._actual.allowEditorOverflow || false;
|
||||
this.suppressMouseDown = this._actual.suppressMouseDown || false;
|
||||
|
||||
this._fixedOverflowWidgets = this._context.configuration.editor.viewInfo.fixedOverflowWidgets;
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
|
||||
this._position = null;
|
||||
this._preference = null;
|
||||
this._isVisible = false;
|
||||
this._renderData = null;
|
||||
|
||||
this.domNode.setPosition((this._fixedOverflowWidgets && this.allowEditorOverflow) ? 'fixed' : 'absolute');
|
||||
this._updateMaxWidth();
|
||||
this.domNode.setVisibility('hidden');
|
||||
this.domNode.setAttribute('widgetId', this.id);
|
||||
}
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): void {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
|
||||
this._updateMaxWidth();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateMaxWidth(): void {
|
||||
const maxWidth = this.allowEditorOverflow
|
||||
? window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
: this._contentWidth;
|
||||
|
||||
this.domNode.setMaxWidth(maxWidth);
|
||||
}
|
||||
|
||||
public setPosition(position: IPosition, preference: ContentWidgetPositionPreference[]): void {
|
||||
this._position = position;
|
||||
this._preference = preference;
|
||||
}
|
||||
|
||||
private _layoutBoxInViewport(topLeft: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
|
||||
// Our visible box is split horizontally by the current line => 2 boxes
|
||||
|
||||
// a) the box above the line
|
||||
let aboveLineTop = topLeft.top;
|
||||
let heightAboveLine = aboveLineTop;
|
||||
|
||||
// b) the box under the line
|
||||
let underLineTop = topLeft.top + this._lineHeight;
|
||||
let heightUnderLine = ctx.viewportHeight - underLineTop;
|
||||
|
||||
let aboveTop = aboveLineTop - height;
|
||||
let fitsAbove = (heightAboveLine >= height);
|
||||
let belowTop = underLineTop;
|
||||
let fitsBelow = (heightUnderLine >= height);
|
||||
|
||||
// And its left
|
||||
let actualLeft = topLeft.left;
|
||||
if (actualLeft + width > ctx.scrollLeft + ctx.viewportWidth) {
|
||||
actualLeft = ctx.scrollLeft + ctx.viewportWidth - width;
|
||||
}
|
||||
if (actualLeft < ctx.scrollLeft) {
|
||||
actualLeft = ctx.scrollLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
aboveTop: aboveTop,
|
||||
fitsAbove: fitsAbove,
|
||||
belowTop: belowTop,
|
||||
fitsBelow: fitsBelow,
|
||||
left: actualLeft
|
||||
};
|
||||
}
|
||||
|
||||
private _layoutBoxInPage(topLeft: Coordinate, width: number, height: number, ctx: RenderingContext): IBoxLayoutResult {
|
||||
let left0 = topLeft.left - ctx.scrollLeft;
|
||||
|
||||
if (left0 + width < 0 || left0 > this._contentWidth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let aboveTop = topLeft.top - height;
|
||||
let belowTop = topLeft.top + this._lineHeight;
|
||||
let left = left0 + this._contentLeft;
|
||||
|
||||
let domNodePosition = dom.getDomNodePagePosition(this._viewDomNode.domNode);
|
||||
let absoluteAboveTop = domNodePosition.top + aboveTop - dom.StandardWindow.scrollY;
|
||||
let absoluteBelowTop = domNodePosition.top + belowTop - dom.StandardWindow.scrollY;
|
||||
let absoluteLeft = domNodePosition.left + left - dom.StandardWindow.scrollX;
|
||||
|
||||
let INNER_WIDTH = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
let INNER_HEIGHT = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
|
||||
|
||||
// Leave some clearance to the bottom
|
||||
let TOP_PADDING = 22;
|
||||
let BOTTOM_PADDING = 22;
|
||||
|
||||
let fitsAbove = (absoluteAboveTop >= TOP_PADDING),
|
||||
fitsBelow = (absoluteBelowTop + height <= INNER_HEIGHT - BOTTOM_PADDING);
|
||||
|
||||
if (absoluteLeft + width + 20 > INNER_WIDTH) {
|
||||
let delta = absoluteLeft - (INNER_WIDTH - width - 20);
|
||||
absoluteLeft -= delta;
|
||||
left -= delta;
|
||||
}
|
||||
if (absoluteLeft < 0) {
|
||||
let delta = absoluteLeft;
|
||||
absoluteLeft -= delta;
|
||||
left -= delta;
|
||||
}
|
||||
|
||||
if (this._fixedOverflowWidgets) {
|
||||
aboveTop = absoluteAboveTop;
|
||||
belowTop = absoluteBelowTop;
|
||||
left = absoluteLeft;
|
||||
}
|
||||
|
||||
return { aboveTop, fitsAbove, belowTop, fitsBelow, left };
|
||||
}
|
||||
|
||||
private _prepareRenderWidgetAtExactPositionOverflowing(topLeft: Coordinate): Coordinate {
|
||||
return new Coordinate(topLeft.top, topLeft.left + this._contentLeft);
|
||||
}
|
||||
|
||||
private _getTopLeft(ctx: RenderingContext, position: Position): Coordinate {
|
||||
const visibleRange = ctx.visibleRangeForPosition(position);
|
||||
if (!visibleRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop;
|
||||
return new Coordinate(top, visibleRange.left);
|
||||
}
|
||||
|
||||
private _prepareRenderWidget(ctx: RenderingContext): Coordinate {
|
||||
if (!this._position || !this._preference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not trust that widgets have a valid position
|
||||
let validModelPosition = this._context.model.validateModelPosition(this._position);
|
||||
|
||||
if (!this._context.model.coordinatesConverter.modelPositionIsVisible(validModelPosition)) {
|
||||
// this position is hidden by the view model
|
||||
return null;
|
||||
}
|
||||
|
||||
let position = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(validModelPosition);
|
||||
|
||||
let placement: IBoxLayoutResult = null;
|
||||
let fetchPlacement = (): void => {
|
||||
if (placement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topLeft = this._getTopLeft(ctx, position);
|
||||
if (!topLeft) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domNode = this.domNode.domNode;
|
||||
const width = domNode.clientWidth;
|
||||
const height = domNode.clientHeight;
|
||||
|
||||
if (this.allowEditorOverflow) {
|
||||
placement = this._layoutBoxInPage(topLeft, width, height, ctx);
|
||||
} else {
|
||||
placement = this._layoutBoxInViewport(topLeft, width, height, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
// Do two passes, first for perfect fit, second picks first option
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
for (let i = 0; i < this._preference.length; i++) {
|
||||
let pref = this._preference[i];
|
||||
if (pref === ContentWidgetPositionPreference.ABOVE) {
|
||||
fetchPlacement();
|
||||
if (!placement) {
|
||||
// Widget outside of viewport
|
||||
return null;
|
||||
}
|
||||
if (pass === 2 || placement.fitsAbove) {
|
||||
return new Coordinate(placement.aboveTop, placement.left);
|
||||
}
|
||||
} else if (pref === ContentWidgetPositionPreference.BELOW) {
|
||||
fetchPlacement();
|
||||
if (!placement) {
|
||||
// Widget outside of viewport
|
||||
return null;
|
||||
}
|
||||
if (pass === 2 || placement.fitsBelow) {
|
||||
return new Coordinate(placement.belowTop, placement.left);
|
||||
}
|
||||
} else {
|
||||
const topLeft = this._getTopLeft(ctx, position);
|
||||
if (!topLeft) {
|
||||
// Widget outside of viewport
|
||||
return null;
|
||||
}
|
||||
if (this.allowEditorOverflow) {
|
||||
return this._prepareRenderWidgetAtExactPositionOverflowing(topLeft);
|
||||
} else {
|
||||
return topLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
this._renderData = this._prepareRenderWidget(ctx);
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
if (!this._renderData) {
|
||||
// This widget should be invisible
|
||||
if (this._isVisible) {
|
||||
this.domNode.removeAttribute('monaco-visible-content-widget');
|
||||
this._isVisible = false;
|
||||
this.domNode.setVisibility('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This widget should be visible
|
||||
if (this.allowEditorOverflow) {
|
||||
this.domNode.setTop(this._renderData.top);
|
||||
this.domNode.setLeft(this._renderData.left);
|
||||
} else {
|
||||
this.domNode.setTop(this._renderData.top + ctx.scrollTop - ctx.bigNumbersDelta);
|
||||
this.domNode.setLeft(this._renderData.left);
|
||||
}
|
||||
|
||||
if (!this._isVisible) {
|
||||
this.domNode.setVisibility('inherit');
|
||||
this.domNode.setAttribute('monaco-visible-content-widget', 'true');
|
||||
this._isVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .view-overlays .current-line {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./currentLineHighlight';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
export class CurrentLineHighlightOverlay extends DynamicViewOverlay {
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _readOnly: boolean;
|
||||
private _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all';
|
||||
private _selectionIsEmpty: boolean;
|
||||
private _primaryCursorIsInEditableRange: boolean;
|
||||
private _primaryCursorLineNumber: number;
|
||||
private _scrollWidth: number;
|
||||
private _contentWidth: number;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._readOnly = this._context.configuration.editor.readOnly;
|
||||
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
|
||||
|
||||
this._selectionIsEmpty = true;
|
||||
this._primaryCursorIsInEditableRange = true;
|
||||
this._primaryCursorLineNumber = 1;
|
||||
this._scrollWidth = 0;
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.readOnly) {
|
||||
this._readOnly = this._context.configuration.editor.readOnly;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
let hasChanged = false;
|
||||
|
||||
if (this._primaryCursorIsInEditableRange !== e.isInEditableRange) {
|
||||
this._primaryCursorIsInEditableRange = e.isInEditableRange;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
const primaryCursorLineNumber = e.selections[0].positionLineNumber;
|
||||
if (this._primaryCursorLineNumber !== primaryCursorLineNumber) {
|
||||
this._primaryCursorLineNumber = primaryCursorLineNumber;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
const selectionIsEmpty = e.selections[0].isEmpty();
|
||||
if (this._selectionIsEmpty !== selectionIsEmpty) {
|
||||
this._selectionIsEmpty = selectionIsEmpty;
|
||||
hasChanged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollWidthChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
this._scrollWidth = ctx.scrollWidth;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (lineNumber === this._primaryCursorLineNumber) {
|
||||
if (this._shouldShowCurrentLine()) {
|
||||
return (
|
||||
'<div class="current-line" style="width:'
|
||||
+ String(Math.max(this._scrollWidth, this._contentWidth))
|
||||
+ 'px; height:'
|
||||
+ String(this._lineHeight)
|
||||
+ 'px;"></div>'
|
||||
);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private _shouldShowCurrentLine(): boolean {
|
||||
return (this._renderLineHighlight === 'line' || this._renderLineHighlight === 'all') &&
|
||||
this._selectionIsEmpty &&
|
||||
this._primaryCursorIsInEditableRange;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let lineHighlight = theme.getColor(editorLineHighlight);
|
||||
if (lineHighlight) {
|
||||
collector.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${lineHighlight}; }`);
|
||||
}
|
||||
if (!lineHighlight || lineHighlight.isTransparent() || theme.defines(editorLineHighlightBorder)) {
|
||||
let lineHighlightBorder = theme.getColor(editorLineHighlightBorder);
|
||||
if (lineHighlightBorder) {
|
||||
collector.addRule(`.monaco-editor .view-overlays .current-line { border: 2px solid ${lineHighlightBorder}; }`);
|
||||
if (theme.type === 'hc') {
|
||||
collector.addRule(`.monaco-editor .view-overlays .current-line { border-width: 1px; }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .margin-view-overlays .current-line-margin {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./currentLineMarginHighlight';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
export class CurrentLineMarginHighlightOverlay extends DynamicViewOverlay {
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _renderLineHighlight: 'none' | 'gutter' | 'line' | 'all';
|
||||
private _primaryCursorIsInEditableRange: boolean;
|
||||
private _primaryCursorLineNumber: number;
|
||||
private _contentLeft: number;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
|
||||
|
||||
this._primaryCursorIsInEditableRange = true;
|
||||
this._primaryCursorLineNumber = 1;
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._renderLineHighlight = this._context.configuration.editor.viewInfo.renderLineHighlight;
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
let hasChanged = false;
|
||||
|
||||
if (this._primaryCursorIsInEditableRange !== e.isInEditableRange) {
|
||||
this._primaryCursorIsInEditableRange = e.isInEditableRange;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
const primaryCursorLineNumber = e.selections[0].positionLineNumber;
|
||||
if (this._primaryCursorLineNumber !== primaryCursorLineNumber) {
|
||||
this._primaryCursorLineNumber = primaryCursorLineNumber;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (lineNumber === this._primaryCursorLineNumber) {
|
||||
if (this._shouldShowCurrentLine()) {
|
||||
return (
|
||||
'<div class="current-line-margin" style="width:'
|
||||
+ String(this._contentLeft)
|
||||
+ 'px; height:'
|
||||
+ String(this._lineHeight)
|
||||
+ 'px;"></div>'
|
||||
);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private _shouldShowCurrentLine(): boolean {
|
||||
return (this._renderLineHighlight === 'gutter' || this._renderLineHighlight === 'all') && this._primaryCursorIsInEditableRange;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let lineHighlight = theme.getColor(editorLineHighlight);
|
||||
if (lineHighlight) {
|
||||
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`);
|
||||
} else {
|
||||
let lineHighlightBorder = theme.getColor(editorLineHighlightBorder);
|
||||
if (lineHighlightBorder) {
|
||||
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { border: 2px solid ${lineHighlightBorder}; }`);
|
||||
}
|
||||
if (theme.type === 'hc') {
|
||||
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { border-width: 1px; }`);
|
||||
}
|
||||
}
|
||||
});
|
||||
12
src/vs/editor/browser/viewParts/decorations/decorations.css
Normal file
12
src/vs/editor/browser/viewParts/decorations/decorations.css
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cdr = core decorations rendering (div)
|
||||
*/
|
||||
.monaco-editor .lines-content .cdr {
|
||||
position: absolute;
|
||||
}
|
||||
209
src/vs/editor/browser/viewParts/decorations/decorations.ts
Normal file
209
src/vs/editor/browser/viewParts/decorations/decorations.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./decorations';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, HorizontalRange } from 'vs/editor/common/view/renderingContext';
|
||||
import { ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export class DecorationsOverlay extends DynamicViewOverlay {
|
||||
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _typicalHalfwidthCharacterWidth: number;
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
this._renderResult = null;
|
||||
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.fontInfo) {
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged || e.scrollWidthChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
let _decorations = ctx.getDecorationsInViewport();
|
||||
|
||||
// Keep only decorations with `className`
|
||||
let decorations: ViewModelDecoration[] = [], decorationsLen = 0;
|
||||
for (let i = 0, len = _decorations.length; i < len; i++) {
|
||||
let d = _decorations[i];
|
||||
if (d.source.options.className) {
|
||||
decorations[decorationsLen++] = d;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort decorations for consistent render output
|
||||
decorations = decorations.sort((a, b) => {
|
||||
let aClassName = a.source.options.className;
|
||||
let bClassName = b.source.options.className;
|
||||
|
||||
if (aClassName < bClassName) {
|
||||
return -1;
|
||||
}
|
||||
if (aClassName > bClassName) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Range.compareRangesUsingStarts(a.range, b.range);
|
||||
});
|
||||
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
output[lineIndex] = '';
|
||||
}
|
||||
|
||||
// Render first whole line decorations and then regular decorations
|
||||
this._renderWholeLineDecorations(ctx, decorations, output);
|
||||
this._renderNormalDecorations(ctx, decorations, output);
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
private _renderWholeLineDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void {
|
||||
let lineHeight = String(this._lineHeight);
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
|
||||
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
|
||||
let d = decorations[i];
|
||||
|
||||
if (!d.source.options.isWholeLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let decorationOutput = (
|
||||
'<div class="cdr '
|
||||
+ d.source.options.className
|
||||
+ '" style="left:0;width:100%;height:'
|
||||
+ lineHeight
|
||||
+ 'px;"></div>'
|
||||
);
|
||||
|
||||
let startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);
|
||||
let endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);
|
||||
for (let j = startLineNumber; j <= endLineNumber; j++) {
|
||||
let lineIndex = j - visibleStartLineNumber;
|
||||
output[lineIndex] += decorationOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _renderNormalDecorations(ctx: RenderingContext, decorations: ViewModelDecoration[], output: string[]): void {
|
||||
let lineHeight = String(this._lineHeight);
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
|
||||
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
|
||||
const d = decorations[i];
|
||||
|
||||
if (d.source.options.isWholeLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const className = d.source.options.className;
|
||||
const showIfCollapsed = d.source.options.showIfCollapsed;
|
||||
|
||||
let range = d.range;
|
||||
if (showIfCollapsed && range.endColumn === 1 && range.endLineNumber !== range.startLineNumber) {
|
||||
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber - 1, this._context.model.getLineMaxColumn(range.endLineNumber - 1));
|
||||
}
|
||||
|
||||
let linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/className === 'findMatch');
|
||||
if (!linesVisibleRanges) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0, lenJ = linesVisibleRanges.length; j < lenJ; j++) {
|
||||
let lineVisibleRanges = linesVisibleRanges[j];
|
||||
const lineIndex = lineVisibleRanges.lineNumber - visibleStartLineNumber;
|
||||
|
||||
if (showIfCollapsed && lineVisibleRanges.ranges.length === 1) {
|
||||
const singleVisibleRange = lineVisibleRanges.ranges[0];
|
||||
if (singleVisibleRange.width === 0) {
|
||||
// collapsed range case => make the decoration visible by faking its width
|
||||
lineVisibleRanges.ranges[0] = new HorizontalRange(singleVisibleRange.left, this._typicalHalfwidthCharacterWidth);
|
||||
}
|
||||
}
|
||||
|
||||
for (let k = 0, lenK = lineVisibleRanges.ranges.length; k < lenK; k++) {
|
||||
const visibleRange = lineVisibleRanges.ranges[k];
|
||||
const decorationOutput = (
|
||||
'<div class="cdr '
|
||||
+ className
|
||||
+ '" style="left:'
|
||||
+ String(visibleRange.left)
|
||||
+ 'px;width:'
|
||||
+ String(visibleRange.width)
|
||||
+ 'px;height:'
|
||||
+ lineHeight
|
||||
+ 'px;"></div>'
|
||||
);
|
||||
output[lineIndex] += decorationOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
let lineIndex = lineNumber - startLineNumber;
|
||||
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
|
||||
throw new Error('Unexpected render request');
|
||||
}
|
||||
return this._renderResult[lineIndex];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ScrollableElementCreationOptions, ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { IOverviewRulerLayoutInfo, SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { INewScrollPosition } from 'vs/editor/common/editorCommon';
|
||||
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { getThemeTypeSelector } from 'vs/platform/theme/common/themeService';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { ISimplifiedMouseEvent } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
|
||||
|
||||
export class EditorScrollbar extends ViewPart {
|
||||
|
||||
private scrollbar: SmoothScrollableElement;
|
||||
private scrollbarDomNode: FastDomNode<HTMLElement>;
|
||||
|
||||
constructor(
|
||||
context: ViewContext,
|
||||
linesContent: FastDomNode<HTMLElement>,
|
||||
viewDomNode: FastDomNode<HTMLElement>,
|
||||
overflowGuardDomNode: FastDomNode<HTMLElement>
|
||||
) {
|
||||
super(context);
|
||||
|
||||
const editor = this._context.configuration.editor;
|
||||
const configScrollbarOpts = editor.viewInfo.scrollbar;
|
||||
|
||||
let scrollbarOptions: ScrollableElementCreationOptions = {
|
||||
listenOnDomNode: viewDomNode.domNode,
|
||||
className: 'editor-scrollable' + ' ' + getThemeTypeSelector(context.theme.type),
|
||||
useShadows: false,
|
||||
lazyRender: true,
|
||||
|
||||
vertical: configScrollbarOpts.vertical,
|
||||
horizontal: configScrollbarOpts.horizontal,
|
||||
verticalHasArrows: configScrollbarOpts.verticalHasArrows,
|
||||
horizontalHasArrows: configScrollbarOpts.horizontalHasArrows,
|
||||
verticalScrollbarSize: configScrollbarOpts.verticalScrollbarSize,
|
||||
verticalSliderSize: configScrollbarOpts.verticalSliderSize,
|
||||
horizontalScrollbarSize: configScrollbarOpts.horizontalScrollbarSize,
|
||||
horizontalSliderSize: configScrollbarOpts.horizontalSliderSize,
|
||||
handleMouseWheel: configScrollbarOpts.handleMouseWheel,
|
||||
arrowSize: configScrollbarOpts.arrowSize,
|
||||
mouseWheelScrollSensitivity: configScrollbarOpts.mouseWheelScrollSensitivity,
|
||||
};
|
||||
|
||||
this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.scrollable));
|
||||
PartFingerprints.write(this.scrollbar.getDomNode(), PartFingerprint.ScrollableElement);
|
||||
|
||||
this.scrollbarDomNode = createFastDomNode(this.scrollbar.getDomNode());
|
||||
this.scrollbarDomNode.setPosition('absolute');
|
||||
this._setLayout();
|
||||
|
||||
// When having a zone widget that calls .focus() on one of its dom elements,
|
||||
// the browser will try desperately to reveal that dom node, unexpectedly
|
||||
// changing the .scrollTop of this.linesContent
|
||||
|
||||
let onBrowserDesperateReveal = (domNode: HTMLElement, lookAtScrollTop: boolean, lookAtScrollLeft: boolean) => {
|
||||
let newScrollPosition: INewScrollPosition = {};
|
||||
|
||||
if (lookAtScrollTop) {
|
||||
let deltaTop = domNode.scrollTop;
|
||||
if (deltaTop) {
|
||||
newScrollPosition.scrollTop = this._context.viewLayout.getCurrentScrollTop() + deltaTop;
|
||||
domNode.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (lookAtScrollLeft) {
|
||||
let deltaLeft = domNode.scrollLeft;
|
||||
if (deltaLeft) {
|
||||
newScrollPosition.scrollLeft = this._context.viewLayout.getCurrentScrollLeft() + deltaLeft;
|
||||
domNode.scrollLeft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this._context.viewLayout.setScrollPositionNow(newScrollPosition);
|
||||
};
|
||||
|
||||
// I've seen this happen both on the view dom node & on the lines content dom node.
|
||||
this._register(dom.addDisposableListener(viewDomNode.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(viewDomNode.domNode, true, true)));
|
||||
this._register(dom.addDisposableListener(linesContent.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(linesContent.domNode, true, false)));
|
||||
this._register(dom.addDisposableListener(overflowGuardDomNode.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(overflowGuardDomNode.domNode, true, false)));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _setLayout(): void {
|
||||
const layoutInfo = this._context.configuration.editor.layoutInfo;
|
||||
|
||||
this.scrollbarDomNode.setLeft(layoutInfo.contentLeft);
|
||||
this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimapWidth);
|
||||
this.scrollbarDomNode.setHeight(layoutInfo.contentHeight);
|
||||
}
|
||||
|
||||
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
|
||||
return this.scrollbar.getOverviewRulerLayoutInfo();
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this.scrollbarDomNode;
|
||||
}
|
||||
|
||||
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
|
||||
this.scrollbar.delegateVerticalScrollbarMouseDown(browserEvent);
|
||||
}
|
||||
|
||||
public delegateSliderMouseDown(e: ISimplifiedMouseEvent, onDragFinished: () => void): void {
|
||||
this.scrollbar.delegateSliderMouseDown(e, onDragFinished);
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.viewInfo) {
|
||||
const editor = this._context.configuration.editor;
|
||||
let newOpts: ScrollableElementChangeOptions = {
|
||||
handleMouseWheel: editor.viewInfo.scrollbar.handleMouseWheel,
|
||||
mouseWheelScrollSensitivity: editor.viewInfo.scrollbar.mouseWheelScrollSensitivity
|
||||
};
|
||||
this.scrollbar.updateOptions(newOpts);
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
this._setLayout();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
|
||||
this.scrollbar.updateClassName('editor-scrollable' + ' ' + getThemeTypeSelector(this._context.theme.type));
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
this.scrollbar.renderNow();
|
||||
}
|
||||
}
|
||||
17
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css
Normal file
17
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .glyph-margin {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cgmr = core glyph margin rendering (div)
|
||||
*/
|
||||
.monaco-editor .margin-view-overlays .cgmr {
|
||||
position: absolute;
|
||||
}
|
||||
200
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts
Normal file
200
src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./glyphMargin';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export class DecorationToRender {
|
||||
_decorationToRenderBrand: void;
|
||||
|
||||
public startLineNumber: number;
|
||||
public endLineNumber: number;
|
||||
public className: string;
|
||||
|
||||
constructor(startLineNumber: number, endLineNumber: number, className: string) {
|
||||
this.startLineNumber = +startLineNumber;
|
||||
this.endLineNumber = +endLineNumber;
|
||||
this.className = String(className);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class DedupOverlay extends DynamicViewOverlay {
|
||||
|
||||
protected _render(visibleStartLineNumber: number, visibleEndLineNumber: number, decorations: DecorationToRender[]): string[][] {
|
||||
|
||||
let output: string[][] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
output[lineIndex] = [];
|
||||
}
|
||||
|
||||
if (decorations.length === 0) {
|
||||
return output;
|
||||
}
|
||||
|
||||
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: string = null;
|
||||
let prevEndLineIndex = 0;
|
||||
for (let i = 0, len = decorations.length; i < len; i++) {
|
||||
let d = decorations[i];
|
||||
let className = d.className;
|
||||
let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;
|
||||
let endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;
|
||||
|
||||
if (prevClassName === className) {
|
||||
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].push(prevClassName);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export class GlyphMarginOverlay extends DedupOverlay {
|
||||
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _glyphMargin: boolean;
|
||||
private _glyphMarginLeft: number;
|
||||
private _glyphMarginWidth: number;
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._glyphMargin = this._context.configuration.editor.viewInfo.glyphMargin;
|
||||
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
|
||||
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
|
||||
this._renderResult = null;
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._glyphMargin = this._context.configuration.editor.viewInfo.glyphMargin;
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
|
||||
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
|
||||
let decorations = ctx.getDecorationsInViewport();
|
||||
let r: DecorationToRender[] = [], rLen = 0;
|
||||
for (let i = 0, len = decorations.length; i < len; i++) {
|
||||
let d = decorations[i];
|
||||
let glyphMarginClassName = d.source.options.glyphMarginClassName;
|
||||
if (glyphMarginClassName) {
|
||||
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
if (!this._glyphMargin) {
|
||||
this._renderResult = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
|
||||
|
||||
let lineHeight = this._lineHeight.toString();
|
||||
let left = this._glyphMarginLeft.toString();
|
||||
let width = this._glyphMarginWidth.toString();
|
||||
let common = '" style="left:' + left + 'px;width:' + width + 'px' + ';height:' + lineHeight + 'px;"></div>';
|
||||
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
let classNames = toRender[lineIndex];
|
||||
|
||||
if (classNames.length === 0) {
|
||||
output[lineIndex] = '';
|
||||
} else {
|
||||
output[lineIndex] = (
|
||||
'<div class="cgmr '
|
||||
+ classNames.join(' ')
|
||||
+ common
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
let lineIndex = lineNumber - startLineNumber;
|
||||
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
|
||||
throw new Error('Unexpected render request');
|
||||
}
|
||||
return this._renderResult[lineIndex];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cigr = core ident guides rendering (div)
|
||||
*/
|
||||
.monaco-editor .lines-content .cigr {
|
||||
position: absolute;
|
||||
}
|
||||
131
src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts
Normal file
131
src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./indentGuides';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorIndentGuides } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export class IndentGuidesOverlay extends DynamicViewOverlay {
|
||||
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _spaceWidth: number;
|
||||
private _renderResult: string[];
|
||||
private _enabled: boolean;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
|
||||
this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides;
|
||||
this._renderResult = null;
|
||||
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.fontInfo) {
|
||||
this._spaceWidth = this._context.configuration.editor.fontInfo.spaceWidth;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._enabled = this._context.configuration.editor.viewInfo.renderIndentGuides;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
// true for inline decorations
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;// || e.scrollWidthChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
if (!this._enabled) {
|
||||
this._renderResult = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
const tabSize = this._context.model.getTabSize();
|
||||
const tabWidth = tabSize * this._spaceWidth;
|
||||
const lineHeight = this._lineHeight;
|
||||
const indentGuideWidth = dom.computeScreenAwareSize(1);
|
||||
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
let indent = this._context.model.getLineIndentGuide(lineNumber);
|
||||
|
||||
let result = '';
|
||||
let leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1));
|
||||
let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0;
|
||||
for (let i = 0; i < indent; i++) {
|
||||
result += `<div class="cigr" style="left:${left}px;height:${lineHeight}px;width:${indentGuideWidth}px"></div>`;
|
||||
left += tabWidth;
|
||||
}
|
||||
|
||||
output[lineIndex] = result;
|
||||
}
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
let lineIndex = lineNumber - startLineNumber;
|
||||
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
|
||||
throw new Error('Unexpected render request');
|
||||
}
|
||||
return this._renderResult[lineIndex];
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let editorGuideColor = theme.getColor(editorIndentGuides);
|
||||
if (editorGuideColor) {
|
||||
collector.addRule(`.monaco-editor .lines-content .cigr { background-color: ${editorGuideColor}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="30" height="42" viewBox="0 0 30 42" style="enable-background:new 0 0 30 42;"><polygon style="fill:#FFFFFF;stroke:#000000;stroke-width:2;" points="29,2.4 3.8,27.6 14.3,27.6 9,38.1 15.4,40.2 20.6,29.7 29,36"/></svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="36" viewBox="0 0 24 36.1"><defs><style>.a{fill:#fff;}</style></defs><title>flipped-cursor-mac-2x</title><polygon points="8.6 33.1 11.8 23.9 2.2 23.9 23 2.5 23 31.3 17.4 26.1 14.2 35.1 8.6 33.1"/><path class="a" d="M22,29.1l-5-4.6-3.062,8.938-4.062-1.5L13,23H5L22,5M0,25H10.4l-3,8.3L15,36.1l3.125-7.662L24,33V0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="18" viewBox="0 0 12 18"><style>.st0{fill:#fff}</style><title>flipped-cursor-mac</title><path d="M4.3 16.5l1.6-4.6H1.1L11.5 1.2v14.4L8.7 13l-1.6 4.5z"/><path class="st0" d="M11 14.5l-2.5-2.3L7 16.7 5 16l1.6-4.5h-4l8.5-9M0 12.5h5.2l-1.5 4.1L7.5 18 9 14.2l2.9 2.3V0L0 12.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="21" x="0px" y="0px" viewBox="0 0 15 21" style="enable-background:new 0 0 15 21;"><polygon style="fill:#FFFFFF;stroke:#000000" points="14.5,1.2 1.9,13.8 7.1,13.8 4.5,19.1 7.7,20.1 10.3,14.9 14.5,18"/></svg>
|
||||
|
After Width: | Height: | Size: 264 B |
38
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css
Normal file
38
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .margin-view-overlays .line-numbers {
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .relative-current-line-number {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays .line-numbers {
|
||||
cursor: -webkit-image-set(
|
||||
url('flipped-cursor.svg') 1x,
|
||||
url('flipped-cursor-2x.svg') 2x
|
||||
) 30 0, default;
|
||||
}
|
||||
|
||||
.monaco-editor.mac .margin-view-overlays .line-numbers {
|
||||
cursor: -webkit-image-set(
|
||||
url('flipped-cursor-mac.svg') 1x,
|
||||
url('flipped-cursor-mac-2x.svg') 2x
|
||||
) 24 3, default;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays .line-numbers.lh-odd {
|
||||
margin-top: 1px;
|
||||
}
|
||||
169
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
Normal file
169
src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./lineNumbers';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export class LineNumbersOverlay extends DynamicViewOverlay {
|
||||
|
||||
public static CLASS_NAME = 'line-numbers';
|
||||
|
||||
private _context: ViewContext;
|
||||
|
||||
private _lineHeight: number;
|
||||
private _renderLineNumbers: boolean;
|
||||
private _renderCustomLineNumbers: (lineNumber: number) => string;
|
||||
private _renderRelativeLineNumbers: boolean;
|
||||
private _lineNumbersLeft: number;
|
||||
private _lineNumbersWidth: number;
|
||||
|
||||
private _lastCursorModelPosition: Position;
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
|
||||
this._readConfig();
|
||||
|
||||
this._lastCursorModelPosition = new Position(1, 1);
|
||||
this._renderResult = null;
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
private _readConfig(): void {
|
||||
const config = this._context.configuration.editor;
|
||||
this._lineHeight = config.lineHeight;
|
||||
this._renderLineNumbers = config.viewInfo.renderLineNumbers;
|
||||
this._renderCustomLineNumbers = config.viewInfo.renderCustomLineNumbers;
|
||||
this._renderRelativeLineNumbers = config.viewInfo.renderRelativeLineNumbers;
|
||||
this._lineNumbersLeft = config.layoutInfo.lineNumbersLeft;
|
||||
this._lineNumbersWidth = config.layoutInfo.lineNumbersWidth;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
this._readConfig();
|
||||
return true;
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
const primaryViewPosition = e.selections[0].getPosition();
|
||||
this._lastCursorModelPosition = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(primaryViewPosition);
|
||||
|
||||
if (this._renderRelativeLineNumbers) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
private _getLineRenderLineNumber(viewLineNumber: number): string {
|
||||
const modelPosition = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(viewLineNumber, 1));
|
||||
if (modelPosition.column !== 1) {
|
||||
return '';
|
||||
}
|
||||
let modelLineNumber = modelPosition.lineNumber;
|
||||
|
||||
if (this._renderCustomLineNumbers) {
|
||||
return this._renderCustomLineNumbers(modelLineNumber);
|
||||
}
|
||||
|
||||
if (this._renderRelativeLineNumbers) {
|
||||
let diff = Math.abs(this._lastCursorModelPosition.lineNumber - modelLineNumber);
|
||||
if (diff === 0) {
|
||||
return '<span class="relative-current-line-number">' + modelLineNumber + '</span>';
|
||||
}
|
||||
return String(diff);
|
||||
}
|
||||
|
||||
return String(modelLineNumber);
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
if (!this._renderLineNumbers) {
|
||||
this._renderResult = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let lineHeightClassName = (platform.isLinux ? (this._lineHeight % 2 === 0 ? ' lh-even' : ' lh-odd') : '');
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
let common = '<div class="' + LineNumbersOverlay.CLASS_NAME + lineHeightClassName + '" style="left:' + this._lineNumbersLeft.toString() + 'px;width:' + this._lineNumbersWidth.toString() + 'px;">';
|
||||
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
|
||||
let renderLineNumber = this._getLineRenderLineNumber(lineNumber);
|
||||
if (renderLineNumber) {
|
||||
output[lineIndex] = (
|
||||
common
|
||||
+ renderLineNumber
|
||||
+ '</div>'
|
||||
);
|
||||
} else {
|
||||
output[lineIndex] = '';
|
||||
}
|
||||
}
|
||||
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
let lineIndex = lineNumber - startLineNumber;
|
||||
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
|
||||
throw new Error('Unexpected render request');
|
||||
}
|
||||
return this._renderResult[lineIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let lineNumbers = theme.getColor(editorLineNumbers);
|
||||
if (lineNumbers) {
|
||||
collector.addRule(`.monaco-editor .line-numbers { color: ${lineNumbers}; }`);
|
||||
}
|
||||
});
|
||||
146
src/vs/editor/browser/viewParts/lines/rangeUtil.ts
Normal file
146
src/vs/editor/browser/viewParts/lines/rangeUtil.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
|
||||
|
||||
class FloatHorizontalRange {
|
||||
_floatHorizontalRangeBrand: void;
|
||||
|
||||
public readonly left: number;
|
||||
public readonly width: number;
|
||||
|
||||
constructor(left: number, width: number) {
|
||||
this.left = left;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[${this.left},${this.width}]`;
|
||||
}
|
||||
|
||||
public static compare(a: FloatHorizontalRange, b: FloatHorizontalRange): number {
|
||||
return a.left - b.left;
|
||||
}
|
||||
}
|
||||
|
||||
export class RangeUtil {
|
||||
|
||||
/**
|
||||
* Reusing the same range here
|
||||
* because IE is buggy and constantly freezes when using a large number
|
||||
* of ranges and calling .detach on them
|
||||
*/
|
||||
private static _handyReadyRange: Range;
|
||||
|
||||
private static _createRange(): Range {
|
||||
if (!this._handyReadyRange) {
|
||||
this._handyReadyRange = document.createRange();
|
||||
}
|
||||
return this._handyReadyRange;
|
||||
}
|
||||
|
||||
private static _detachRange(range: Range, endNode: HTMLElement): void {
|
||||
// Move range out of the span node, IE doesn't like having many ranges in
|
||||
// the same spot and will act badly for lines containing dashes ('-')
|
||||
range.selectNodeContents(endNode);
|
||||
}
|
||||
|
||||
private static _readClientRects(startElement: Node, startOffset: number, endElement: Node, endOffset: number, endNode: HTMLElement): ClientRectList {
|
||||
let range = this._createRange();
|
||||
try {
|
||||
range.setStart(startElement, startOffset);
|
||||
range.setEnd(endElement, endOffset);
|
||||
|
||||
return range.getClientRects();
|
||||
} catch (e) {
|
||||
// This is life ...
|
||||
return null;
|
||||
} finally {
|
||||
this._detachRange(range, endNode);
|
||||
}
|
||||
}
|
||||
|
||||
private static _mergeAdjacentRanges(ranges: FloatHorizontalRange[]): HorizontalRange[] {
|
||||
if (ranges.length === 1) {
|
||||
// There is nothing to merge
|
||||
return [new HorizontalRange(ranges[0].left, ranges[0].width)];
|
||||
}
|
||||
|
||||
ranges.sort(FloatHorizontalRange.compare);
|
||||
|
||||
let result: HorizontalRange[] = [], resultLen = 0;
|
||||
let prevLeft = ranges[0].left;
|
||||
let prevWidth = ranges[0].width;
|
||||
|
||||
for (let i = 1, len = ranges.length; i < len; i++) {
|
||||
const range = ranges[i];
|
||||
const myLeft = range.left;
|
||||
const myWidth = range.width;
|
||||
|
||||
if (prevLeft + prevWidth + 0.9 /* account for browser's rounding errors*/ >= myLeft) {
|
||||
prevWidth = Math.max(prevWidth, myLeft + myWidth - prevLeft);
|
||||
} else {
|
||||
result[resultLen++] = new HorizontalRange(prevLeft, prevWidth);
|
||||
prevLeft = myLeft;
|
||||
prevWidth = myWidth;
|
||||
}
|
||||
}
|
||||
|
||||
result[resultLen++] = new HorizontalRange(prevLeft, prevWidth);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _createHorizontalRangesFromClientRects(clientRects: ClientRectList, clientRectDeltaLeft: number): HorizontalRange[] {
|
||||
if (!clientRects || clientRects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We go through FloatHorizontalRange because it has been observed in bi-di text
|
||||
// that the clientRects are not coming in sorted from the browser
|
||||
|
||||
let result: FloatHorizontalRange[] = [];
|
||||
for (let i = 0, len = clientRects.length; i < len; i++) {
|
||||
const clientRect = clientRects[i];
|
||||
result[i] = new FloatHorizontalRange(Math.max(0, clientRect.left - clientRectDeltaLeft), clientRect.width);
|
||||
}
|
||||
|
||||
return this._mergeAdjacentRanges(result);
|
||||
}
|
||||
|
||||
public static readHorizontalRanges(domNode: HTMLElement, startChildIndex: number, startOffset: number, endChildIndex: number, endOffset: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] {
|
||||
// Panic check
|
||||
let min = 0;
|
||||
let max = domNode.children.length - 1;
|
||||
if (min > max) {
|
||||
return null;
|
||||
}
|
||||
startChildIndex = Math.min(max, Math.max(min, startChildIndex));
|
||||
endChildIndex = Math.min(max, Math.max(min, endChildIndex));
|
||||
|
||||
// If crossing over to a span only to select offset 0, then use the previous span's maximum offset
|
||||
// Chrome is buggy and doesn't handle 0 offsets well sometimes.
|
||||
if (startChildIndex !== endChildIndex) {
|
||||
if (endChildIndex > 0 && endOffset === 0) {
|
||||
endChildIndex--;
|
||||
endOffset = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
let startElement = domNode.children[startChildIndex].firstChild;
|
||||
let endElement = domNode.children[endChildIndex].firstChild;
|
||||
|
||||
if (!startElement || !endElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
startOffset = Math.min(startElement.textContent.length, Math.max(0, startOffset));
|
||||
endOffset = Math.min(endElement.textContent.length, Math.max(0, endOffset));
|
||||
|
||||
let clientRects = this._readClientRects(startElement, startOffset, endElement, endOffset, endNode);
|
||||
return this._createHorizontalRangesFromClientRects(clientRects, clientRectDeltaLeft);
|
||||
}
|
||||
}
|
||||
613
src/vs/editor/browser/viewParts/lines/viewLine.ts
Normal file
613
src/vs/editor/browser/viewParts/lines/viewLine.ts
Normal file
@@ -0,0 +1,613 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { IConfiguration } from 'vs/editor/common/editorCommon';
|
||||
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
|
||||
import { renderViewLine, RenderLineInput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer';
|
||||
import { IVisibleLine } from 'vs/editor/browser/view/viewLayer';
|
||||
import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil';
|
||||
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
|
||||
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
|
||||
import { ThemeType, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
|
||||
import { IStringBuilder } from 'vs/editor/common/core/stringBuilder';
|
||||
|
||||
const canUseFastRenderedViewLine = (function () {
|
||||
if (platform.isNative) {
|
||||
// In VSCode we know very well when the zoom level changes
|
||||
return true;
|
||||
}
|
||||
|
||||
if (platform.isLinux || browser.isFirefox || browser.isSafari) {
|
||||
// On Linux, it appears that zooming affects char widths (in pixels), which is unexpected.
|
||||
// --
|
||||
// Even though we read character widths correctly, having read them at a specific zoom level
|
||||
// does not mean they are the same at the current zoom level.
|
||||
// --
|
||||
// This could be improved if we ever figure out how to get an event when browsers zoom,
|
||||
// but until then we have to stick with reading client rects.
|
||||
// --
|
||||
// The same has been observed with Firefox on Windows7
|
||||
// --
|
||||
// The same has been oversved with Safari
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
const alwaysRenderInlineSelection = (browser.isEdgeOrIE);
|
||||
|
||||
export class DomReadingContext {
|
||||
|
||||
private readonly _domNode: HTMLElement;
|
||||
private _clientRectDeltaLeft: number;
|
||||
private _clientRectDeltaLeftRead: boolean;
|
||||
public get clientRectDeltaLeft(): number {
|
||||
if (!this._clientRectDeltaLeftRead) {
|
||||
this._clientRectDeltaLeftRead = true;
|
||||
this._clientRectDeltaLeft = this._domNode.getBoundingClientRect().left;
|
||||
}
|
||||
return this._clientRectDeltaLeft;
|
||||
}
|
||||
|
||||
public readonly endNode: HTMLElement;
|
||||
|
||||
constructor(domNode: HTMLElement, endNode: HTMLElement) {
|
||||
this._domNode = domNode;
|
||||
this._clientRectDeltaLeft = 0;
|
||||
this._clientRectDeltaLeftRead = false;
|
||||
this.endNode = endNode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ViewLineOptions {
|
||||
public readonly themeType: ThemeType;
|
||||
public readonly renderWhitespace: 'none' | 'boundary' | 'all';
|
||||
public readonly renderControlCharacters: boolean;
|
||||
public readonly spaceWidth: number;
|
||||
public readonly useMonospaceOptimizations: boolean;
|
||||
public readonly lineHeight: number;
|
||||
public readonly stopRenderingLineAfter: number;
|
||||
public readonly fontLigatures: boolean;
|
||||
|
||||
constructor(config: IConfiguration, themeType: ThemeType) {
|
||||
this.themeType = themeType;
|
||||
this.renderWhitespace = config.editor.viewInfo.renderWhitespace;
|
||||
this.renderControlCharacters = config.editor.viewInfo.renderControlCharacters;
|
||||
this.spaceWidth = config.editor.fontInfo.spaceWidth;
|
||||
this.useMonospaceOptimizations = (
|
||||
config.editor.fontInfo.isMonospace
|
||||
&& !config.editor.viewInfo.disableMonospaceOptimizations
|
||||
);
|
||||
this.lineHeight = config.editor.lineHeight;
|
||||
this.stopRenderingLineAfter = config.editor.viewInfo.stopRenderingLineAfter;
|
||||
this.fontLigatures = config.editor.viewInfo.fontLigatures;
|
||||
}
|
||||
|
||||
public equals(other: ViewLineOptions): boolean {
|
||||
return (
|
||||
this.themeType === other.themeType
|
||||
&& this.renderWhitespace === other.renderWhitespace
|
||||
&& this.renderControlCharacters === other.renderControlCharacters
|
||||
&& this.spaceWidth === other.spaceWidth
|
||||
&& this.useMonospaceOptimizations === other.useMonospaceOptimizations
|
||||
&& this.lineHeight === other.lineHeight
|
||||
&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
|
||||
&& this.fontLigatures === other.fontLigatures
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewLine implements IVisibleLine {
|
||||
|
||||
public static CLASS_NAME = 'view-line';
|
||||
|
||||
private _options: ViewLineOptions;
|
||||
private _isMaybeInvalid: boolean;
|
||||
private _renderedViewLine: IRenderedViewLine;
|
||||
|
||||
constructor(options: ViewLineOptions) {
|
||||
this._options = options;
|
||||
this._isMaybeInvalid = true;
|
||||
this._renderedViewLine = null;
|
||||
}
|
||||
|
||||
// --- begin IVisibleLineData
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
if (this._renderedViewLine && this._renderedViewLine.domNode) {
|
||||
return this._renderedViewLine.domNode.domNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public setDomNode(domNode: HTMLElement): void {
|
||||
if (this._renderedViewLine) {
|
||||
this._renderedViewLine.domNode = createFastDomNode(domNode);
|
||||
} else {
|
||||
throw new Error('I have no rendered view line to set the dom node to...');
|
||||
}
|
||||
}
|
||||
|
||||
public onContentChanged(): void {
|
||||
this._isMaybeInvalid = true;
|
||||
}
|
||||
public onTokensChanged(): void {
|
||||
this._isMaybeInvalid = true;
|
||||
}
|
||||
public onDecorationsChanged(): void {
|
||||
this._isMaybeInvalid = true;
|
||||
}
|
||||
public onOptionsChanged(newOptions: ViewLineOptions): void {
|
||||
this._isMaybeInvalid = true;
|
||||
this._options = newOptions;
|
||||
}
|
||||
public onSelectionChanged(): boolean {
|
||||
if (alwaysRenderInlineSelection || this._options.themeType === HIGH_CONTRAST) {
|
||||
this._isMaybeInvalid = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public renderLine(lineNumber: number, deltaTop: number, viewportData: ViewportData, sb: IStringBuilder): boolean {
|
||||
if (this._isMaybeInvalid === false) {
|
||||
// it appears that nothing relevant has changed
|
||||
return false;
|
||||
}
|
||||
|
||||
this._isMaybeInvalid = false;
|
||||
|
||||
const lineData = viewportData.getViewLineRenderingData(lineNumber);
|
||||
const options = this._options;
|
||||
const actualInlineDecorations = LineDecoration.filter(lineData.inlineDecorations, lineNumber, lineData.minColumn, lineData.maxColumn);
|
||||
|
||||
if (alwaysRenderInlineSelection || options.themeType === HIGH_CONTRAST) {
|
||||
const selections = viewportData.selections;
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
const selection = selections[i];
|
||||
|
||||
if (selection.endLineNumber < lineNumber || selection.startLineNumber > lineNumber) {
|
||||
// Selection does not intersect line
|
||||
continue;
|
||||
}
|
||||
|
||||
let startColumn = (selection.startLineNumber === lineNumber ? selection.startColumn : lineData.minColumn);
|
||||
let endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn);
|
||||
|
||||
if (startColumn < endColumn) {
|
||||
actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let renderLineInput = new RenderLineInput(
|
||||
options.useMonospaceOptimizations,
|
||||
lineData.content,
|
||||
lineData.mightContainRTL,
|
||||
lineData.minColumn - 1,
|
||||
lineData.tokens,
|
||||
actualInlineDecorations,
|
||||
lineData.tabSize,
|
||||
options.spaceWidth,
|
||||
options.stopRenderingLineAfter,
|
||||
options.renderWhitespace,
|
||||
options.renderControlCharacters,
|
||||
options.fontLigatures
|
||||
);
|
||||
|
||||
if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) {
|
||||
// no need to do anything, we have the same render input
|
||||
return false;
|
||||
}
|
||||
|
||||
sb.appendASCIIString('<div style="top:');
|
||||
sb.appendASCIIString(String(deltaTop));
|
||||
sb.appendASCIIString('px;height:');
|
||||
sb.appendASCIIString(String(this._options.lineHeight));
|
||||
sb.appendASCIIString('px;" class="');
|
||||
sb.appendASCIIString(ViewLine.CLASS_NAME);
|
||||
sb.appendASCIIString('">');
|
||||
|
||||
const output = renderViewLine(renderLineInput, sb);
|
||||
|
||||
sb.appendASCIIString('</div>');
|
||||
|
||||
let renderedViewLine: IRenderedViewLine = null;
|
||||
if (canUseFastRenderedViewLine && options.useMonospaceOptimizations && !output.containsForeignElements) {
|
||||
let isRegularASCII = true;
|
||||
if (lineData.mightContainNonBasicASCII) {
|
||||
isRegularASCII = strings.isBasicASCII(lineData.content);
|
||||
}
|
||||
|
||||
if (isRegularASCII && lineData.content.length < 1000) {
|
||||
// Browser rounding errors have been observed in Chrome and IE, so using the fast
|
||||
// view line only for short lines. Please test before removing the length check...
|
||||
renderedViewLine = new FastRenderedViewLine(
|
||||
this._renderedViewLine ? this._renderedViewLine.domNode : null,
|
||||
renderLineInput,
|
||||
output.characterMapping
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!renderedViewLine) {
|
||||
renderedViewLine = createRenderedLine(
|
||||
this._renderedViewLine ? this._renderedViewLine.domNode : null,
|
||||
renderLineInput,
|
||||
output.characterMapping,
|
||||
output.containsRTL,
|
||||
output.containsForeignElements
|
||||
);
|
||||
}
|
||||
|
||||
this._renderedViewLine = renderedViewLine;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public layoutLine(lineNumber: number, deltaTop: number): void {
|
||||
if (this._renderedViewLine && this._renderedViewLine.domNode) {
|
||||
this._renderedViewLine.domNode.setTop(deltaTop);
|
||||
this._renderedViewLine.domNode.setHeight(this._options.lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// --- end IVisibleLineData
|
||||
|
||||
public getWidth(): number {
|
||||
if (!this._renderedViewLine) {
|
||||
return 0;
|
||||
}
|
||||
return this._renderedViewLine.getWidth();
|
||||
}
|
||||
|
||||
public getWidthIsFast(): boolean {
|
||||
if (!this._renderedViewLine) {
|
||||
return true;
|
||||
}
|
||||
return this._renderedViewLine.getWidthIsFast();
|
||||
}
|
||||
|
||||
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
startColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, startColumn));
|
||||
endColumn = Math.min(this._renderedViewLine.input.lineContent.length + 1, Math.max(1, endColumn));
|
||||
return this._renderedViewLine.getVisibleRangesForRange(startColumn, endColumn, context);
|
||||
}
|
||||
|
||||
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
|
||||
return this._renderedViewLine.getColumnOfNodeOffset(lineNumber, spanNode, offset);
|
||||
}
|
||||
}
|
||||
|
||||
interface IRenderedViewLine {
|
||||
domNode: FastDomNode<HTMLElement>;
|
||||
readonly input: RenderLineInput;
|
||||
getWidth(): number;
|
||||
getWidthIsFast(): boolean;
|
||||
getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[];
|
||||
getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A rendered line which is guaranteed to contain only regular ASCII and is rendered with a monospace font.
|
||||
*/
|
||||
class FastRenderedViewLine implements IRenderedViewLine {
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
public readonly input: RenderLineInput;
|
||||
|
||||
private readonly _characterMapping: CharacterMapping;
|
||||
private readonly _charWidth: number;
|
||||
|
||||
constructor(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping) {
|
||||
this.domNode = domNode;
|
||||
this.input = renderLineInput;
|
||||
|
||||
this._characterMapping = characterMapping;
|
||||
this._charWidth = renderLineInput.spaceWidth;
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return this._getCharPosition(this._characterMapping.length);
|
||||
}
|
||||
|
||||
public getWidthIsFast(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
startColumn = startColumn | 0; // @perf
|
||||
endColumn = endColumn | 0; // @perf
|
||||
const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) {
|
||||
// This range is obviously not visible
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) {
|
||||
startColumn = stopRenderingLineAfter;
|
||||
}
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) {
|
||||
endColumn = stopRenderingLineAfter;
|
||||
}
|
||||
|
||||
const startPosition = this._getCharPosition(startColumn);
|
||||
const endPosition = this._getCharPosition(endColumn);
|
||||
return [new HorizontalRange(startPosition, endPosition - startPosition)];
|
||||
}
|
||||
|
||||
private _getCharPosition(column: number): number {
|
||||
const charOffset = this._characterMapping.getAbsoluteOffsets();
|
||||
if (charOffset.length === 0) {
|
||||
// No characters on this line
|
||||
return 0;
|
||||
}
|
||||
return Math.round(this._charWidth * charOffset[column - 1]);
|
||||
}
|
||||
|
||||
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
|
||||
let spanNodeTextContentLength = spanNode.textContent.length;
|
||||
|
||||
let spanIndex = -1;
|
||||
while (spanNode) {
|
||||
spanNode = <HTMLElement>spanNode.previousSibling;
|
||||
spanIndex++;
|
||||
}
|
||||
|
||||
let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset);
|
||||
return charOffset + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time we render a line, we save what we have rendered in an instance of this class.
|
||||
*/
|
||||
class RenderedViewLine implements IRenderedViewLine {
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
public readonly input: RenderLineInput;
|
||||
|
||||
protected readonly _characterMapping: CharacterMapping;
|
||||
private readonly _isWhitespaceOnly: boolean;
|
||||
private readonly _containsForeignElements: boolean;
|
||||
private _cachedWidth: number;
|
||||
|
||||
/**
|
||||
* This is a map that is used only when the line is guaranteed to have no RTL text.
|
||||
*/
|
||||
private _pixelOffsetCache: Int32Array;
|
||||
|
||||
constructor(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) {
|
||||
this.domNode = domNode;
|
||||
this.input = renderLineInput;
|
||||
this._characterMapping = characterMapping;
|
||||
this._isWhitespaceOnly = /^\s*$/.test(renderLineInput.lineContent);
|
||||
this._containsForeignElements = containsForeignElements;
|
||||
this._cachedWidth = -1;
|
||||
|
||||
this._pixelOffsetCache = null;
|
||||
if (!containsRTL || this._characterMapping.length === 0 /* the line is empty */) {
|
||||
this._pixelOffsetCache = new Int32Array(this._characterMapping.length + 1);
|
||||
for (let column = 0, len = this._characterMapping.length; column <= len; column++) {
|
||||
this._pixelOffsetCache[column] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reading from the DOM methods
|
||||
|
||||
protected _getReadingTarget(): HTMLElement {
|
||||
return <HTMLSpanElement>this.domNode.domNode.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the line in pixels
|
||||
*/
|
||||
public getWidth(): number {
|
||||
if (this._cachedWidth === -1) {
|
||||
this._cachedWidth = this._getReadingTarget().offsetWidth;
|
||||
}
|
||||
return this._cachedWidth;
|
||||
}
|
||||
|
||||
public getWidthIsFast(): boolean {
|
||||
if (this._cachedWidth === -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible ranges for a model range
|
||||
*/
|
||||
public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
startColumn = startColumn | 0; // @perf
|
||||
endColumn = endColumn | 0; // @perf
|
||||
const stopRenderingLineAfter = this.input.stopRenderingLineAfter | 0; // @perf
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter && endColumn > stopRenderingLineAfter) {
|
||||
// This range is obviously not visible
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && startColumn > stopRenderingLineAfter) {
|
||||
startColumn = stopRenderingLineAfter;
|
||||
}
|
||||
|
||||
if (stopRenderingLineAfter !== -1 && endColumn > stopRenderingLineAfter) {
|
||||
endColumn = stopRenderingLineAfter;
|
||||
}
|
||||
|
||||
if (this._pixelOffsetCache !== null) {
|
||||
// the text is LTR
|
||||
let startOffset = this._readPixelOffset(startColumn, context);
|
||||
if (startOffset === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let endOffset = this._readPixelOffset(endColumn, context);
|
||||
if (endOffset === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [new HorizontalRange(startOffset, endOffset - startOffset)];
|
||||
}
|
||||
|
||||
return this._readVisibleRangesForRange(startColumn, endColumn, context);
|
||||
}
|
||||
|
||||
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
if (startColumn === endColumn) {
|
||||
let pixelOffset = this._readPixelOffset(startColumn, context);
|
||||
if (pixelOffset === -1) {
|
||||
return null;
|
||||
} else {
|
||||
return [new HorizontalRange(pixelOffset, 0)];
|
||||
}
|
||||
} else {
|
||||
return this._readRawVisibleRangesForRange(startColumn, endColumn, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected _readPixelOffset(column: number, context: DomReadingContext): number {
|
||||
if (this._characterMapping.length === 0) {
|
||||
// This line has no content
|
||||
if (!this._containsForeignElements) {
|
||||
// We can assume the line is really empty
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._pixelOffsetCache !== null) {
|
||||
// the text is LTR
|
||||
|
||||
let cachedPixelOffset = this._pixelOffsetCache[column];
|
||||
if (cachedPixelOffset !== -1) {
|
||||
return cachedPixelOffset;
|
||||
}
|
||||
|
||||
let result = this._actualReadPixelOffset(column, context);
|
||||
this._pixelOffsetCache[column] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
return this._actualReadPixelOffset(column, context);
|
||||
}
|
||||
|
||||
private _actualReadPixelOffset(column: number, context: DomReadingContext): number {
|
||||
if (this._characterMapping.length === 0) {
|
||||
// This line has no content
|
||||
let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), 0, 0, 0, 0, context.clientRectDeltaLeft, context.endNode);
|
||||
if (!r || r.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
return r[0].left;
|
||||
}
|
||||
|
||||
if (column === this._characterMapping.length && this._isWhitespaceOnly && !this._containsForeignElements) {
|
||||
// This branch helps in the case of whitespace only lines which have a width set
|
||||
return this.getWidth();
|
||||
}
|
||||
|
||||
let partData = this._characterMapping.charOffsetToPartData(column - 1);
|
||||
let partIndex = CharacterMapping.getPartIndex(partData);
|
||||
let charOffsetInPart = CharacterMapping.getCharIndex(partData);
|
||||
|
||||
let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, context.clientRectDeltaLeft, context.endNode);
|
||||
if (!r || r.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
return r[0].left;
|
||||
}
|
||||
|
||||
private _readRawVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
|
||||
if (startColumn === 1 && endColumn === this._characterMapping.length) {
|
||||
// This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line
|
||||
|
||||
return [new HorizontalRange(0, this.getWidth())];
|
||||
}
|
||||
|
||||
let startPartData = this._characterMapping.charOffsetToPartData(startColumn - 1);
|
||||
let startPartIndex = CharacterMapping.getPartIndex(startPartData);
|
||||
let startCharOffsetInPart = CharacterMapping.getCharIndex(startPartData);
|
||||
|
||||
let endPartData = this._characterMapping.charOffsetToPartData(endColumn - 1);
|
||||
let endPartIndex = CharacterMapping.getPartIndex(endPartData);
|
||||
let endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData);
|
||||
|
||||
return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, context.clientRectDeltaLeft, context.endNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the column for the text found at a specific offset inside a rendered dom node
|
||||
*/
|
||||
public getColumnOfNodeOffset(lineNumber: number, spanNode: HTMLElement, offset: number): number {
|
||||
let spanNodeTextContentLength = spanNode.textContent.length;
|
||||
|
||||
let spanIndex = -1;
|
||||
while (spanNode) {
|
||||
spanNode = <HTMLElement>spanNode.previousSibling;
|
||||
spanIndex++;
|
||||
}
|
||||
|
||||
let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset);
|
||||
return charOffset + 1;
|
||||
}
|
||||
}
|
||||
|
||||
class WebKitRenderedViewLine extends RenderedViewLine {
|
||||
protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] {
|
||||
let output = super._readVisibleRangesForRange(startColumn, endColumn, context);
|
||||
|
||||
if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) {
|
||||
return output;
|
||||
}
|
||||
|
||||
// WebKit is buggy and returns an expanded range (to contain words in some cases)
|
||||
// The last client rect is enlarged (I think)
|
||||
|
||||
// This is an attempt to patch things up
|
||||
// Find position of previous column
|
||||
let beforeEndPixelOffset = this._readPixelOffset(endColumn - 1, context);
|
||||
// Find position of last column
|
||||
let endPixelOffset = this._readPixelOffset(endColumn, context);
|
||||
|
||||
if (beforeEndPixelOffset !== -1 && endPixelOffset !== -1) {
|
||||
let isLTR = (beforeEndPixelOffset <= endPixelOffset);
|
||||
let lastRange = output[output.length - 1];
|
||||
|
||||
if (isLTR && lastRange.left < endPixelOffset) {
|
||||
// Trim down the width of the last visible range to not go after the last column's position
|
||||
lastRange.width = endPixelOffset - lastRange.left;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
const createRenderedLine: (domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean) => RenderedViewLine = (function () {
|
||||
if (browser.isWebKit) {
|
||||
return createWebKitRenderedLine;
|
||||
}
|
||||
return createNormalRenderedLine;
|
||||
})();
|
||||
|
||||
function createWebKitRenderedLine(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine {
|
||||
return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements);
|
||||
}
|
||||
|
||||
function createNormalRenderedLine(domNode: FastDomNode<HTMLElement>, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: boolean): RenderedViewLine {
|
||||
return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements);
|
||||
}
|
||||
55
src/vs/editor/browser/viewParts/lines/viewLines.css
Normal file
55
src/vs/editor/browser/viewParts/lines/viewLines.css
Normal file
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Uncomment to see lines flashing when they're painted */
|
||||
/*.monaco-editor .view-lines > .view-line {
|
||||
background-color: none;
|
||||
animation-name: flash-background;
|
||||
animation-duration: 800ms;
|
||||
}
|
||||
@keyframes flash-background {
|
||||
0% { background-color: lightgreen; }
|
||||
100% { background-color: none }
|
||||
}*/
|
||||
|
||||
.monaco-editor .lines-content,
|
||||
.monaco-editor .view-line,
|
||||
.monaco-editor .view-lines {
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.monaco-editor.ie .lines-content,
|
||||
.monaco-editor.ie .view-line,
|
||||
.monaco-editor.ie .view-lines {
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.monaco-editor .view-lines {
|
||||
cursor: text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark.mac .view-lines,
|
||||
.monaco-editor.hc-black.mac .view-lines {
|
||||
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
|
||||
}
|
||||
|
||||
.monaco-editor .view-line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* TODO@tokenization bootstrap fix */
|
||||
/*.monaco-editor .view-line > span > span {
|
||||
float: none;
|
||||
min-height: inherit;
|
||||
margin-left: inherit;
|
||||
}*/
|
||||
689
src/vs/editor/browser/viewParts/lines/viewLines.ts
Normal file
689
src/vs/editor/browser/viewParts/lines/viewLines.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./viewLines';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { FastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { VisibleLinesCollection, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer';
|
||||
import { ViewLineOptions, DomReadingContext, ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine';
|
||||
import { Configuration } from 'vs/editor/browser/config/configuration';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
|
||||
import { IViewLines, HorizontalRange, LineVisibleRanges } from 'vs/editor/common/view/renderingContext';
|
||||
import { Viewport } from 'vs/editor/common/viewModel/viewModel';
|
||||
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
|
||||
class LastRenderedData {
|
||||
|
||||
private _currentVisibleRange: Range;
|
||||
|
||||
constructor() {
|
||||
this._currentVisibleRange = new Range(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
public getCurrentVisibleRange(): Range {
|
||||
return this._currentVisibleRange;
|
||||
}
|
||||
|
||||
public setCurrentVisibleRange(currentVisibleRange: Range): void {
|
||||
this._currentVisibleRange = currentVisibleRange;
|
||||
}
|
||||
}
|
||||
|
||||
class HorizontalRevealRequest {
|
||||
|
||||
public readonly lineNumber: number;
|
||||
public readonly startColumn: number;
|
||||
public readonly endColumn: number;
|
||||
public readonly startScrollTop: number;
|
||||
public readonly stopScrollTop: number;
|
||||
public readonly scrollType: ScrollType;
|
||||
|
||||
constructor(lineNumber: number, startColumn: number, endColumn: number, startScrollTop: number, stopScrollTop: number, scrollType: ScrollType) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.startColumn = startColumn;
|
||||
this.endColumn = endColumn;
|
||||
this.startScrollTop = startScrollTop;
|
||||
this.stopScrollTop = stopScrollTop;
|
||||
this.scrollType = scrollType;
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>, IViewLines {
|
||||
/**
|
||||
* Adds this ammount of pixels to the right of lines (no-one wants to type near the edge of the viewport)
|
||||
*/
|
||||
private static HORIZONTAL_EXTRA_PX = 30;
|
||||
|
||||
private readonly _linesContent: FastDomNode<HTMLElement>;
|
||||
private readonly _textRangeRestingSpot: HTMLElement;
|
||||
private readonly _visibleLines: VisibleLinesCollection<ViewLine>;
|
||||
private readonly domNode: FastDomNode<HTMLElement>;
|
||||
|
||||
// --- config
|
||||
private _lineHeight: number;
|
||||
private _typicalHalfwidthCharacterWidth: number;
|
||||
private _isViewportWrapping: boolean;
|
||||
private _revealHorizontalRightPadding: number;
|
||||
private _canUseLayerHinting: boolean;
|
||||
private _viewLineOptions: ViewLineOptions;
|
||||
|
||||
// --- width
|
||||
private _maxLineWidth: number;
|
||||
private _asyncUpdateLineWidths: RunOnceScheduler;
|
||||
|
||||
private _horizontalRevealRequest: HorizontalRevealRequest;
|
||||
private _lastRenderedData: LastRenderedData;
|
||||
|
||||
constructor(context: ViewContext, linesContent: FastDomNode<HTMLElement>) {
|
||||
super(context);
|
||||
this._linesContent = linesContent;
|
||||
this._textRangeRestingSpot = document.createElement('div');
|
||||
this._visibleLines = new VisibleLinesCollection(this);
|
||||
this.domNode = this._visibleLines.domNode;
|
||||
|
||||
const conf = this._context.configuration;
|
||||
|
||||
this._lineHeight = conf.editor.lineHeight;
|
||||
this._typicalHalfwidthCharacterWidth = conf.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
this._isViewportWrapping = conf.editor.wrappingInfo.isViewportWrapping;
|
||||
this._revealHorizontalRightPadding = conf.editor.viewInfo.revealHorizontalRightPadding;
|
||||
this._canUseLayerHinting = conf.editor.canUseLayerHinting;
|
||||
this._viewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
|
||||
|
||||
PartFingerprints.write(this.domNode, PartFingerprint.ViewLines);
|
||||
this.domNode.setClassName('view-lines');
|
||||
Configuration.applyFontInfo(this.domNode, conf.editor.fontInfo);
|
||||
|
||||
// --- width & height
|
||||
this._maxLineWidth = 0;
|
||||
this._asyncUpdateLineWidths = new RunOnceScheduler(() => {
|
||||
this._updateLineWidthsSlow();
|
||||
}, 200);
|
||||
|
||||
this._lastRenderedData = new LastRenderedData();
|
||||
|
||||
this._horizontalRevealRequest = null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._asyncUpdateLineWidths.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
// ---- begin IVisibleLinesHost
|
||||
|
||||
public createVisibleLine(): ViewLine {
|
||||
return new ViewLine(this._viewLineOptions);
|
||||
}
|
||||
|
||||
// ---- end IVisibleLinesHost
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
this._visibleLines.onConfigurationChanged(e);
|
||||
if (e.wrappingInfo) {
|
||||
this._maxLineWidth = 0;
|
||||
}
|
||||
|
||||
const conf = this._context.configuration;
|
||||
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = conf.editor.lineHeight;
|
||||
}
|
||||
if (e.fontInfo) {
|
||||
this._typicalHalfwidthCharacterWidth = conf.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
}
|
||||
if (e.wrappingInfo) {
|
||||
this._isViewportWrapping = conf.editor.wrappingInfo.isViewportWrapping;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._revealHorizontalRightPadding = conf.editor.viewInfo.revealHorizontalRightPadding;
|
||||
}
|
||||
if (e.canUseLayerHinting) {
|
||||
this._canUseLayerHinting = conf.editor.canUseLayerHinting;
|
||||
}
|
||||
if (e.fontInfo) {
|
||||
Configuration.applyFontInfo(this.domNode, conf.editor.fontInfo);
|
||||
}
|
||||
|
||||
this._onOptionsMaybeChanged();
|
||||
|
||||
if (e.layoutInfo) {
|
||||
this._maxLineWidth = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
private _onOptionsMaybeChanged(): boolean {
|
||||
const conf = this._context.configuration;
|
||||
|
||||
let newViewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
|
||||
if (!this._viewLineOptions.equals(newViewLineOptions)) {
|
||||
this._viewLineOptions = newViewLineOptions;
|
||||
|
||||
let startLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let endLineNumber = this._visibleLines.getEndLineNumber();
|
||||
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
|
||||
let line = this._visibleLines.getVisibleLine(lineNumber);
|
||||
line.onOptionsChanged(this._viewLineOptions);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
let r = false;
|
||||
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
|
||||
r = this._visibleLines.getVisibleLine(lineNumber).onSelectionChanged() || r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
if (true/*e.inlineDecorationsChanged*/) {
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
|
||||
this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
let shouldRender = this._visibleLines.onFlushed(e);
|
||||
this._maxLineWidth = 0;
|
||||
return shouldRender;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return this._visibleLines.onLinesChanged(e);
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return this._visibleLines.onLinesDeleted(e);
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return this._visibleLines.onLinesInserted(e);
|
||||
}
|
||||
public onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean {
|
||||
// Using the future viewport here in order to handle multiple
|
||||
// incoming reveal range requests that might all desire to be animated
|
||||
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.range, e.verticalType);
|
||||
|
||||
// validate the new desired scroll top
|
||||
let newScrollPosition = this._context.viewLayout.validateScrollPosition({ scrollTop: desiredScrollTop });
|
||||
|
||||
if (e.revealHorizontal) {
|
||||
if (e.range.startLineNumber !== e.range.endLineNumber) {
|
||||
// Two or more lines? => scroll to base (That's how you see most of the two lines)
|
||||
newScrollPosition = {
|
||||
scrollTop: newScrollPosition.scrollTop,
|
||||
scrollLeft: 0
|
||||
};
|
||||
} else {
|
||||
// We don't necessarily know the horizontal offset of this range since the line might not be in the view...
|
||||
this._horizontalRevealRequest = new HorizontalRevealRequest(e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
|
||||
}
|
||||
} else {
|
||||
this._horizontalRevealRequest = null;
|
||||
}
|
||||
|
||||
const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop);
|
||||
if (e.scrollType === ScrollType.Smooth && scrollTopDelta > this._lineHeight) {
|
||||
this._context.viewLayout.setScrollPositionSmooth(newScrollPosition);
|
||||
} else {
|
||||
this._context.viewLayout.setScrollPositionNow(newScrollPosition);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
if (this._horizontalRevealRequest && e.scrollLeftChanged) {
|
||||
// cancel any outstanding horizontal reveal request if someone else scrolls horizontally.
|
||||
this._horizontalRevealRequest = null;
|
||||
}
|
||||
if (this._horizontalRevealRequest && e.scrollTopChanged) {
|
||||
const min = Math.min(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
|
||||
const max = Math.max(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
|
||||
if (e.scrollTop < min || e.scrollTop > max) {
|
||||
// cancel any outstanding horizontal reveal request if someone else scrolls vertically.
|
||||
this._horizontalRevealRequest = null;
|
||||
}
|
||||
}
|
||||
this.domNode.setWidth(e.scrollWidth);
|
||||
return this._visibleLines.onScrollChanged(e) || true;
|
||||
}
|
||||
|
||||
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
|
||||
return this._visibleLines.onTokensChanged(e);
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return this._visibleLines.onZonesChanged(e);
|
||||
}
|
||||
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
|
||||
return this._onOptionsMaybeChanged();
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
// ----------- HELPERS FOR OTHERS
|
||||
|
||||
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position {
|
||||
let viewLineDomNode = this._getViewLineDomNode(spanNode);
|
||||
if (viewLineDomNode === null) {
|
||||
// Couldn't find view line node
|
||||
return null;
|
||||
}
|
||||
let lineNumber = this._getLineNumberFor(viewLineDomNode);
|
||||
|
||||
if (lineNumber === -1) {
|
||||
// Couldn't find view line node
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lineNumber < 1 || lineNumber > this._context.model.getLineCount()) {
|
||||
// lineNumber is outside range
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._context.model.getLineMaxColumn(lineNumber) === 1) {
|
||||
// Line is empty
|
||||
return new Position(lineNumber, 1);
|
||||
}
|
||||
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
|
||||
// Couldn't find line
|
||||
return null;
|
||||
}
|
||||
|
||||
let column = this._visibleLines.getVisibleLine(lineNumber).getColumnOfNodeOffset(lineNumber, spanNode, offset);
|
||||
let minColumn = this._context.model.getLineMinColumn(lineNumber);
|
||||
if (column < minColumn) {
|
||||
column = minColumn;
|
||||
}
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
private _getViewLineDomNode(node: HTMLElement): HTMLElement {
|
||||
while (node && node.nodeType === 1) {
|
||||
if (node.className === ViewLine.CLASS_NAME) {
|
||||
return node;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the line number of this view line dom node.
|
||||
*/
|
||||
private _getLineNumberFor(domNode: HTMLElement): number {
|
||||
let startLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let endLineNumber = this._visibleLines.getEndLineNumber();
|
||||
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
|
||||
let line = this._visibleLines.getVisibleLine(lineNumber);
|
||||
if (domNode === line.getDomNode()) {
|
||||
return lineNumber;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public getLineWidth(lineNumber: number): number {
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
|
||||
// Couldn't find line
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this._visibleLines.getVisibleLine(lineNumber).getWidth();
|
||||
}
|
||||
|
||||
public linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] {
|
||||
if (this.shouldRender()) {
|
||||
// Cannot read from the DOM because it is dirty
|
||||
// i.e. the model & the dom are out of sync, so I'd be reading something stale
|
||||
return null;
|
||||
}
|
||||
|
||||
let originalEndLineNumber = range.endLineNumber;
|
||||
range = Range.intersectRanges(range, this._lastRenderedData.getCurrentVisibleRange());
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let visibleRanges: LineVisibleRanges[] = [], visibleRangesLen = 0;
|
||||
let domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
|
||||
|
||||
let nextLineModelLineNumber: number;
|
||||
if (includeNewLines) {
|
||||
nextLineModelLineNumber = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
|
||||
}
|
||||
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
|
||||
|
||||
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
|
||||
let endColumn = lineNumber === range.endLineNumber ? range.endColumn : this._context.model.getLineMaxColumn(lineNumber);
|
||||
let visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, domReadingContext);
|
||||
|
||||
if (!visibleRangesForLine || visibleRangesForLine.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (includeNewLines && lineNumber < originalEndLineNumber) {
|
||||
let currentLineModelLineNumber = nextLineModelLineNumber;
|
||||
nextLineModelLineNumber = this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;
|
||||
|
||||
if (currentLineModelLineNumber !== nextLineModelLineNumber) {
|
||||
visibleRangesForLine[visibleRangesForLine.length - 1].width += this._typicalHalfwidthCharacterWidth;
|
||||
}
|
||||
}
|
||||
|
||||
visibleRanges[visibleRangesLen++] = new LineVisibleRanges(lineNumber, visibleRangesForLine);
|
||||
}
|
||||
|
||||
if (visibleRangesLen === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return visibleRanges;
|
||||
}
|
||||
|
||||
public visibleRangesForRange2(range: Range): HorizontalRange[] {
|
||||
|
||||
if (this.shouldRender()) {
|
||||
// Cannot read from the DOM because it is dirty
|
||||
// i.e. the model & the dom are out of sync, so I'd be reading something stale
|
||||
return null;
|
||||
}
|
||||
|
||||
range = Range.intersectRanges(range, this._lastRenderedData.getCurrentVisibleRange());
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: HorizontalRange[] = [];
|
||||
let domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
|
||||
|
||||
let rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
let rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
|
||||
|
||||
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
|
||||
let endColumn = lineNumber === range.endLineNumber ? range.endColumn : this._context.model.getLineMaxColumn(lineNumber);
|
||||
let visibleRangesForLine = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(startColumn, endColumn, domReadingContext);
|
||||
|
||||
if (!visibleRangesForLine || visibleRangesForLine.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result = result.concat(visibleRangesForLine);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- implementation
|
||||
|
||||
/**
|
||||
* Updates the max line width if it is fast to compute.
|
||||
* Returns true if all lines were taken into account.
|
||||
* Returns false if some lines need to be reevaluated (in a slow fashion).
|
||||
*/
|
||||
private _updateLineWidthsFast(): boolean {
|
||||
return this._updateLineWidths(true);
|
||||
}
|
||||
|
||||
private _updateLineWidthsSlow(): void {
|
||||
this._updateLineWidths(false);
|
||||
}
|
||||
|
||||
private _updateLineWidths(fast: boolean): boolean {
|
||||
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
|
||||
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
|
||||
|
||||
let localMaxLineWidth = 1;
|
||||
let allWidthsComputed = true;
|
||||
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
|
||||
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
|
||||
|
||||
if (fast && !visibleLine.getWidthIsFast()) {
|
||||
// Cannot compute width in a fast way for this line
|
||||
allWidthsComputed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
localMaxLineWidth = Math.max(localMaxLineWidth, visibleLine.getWidth());
|
||||
}
|
||||
|
||||
if (allWidthsComputed && rendStartLineNumber === 1 && rendEndLineNumber === this._context.model.getLineCount()) {
|
||||
// we know the max line width for all the lines
|
||||
this._maxLineWidth = 0;
|
||||
}
|
||||
|
||||
this._ensureMaxLineWidth(localMaxLineWidth);
|
||||
|
||||
return allWidthsComputed;
|
||||
}
|
||||
|
||||
public prepareRender(): void {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
public renderText(viewportData: ViewportData): void {
|
||||
// (1) render lines - ensures lines are in the DOM
|
||||
this._visibleLines.renderLines(viewportData);
|
||||
this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange);
|
||||
this.domNode.setWidth(this._context.viewLayout.getScrollWidth());
|
||||
this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000));
|
||||
|
||||
// (2) compute horizontal scroll position:
|
||||
// - this must happen after the lines are in the DOM since it might need a line that rendered just now
|
||||
// - it might change `scrollWidth` and `scrollLeft`
|
||||
if (this._horizontalRevealRequest) {
|
||||
|
||||
const revealLineNumber = this._horizontalRevealRequest.lineNumber;
|
||||
const revealStartColumn = this._horizontalRevealRequest.startColumn;
|
||||
const revealEndColumn = this._horizontalRevealRequest.endColumn;
|
||||
const scrollType = this._horizontalRevealRequest.scrollType;
|
||||
|
||||
// Check that we have the line that contains the horizontal range in the viewport
|
||||
if (viewportData.startLineNumber <= revealLineNumber && revealLineNumber <= viewportData.endLineNumber) {
|
||||
|
||||
this._horizontalRevealRequest = null;
|
||||
|
||||
// allow `visibleRangesForRange2` to work
|
||||
this.onDidRender();
|
||||
|
||||
// compute new scroll position
|
||||
let newScrollLeft = this._computeScrollLeftToRevealRange(revealLineNumber, revealStartColumn, revealEndColumn);
|
||||
|
||||
let isViewportWrapping = this._isViewportWrapping;
|
||||
if (!isViewportWrapping) {
|
||||
// ensure `scrollWidth` is large enough
|
||||
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
|
||||
}
|
||||
|
||||
// set `scrollLeft`
|
||||
if (scrollType === ScrollType.Smooth) {
|
||||
this._context.viewLayout.setScrollPositionSmooth({
|
||||
scrollLeft: newScrollLeft.scrollLeft
|
||||
});
|
||||
} else {
|
||||
this._context.viewLayout.setScrollPositionNow({
|
||||
scrollLeft: newScrollLeft.scrollLeft
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (3) handle scrolling
|
||||
this._linesContent.setLayerHinting(this._canUseLayerHinting);
|
||||
const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta;
|
||||
this._linesContent.setTop(-adjustedScrollTop);
|
||||
this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft());
|
||||
|
||||
// Update max line width (not so important, it is just so the horizontal scrollbar doesn't get too small)
|
||||
if (!this._updateLineWidthsFast()) {
|
||||
// Computing the width of some lines would be slow => delay it
|
||||
this._asyncUpdateLineWidths.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
// --- width
|
||||
|
||||
private _ensureMaxLineWidth(lineWidth: number): void {
|
||||
let iLineWidth = Math.ceil(lineWidth);
|
||||
if (this._maxLineWidth < iLineWidth) {
|
||||
this._maxLineWidth = iLineWidth;
|
||||
this._context.viewLayout.onMaxLineWidthChanged(this._maxLineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private _computeScrollTopToRevealRange(viewport: Viewport, range: Range, verticalType: viewEvents.VerticalRevealType): number {
|
||||
let viewportStartY = viewport.top;
|
||||
let viewportHeight = viewport.height;
|
||||
let viewportEndY = viewportStartY + viewportHeight;
|
||||
let boxStartY: number;
|
||||
let boxEndY: number;
|
||||
|
||||
// Have a box that includes one extra line height (for the horizontal scrollbar)
|
||||
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
|
||||
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
|
||||
if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) {
|
||||
// Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom
|
||||
boxEndY += this._lineHeight;
|
||||
}
|
||||
|
||||
let newScrollTop: number;
|
||||
|
||||
if (verticalType === viewEvents.VerticalRevealType.Center || verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport) {
|
||||
if (verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
|
||||
// Box is already in the viewport... do nothing
|
||||
newScrollTop = viewportStartY;
|
||||
} else {
|
||||
// Box is outside the viewport... center it
|
||||
let boxMiddleY = (boxStartY + boxEndY) / 2;
|
||||
newScrollTop = Math.max(0, boxMiddleY - viewportHeight / 2);
|
||||
}
|
||||
} else {
|
||||
newScrollTop = this._computeMinimumScrolling(viewportStartY, viewportEndY, boxStartY, boxEndY, verticalType === viewEvents.VerticalRevealType.Top, verticalType === viewEvents.VerticalRevealType.Bottom);
|
||||
}
|
||||
|
||||
return newScrollTop;
|
||||
}
|
||||
|
||||
private _computeScrollLeftToRevealRange(lineNumber: number, startColumn: number, endColumn: number): { scrollLeft: number; maxHorizontalOffset: number; } {
|
||||
|
||||
let maxHorizontalOffset = 0;
|
||||
|
||||
let viewport = this._context.viewLayout.getCurrentViewport();
|
||||
let viewportStartX = viewport.left;
|
||||
let viewportEndX = viewportStartX + viewport.width;
|
||||
|
||||
let visibleRanges = this.visibleRangesForRange2(new Range(lineNumber, startColumn, lineNumber, endColumn));
|
||||
let boxStartX = Number.MAX_VALUE;
|
||||
let boxEndX = 0;
|
||||
|
||||
if (!visibleRanges) {
|
||||
// Unknown
|
||||
return {
|
||||
scrollLeft: viewportStartX,
|
||||
maxHorizontalOffset: maxHorizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < visibleRanges.length; i++) {
|
||||
let visibleRange = visibleRanges[i];
|
||||
if (visibleRange.left < boxStartX) {
|
||||
boxStartX = visibleRange.left;
|
||||
}
|
||||
if (visibleRange.left + visibleRange.width > boxEndX) {
|
||||
boxEndX = visibleRange.left + visibleRange.width;
|
||||
}
|
||||
}
|
||||
|
||||
maxHorizontalOffset = boxEndX;
|
||||
|
||||
boxStartX = Math.max(0, boxStartX - ViewLines.HORIZONTAL_EXTRA_PX);
|
||||
boxEndX += this._revealHorizontalRightPadding;
|
||||
|
||||
let newScrollLeft = this._computeMinimumScrolling(viewportStartX, viewportEndX, boxStartX, boxEndX);
|
||||
return {
|
||||
scrollLeft: newScrollLeft,
|
||||
maxHorizontalOffset: maxHorizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
private _computeMinimumScrolling(viewportStart: number, viewportEnd: number, boxStart: number, boxEnd: number, revealAtStart?: boolean, revealAtEnd?: boolean): number {
|
||||
viewportStart = viewportStart | 0;
|
||||
viewportEnd = viewportEnd | 0;
|
||||
boxStart = boxStart | 0;
|
||||
boxEnd = boxEnd | 0;
|
||||
revealAtStart = !!revealAtStart;
|
||||
revealAtEnd = !!revealAtEnd;
|
||||
|
||||
let viewportLength = viewportEnd - viewportStart;
|
||||
let boxLength = boxEnd - boxStart;
|
||||
|
||||
if (boxLength < viewportLength) {
|
||||
// The box would fit in the viewport
|
||||
|
||||
if (revealAtStart) {
|
||||
return boxStart;
|
||||
}
|
||||
|
||||
if (revealAtEnd) {
|
||||
return Math.max(0, boxEnd - viewportLength);
|
||||
}
|
||||
|
||||
if (boxStart < viewportStart) {
|
||||
// The box is above the viewport
|
||||
return boxStart;
|
||||
} else if (boxEnd > viewportEnd) {
|
||||
// The box is below the viewport
|
||||
return Math.max(0, boxEnd - viewportLength);
|
||||
}
|
||||
} else {
|
||||
// The box would not fit in the viewport
|
||||
// Reveal the beginning of the box
|
||||
return boxStart;
|
||||
}
|
||||
|
||||
return viewportStart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.monaco-editor .lines-decorations {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cldr = core lines decorations rendering (div)
|
||||
*/
|
||||
.monaco-editor .margin-view-overlays .cldr {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./linesDecorations';
|
||||
import { DecorationToRender, DedupOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export class LinesDecorationsOverlay extends DedupOverlay {
|
||||
|
||||
private _context: ViewContext;
|
||||
|
||||
private _decorationsLeft: number;
|
||||
private _decorationsWidth: number;
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._decorationsLeft = this._context.configuration.editor.layoutInfo.decorationsLeft;
|
||||
this._decorationsWidth = this._context.configuration.editor.layoutInfo.decorationsWidth;
|
||||
this._renderResult = null;
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.layoutInfo) {
|
||||
this._decorationsLeft = this._context.configuration.editor.layoutInfo.decorationsLeft;
|
||||
this._decorationsWidth = this._context.configuration.editor.layoutInfo.decorationsWidth;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
|
||||
let decorations = ctx.getDecorationsInViewport();
|
||||
let r: DecorationToRender[] = [], rLen = 0;
|
||||
for (let i = 0, len = decorations.length; i < len; i++) {
|
||||
let d = decorations[i];
|
||||
let linesDecorationsClassName = d.source.options.linesDecorationsClassName;
|
||||
if (linesDecorationsClassName) {
|
||||
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
|
||||
|
||||
let left = this._decorationsLeft.toString();
|
||||
let width = this._decorationsWidth.toString();
|
||||
let common = '" style="left:' + left + 'px;width:' + width + 'px;"></div>';
|
||||
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
let classNames = toRender[lineIndex];
|
||||
let lineOutput = '';
|
||||
for (let i = 0, len = classNames.length; i < len; i++) {
|
||||
lineOutput += '<div class="cldr ' + classNames[i] + common;
|
||||
}
|
||||
output[lineIndex] = lineOutput;
|
||||
}
|
||||
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
return this._renderResult[lineNumber - startLineNumber];
|
||||
}
|
||||
}
|
||||
95
src/vs/editor/browser/viewParts/margin/margin.ts
Normal file
95
src/vs/editor/browser/viewParts/margin/margin.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export class Margin extends ViewPart {
|
||||
|
||||
public static CLASS_NAME = 'glyph-margin';
|
||||
|
||||
private _domNode: FastDomNode<HTMLElement>;
|
||||
private _canUseLayerHinting: boolean;
|
||||
private _contentLeft: number;
|
||||
private _glyphMarginLeft: number;
|
||||
private _glyphMarginWidth: number;
|
||||
private _glyphMarginBackgroundDomNode: FastDomNode<HTMLElement>;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
this._canUseLayerHinting = this._context.configuration.editor.canUseLayerHinting;
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
|
||||
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
|
||||
|
||||
this._domNode = this._createDomNode();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
private _createDomNode(): FastDomNode<HTMLElement> {
|
||||
let domNode = createFastDomNode(document.createElement('div'));
|
||||
domNode.setClassName('margin');
|
||||
domNode.setPosition('absolute');
|
||||
domNode.setAttribute('role', 'presentation');
|
||||
domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._glyphMarginBackgroundDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._glyphMarginBackgroundDomNode.setClassName(Margin.CLASS_NAME);
|
||||
|
||||
domNode.appendChild(this._glyphMarginBackgroundDomNode);
|
||||
return domNode;
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.canUseLayerHinting) {
|
||||
this._canUseLayerHinting = this._context.configuration.editor.canUseLayerHinting;
|
||||
}
|
||||
|
||||
if (e.layoutInfo) {
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
this._glyphMarginLeft = this._context.configuration.editor.layoutInfo.glyphMarginLeft;
|
||||
this._glyphMarginWidth = this._context.configuration.editor.layoutInfo.glyphMarginWidth;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return super.onScrollChanged(e) || e.scrollTopChanged;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
this._domNode.setLayerHinting(this._canUseLayerHinting);
|
||||
const adjustedScrollTop = ctx.scrollTop - ctx.bigNumbersDelta;
|
||||
this._domNode.setTop(-adjustedScrollTop);
|
||||
|
||||
let height = Math.min(ctx.scrollHeight, 1000000);
|
||||
this._domNode.setHeight(height);
|
||||
this._domNode.setWidth(this._contentLeft);
|
||||
|
||||
this._glyphMarginBackgroundDomNode.setLeft(this._glyphMarginLeft);
|
||||
this._glyphMarginBackgroundDomNode.setWidth(this._glyphMarginWidth);
|
||||
this._glyphMarginBackgroundDomNode.setHeight(height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cmdr = core margin decorations rendering (div)
|
||||
*/
|
||||
.monaco-editor .margin-view-overlays .cmdr {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./marginDecorations';
|
||||
import { DecorationToRender, DedupOverlay } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export class MarginViewLineDecorationsOverlay extends DedupOverlay {
|
||||
private _context: ViewContext;
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._renderResult = null;
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
protected _getDecorations(ctx: RenderingContext): DecorationToRender[] {
|
||||
let decorations = ctx.getDecorationsInViewport();
|
||||
let r: DecorationToRender[] = [], rLen = 0;
|
||||
for (let i = 0, len = decorations.length; i < len; i++) {
|
||||
let d = decorations[i];
|
||||
let marginClassName = d.source.options.marginClassName;
|
||||
if (marginClassName) {
|
||||
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
let toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
|
||||
|
||||
let output: string[] = [];
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
let classNames = toRender[lineIndex];
|
||||
let lineOutput = '';
|
||||
for (let i = 0, len = classNames.length; i < len; i++) {
|
||||
lineOutput += '<div class="cmdr ' + classNames[i] + '" style=""></div>';
|
||||
}
|
||||
output[lineIndex] = lineOutput;
|
||||
}
|
||||
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
return this._renderResult[lineNumber - startLineNumber];
|
||||
}
|
||||
}
|
||||
27
src/vs/editor/browser/viewParts/minimap/minimap.css
Normal file
27
src/vs/editor/browser/viewParts/minimap/minimap.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* START cover the case that slider is visible on mouseover */
|
||||
.monaco-editor .minimap.slider-mouseover .minimap-slider {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms linear;
|
||||
}
|
||||
.monaco-editor .minimap.slider-mouseover:hover .minimap-slider {
|
||||
opacity: 1;
|
||||
}
|
||||
.monaco-editor .minimap.slider-mouseover .minimap-slider.active {
|
||||
opacity: 1;
|
||||
}
|
||||
/* END cover the case that slider is visible on mouseover */
|
||||
|
||||
.monaco-editor .minimap-shadow-hidden {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
}
|
||||
.monaco-editor .minimap-shadow-visible {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
width: 6px;
|
||||
}
|
||||
911
src/vs/editor/browser/viewParts/minimap/minimap.ts
Normal file
911
src/vs/editor/browser/viewParts/minimap/minimap.ts
Normal file
@@ -0,0 +1,911 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./minimap';
|
||||
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { getOrCreateMinimapCharRenderer } from 'vs/editor/common/view/runtimeMinimapCharRenderer';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { MinimapCharRenderer, MinimapTokensColorTracker, Constants } from 'vs/editor/common/view/minimapCharRenderer';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { ViewLineData } from 'vs/editor/common/viewModel/viewModel';
|
||||
import { ColorId } from 'vs/editor/common/modes';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { RenderedLinesCollection, ILine } from 'vs/editor/browser/view/viewLayer';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { RGBA8 } from 'vs/editor/common/core/rgba';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
const enum RenderMinimap {
|
||||
None = 0,
|
||||
Small = 1,
|
||||
Large = 2,
|
||||
SmallBlocks = 3,
|
||||
LargeBlocks = 4,
|
||||
}
|
||||
|
||||
function getMinimapLineHeight(renderMinimap: RenderMinimap): number {
|
||||
if (renderMinimap === RenderMinimap.Large) {
|
||||
return Constants.x2_CHAR_HEIGHT;
|
||||
}
|
||||
if (renderMinimap === RenderMinimap.LargeBlocks) {
|
||||
return Constants.x2_CHAR_HEIGHT + 2;
|
||||
}
|
||||
if (renderMinimap === RenderMinimap.Small) {
|
||||
return Constants.x1_CHAR_HEIGHT;
|
||||
}
|
||||
// RenderMinimap.SmallBlocks
|
||||
return Constants.x1_CHAR_HEIGHT + 1;
|
||||
}
|
||||
|
||||
function getMinimapCharWidth(renderMinimap: RenderMinimap): number {
|
||||
if (renderMinimap === RenderMinimap.Large) {
|
||||
return Constants.x2_CHAR_WIDTH;
|
||||
}
|
||||
if (renderMinimap === RenderMinimap.LargeBlocks) {
|
||||
return Constants.x2_CHAR_WIDTH;
|
||||
}
|
||||
if (renderMinimap === RenderMinimap.Small) {
|
||||
return Constants.x1_CHAR_WIDTH;
|
||||
}
|
||||
// RenderMinimap.SmallBlocks
|
||||
return Constants.x1_CHAR_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
|
||||
*/
|
||||
const MOUSE_DRAG_RESET_DISTANCE = 140;
|
||||
|
||||
class MinimapOptions {
|
||||
|
||||
public readonly renderMinimap: RenderMinimap;
|
||||
|
||||
public readonly scrollBeyondLastLine: boolean;
|
||||
|
||||
public readonly showSlider: 'always' | 'mouseover';
|
||||
|
||||
public readonly pixelRatio: number;
|
||||
|
||||
public readonly typicalHalfwidthCharacterWidth: number;
|
||||
|
||||
public readonly lineHeight: number;
|
||||
|
||||
/**
|
||||
* container dom node width (in CSS px)
|
||||
*/
|
||||
public readonly minimapWidth: number;
|
||||
/**
|
||||
* container dom node height (in CSS px)
|
||||
*/
|
||||
public readonly minimapHeight: number;
|
||||
|
||||
/**
|
||||
* canvas backing store width (in device px)
|
||||
*/
|
||||
public readonly canvasInnerWidth: number;
|
||||
/**
|
||||
* canvas backing store height (in device px)
|
||||
*/
|
||||
public readonly canvasInnerHeight: number;
|
||||
|
||||
/**
|
||||
* canvas width (in CSS px)
|
||||
*/
|
||||
public readonly canvasOuterWidth: number;
|
||||
/**
|
||||
* canvas height (in CSS px)
|
||||
*/
|
||||
public readonly canvasOuterHeight: number;
|
||||
|
||||
constructor(configuration: editorCommon.IConfiguration) {
|
||||
const pixelRatio = configuration.editor.pixelRatio;
|
||||
const layoutInfo = configuration.editor.layoutInfo;
|
||||
const viewInfo = configuration.editor.viewInfo;
|
||||
const fontInfo = configuration.editor.fontInfo;
|
||||
|
||||
this.renderMinimap = layoutInfo.renderMinimap | 0;
|
||||
this.scrollBeyondLastLine = viewInfo.scrollBeyondLastLine;
|
||||
this.showSlider = viewInfo.minimap.showSlider;
|
||||
this.pixelRatio = pixelRatio;
|
||||
this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
|
||||
this.lineHeight = configuration.editor.lineHeight;
|
||||
this.minimapWidth = layoutInfo.minimapWidth;
|
||||
this.minimapHeight = layoutInfo.height;
|
||||
|
||||
this.canvasInnerWidth = Math.max(1, Math.floor(pixelRatio * this.minimapWidth));
|
||||
this.canvasInnerHeight = Math.max(1, Math.floor(pixelRatio * this.minimapHeight));
|
||||
|
||||
this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio;
|
||||
this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio;
|
||||
}
|
||||
|
||||
public equals(other: MinimapOptions): boolean {
|
||||
return (this.renderMinimap === other.renderMinimap
|
||||
&& this.scrollBeyondLastLine === other.scrollBeyondLastLine
|
||||
&& this.showSlider === other.showSlider
|
||||
&& this.pixelRatio === other.pixelRatio
|
||||
&& this.typicalHalfwidthCharacterWidth === other.typicalHalfwidthCharacterWidth
|
||||
&& this.lineHeight === other.lineHeight
|
||||
&& this.minimapWidth === other.minimapWidth
|
||||
&& this.minimapHeight === other.minimapHeight
|
||||
&& this.canvasInnerWidth === other.canvasInnerWidth
|
||||
&& this.canvasInnerHeight === other.canvasInnerHeight
|
||||
&& this.canvasOuterWidth === other.canvasOuterWidth
|
||||
&& this.canvasOuterHeight === other.canvasOuterHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MinimapLayout {
|
||||
|
||||
/**
|
||||
* The given editor scrollTop (input).
|
||||
*/
|
||||
public readonly scrollTop: number;
|
||||
|
||||
/**
|
||||
* The given editor scrollHeight (input).
|
||||
*/
|
||||
public readonly scrollHeight: number;
|
||||
|
||||
private readonly _computedSliderRatio: number;
|
||||
|
||||
/**
|
||||
* slider dom node top (in CSS px)
|
||||
*/
|
||||
public readonly sliderTop: number;
|
||||
/**
|
||||
* slider dom node height (in CSS px)
|
||||
*/
|
||||
public readonly sliderHeight: number;
|
||||
|
||||
/**
|
||||
* minimap render start line number.
|
||||
*/
|
||||
public readonly startLineNumber: number;
|
||||
/**
|
||||
* minimap render end line number.
|
||||
*/
|
||||
public readonly endLineNumber: number;
|
||||
|
||||
constructor(
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
computedSliderRatio: number,
|
||||
sliderTop: number,
|
||||
sliderHeight: number,
|
||||
startLineNumber: number,
|
||||
endLineNumber: number
|
||||
) {
|
||||
this.scrollTop = scrollTop;
|
||||
this.scrollHeight = scrollHeight;
|
||||
this._computedSliderRatio = computedSliderRatio;
|
||||
this.sliderTop = sliderTop;
|
||||
this.sliderHeight = sliderHeight;
|
||||
this.startLineNumber = startLineNumber;
|
||||
this.endLineNumber = endLineNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
|
||||
*/
|
||||
public getDesiredScrollTopFromDelta(delta: number): number {
|
||||
let desiredSliderPosition = this.sliderTop + delta;
|
||||
return Math.round(desiredSliderPosition / this._computedSliderRatio);
|
||||
}
|
||||
|
||||
public static create(
|
||||
options: MinimapOptions,
|
||||
viewportStartLineNumber: number,
|
||||
viewportEndLineNumber: number,
|
||||
viewportHeight: number,
|
||||
viewportContainsWhitespaceGaps: boolean,
|
||||
lineCount: number,
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
previousLayout: MinimapLayout
|
||||
): MinimapLayout {
|
||||
const pixelRatio = options.pixelRatio;
|
||||
const minimapLineHeight = getMinimapLineHeight(options.renderMinimap);
|
||||
const minimapLinesFitting = Math.floor(options.canvasInnerHeight / minimapLineHeight);
|
||||
const lineHeight = options.lineHeight;
|
||||
|
||||
// The visible line count in a viewport can change due to a number of reasons:
|
||||
// a) with the same viewport width, different scroll positions can result in partial lines being visible:
|
||||
// e.g. for a line height of 20, and a viewport height of 600
|
||||
// * scrollTop = 0 => visible lines are [1, 30]
|
||||
// * scrollTop = 10 => visible lines are [1, 31] (with lines 1 and 31 partially visible)
|
||||
// * scrollTop = 20 => visible lines are [2, 31]
|
||||
// b) whitespace gaps might make their way in the viewport (which results in a decrease in the visible line count)
|
||||
// c) we could be in the scroll beyond last line case (which also results in a decrease in the visible line count, down to possibly only one line being visible)
|
||||
|
||||
// We must first establish a desirable slider height.
|
||||
let sliderHeight: number;
|
||||
if (viewportContainsWhitespaceGaps && viewportEndLineNumber !== lineCount) {
|
||||
// case b) from above: there are whitespace gaps in the viewport.
|
||||
// In this case, the height of the slider directly reflects the visible line count.
|
||||
const viewportLineCount = viewportEndLineNumber - viewportStartLineNumber + 1;
|
||||
sliderHeight = Math.floor(viewportLineCount * minimapLineHeight / pixelRatio);
|
||||
} else {
|
||||
// The slider has a stable height
|
||||
const expectedViewportLineCount = viewportHeight / lineHeight;
|
||||
sliderHeight = Math.floor(expectedViewportLineCount * minimapLineHeight / pixelRatio);
|
||||
}
|
||||
|
||||
let maxMinimapSliderTop: number;
|
||||
if (options.scrollBeyondLastLine) {
|
||||
// The minimap slider, when dragged all the way down, will contain the last line at its top
|
||||
maxMinimapSliderTop = (lineCount - 1) * minimapLineHeight / pixelRatio;
|
||||
} else {
|
||||
// The minimap slider, when dragged all the way down, will contain the last line at its bottom
|
||||
maxMinimapSliderTop = Math.max(0, lineCount * minimapLineHeight / pixelRatio - sliderHeight);
|
||||
}
|
||||
maxMinimapSliderTop = Math.min(options.minimapHeight - sliderHeight, maxMinimapSliderTop);
|
||||
|
||||
// The slider can move from 0 to `maxMinimapSliderTop`
|
||||
// in the same way `scrollTop` can move from 0 to `scrollHeight` - `viewportHeight`.
|
||||
const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight);
|
||||
const sliderTop = (scrollTop * computedSliderRatio);
|
||||
|
||||
if (minimapLinesFitting >= lineCount) {
|
||||
// All lines fit in the minimap
|
||||
const startLineNumber = 1;
|
||||
const endLineNumber = lineCount;
|
||||
|
||||
return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
|
||||
} else {
|
||||
let startLineNumber = Math.max(1, Math.floor(viewportStartLineNumber - sliderTop * pixelRatio / minimapLineHeight));
|
||||
|
||||
// Avoid flickering caused by a partial viewport start line
|
||||
// by being consistent w.r.t. the previous layout decision
|
||||
if (previousLayout && previousLayout.scrollHeight === scrollHeight) {
|
||||
if (previousLayout.scrollTop > scrollTop) {
|
||||
// Scrolling up => never increase `startLineNumber`
|
||||
startLineNumber = Math.min(startLineNumber, previousLayout.startLineNumber);
|
||||
}
|
||||
if (previousLayout.scrollTop < scrollTop) {
|
||||
// Scrolling down => never decrease `startLineNumber`
|
||||
startLineNumber = Math.max(startLineNumber, previousLayout.startLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1);
|
||||
|
||||
return new MinimapLayout(scrollTop, scrollHeight, computedSliderRatio, sliderTop, sliderHeight, startLineNumber, endLineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MinimapLine implements ILine {
|
||||
|
||||
public static INVALID = new MinimapLine(-1);
|
||||
|
||||
dy: number;
|
||||
|
||||
constructor(dy: number) {
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
public onContentChanged(): void {
|
||||
this.dy = -1;
|
||||
}
|
||||
|
||||
public onTokensChanged(): void {
|
||||
this.dy = -1;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderData {
|
||||
/**
|
||||
* last rendered layout.
|
||||
*/
|
||||
public readonly renderedLayout: MinimapLayout;
|
||||
private readonly _imageData: ImageData;
|
||||
private readonly _renderedLines: RenderedLinesCollection<MinimapLine>;
|
||||
|
||||
constructor(
|
||||
renderedLayout: MinimapLayout,
|
||||
imageData: ImageData,
|
||||
lines: MinimapLine[]
|
||||
) {
|
||||
this.renderedLayout = renderedLayout;
|
||||
this._imageData = imageData;
|
||||
this._renderedLines = new RenderedLinesCollection(
|
||||
() => MinimapLine.INVALID
|
||||
);
|
||||
this._renderedLines._set(renderedLayout.startLineNumber, lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current RenderData matches accurately the new desired layout and no painting is needed.
|
||||
*/
|
||||
public linesEquals(layout: MinimapLayout): boolean {
|
||||
if (this.renderedLayout.startLineNumber !== layout.startLineNumber) {
|
||||
return false;
|
||||
}
|
||||
if (this.renderedLayout.endLineNumber !== layout.endLineNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tmp = this._renderedLines._get();
|
||||
const lines = tmp.lines;
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
if (lines[i].dy === -1) {
|
||||
// This line is invalid
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_get(): { imageData: ImageData; rendLineNumberStart: number; lines: MinimapLine[]; } {
|
||||
let tmp = this._renderedLines._get();
|
||||
return {
|
||||
imageData: this._imageData,
|
||||
rendLineNumberStart: tmp.rendLineNumberStart,
|
||||
lines: tmp.lines
|
||||
};
|
||||
}
|
||||
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return this._renderedLines.onLinesChanged(e.fromLineNumber, e.toLineNumber);
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): void {
|
||||
this._renderedLines.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): void {
|
||||
this._renderedLines.onLinesInserted(e.fromLineNumber, e.toLineNumber);
|
||||
}
|
||||
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
|
||||
return this._renderedLines.onTokensChanged(e.ranges);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some sort of double buffering.
|
||||
*
|
||||
* Keeps two buffers around that will be rotated for painting.
|
||||
* Always gives a buffer that is filled with the background color.
|
||||
*/
|
||||
class MinimapBuffers {
|
||||
|
||||
private readonly _backgroundFillData: Uint8ClampedArray;
|
||||
private readonly _buffers: [ImageData, ImageData];
|
||||
private _lastUsedBuffer: number;
|
||||
|
||||
constructor(ctx: CanvasRenderingContext2D, WIDTH: number, HEIGHT: number, background: RGBA8) {
|
||||
this._backgroundFillData = MinimapBuffers._createBackgroundFillData(WIDTH, HEIGHT, background);
|
||||
this._buffers = [
|
||||
ctx.createImageData(WIDTH, HEIGHT),
|
||||
ctx.createImageData(WIDTH, HEIGHT)
|
||||
];
|
||||
this._lastUsedBuffer = 0;
|
||||
}
|
||||
|
||||
public getBuffer(): ImageData {
|
||||
// rotate buffers
|
||||
this._lastUsedBuffer = 1 - this._lastUsedBuffer;
|
||||
let result = this._buffers[this._lastUsedBuffer];
|
||||
|
||||
// fill with background color
|
||||
result.data.set(this._backgroundFillData);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _createBackgroundFillData(WIDTH: number, HEIGHT: number, background: RGBA8): Uint8ClampedArray {
|
||||
const backgroundR = background.r;
|
||||
const backgroundG = background.g;
|
||||
const backgroundB = background.b;
|
||||
|
||||
let result = new Uint8ClampedArray(WIDTH * HEIGHT * 4);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < HEIGHT; i++) {
|
||||
for (let j = 0; j < WIDTH; j++) {
|
||||
result[offset] = backgroundR;
|
||||
result[offset + 1] = backgroundG;
|
||||
result[offset + 2] = backgroundB;
|
||||
result[offset + 3] = 255;
|
||||
offset += 4;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class Minimap extends ViewPart {
|
||||
|
||||
private readonly _domNode: FastDomNode<HTMLElement>;
|
||||
private readonly _shadow: FastDomNode<HTMLElement>;
|
||||
private readonly _canvas: FastDomNode<HTMLCanvasElement>;
|
||||
private readonly _slider: FastDomNode<HTMLElement>;
|
||||
private readonly _sliderHorizontal: FastDomNode<HTMLElement>;
|
||||
private readonly _tokensColorTracker: MinimapTokensColorTracker;
|
||||
private readonly _mouseDownListener: IDisposable;
|
||||
private readonly _sliderMouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
|
||||
private readonly _sliderMouseDownListener: IDisposable;
|
||||
|
||||
private readonly _minimapCharRenderer: MinimapCharRenderer;
|
||||
|
||||
private _options: MinimapOptions;
|
||||
private _lastRenderData: RenderData;
|
||||
private _buffers: MinimapBuffers;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
|
||||
this._options = new MinimapOptions(this._context.configuration);
|
||||
this._lastRenderData = null;
|
||||
this._buffers = null;
|
||||
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
PartFingerprints.write(this._domNode, PartFingerprint.Minimap);
|
||||
this._domNode.setClassName(this._getMinimapDomNodeClassName());
|
||||
this._domNode.setPosition('absolute');
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
this._domNode.setRight(this._context.configuration.editor.layoutInfo.verticalScrollbarWidth);
|
||||
|
||||
this._shadow = createFastDomNode(document.createElement('div'));
|
||||
this._shadow.setClassName('minimap-shadow-hidden');
|
||||
this._domNode.appendChild(this._shadow);
|
||||
|
||||
this._canvas = createFastDomNode(document.createElement('canvas'));
|
||||
this._canvas.setPosition('absolute');
|
||||
this._canvas.setLeft(0);
|
||||
this._domNode.appendChild(this._canvas);
|
||||
|
||||
this._slider = createFastDomNode(document.createElement('div'));
|
||||
this._slider.setPosition('absolute');
|
||||
this._slider.setClassName('minimap-slider');
|
||||
this._slider.setLayerHinting(true);
|
||||
this._domNode.appendChild(this._slider);
|
||||
|
||||
this._sliderHorizontal = createFastDomNode(document.createElement('div'));
|
||||
this._sliderHorizontal.setPosition('absolute');
|
||||
this._sliderHorizontal.setClassName('minimap-slider-horizontal');
|
||||
this._slider.appendChild(this._sliderHorizontal);
|
||||
|
||||
this._tokensColorTracker = MinimapTokensColorTracker.getInstance();
|
||||
|
||||
this._minimapCharRenderer = getOrCreateMinimapCharRenderer();
|
||||
|
||||
this._applyLayout();
|
||||
|
||||
this._mouseDownListener = dom.addStandardDisposableListener(this._canvas.domNode, 'mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const renderMinimap = this._options.renderMinimap;
|
||||
if (renderMinimap === RenderMinimap.None) {
|
||||
return;
|
||||
}
|
||||
if (!this._lastRenderData) {
|
||||
return;
|
||||
}
|
||||
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
|
||||
const internalOffsetY = this._options.pixelRatio * e.browserEvent.offsetY;
|
||||
const lineIndex = Math.floor(internalOffsetY / minimapLineHeight);
|
||||
|
||||
let lineNumber = lineIndex + this._lastRenderData.renderedLayout.startLineNumber;
|
||||
lineNumber = Math.min(lineNumber, this._context.model.getLineCount());
|
||||
|
||||
this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent(
|
||||
new Range(lineNumber, 1, lineNumber, 1),
|
||||
viewEvents.VerticalRevealType.Center,
|
||||
false,
|
||||
editorCommon.ScrollType.Smooth
|
||||
));
|
||||
});
|
||||
|
||||
this._sliderMouseMoveMonitor = new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>();
|
||||
|
||||
this._sliderMouseDownListener = dom.addStandardDisposableListener(this._slider.domNode, 'mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
if (e.leftButton && this._lastRenderData) {
|
||||
|
||||
const initialMousePosition = e.posy;
|
||||
const initialMouseOrthogonalPosition = e.posx;
|
||||
const initialSliderState = this._lastRenderData.renderedLayout;
|
||||
this._slider.toggleClassName('active', true);
|
||||
|
||||
this._sliderMouseMoveMonitor.startMonitoring(
|
||||
standardMouseMoveMerger,
|
||||
(mouseMoveData: IStandardMouseMoveEventData) => {
|
||||
const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition);
|
||||
|
||||
if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
|
||||
// The mouse has wondered away from the scrollbar => reset dragging
|
||||
this._context.viewLayout.setScrollPositionNow({
|
||||
scrollTop: initialSliderState.scrollTop
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseDelta = mouseMoveData.posy - initialMousePosition;
|
||||
this._context.viewLayout.setScrollPositionNow({
|
||||
scrollTop: initialSliderState.getDesiredScrollTopFromDelta(mouseDelta)
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this._slider.toggleClassName('active', false);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._mouseDownListener.dispose();
|
||||
this._sliderMouseMoveMonitor.dispose();
|
||||
this._sliderMouseDownListener.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _getMinimapDomNodeClassName(): string {
|
||||
if (this._options.showSlider === 'always') {
|
||||
return 'minimap slider-always';
|
||||
}
|
||||
return 'minimap slider-mouseover';
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
private _applyLayout(): void {
|
||||
this._domNode.setWidth(this._options.minimapWidth);
|
||||
this._domNode.setHeight(this._options.minimapHeight);
|
||||
this._shadow.setHeight(this._options.minimapHeight);
|
||||
this._canvas.setWidth(this._options.canvasOuterWidth);
|
||||
this._canvas.setHeight(this._options.canvasOuterHeight);
|
||||
this._canvas.domNode.width = this._options.canvasInnerWidth;
|
||||
this._canvas.domNode.height = this._options.canvasInnerHeight;
|
||||
this._slider.setWidth(this._options.minimapWidth);
|
||||
}
|
||||
|
||||
private _getBuffer(): ImageData {
|
||||
if (!this._buffers) {
|
||||
this._buffers = new MinimapBuffers(
|
||||
this._canvas.domNode.getContext('2d'),
|
||||
this._options.canvasInnerWidth,
|
||||
this._options.canvasInnerHeight,
|
||||
this._tokensColorTracker.getColor(ColorId.DefaultBackground)
|
||||
);
|
||||
}
|
||||
return this._buffers.getBuffer();
|
||||
}
|
||||
|
||||
private _onOptionsMaybeChanged(): boolean {
|
||||
let opts = new MinimapOptions(this._context.configuration);
|
||||
if (this._options.equals(opts)) {
|
||||
return false;
|
||||
}
|
||||
this._options = opts;
|
||||
this._lastRenderData = null;
|
||||
this._buffers = null;
|
||||
this._applyLayout();
|
||||
this._domNode.setClassName(this._getMinimapDomNodeClassName());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
return this._onOptionsMaybeChanged();
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
this._lastRenderData = null;
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
if (this._lastRenderData) {
|
||||
return this._lastRenderData.onLinesChanged(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
if (this._lastRenderData) {
|
||||
this._lastRenderData.onLinesDeleted(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
if (this._lastRenderData) {
|
||||
this._lastRenderData.onLinesInserted(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
|
||||
if (this._lastRenderData) {
|
||||
return this._lastRenderData.onTokensChanged(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public onTokensColorsChanged(e: viewEvents.ViewTokensColorsChangedEvent): boolean {
|
||||
this._lastRenderData = null;
|
||||
this._buffers = null;
|
||||
return true;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
this._lastRenderData = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(renderingCtx: RestrictedRenderingContext): void {
|
||||
const renderMinimap = this._options.renderMinimap;
|
||||
if (renderMinimap === RenderMinimap.None) {
|
||||
this._shadow.setClassName('minimap-shadow-hidden');
|
||||
return;
|
||||
}
|
||||
if (renderingCtx.scrollLeft + renderingCtx.viewportWidth >= renderingCtx.scrollWidth) {
|
||||
this._shadow.setClassName('minimap-shadow-hidden');
|
||||
} else {
|
||||
this._shadow.setClassName('minimap-shadow-visible');
|
||||
}
|
||||
|
||||
const layout = MinimapLayout.create(
|
||||
this._options,
|
||||
renderingCtx.visibleRange.startLineNumber,
|
||||
renderingCtx.visibleRange.endLineNumber,
|
||||
renderingCtx.viewportHeight,
|
||||
(renderingCtx.viewportData.whitespaceViewportData.length > 0),
|
||||
this._context.model.getLineCount(),
|
||||
renderingCtx.scrollTop,
|
||||
renderingCtx.scrollHeight,
|
||||
this._lastRenderData ? this._lastRenderData.renderedLayout : null
|
||||
);
|
||||
this._slider.setTop(layout.sliderTop);
|
||||
this._slider.setHeight(layout.sliderHeight);
|
||||
|
||||
// Compute horizontal slider coordinates
|
||||
const scrollLeftChars = renderingCtx.scrollLeft / this._options.typicalHalfwidthCharacterWidth;
|
||||
const horizontalSliderLeft = Math.min(this._options.minimapWidth, Math.round(scrollLeftChars * getMinimapCharWidth(this._options.renderMinimap) / this._options.pixelRatio));
|
||||
this._sliderHorizontal.setLeft(horizontalSliderLeft);
|
||||
this._sliderHorizontal.setWidth(this._options.minimapWidth - horizontalSliderLeft);
|
||||
this._sliderHorizontal.setTop(0);
|
||||
this._sliderHorizontal.setHeight(layout.sliderHeight);
|
||||
|
||||
this._lastRenderData = this.renderLines(layout);
|
||||
}
|
||||
|
||||
private renderLines(layout: MinimapLayout): RenderData {
|
||||
const renderMinimap = this._options.renderMinimap;
|
||||
const startLineNumber = layout.startLineNumber;
|
||||
const endLineNumber = layout.endLineNumber;
|
||||
const minimapLineHeight = getMinimapLineHeight(renderMinimap);
|
||||
|
||||
// Check if nothing changed w.r.t. lines from last frame
|
||||
if (this._lastRenderData && this._lastRenderData.linesEquals(layout)) {
|
||||
const _lastData = this._lastRenderData._get();
|
||||
// Nice!! Nothing changed from last frame
|
||||
return new RenderData(layout, _lastData.imageData, _lastData.lines);
|
||||
}
|
||||
|
||||
// Oh well!! We need to repaint some lines...
|
||||
|
||||
const imageData = this._getBuffer();
|
||||
|
||||
// Render untouched lines by using last rendered data.
|
||||
let needed = Minimap._renderUntouchedLines(
|
||||
imageData,
|
||||
startLineNumber,
|
||||
endLineNumber,
|
||||
minimapLineHeight,
|
||||
this._lastRenderData
|
||||
);
|
||||
|
||||
// Fetch rendering info from view model for rest of lines that need rendering.
|
||||
const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed);
|
||||
const tabSize = lineInfo.tabSize;
|
||||
const background = this._tokensColorTracker.getColor(ColorId.DefaultBackground);
|
||||
const useLighterFont = this._tokensColorTracker.backgroundIsLight();
|
||||
|
||||
// Render the rest of lines
|
||||
let dy = 0;
|
||||
let renderedLines: MinimapLine[] = [];
|
||||
for (let lineIndex = 0, lineCount = endLineNumber - startLineNumber + 1; lineIndex < lineCount; lineIndex++) {
|
||||
if (needed[lineIndex]) {
|
||||
Minimap._renderLine(
|
||||
imageData,
|
||||
background,
|
||||
useLighterFont,
|
||||
renderMinimap,
|
||||
this._tokensColorTracker,
|
||||
this._minimapCharRenderer,
|
||||
dy,
|
||||
tabSize,
|
||||
lineInfo.data[lineIndex]
|
||||
);
|
||||
}
|
||||
renderedLines[lineIndex] = new MinimapLine(dy);
|
||||
dy += minimapLineHeight;
|
||||
}
|
||||
|
||||
// Finally, paint to the canvas
|
||||
const ctx = this._canvas.domNode.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Save rendered data for reuse on next frame if possible
|
||||
return new RenderData(
|
||||
layout,
|
||||
imageData,
|
||||
renderedLines
|
||||
);
|
||||
}
|
||||
|
||||
private static _renderUntouchedLines(
|
||||
target: ImageData,
|
||||
startLineNumber: number,
|
||||
endLineNumber: number,
|
||||
minimapLineHeight: number,
|
||||
lastRenderData: RenderData,
|
||||
): boolean[] {
|
||||
|
||||
let needed: boolean[] = [];
|
||||
if (!lastRenderData) {
|
||||
for (let i = 0, len = endLineNumber - startLineNumber + 1; i < len; i++) {
|
||||
needed[i] = true;
|
||||
}
|
||||
return needed;
|
||||
}
|
||||
|
||||
const _lastData = lastRenderData._get();
|
||||
const lastTargetData = _lastData.imageData.data;
|
||||
const lastStartLineNumber = _lastData.rendLineNumberStart;
|
||||
const lastLines = _lastData.lines;
|
||||
const lastLinesLength = lastLines.length;
|
||||
const WIDTH = target.width;
|
||||
const targetData = target.data;
|
||||
|
||||
let copySourceStart = -1;
|
||||
let copySourceEnd = -1;
|
||||
let copyDestStart = -1;
|
||||
let copyDestEnd = -1;
|
||||
|
||||
let dest_dy = 0;
|
||||
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
|
||||
const lineIndex = lineNumber - startLineNumber;
|
||||
const lastLineIndex = lineNumber - lastStartLineNumber;
|
||||
const source_dy = (lastLineIndex >= 0 && lastLineIndex < lastLinesLength ? lastLines[lastLineIndex].dy : -1);
|
||||
|
||||
if (source_dy === -1) {
|
||||
needed[lineIndex] = true;
|
||||
dest_dy += minimapLineHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
let sourceStart = source_dy * WIDTH * 4;
|
||||
let sourceEnd = (source_dy + minimapLineHeight) * WIDTH * 4;
|
||||
let destStart = dest_dy * WIDTH * 4;
|
||||
let destEnd = (dest_dy + minimapLineHeight) * WIDTH * 4;
|
||||
|
||||
if (copySourceEnd === sourceStart && copyDestEnd === destStart) {
|
||||
// contiguous zone => extend copy request
|
||||
copySourceEnd = sourceEnd;
|
||||
copyDestEnd = destEnd;
|
||||
} else {
|
||||
if (copySourceStart !== -1) {
|
||||
// flush existing copy request
|
||||
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
|
||||
}
|
||||
copySourceStart = sourceStart;
|
||||
copySourceEnd = sourceEnd;
|
||||
copyDestStart = destStart;
|
||||
copyDestEnd = destEnd;
|
||||
}
|
||||
|
||||
needed[lineIndex] = false;
|
||||
dest_dy += minimapLineHeight;
|
||||
}
|
||||
|
||||
if (copySourceStart !== -1) {
|
||||
// flush existing copy request
|
||||
targetData.set(lastTargetData.subarray(copySourceStart, copySourceEnd), copyDestStart);
|
||||
}
|
||||
|
||||
return needed;
|
||||
}
|
||||
|
||||
private static _renderLine(
|
||||
target: ImageData,
|
||||
backgroundColor: RGBA8,
|
||||
useLighterFont: boolean,
|
||||
renderMinimap: RenderMinimap,
|
||||
colorTracker: MinimapTokensColorTracker,
|
||||
minimapCharRenderer: MinimapCharRenderer,
|
||||
dy: number,
|
||||
tabSize: number,
|
||||
lineData: ViewLineData
|
||||
): void {
|
||||
const content = lineData.content;
|
||||
const tokens = lineData.tokens;
|
||||
const charWidth = getMinimapCharWidth(renderMinimap);
|
||||
const maxDx = target.width - charWidth;
|
||||
|
||||
let dx = 0;
|
||||
let charIndex = 0;
|
||||
let tabsCharDelta = 0;
|
||||
|
||||
for (let tokenIndex = 0, tokensLen = tokens.length; tokenIndex < tokensLen; tokenIndex++) {
|
||||
const token = tokens[tokenIndex];
|
||||
const tokenEndIndex = token.endIndex;
|
||||
const tokenColorId = token.getForeground();
|
||||
const tokenColor = colorTracker.getColor(tokenColorId);
|
||||
|
||||
for (; charIndex < tokenEndIndex; charIndex++) {
|
||||
if (dx > maxDx) {
|
||||
// hit edge of minimap
|
||||
return;
|
||||
}
|
||||
const charCode = content.charCodeAt(charIndex);
|
||||
|
||||
if (charCode === CharCode.Tab) {
|
||||
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
|
||||
tabsCharDelta += insertSpacesCount - 1;
|
||||
// No need to render anything since tab is invisible
|
||||
dx += insertSpacesCount * charWidth;
|
||||
} else if (charCode === CharCode.Space) {
|
||||
// No need to render anything since space is invisible
|
||||
dx += charWidth;
|
||||
} else {
|
||||
if (renderMinimap === RenderMinimap.Large) {
|
||||
minimapCharRenderer.x2RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
|
||||
} else if (renderMinimap === RenderMinimap.Small) {
|
||||
minimapCharRenderer.x1RenderChar(target, dx, dy, charCode, tokenColor, backgroundColor, useLighterFont);
|
||||
} else if (renderMinimap === RenderMinimap.LargeBlocks) {
|
||||
minimapCharRenderer.x2BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
|
||||
} else {
|
||||
// RenderMinimap.SmallBlocks
|
||||
minimapCharRenderer.x1BlockRenderChar(target, dx, dy, tokenColor, backgroundColor, useLighterFont);
|
||||
}
|
||||
dx += charWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const sliderBackground = theme.getColor(scrollbarSliderBackground);
|
||||
if (sliderBackground) {
|
||||
const halfSliderBackground = sliderBackground.transparent(0.5);
|
||||
collector.addRule(`.monaco-editor .minimap-slider, .monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${halfSliderBackground}; }`);
|
||||
}
|
||||
const sliderHoverBackground = theme.getColor(scrollbarSliderHoverBackground);
|
||||
if (sliderHoverBackground) {
|
||||
const halfSliderHoverBackground = sliderHoverBackground.transparent(0.5);
|
||||
collector.addRule(`.monaco-editor .minimap-slider:hover, .monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${halfSliderHoverBackground}; }`);
|
||||
}
|
||||
const sliderActiveBackground = theme.getColor(scrollbarSliderActiveBackground);
|
||||
if (sliderActiveBackground) {
|
||||
const halfSliderActiveBackground = sliderActiveBackground.transparent(0.5);
|
||||
collector.addRule(`.monaco-editor .minimap-slider.active, .monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${halfSliderActiveBackground}; }`);
|
||||
}
|
||||
const shadow = theme.getColor(scrollbarShadow);
|
||||
if (shadow) {
|
||||
collector.addRule(`.monaco-editor .minimap-shadow-visible { box-shadow: ${shadow} -6px 0 6px -6px inset; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.monaco-editor .overlayWidgets {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left:0;
|
||||
}
|
||||
152
src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts
Normal file
152
src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./overlayWidgets';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { IOverlayWidget, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
interface IWidgetData {
|
||||
widget: IOverlayWidget;
|
||||
preference: OverlayWidgetPositionPreference;
|
||||
domNode: FastDomNode<HTMLElement>;
|
||||
}
|
||||
|
||||
interface IWidgetMap {
|
||||
[key: string]: IWidgetData;
|
||||
}
|
||||
|
||||
export class ViewOverlayWidgets extends ViewPart {
|
||||
|
||||
private _widgets: IWidgetMap;
|
||||
private _domNode: FastDomNode<HTMLElement>;
|
||||
|
||||
private _verticalScrollbarWidth: number;
|
||||
private _minimapWidth: number;
|
||||
private _horizontalScrollbarHeight: number;
|
||||
private _editorHeight: number;
|
||||
private _editorWidth: number;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
|
||||
this._widgets = {};
|
||||
this._verticalScrollbarWidth = this._context.configuration.editor.layoutInfo.verticalScrollbarWidth;
|
||||
this._minimapWidth = this._context.configuration.editor.layoutInfo.minimapWidth;
|
||||
this._horizontalScrollbarHeight = this._context.configuration.editor.layoutInfo.horizontalScrollbarHeight;
|
||||
this._editorHeight = this._context.configuration.editor.layoutInfo.height;
|
||||
this._editorWidth = this._context.configuration.editor.layoutInfo.width;
|
||||
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
PartFingerprints.write(this._domNode, PartFingerprint.OverlayWidgets);
|
||||
this._domNode.setClassName('overlayWidgets');
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._widgets = null;
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.layoutInfo) {
|
||||
this._verticalScrollbarWidth = this._context.configuration.editor.layoutInfo.verticalScrollbarWidth;
|
||||
this._minimapWidth = this._context.configuration.editor.layoutInfo.minimapWidth;
|
||||
this._horizontalScrollbarHeight = this._context.configuration.editor.layoutInfo.horizontalScrollbarHeight;
|
||||
this._editorHeight = this._context.configuration.editor.layoutInfo.height;
|
||||
this._editorWidth = this._context.configuration.editor.layoutInfo.width;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
public addWidget(widget: IOverlayWidget): void {
|
||||
const domNode = createFastDomNode(widget.getDomNode());
|
||||
|
||||
this._widgets[widget.getId()] = {
|
||||
widget: widget,
|
||||
preference: null,
|
||||
domNode: domNode
|
||||
};
|
||||
|
||||
// This is sync because a widget wants to be in the dom
|
||||
domNode.setPosition('absolute');
|
||||
domNode.setAttribute('widgetId', widget.getId());
|
||||
this._domNode.appendChild(domNode);
|
||||
|
||||
this.setShouldRender();
|
||||
}
|
||||
|
||||
public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference): boolean {
|
||||
let widgetData = this._widgets[widget.getId()];
|
||||
if (widgetData.preference === preference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
widgetData.preference = preference;
|
||||
this.setShouldRender();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public removeWidget(widget: IOverlayWidget): void {
|
||||
let widgetId = widget.getId();
|
||||
if (this._widgets.hasOwnProperty(widgetId)) {
|
||||
const widgetData = this._widgets[widgetId];
|
||||
const domNode = widgetData.domNode.domNode;
|
||||
delete this._widgets[widgetId];
|
||||
|
||||
domNode.parentNode.removeChild(domNode);
|
||||
this.setShouldRender();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderWidget(widgetData: IWidgetData): void {
|
||||
const domNode = widgetData.domNode;
|
||||
|
||||
if (widgetData.preference === null) {
|
||||
domNode.unsetTop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) {
|
||||
domNode.setTop(0);
|
||||
domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth);
|
||||
} else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) {
|
||||
let widgetHeight = domNode.domNode.clientHeight;
|
||||
domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight));
|
||||
domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth);
|
||||
} else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) {
|
||||
domNode.setTop(0);
|
||||
domNode.domNode.style.right = '50%';
|
||||
}
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
this._domNode.setWidth(this._editorWidth);
|
||||
|
||||
let keys = Object.keys(this._widgets);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
let widgetId = keys[i];
|
||||
this._renderWidget(this._widgets[widgetId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { TokenizationRegistry } from 'vs/editor/common/modes';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
|
||||
import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ThemeColor } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class DecorationsOverviewRuler extends ViewPart {
|
||||
|
||||
static MIN_DECORATION_HEIGHT = 6;
|
||||
static MAX_DECORATION_HEIGHT = 60;
|
||||
|
||||
private readonly _tokensColorTrackerListener: IDisposable;
|
||||
|
||||
private _overviewRuler: OverviewRulerImpl;
|
||||
|
||||
private _renderBorder: boolean;
|
||||
private _borderColor: string;
|
||||
private _cursorColor: string;
|
||||
|
||||
private _shouldUpdateDecorations: boolean;
|
||||
private _shouldUpdateCursorPosition: boolean;
|
||||
|
||||
private _hideCursor: boolean;
|
||||
private _cursorPositions: Position[];
|
||||
|
||||
private _zonesFromDecorations: OverviewRulerZone[];
|
||||
private _zonesFromCursors: OverviewRulerZone[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
this._overviewRuler = new OverviewRulerImpl(
|
||||
1,
|
||||
'decorationsOverviewRuler',
|
||||
this._context.viewLayout.getScrollHeight(),
|
||||
this._context.configuration.editor.lineHeight,
|
||||
this._context.configuration.editor.pixelRatio,
|
||||
DecorationsOverviewRuler.MIN_DECORATION_HEIGHT,
|
||||
DecorationsOverviewRuler.MAX_DECORATION_HEIGHT,
|
||||
(lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber)
|
||||
);
|
||||
this._overviewRuler.setLanesCount(this._context.configuration.editor.viewInfo.overviewRulerLanes, false);
|
||||
this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false);
|
||||
|
||||
this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder;
|
||||
|
||||
this._updateColors();
|
||||
|
||||
this._updateBackground(false);
|
||||
this._tokensColorTrackerListener = TokenizationRegistry.onDidChange((e) => {
|
||||
if (e.changedColorMap) {
|
||||
this._updateBackground(true);
|
||||
}
|
||||
});
|
||||
|
||||
this._shouldUpdateDecorations = true;
|
||||
this._zonesFromDecorations = [];
|
||||
|
||||
this._shouldUpdateCursorPosition = true;
|
||||
this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler;
|
||||
|
||||
this._zonesFromCursors = [];
|
||||
this._cursorPositions = [];
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._overviewRuler.dispose();
|
||||
this._tokensColorTrackerListener.dispose();
|
||||
}
|
||||
|
||||
private _updateBackground(render: boolean): void {
|
||||
const minimapEnabled = this._context.configuration.editor.viewInfo.minimap.enabled;
|
||||
this._overviewRuler.setUseBackground((minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null), render);
|
||||
}
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
let prevLanesCount = this._overviewRuler.getLanesCount();
|
||||
let newLanesCount = this._context.configuration.editor.viewInfo.overviewRulerLanes;
|
||||
|
||||
if (prevLanesCount !== newLanesCount) {
|
||||
this._overviewRuler.setLanesCount(newLanesCount, false);
|
||||
}
|
||||
|
||||
if (e.lineHeight) {
|
||||
this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, false);
|
||||
}
|
||||
|
||||
if (e.pixelRatio) {
|
||||
this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, false);
|
||||
}
|
||||
|
||||
if (e.viewInfo) {
|
||||
this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder;
|
||||
this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler;
|
||||
this._shouldUpdateCursorPosition = true;
|
||||
this._updateBackground(false);
|
||||
}
|
||||
|
||||
if (e.layoutInfo) {
|
||||
this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
this._shouldUpdateCursorPosition = true;
|
||||
this._cursorPositions = [];
|
||||
for (let i = 0, len = e.selections.length; i < len; i++) {
|
||||
this._cursorPositions[i] = e.selections[i].getPosition();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
this._shouldUpdateDecorations = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
this._shouldUpdateCursorPosition = true;
|
||||
this._shouldUpdateDecorations = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
this._overviewRuler.setScrollHeight(e.scrollHeight, false);
|
||||
return super.onScrollChanged(e) || e.scrollHeightChanged;
|
||||
}
|
||||
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
|
||||
this._updateColors();
|
||||
this._shouldUpdateDecorations = true;
|
||||
this._shouldUpdateCursorPosition = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._overviewRuler.getDomNode();
|
||||
}
|
||||
|
||||
private _updateColors() {
|
||||
let borderColor = this._context.theme.getColor(editorOverviewRulerBorder);
|
||||
this._borderColor = borderColor ? borderColor.toString() : null;
|
||||
|
||||
let cursorColor = this._context.theme.getColor(editorCursorForeground);
|
||||
this._cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null;
|
||||
|
||||
this._overviewRuler.setThemeType(this._context.theme.type, false);
|
||||
}
|
||||
|
||||
private _createZonesFromDecorations(): OverviewRulerZone[] {
|
||||
let decorations = this._context.model.getAllOverviewRulerDecorations();
|
||||
let zones: OverviewRulerZone[] = [];
|
||||
|
||||
for (let i = 0, len = decorations.length; i < len; i++) {
|
||||
let dec = decorations[i];
|
||||
let overviewRuler = dec.source.options.overviewRuler;
|
||||
zones[i] = new OverviewRulerZone(
|
||||
dec.range.startLineNumber,
|
||||
dec.range.endLineNumber,
|
||||
overviewRuler.position,
|
||||
0,
|
||||
this.resolveRulerColor(overviewRuler.color),
|
||||
this.resolveRulerColor(overviewRuler.darkColor),
|
||||
this.resolveRulerColor(overviewRuler.hcColor)
|
||||
);
|
||||
}
|
||||
|
||||
return zones;
|
||||
}
|
||||
|
||||
private resolveRulerColor(color: string | ThemeColor): string {
|
||||
if (editorCommon.isThemeColor(color)) {
|
||||
let c = this._context.theme.getColor(color.id) || Color.transparent;
|
||||
return c.toString();
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
private _createZonesFromCursors(): OverviewRulerZone[] {
|
||||
let zones: OverviewRulerZone[] = [];
|
||||
|
||||
for (let i = 0, len = this._cursorPositions.length; i < len; i++) {
|
||||
let cursor = this._cursorPositions[i];
|
||||
|
||||
zones[i] = new OverviewRulerZone(
|
||||
cursor.lineNumber,
|
||||
cursor.lineNumber,
|
||||
editorCommon.OverviewRulerLane.Full,
|
||||
2,
|
||||
this._cursorColor,
|
||||
this._cursorColor,
|
||||
this._cursorColor
|
||||
);
|
||||
}
|
||||
|
||||
return zones;
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
if (this._shouldUpdateDecorations || this._shouldUpdateCursorPosition) {
|
||||
|
||||
if (this._shouldUpdateDecorations) {
|
||||
this._shouldUpdateDecorations = false;
|
||||
this._zonesFromDecorations = this._createZonesFromDecorations();
|
||||
}
|
||||
|
||||
if (this._shouldUpdateCursorPosition) {
|
||||
this._shouldUpdateCursorPosition = false;
|
||||
if (this._hideCursor) {
|
||||
this._zonesFromCursors = [];
|
||||
} else {
|
||||
this._zonesFromCursors = this._createZonesFromCursors();
|
||||
}
|
||||
}
|
||||
|
||||
let allZones: OverviewRulerZone[] = [];
|
||||
allZones = allZones.concat(this._zonesFromCursors);
|
||||
allZones = allZones.concat(this._zonesFromDecorations);
|
||||
|
||||
this._overviewRuler.setZones(allZones, false);
|
||||
}
|
||||
|
||||
let hasRendered = this._overviewRuler.render(false);
|
||||
|
||||
if (hasRendered && this._renderBorder && this._borderColor && this._overviewRuler.getLanesCount() > 0 && (this._zonesFromDecorations.length > 0 || this._zonesFromCursors.length > 0)) {
|
||||
let ctx2 = this._overviewRuler.getDomNode().getContext('2d');
|
||||
ctx2.beginPath();
|
||||
ctx2.lineWidth = 1;
|
||||
ctx2.strokeStyle = this._borderColor;
|
||||
ctx2.moveTo(0, 0);
|
||||
ctx2.lineTo(0, this._overviewRuler.getPixelHeight());
|
||||
ctx2.stroke();
|
||||
|
||||
ctx2.moveTo(0, 0);
|
||||
ctx2.lineTo(this._overviewRuler.getPixelWidth(), 0);
|
||||
ctx2.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
|
||||
import { IOverviewRuler } from 'vs/editor/browser/editorBrowser';
|
||||
import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { OverviewRulerPosition } from 'vs/editor/common/config/editorOptions';
|
||||
import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
|
||||
|
||||
export class OverviewRuler extends ViewEventHandler implements IOverviewRuler {
|
||||
|
||||
private _context: ViewContext;
|
||||
private _overviewRuler: OverviewRulerImpl;
|
||||
|
||||
constructor(context: ViewContext, cssClassName: string, minimumHeight: number, maximumHeight: number) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._overviewRuler = new OverviewRulerImpl(
|
||||
0,
|
||||
cssClassName,
|
||||
this._context.viewLayout.getScrollHeight(),
|
||||
this._context.configuration.editor.lineHeight,
|
||||
this._context.configuration.editor.pixelRatio,
|
||||
minimumHeight,
|
||||
maximumHeight,
|
||||
(lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber)
|
||||
);
|
||||
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._overviewRuler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, true);
|
||||
}
|
||||
|
||||
if (e.pixelRatio) {
|
||||
this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
this._overviewRuler.setScrollHeight(e.scrollHeight, true);
|
||||
return super.onScrollChanged(e) || e.scrollHeightChanged;
|
||||
}
|
||||
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._overviewRuler.getDomNode();
|
||||
}
|
||||
|
||||
public setLayout(position: OverviewRulerPosition): void {
|
||||
this._overviewRuler.setLayout(position, true);
|
||||
}
|
||||
|
||||
public setZones(zones: OverviewRulerZone[]): void {
|
||||
this._overviewRuler.setZones(zones, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { OverviewRulerLane } from 'vs/editor/common/editorCommon';
|
||||
import { OverviewZoneManager, ColorZone, OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { OverviewRulerPosition } from 'vs/editor/common/config/editorOptions';
|
||||
import { ThemeType, LIGHT } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class OverviewRulerImpl {
|
||||
|
||||
private _canvasLeftOffset: number;
|
||||
private _domNode: FastDomNode<HTMLCanvasElement>;
|
||||
private _lanesCount: number;
|
||||
private _zoneManager: OverviewZoneManager;
|
||||
private _background: Color;
|
||||
|
||||
constructor(
|
||||
canvasLeftOffset: number, cssClassName: string, scrollHeight: number, lineHeight: number,
|
||||
pixelRatio: number, minimumHeight: number, maximumHeight: number,
|
||||
getVerticalOffsetForLine: (lineNumber: number) => number
|
||||
) {
|
||||
this._canvasLeftOffset = canvasLeftOffset;
|
||||
|
||||
this._domNode = createFastDomNode(document.createElement('canvas'));
|
||||
|
||||
this._domNode.setClassName(cssClassName);
|
||||
this._domNode.setPosition('absolute');
|
||||
this._domNode.setLayerHinting(true);
|
||||
|
||||
this._lanesCount = 3;
|
||||
|
||||
this._background = null;
|
||||
|
||||
this._zoneManager = new OverviewZoneManager(getVerticalOffsetForLine);
|
||||
this._zoneManager.setMinimumHeight(minimumHeight);
|
||||
this._zoneManager.setMaximumHeight(maximumHeight);
|
||||
this._zoneManager.setThemeType(LIGHT);
|
||||
this._zoneManager.setDOMWidth(0);
|
||||
this._zoneManager.setDOMHeight(0);
|
||||
this._zoneManager.setOuterHeight(scrollHeight);
|
||||
this._zoneManager.setLineHeight(lineHeight);
|
||||
|
||||
this._zoneManager.setPixelRatio(pixelRatio);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._zoneManager = null;
|
||||
}
|
||||
|
||||
public setLayout(position: OverviewRulerPosition, render: boolean): void {
|
||||
this._domNode.setTop(position.top);
|
||||
this._domNode.setRight(position.right);
|
||||
|
||||
let hasChanged = false;
|
||||
hasChanged = this._zoneManager.setDOMWidth(position.width) || hasChanged;
|
||||
hasChanged = this._zoneManager.setDOMHeight(position.height) || hasChanged;
|
||||
|
||||
if (hasChanged) {
|
||||
this._domNode.setWidth(this._zoneManager.getDOMWidth());
|
||||
this._domNode.setHeight(this._zoneManager.getDOMHeight());
|
||||
this._domNode.domNode.width = this._zoneManager.getCanvasWidth();
|
||||
this._domNode.domNode.height = this._zoneManager.getCanvasHeight();
|
||||
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getLanesCount(): number {
|
||||
return this._lanesCount;
|
||||
}
|
||||
|
||||
public setLanesCount(newLanesCount: number, render: boolean): void {
|
||||
this._lanesCount = newLanesCount;
|
||||
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public setThemeType(themeType: ThemeType, render: boolean): void {
|
||||
this._zoneManager.setThemeType(themeType);
|
||||
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public setUseBackground(background: Color, render: boolean): void {
|
||||
this._background = background;
|
||||
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLCanvasElement {
|
||||
return this._domNode.domNode;
|
||||
}
|
||||
|
||||
public getPixelWidth(): number {
|
||||
return this._zoneManager.getCanvasWidth();
|
||||
}
|
||||
|
||||
public getPixelHeight(): number {
|
||||
return this._zoneManager.getCanvasHeight();
|
||||
}
|
||||
|
||||
public setScrollHeight(scrollHeight: number, render: boolean): void {
|
||||
this._zoneManager.setOuterHeight(scrollHeight);
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public setLineHeight(lineHeight: number, render: boolean): void {
|
||||
this._zoneManager.setLineHeight(lineHeight);
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public setPixelRatio(pixelRatio: number, render: boolean): void {
|
||||
this._zoneManager.setPixelRatio(pixelRatio);
|
||||
this._domNode.setWidth(this._zoneManager.getDOMWidth());
|
||||
this._domNode.setHeight(this._zoneManager.getDOMHeight());
|
||||
this._domNode.domNode.width = this._zoneManager.getCanvasWidth();
|
||||
this._domNode.domNode.height = this._zoneManager.getCanvasHeight();
|
||||
if (render) {
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
public setZones(zones: OverviewRulerZone[], render: boolean): void {
|
||||
this._zoneManager.setZones(zones);
|
||||
if (render) {
|
||||
this.render(false);
|
||||
}
|
||||
}
|
||||
|
||||
public render(forceRender: boolean): boolean {
|
||||
if (this._zoneManager.getOuterHeight() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const width = this._zoneManager.getCanvasWidth();
|
||||
const height = this._zoneManager.getCanvasHeight();
|
||||
|
||||
let colorZones = this._zoneManager.resolveColorZones();
|
||||
let id2Color = this._zoneManager.getId2Color();
|
||||
|
||||
let ctx = this._domNode.domNode.getContext('2d');
|
||||
if (this._background === null) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
} else {
|
||||
ctx.fillStyle = Color.Format.CSS.formatHex(this._background);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
if (colorZones.length > 0) {
|
||||
let remainingWidth = width - this._canvasLeftOffset;
|
||||
|
||||
if (this._lanesCount >= 3) {
|
||||
this._renderThreeLanes(ctx, colorZones, id2Color, remainingWidth);
|
||||
} else if (this._lanesCount === 2) {
|
||||
this._renderTwoLanes(ctx, colorZones, id2Color, remainingWidth);
|
||||
} else if (this._lanesCount === 1) {
|
||||
this._renderOneLane(ctx, colorZones, id2Color, remainingWidth);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _renderOneLane(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
|
||||
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left | OverviewRulerLane.Center | OverviewRulerLane.Right, this._canvasLeftOffset, w);
|
||||
|
||||
}
|
||||
|
||||
private _renderTwoLanes(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
|
||||
|
||||
let leftWidth = Math.floor(w / 2);
|
||||
let rightWidth = w - leftWidth;
|
||||
let leftOffset = this._canvasLeftOffset;
|
||||
let rightOffset = this._canvasLeftOffset + leftWidth;
|
||||
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left | OverviewRulerLane.Center, leftOffset, leftWidth);
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Right, rightOffset, rightWidth);
|
||||
}
|
||||
|
||||
private _renderThreeLanes(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], w: number): void {
|
||||
|
||||
let leftWidth = Math.floor(w / 3);
|
||||
let rightWidth = Math.floor(w / 3);
|
||||
let centerWidth = w - leftWidth - rightWidth;
|
||||
let leftOffset = this._canvasLeftOffset;
|
||||
let centerOffset = this._canvasLeftOffset + leftWidth;
|
||||
let rightOffset = this._canvasLeftOffset + leftWidth + centerWidth;
|
||||
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Left, leftOffset, leftWidth);
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Center, centerOffset, centerWidth);
|
||||
this._renderVerticalPatch(ctx, colorZones, id2Color, OverviewRulerLane.Right, rightOffset, rightWidth);
|
||||
}
|
||||
|
||||
private _renderVerticalPatch(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], laneMask: number, xpos: number, width: number): void {
|
||||
|
||||
let currentColorId = 0;
|
||||
let currentFrom = 0;
|
||||
let currentTo = 0;
|
||||
|
||||
for (let i = 0, len = colorZones.length; i < len; i++) {
|
||||
let zone = colorZones[i];
|
||||
|
||||
if (!(zone.position & laneMask)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let zoneColorId = zone.colorId;
|
||||
let zoneFrom = zone.from;
|
||||
let zoneTo = zone.to;
|
||||
|
||||
if (zoneColorId !== currentColorId) {
|
||||
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
|
||||
|
||||
currentColorId = zoneColorId;
|
||||
ctx.fillStyle = id2Color[currentColorId];
|
||||
currentFrom = zoneFrom;
|
||||
currentTo = zoneTo;
|
||||
} else {
|
||||
if (currentTo >= zoneFrom) {
|
||||
currentTo = Math.max(currentTo, zoneTo);
|
||||
} else {
|
||||
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
|
||||
currentFrom = zoneFrom;
|
||||
currentTo = zoneTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillRect(xpos, currentFrom, width, currentTo - currentFrom);
|
||||
|
||||
}
|
||||
}
|
||||
9
src/vs/editor/browser/viewParts/rulers/rulers.css
Normal file
9
src/vs/editor/browser/viewParts/rulers/rulers.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .view-ruler {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
112
src/vs/editor/browser/viewParts/rulers/rulers.ts
Normal file
112
src/vs/editor/browser/viewParts/rulers/rulers.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./rulers';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorRuler } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
export class Rulers extends ViewPart {
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
private _renderedRulers: FastDomNode<HTMLElement>[];
|
||||
private _rulers: number[];
|
||||
private _height: number;
|
||||
private _typicalHalfwidthCharacterWidth: number;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
this.domNode = createFastDomNode<HTMLElement>(document.createElement('div'));
|
||||
this.domNode.setAttribute('role', 'presentation');
|
||||
this.domNode.setAttribute('aria-hidden', 'true');
|
||||
this.domNode.setClassName('view-rulers');
|
||||
this._renderedRulers = [];
|
||||
this._rulers = this._context.configuration.editor.viewInfo.rulers;
|
||||
this._height = this._context.configuration.editor.layoutInfo.contentHeight;
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.viewInfo || e.layoutInfo || e.fontInfo) {
|
||||
this._rulers = this._context.configuration.editor.viewInfo.rulers;
|
||||
this._height = this._context.configuration.editor.layoutInfo.contentHeight;
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollHeightChanged;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
private _ensureRulersCount(): void {
|
||||
const currentCount = this._renderedRulers.length;
|
||||
const desiredCount = this._rulers.length;
|
||||
|
||||
if (currentCount === desiredCount) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCount < desiredCount) {
|
||||
const rulerWidth = dom.computeScreenAwareSize(1);
|
||||
let addCount = desiredCount - currentCount;
|
||||
while (addCount > 0) {
|
||||
let node = createFastDomNode(document.createElement('div'));
|
||||
node.setClassName('view-ruler');
|
||||
node.setWidth(rulerWidth);
|
||||
this.domNode.appendChild(node);
|
||||
this._renderedRulers.push(node);
|
||||
addCount--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let removeCount = currentCount - desiredCount;
|
||||
while (removeCount > 0) {
|
||||
let node = this._renderedRulers.pop();
|
||||
this.domNode.removeChild(node);
|
||||
removeCount--;
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
|
||||
this._ensureRulersCount();
|
||||
|
||||
for (let i = 0, len = this._rulers.length; i < len; i++) {
|
||||
let node = this._renderedRulers[i];
|
||||
|
||||
node.setHeight(Math.min(ctx.scrollHeight, 1000000));
|
||||
node.setLeft(this._rulers[i] * this._typicalHalfwidthCharacterWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let rulerColor = theme.getColor(editorRuler);
|
||||
if (rulerColor) {
|
||||
collector.addRule(`.monaco-editor .view-ruler { background-color: ${rulerColor}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .scroll-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./scrollDecoration';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { scrollbarShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
export class ScrollDecorationViewPart extends ViewPart {
|
||||
|
||||
private _domNode: FastDomNode<HTMLElement>;
|
||||
private _scrollTop: number;
|
||||
private _width: number;
|
||||
private _shouldShow: boolean;
|
||||
private _useShadows: boolean;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
|
||||
this._scrollTop = 0;
|
||||
this._width = 0;
|
||||
this._updateWidth();
|
||||
this._shouldShow = false;
|
||||
this._useShadows = this._context.configuration.editor.viewInfo.scrollbar.useShadows;
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _updateShouldShow(): boolean {
|
||||
let newShouldShow = (this._useShadows && this._scrollTop > 0);
|
||||
if (this._shouldShow !== newShouldShow) {
|
||||
this._shouldShow = newShouldShow;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
private _updateWidth(): boolean {
|
||||
const layoutInfo = this._context.configuration.editor.layoutInfo;
|
||||
let newWidth = layoutInfo.width - layoutInfo.minimapWidth;
|
||||
if (this._width !== newWidth) {
|
||||
this._width = newWidth;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
let shouldRender = false;
|
||||
if (e.viewInfo) {
|
||||
this._useShadows = this._context.configuration.editor.viewInfo.scrollbar.useShadows;
|
||||
}
|
||||
if (e.layoutInfo) {
|
||||
shouldRender = this._updateWidth();
|
||||
}
|
||||
return this._updateShouldShow() || shouldRender;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
this._scrollTop = e.scrollTop;
|
||||
return this._updateShouldShow();
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
this._domNode.setWidth(this._width);
|
||||
this._domNode.setClassName(this._shouldShow ? 'scroll-decoration' : '');
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let shadow = theme.getColor(scrollbarShadow);
|
||||
if (shadow) {
|
||||
collector.addRule(`.monaco-editor .scroll-decoration { box-shadow: ${shadow} 0 6px 6px -6px inset; }`);
|
||||
}
|
||||
});
|
||||
22
src/vs/editor/browser/viewParts/selections/selections.css
Normal file
22
src/vs/editor/browser/viewParts/selections/selections.css
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
Keeping name short for faster parsing.
|
||||
cslr = core selections layer rendering (div)
|
||||
*/
|
||||
.monaco-editor .lines-content .cslr {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-editor .top-left-radius { border-top-left-radius: 3px; }
|
||||
.monaco-editor .bottom-left-radius { border-bottom-left-radius: 3px; }
|
||||
.monaco-editor .top-right-radius { border-top-right-radius: 3px; }
|
||||
.monaco-editor .bottom-right-radius { border-bottom-right-radius: 3px; }
|
||||
|
||||
.monaco-editor.hc-black .top-left-radius { border-top-left-radius: 0; }
|
||||
.monaco-editor.hc-black .bottom-left-radius { border-bottom-left-radius: 0; }
|
||||
.monaco-editor.hc-black .top-right-radius { border-top-right-radius: 0; }
|
||||
.monaco-editor.hc-black .bottom-right-radius { border-bottom-right-radius: 0; }
|
||||
409
src/vs/editor/browser/viewParts/selections/selections.ts
Normal file
409
src/vs/editor/browser/viewParts/selections/selections.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./selections';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorSelectionBackground, editorInactiveSelection, editorSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { HorizontalRange, LineVisibleRanges, RenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
const enum CornerStyle {
|
||||
EXTERN,
|
||||
INTERN,
|
||||
FLAT
|
||||
}
|
||||
|
||||
interface IVisibleRangeEndPointStyle {
|
||||
top: CornerStyle;
|
||||
bottom: CornerStyle;
|
||||
}
|
||||
|
||||
class HorizontalRangeWithStyle {
|
||||
public left: number;
|
||||
public width: number;
|
||||
public startStyle: IVisibleRangeEndPointStyle;
|
||||
public endStyle: IVisibleRangeEndPointStyle;
|
||||
|
||||
constructor(other: HorizontalRange) {
|
||||
this.left = other.left;
|
||||
this.width = other.width;
|
||||
this.startStyle = null;
|
||||
this.endStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
class LineVisibleRangesWithStyle {
|
||||
public lineNumber: number;
|
||||
public ranges: HorizontalRangeWithStyle[];
|
||||
|
||||
constructor(lineNumber: number, ranges: HorizontalRangeWithStyle[]) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.ranges = ranges;
|
||||
}
|
||||
}
|
||||
|
||||
function toStyledRange(item: HorizontalRange): HorizontalRangeWithStyle {
|
||||
return new HorizontalRangeWithStyle(item);
|
||||
}
|
||||
|
||||
function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle {
|
||||
return new LineVisibleRangesWithStyle(item.lineNumber, item.ranges.map(toStyledRange));
|
||||
}
|
||||
|
||||
// TODO@Alex: Remove this once IE11 fixes Bug #524217
|
||||
// The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density.
|
||||
// Unfortunately, this auto-zooming is buggy around dealing with rounded borders
|
||||
const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeOrIE;
|
||||
|
||||
|
||||
export class SelectionsOverlay extends DynamicViewOverlay {
|
||||
|
||||
private static SELECTION_CLASS_NAME = 'selected-text';
|
||||
private static SELECTION_TOP_LEFT = 'top-left-radius';
|
||||
private static SELECTION_BOTTOM_LEFT = 'bottom-left-radius';
|
||||
private static SELECTION_TOP_RIGHT = 'top-right-radius';
|
||||
private static SELECTION_BOTTOM_RIGHT = 'bottom-right-radius';
|
||||
private static EDITOR_BACKGROUND_CLASS_NAME = 'monaco-editor-background';
|
||||
|
||||
private static ROUNDED_PIECE_WIDTH = 10;
|
||||
|
||||
private _context: ViewContext;
|
||||
private _lineHeight: number;
|
||||
private _roundedSelection: boolean;
|
||||
private _selections: Range[];
|
||||
private _renderResult: string[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection;
|
||||
this._selections = [];
|
||||
this._renderResult = null;
|
||||
this._context.addEventHandler(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._context.removeEventHandler(this);
|
||||
this._context = null;
|
||||
this._selections = null;
|
||||
this._renderResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._roundedSelection = this._context.configuration.editor.viewInfo.roundedSelection;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
this._selections = e.selections.slice(0);
|
||||
return true;
|
||||
}
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
// true for inline decorations that can end up relayouting text
|
||||
return true;//e.inlineDecorationsChanged;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
private _visibleRangesHaveGaps(linesVisibleRanges: LineVisibleRangesWithStyle[]): boolean {
|
||||
|
||||
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
|
||||
let lineVisibleRanges = linesVisibleRanges[i];
|
||||
|
||||
if (lineVisibleRanges.ranges.length > 1) {
|
||||
// There are two ranges on the same line
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _enrichVisibleRangesWithStyle(linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[]): void {
|
||||
let previousFrameTop: HorizontalRangeWithStyle = null;
|
||||
let previousFrameBottom: HorizontalRangeWithStyle = null;
|
||||
|
||||
if (previousFrame && previousFrame.length > 0 && linesVisibleRanges.length > 0) {
|
||||
|
||||
let topLineNumber = linesVisibleRanges[0].lineNumber;
|
||||
for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) {
|
||||
if (previousFrame[i].lineNumber === topLineNumber) {
|
||||
previousFrameTop = previousFrame[i].ranges[0];
|
||||
}
|
||||
}
|
||||
|
||||
let bottomLineNumber = linesVisibleRanges[linesVisibleRanges.length - 1].lineNumber;
|
||||
for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) {
|
||||
if (previousFrame[i].lineNumber === bottomLineNumber) {
|
||||
previousFrameBottom = previousFrame[i].ranges[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (previousFrameTop && !previousFrameTop.startStyle) {
|
||||
previousFrameTop = null;
|
||||
}
|
||||
if (previousFrameBottom && !previousFrameBottom.startStyle) {
|
||||
previousFrameBottom = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
|
||||
// We know for a fact that there is precisely one range on each line
|
||||
let curLineRange = linesVisibleRanges[i].ranges[0];
|
||||
let curLeft = curLineRange.left;
|
||||
let curRight = curLineRange.left + curLineRange.width;
|
||||
|
||||
let startStyle = {
|
||||
top: CornerStyle.EXTERN,
|
||||
bottom: CornerStyle.EXTERN
|
||||
};
|
||||
|
||||
let endStyle = {
|
||||
top: CornerStyle.EXTERN,
|
||||
bottom: CornerStyle.EXTERN
|
||||
};
|
||||
|
||||
if (i > 0) {
|
||||
// Look above
|
||||
let prevLeft = linesVisibleRanges[i - 1].ranges[0].left;
|
||||
let prevRight = linesVisibleRanges[i - 1].ranges[0].left + linesVisibleRanges[i - 1].ranges[0].width;
|
||||
|
||||
if (curLeft === prevLeft) {
|
||||
startStyle.top = CornerStyle.FLAT;
|
||||
} else if (curLeft > prevLeft) {
|
||||
startStyle.top = CornerStyle.INTERN;
|
||||
}
|
||||
|
||||
if (curRight === prevRight) {
|
||||
endStyle.top = CornerStyle.FLAT;
|
||||
} else if (prevLeft < curRight && curRight < prevRight) {
|
||||
endStyle.top = CornerStyle.INTERN;
|
||||
}
|
||||
} else if (previousFrameTop) {
|
||||
// Accept some hick-ups near the viewport edges to save on repaints
|
||||
startStyle.top = previousFrameTop.startStyle.top;
|
||||
endStyle.top = previousFrameTop.endStyle.top;
|
||||
}
|
||||
|
||||
if (i + 1 < len) {
|
||||
// Look below
|
||||
let nextLeft = linesVisibleRanges[i + 1].ranges[0].left;
|
||||
let nextRight = linesVisibleRanges[i + 1].ranges[0].left + linesVisibleRanges[i + 1].ranges[0].width;
|
||||
|
||||
if (curLeft === nextLeft) {
|
||||
startStyle.bottom = CornerStyle.FLAT;
|
||||
} else if (nextLeft < curLeft && curLeft < nextRight) {
|
||||
startStyle.bottom = CornerStyle.INTERN;
|
||||
}
|
||||
|
||||
if (curRight === nextRight) {
|
||||
endStyle.bottom = CornerStyle.FLAT;
|
||||
} else if (curRight < nextRight) {
|
||||
endStyle.bottom = CornerStyle.INTERN;
|
||||
}
|
||||
} else if (previousFrameBottom) {
|
||||
// Accept some hick-ups near the viewport edges to save on repaints
|
||||
startStyle.bottom = previousFrameBottom.startStyle.bottom;
|
||||
endStyle.bottom = previousFrameBottom.endStyle.bottom;
|
||||
}
|
||||
|
||||
curLineRange.startStyle = startStyle;
|
||||
curLineRange.endStyle = endStyle;
|
||||
}
|
||||
}
|
||||
|
||||
private _getVisibleRangesWithStyle(selection: Range, ctx: RenderingContext, previousFrame: LineVisibleRangesWithStyle[]): LineVisibleRangesWithStyle[] {
|
||||
let _linesVisibleRanges = ctx.linesVisibleRangesForRange(selection, true) || [];
|
||||
let linesVisibleRanges = _linesVisibleRanges.map(toStyled);
|
||||
let visibleRangesHaveGaps = this._visibleRangesHaveGaps(linesVisibleRanges);
|
||||
|
||||
if (!isIEWithZoomingIssuesNearRoundedBorders && !visibleRangesHaveGaps && this._roundedSelection) {
|
||||
this._enrichVisibleRangesWithStyle(linesVisibleRanges, previousFrame);
|
||||
}
|
||||
|
||||
// The visible ranges are sorted TOP-BOTTOM and LEFT-RIGHT
|
||||
return linesVisibleRanges;
|
||||
}
|
||||
|
||||
private _createSelectionPiece(top: number, height: string, className: string, left: number, width: number): string {
|
||||
return (
|
||||
'<div class="cslr '
|
||||
+ className
|
||||
+ '" style="top:'
|
||||
+ top.toString()
|
||||
+ 'px;left:'
|
||||
+ left.toString()
|
||||
+ 'px;width:'
|
||||
+ width.toString()
|
||||
+ 'px;height:'
|
||||
+ height
|
||||
+ 'px;"></div>'
|
||||
);
|
||||
}
|
||||
|
||||
private _actualRenderOneSelection(output2: string[], visibleStartLineNumber: number, hasMultipleSelections: boolean, visibleRanges: LineVisibleRangesWithStyle[]): void {
|
||||
let visibleRangesHaveStyle = (visibleRanges.length > 0 && visibleRanges[0].ranges[0].startStyle);
|
||||
let fullLineHeight = (this._lineHeight).toString();
|
||||
let reducedLineHeight = (this._lineHeight - 1).toString();
|
||||
|
||||
let firstLineNumber = (visibleRanges.length > 0 ? visibleRanges[0].lineNumber : 0);
|
||||
let lastLineNumber = (visibleRanges.length > 0 ? visibleRanges[visibleRanges.length - 1].lineNumber : 0);
|
||||
|
||||
for (let i = 0, len = visibleRanges.length; i < len; i++) {
|
||||
let lineVisibleRanges = visibleRanges[i];
|
||||
let lineNumber = lineVisibleRanges.lineNumber;
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
|
||||
let lineHeight = hasMultipleSelections ? (lineNumber === lastLineNumber || lineNumber === firstLineNumber ? reducedLineHeight : fullLineHeight) : fullLineHeight;
|
||||
let top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0;
|
||||
|
||||
let lineOutput = '';
|
||||
|
||||
for (let j = 0, lenJ = lineVisibleRanges.ranges.length; j < lenJ; j++) {
|
||||
let visibleRange = lineVisibleRanges.ranges[j];
|
||||
|
||||
if (visibleRangesHaveStyle) {
|
||||
if (visibleRange.startStyle.top === CornerStyle.INTERN || visibleRange.startStyle.bottom === CornerStyle.INTERN) {
|
||||
// Reverse rounded corner to the left
|
||||
|
||||
// First comes the selection (blue layer)
|
||||
lineOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
|
||||
|
||||
// Second comes the background (white layer) with inverse border radius
|
||||
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
|
||||
if (visibleRange.startStyle.top === CornerStyle.INTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
|
||||
}
|
||||
if (visibleRange.startStyle.bottom === CornerStyle.INTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
|
||||
}
|
||||
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
|
||||
}
|
||||
if (visibleRange.endStyle.top === CornerStyle.INTERN || visibleRange.endStyle.bottom === CornerStyle.INTERN) {
|
||||
// Reverse rounded corner to the right
|
||||
|
||||
// First comes the selection (blue layer)
|
||||
lineOutput += this._createSelectionPiece(top, lineHeight, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
|
||||
|
||||
// Second comes the background (white layer) with inverse border radius
|
||||
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
|
||||
if (visibleRange.endStyle.top === CornerStyle.INTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
|
||||
}
|
||||
if (visibleRange.endStyle.bottom === CornerStyle.INTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
|
||||
}
|
||||
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
let className = SelectionsOverlay.SELECTION_CLASS_NAME;
|
||||
if (visibleRangesHaveStyle) {
|
||||
if (visibleRange.startStyle.top === CornerStyle.EXTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
|
||||
}
|
||||
if (visibleRange.startStyle.bottom === CornerStyle.EXTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
|
||||
}
|
||||
if (visibleRange.endStyle.top === CornerStyle.EXTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
|
||||
}
|
||||
if (visibleRange.endStyle.bottom === CornerStyle.EXTERN) {
|
||||
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
|
||||
}
|
||||
}
|
||||
lineOutput += this._createSelectionPiece(top, lineHeight, className, visibleRange.left, visibleRange.width);
|
||||
}
|
||||
|
||||
output2[lineIndex] += lineOutput;
|
||||
}
|
||||
}
|
||||
|
||||
private _previousFrameVisibleRangesWithStyle: LineVisibleRangesWithStyle[][] = [];
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
|
||||
let output: string[] = [];
|
||||
let visibleStartLineNumber = ctx.visibleRange.startLineNumber;
|
||||
let visibleEndLineNumber = ctx.visibleRange.endLineNumber;
|
||||
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
|
||||
let lineIndex = lineNumber - visibleStartLineNumber;
|
||||
output[lineIndex] = '';
|
||||
}
|
||||
|
||||
let thisFrameVisibleRangesWithStyle: LineVisibleRangesWithStyle[][] = [];
|
||||
for (let i = 0, len = this._selections.length; i < len; i++) {
|
||||
let selection = this._selections[i];
|
||||
if (selection.isEmpty()) {
|
||||
thisFrameVisibleRangesWithStyle[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
let visibleRangesWithStyle = this._getVisibleRangesWithStyle(selection, ctx, this._previousFrameVisibleRangesWithStyle[i]);
|
||||
thisFrameVisibleRangesWithStyle[i] = visibleRangesWithStyle;
|
||||
this._actualRenderOneSelection(output, visibleStartLineNumber, this._selections.length > 1, visibleRangesWithStyle);
|
||||
}
|
||||
|
||||
this._previousFrameVisibleRangesWithStyle = thisFrameVisibleRangesWithStyle;
|
||||
this._renderResult = output;
|
||||
}
|
||||
|
||||
public render(startLineNumber: number, lineNumber: number): string {
|
||||
if (!this._renderResult) {
|
||||
return '';
|
||||
}
|
||||
let lineIndex = lineNumber - startLineNumber;
|
||||
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
|
||||
throw new Error('Unexpected render request');
|
||||
}
|
||||
return this._renderResult[lineIndex];
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let editorSelectionColor = theme.getColor(editorSelectionBackground);
|
||||
if (editorSelectionColor) {
|
||||
collector.addRule(`.monaco-editor .focused .selected-text { background-color: ${editorSelectionColor}; }`);
|
||||
}
|
||||
let editorInactiveSelectionColor = theme.getColor(editorInactiveSelection);
|
||||
if (editorInactiveSelectionColor) {
|
||||
collector.addRule(`.monaco-editor .selected-text { background-color: ${editorInactiveSelectionColor}; }`);
|
||||
}
|
||||
let editorSelectionForegroundColor = theme.getColor(editorSelectionForeground);
|
||||
if (editorSelectionForegroundColor) {
|
||||
collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`);
|
||||
}
|
||||
});
|
||||
203
src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts
Normal file
203
src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
|
||||
import { Configuration } from 'vs/editor/browser/config/configuration';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
export interface IViewCursorRenderData {
|
||||
domNode: HTMLElement;
|
||||
position: Position;
|
||||
contentLeft: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
class ViewCursorRenderData {
|
||||
public readonly top: number;
|
||||
public readonly left: number;
|
||||
public readonly width: number;
|
||||
public readonly textContent: string;
|
||||
|
||||
constructor(top: number, left: number, width: number, textContent: string) {
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
this.width = width;
|
||||
this.textContent = textContent;
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewCursor {
|
||||
private readonly _context: ViewContext;
|
||||
private readonly _isSecondary: boolean;
|
||||
private readonly _domNode: FastDomNode<HTMLElement>;
|
||||
|
||||
private _cursorStyle: TextEditorCursorStyle;
|
||||
private _lineHeight: number;
|
||||
private _typicalHalfwidthCharacterWidth: number;
|
||||
|
||||
private _isVisible: boolean;
|
||||
|
||||
private _position: Position;
|
||||
private _isInEditableRange: boolean;
|
||||
|
||||
private _lastRenderedContent: string;
|
||||
private _renderData: ViewCursorRenderData;
|
||||
|
||||
constructor(context: ViewContext, isSecondary: boolean) {
|
||||
this._context = context;
|
||||
this._isSecondary = isSecondary;
|
||||
|
||||
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
// Create the dom node
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
if (this._isSecondary) {
|
||||
this._domNode.setClassName('cursor secondary');
|
||||
} else {
|
||||
this._domNode.setClassName('cursor');
|
||||
}
|
||||
this._domNode.setHeight(this._lineHeight);
|
||||
this._domNode.setTop(0);
|
||||
this._domNode.setLeft(0);
|
||||
Configuration.applyFontInfo(this._domNode, this._context.configuration.editor.fontInfo);
|
||||
this._domNode.setDisplay('none');
|
||||
|
||||
this.updatePosition(new Position(1, 1));
|
||||
this._isInEditableRange = true;
|
||||
|
||||
this._lastRenderedContent = '';
|
||||
this._renderData = null;
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public getIsInEditableRange(): boolean {
|
||||
return this._isInEditableRange;
|
||||
}
|
||||
|
||||
public getPosition(): Position {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
if (!this._isVisible) {
|
||||
this._domNode.setVisibility('inherit');
|
||||
this._isVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._isVisible) {
|
||||
this._domNode.setVisibility('hidden');
|
||||
this._isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
|
||||
}
|
||||
if (e.fontInfo) {
|
||||
Configuration.applyFontInfo(this._domNode, this._context.configuration.editor.fontInfo);
|
||||
this._typicalHalfwidthCharacterWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public onCursorPositionChanged(position: Position, isInEditableRange: boolean): boolean {
|
||||
this.updatePosition(position);
|
||||
this._isInEditableRange = isInEditableRange;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _prepareRender(ctx: RenderingContext): ViewCursorRenderData {
|
||||
if (this._cursorStyle === TextEditorCursorStyle.Line || this._cursorStyle === TextEditorCursorStyle.LineThin) {
|
||||
const visibleRange = ctx.visibleRangeForPosition(this._position);
|
||||
if (!visibleRange) {
|
||||
// Outside viewport
|
||||
return null;
|
||||
}
|
||||
let width: number;
|
||||
if (this._cursorStyle === TextEditorCursorStyle.Line) {
|
||||
width = dom.computeScreenAwareSize(2);
|
||||
} else {
|
||||
width = dom.computeScreenAwareSize(1);
|
||||
}
|
||||
const top = ctx.getVerticalOffsetForLineNumber(this._position.lineNumber) - ctx.bigNumbersDelta;
|
||||
return new ViewCursorRenderData(top, visibleRange.left, width, '');
|
||||
}
|
||||
|
||||
const visibleRangeForCharacter = ctx.linesVisibleRangesForRange(new Range(this._position.lineNumber, this._position.column, this._position.lineNumber, this._position.column + 1), false);
|
||||
|
||||
if (!visibleRangeForCharacter || visibleRangeForCharacter.length === 0 || visibleRangeForCharacter[0].ranges.length === 0) {
|
||||
// Outside viewport
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = visibleRangeForCharacter[0].ranges[0];
|
||||
const width = range.width < 1 ? this._typicalHalfwidthCharacterWidth : range.width;
|
||||
|
||||
let textContent = '';
|
||||
if (this._cursorStyle === TextEditorCursorStyle.Block) {
|
||||
const lineContent = this._context.model.getLineContent(this._position.lineNumber);
|
||||
textContent = lineContent.charAt(this._position.column - 1);
|
||||
}
|
||||
|
||||
const top = ctx.getVerticalOffsetForLineNumber(this._position.lineNumber) - ctx.bigNumbersDelta;
|
||||
return new ViewCursorRenderData(top, range.left, width, textContent);
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
this._renderData = this._prepareRender(ctx);
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): IViewCursorRenderData {
|
||||
if (!this._renderData) {
|
||||
this._domNode.setDisplay('none');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._lastRenderedContent !== this._renderData.textContent) {
|
||||
this._lastRenderedContent = this._renderData.textContent;
|
||||
this._domNode.domNode.textContent = this._lastRenderedContent;
|
||||
}
|
||||
|
||||
this._domNode.setDisplay('block');
|
||||
this._domNode.setTop(this._renderData.top);
|
||||
this._domNode.setLeft(this._renderData.left);
|
||||
this._domNode.setWidth(this._renderData.width);
|
||||
this._domNode.setLineHeight(this._lineHeight);
|
||||
this._domNode.setHeight(this._lineHeight);
|
||||
|
||||
return {
|
||||
domNode: this._domNode.domNode,
|
||||
position: this._position,
|
||||
contentLeft: this._renderData.left,
|
||||
height: this._lineHeight,
|
||||
width: 2
|
||||
};
|
||||
}
|
||||
|
||||
private updatePosition(newPosition: Position): void {
|
||||
this._position = newPosition;
|
||||
}
|
||||
}
|
||||
85
src/vs/editor/browser/viewParts/viewCursors/viewCursors.css
Normal file
85
src/vs/editor/browser/viewParts/viewCursors/viewCursors.css
Normal file
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.monaco-editor .cursors-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .cursors-layer > .cursor {
|
||||
position: absolute;
|
||||
cursor: text;
|
||||
}
|
||||
.monaco-editor .cursors-layer > .cursor.secondary {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* -- block-outline-style -- */
|
||||
.monaco-editor .cursors-layer.cursor-block-outline-style > .cursor {
|
||||
box-sizing: border-box;
|
||||
background: transparent !important;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* -- underline-style -- */
|
||||
.monaco-editor .cursors-layer.cursor-underline-style > .cursor {
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
background: transparent !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* -- underline-thin-style -- */
|
||||
.monaco-editor .cursors-layer.cursor-underline-thin-style > .cursor {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
background: transparent !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes monaco-cursor-smooth {
|
||||
0%,
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
60%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes monaco-cursor-phase {
|
||||
0%,
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
90%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes monaco-cursor-expand {
|
||||
0%,
|
||||
20% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
80%,
|
||||
100% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-smooth {
|
||||
animation: monaco-cursor-smooth 0.5s ease-in-out 0s 20 alternate;
|
||||
}
|
||||
|
||||
.cursor-phase {
|
||||
animation: monaco-cursor-phase 0.5s ease-in-out 0s 20 alternate;
|
||||
}
|
||||
|
||||
.cursor-expand > .cursor {
|
||||
animation: monaco-cursor-expand 0.5s ease-in-out 0s 20 alternate;
|
||||
}
|
||||
368
src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts
Normal file
368
src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./viewCursors';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { TimeoutTimer, IntervalTimer } from 'vs/base/common/async';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorCursorForeground, editorCursorBackground } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { TextEditorCursorBlinkingStyle, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export class ViewCursors extends ViewPart {
|
||||
|
||||
static BLINK_INTERVAL = 500;
|
||||
|
||||
private _readOnly: boolean;
|
||||
private _cursorBlinking: TextEditorCursorBlinkingStyle;
|
||||
private _cursorStyle: TextEditorCursorStyle;
|
||||
private _selectionIsEmpty: boolean;
|
||||
|
||||
private _isVisible: boolean;
|
||||
|
||||
private _domNode: FastDomNode<HTMLElement>;
|
||||
|
||||
private _startCursorBlinkAnimation: TimeoutTimer;
|
||||
private _cursorFlatBlinkInterval: IntervalTimer;
|
||||
private _blinkingEnabled: boolean;
|
||||
|
||||
private _editorHasFocus: boolean;
|
||||
|
||||
private _primaryCursor: ViewCursor;
|
||||
private _secondaryCursors: ViewCursor[];
|
||||
private _renderData: IViewCursorRenderData[];
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
|
||||
this._readOnly = this._context.configuration.editor.readOnly;
|
||||
this._cursorBlinking = this._context.configuration.editor.viewInfo.cursorBlinking;
|
||||
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
|
||||
this._selectionIsEmpty = true;
|
||||
|
||||
this._primaryCursor = new ViewCursor(this._context, false);
|
||||
this._secondaryCursors = [];
|
||||
this._renderData = [];
|
||||
|
||||
this._domNode = createFastDomNode(document.createElement('div'));
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
this._updateDomClassName();
|
||||
|
||||
this._domNode.appendChild(this._primaryCursor.getDomNode());
|
||||
|
||||
this._startCursorBlinkAnimation = new TimeoutTimer();
|
||||
this._cursorFlatBlinkInterval = new IntervalTimer();
|
||||
|
||||
this._blinkingEnabled = false;
|
||||
|
||||
this._editorHasFocus = false;
|
||||
this._updateBlinking();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._startCursorBlinkAnimation.dispose();
|
||||
this._cursorFlatBlinkInterval.dispose();
|
||||
}
|
||||
|
||||
public getDomNode(): FastDomNode<HTMLElement> {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
// --- begin event handlers
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
|
||||
if (e.readOnly) {
|
||||
this._readOnly = this._context.configuration.editor.readOnly;
|
||||
}
|
||||
if (e.viewInfo) {
|
||||
this._cursorBlinking = this._context.configuration.editor.viewInfo.cursorBlinking;
|
||||
this._cursorStyle = this._context.configuration.editor.viewInfo.cursorStyle;
|
||||
}
|
||||
|
||||
this._primaryCursor.onConfigurationChanged(e);
|
||||
this._updateBlinking();
|
||||
if (e.viewInfo) {
|
||||
this._updateDomClassName();
|
||||
}
|
||||
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
|
||||
this._secondaryCursors[i].onConfigurationChanged(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private _onCursorPositionChanged(position: Position, secondaryPositions: Position[], isInEditableRange: boolean): void {
|
||||
this._primaryCursor.onCursorPositionChanged(position, isInEditableRange);
|
||||
this._updateBlinking();
|
||||
|
||||
if (this._secondaryCursors.length < secondaryPositions.length) {
|
||||
// Create new cursors
|
||||
let addCnt = secondaryPositions.length - this._secondaryCursors.length;
|
||||
for (let i = 0; i < addCnt; i++) {
|
||||
let newCursor = new ViewCursor(this._context, true);
|
||||
this._domNode.domNode.insertBefore(newCursor.getDomNode().domNode, this._primaryCursor.getDomNode().domNode.nextSibling);
|
||||
this._secondaryCursors.push(newCursor);
|
||||
}
|
||||
} else if (this._secondaryCursors.length > secondaryPositions.length) {
|
||||
// Remove some cursors
|
||||
let removeCnt = this._secondaryCursors.length - secondaryPositions.length;
|
||||
for (let i = 0; i < removeCnt; i++) {
|
||||
this._domNode.removeChild(this._secondaryCursors[0].getDomNode());
|
||||
this._secondaryCursors.splice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < secondaryPositions.length; i++) {
|
||||
this._secondaryCursors[i].onCursorPositionChanged(secondaryPositions[i], isInEditableRange);
|
||||
}
|
||||
|
||||
}
|
||||
public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
|
||||
let positions: Position[] = [];
|
||||
for (let i = 0, len = e.selections.length; i < len; i++) {
|
||||
positions[i] = e.selections[i].getPosition();
|
||||
}
|
||||
this._onCursorPositionChanged(positions[0], positions.slice(1), e.isInEditableRange);
|
||||
|
||||
const selectionIsEmpty = e.selections[0].isEmpty();
|
||||
if (this._selectionIsEmpty !== selectionIsEmpty) {
|
||||
this._selectionIsEmpty = selectionIsEmpty;
|
||||
this._updateDomClassName();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
|
||||
// true for inline decorations that can end up relayouting text
|
||||
return true;
|
||||
}
|
||||
public onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean {
|
||||
this._editorHasFocus = e.isFocused;
|
||||
this._updateBlinking();
|
||||
return false;
|
||||
}
|
||||
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
|
||||
let shouldRender = (position: Position) => {
|
||||
for (let i = 0, len = e.ranges.length; i < len; i++) {
|
||||
if (e.ranges[i].fromLineNumber <= position.lineNumber && position.lineNumber <= e.ranges[i].toLineNumber) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (shouldRender(this._primaryCursor.getPosition())) {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < this._secondaryCursors.length; i++) {
|
||||
if (shouldRender(this._secondaryCursors[i].getPosition())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- end event handlers
|
||||
|
||||
public getPosition(): Position {
|
||||
return this._primaryCursor.getPosition();
|
||||
}
|
||||
|
||||
// ---- blinking logic
|
||||
|
||||
private _getCursorBlinking(): TextEditorCursorBlinkingStyle {
|
||||
if (!this._editorHasFocus) {
|
||||
return TextEditorCursorBlinkingStyle.Hidden;
|
||||
}
|
||||
if (this._readOnly || !this._primaryCursor.getIsInEditableRange()) {
|
||||
return TextEditorCursorBlinkingStyle.Solid;
|
||||
}
|
||||
return this._cursorBlinking;
|
||||
}
|
||||
|
||||
private _updateBlinking(): void {
|
||||
this._startCursorBlinkAnimation.cancel();
|
||||
this._cursorFlatBlinkInterval.cancel();
|
||||
|
||||
let blinkingStyle = this._getCursorBlinking();
|
||||
|
||||
// hidden and solid are special as they involve no animations
|
||||
let isHidden = (blinkingStyle === TextEditorCursorBlinkingStyle.Hidden);
|
||||
let isSolid = (blinkingStyle === TextEditorCursorBlinkingStyle.Solid);
|
||||
|
||||
if (isHidden) {
|
||||
this._hide();
|
||||
} else {
|
||||
this._show();
|
||||
}
|
||||
|
||||
this._blinkingEnabled = false;
|
||||
this._updateDomClassName();
|
||||
|
||||
if (!isHidden && !isSolid) {
|
||||
if (blinkingStyle === TextEditorCursorBlinkingStyle.Blink) {
|
||||
// flat blinking is handled by JavaScript to save battery life due to Chromium step timing issue https://bugs.chromium.org/p/chromium/issues/detail?id=361587
|
||||
this._cursorFlatBlinkInterval.cancelAndSet(() => {
|
||||
if (this._isVisible) {
|
||||
this._hide();
|
||||
} else {
|
||||
this._show();
|
||||
}
|
||||
}, ViewCursors.BLINK_INTERVAL);
|
||||
} else {
|
||||
this._startCursorBlinkAnimation.setIfNotSet(() => {
|
||||
this._blinkingEnabled = true;
|
||||
this._updateDomClassName();
|
||||
}, ViewCursors.BLINK_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- end blinking logic
|
||||
|
||||
private _updateDomClassName(): void {
|
||||
this._domNode.setClassName(this._getClassName());
|
||||
}
|
||||
|
||||
private _getClassName(): string {
|
||||
let result = 'cursors-layer';
|
||||
if (!this._selectionIsEmpty) {
|
||||
result += ' has-selection';
|
||||
}
|
||||
switch (this._cursorStyle) {
|
||||
case TextEditorCursorStyle.Line:
|
||||
result += ' cursor-line-style';
|
||||
break;
|
||||
case TextEditorCursorStyle.Block:
|
||||
result += ' cursor-block-style';
|
||||
break;
|
||||
case TextEditorCursorStyle.Underline:
|
||||
result += ' cursor-underline-style';
|
||||
break;
|
||||
case TextEditorCursorStyle.LineThin:
|
||||
result += ' cursor-line-thin-style';
|
||||
break;
|
||||
case TextEditorCursorStyle.BlockOutline:
|
||||
result += ' cursor-block-outline-style';
|
||||
break;
|
||||
case TextEditorCursorStyle.UnderlineThin:
|
||||
result += ' cursor-underline-thin-style';
|
||||
break;
|
||||
default:
|
||||
result += ' cursor-line-style';
|
||||
}
|
||||
if (this._blinkingEnabled) {
|
||||
switch (this._getCursorBlinking()) {
|
||||
case TextEditorCursorBlinkingStyle.Blink:
|
||||
result += ' cursor-blink';
|
||||
break;
|
||||
case TextEditorCursorBlinkingStyle.Smooth:
|
||||
result += ' cursor-smooth';
|
||||
break;
|
||||
case TextEditorCursorBlinkingStyle.Phase:
|
||||
result += ' cursor-phase';
|
||||
break;
|
||||
case TextEditorCursorBlinkingStyle.Expand:
|
||||
result += ' cursor-expand';
|
||||
break;
|
||||
case TextEditorCursorBlinkingStyle.Solid:
|
||||
result += ' cursor-solid';
|
||||
break;
|
||||
default:
|
||||
result += ' cursor-solid';
|
||||
}
|
||||
} else {
|
||||
result += ' cursor-solid';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _show(): void {
|
||||
this._primaryCursor.show();
|
||||
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
|
||||
this._secondaryCursors[i].show();
|
||||
}
|
||||
this._isVisible = true;
|
||||
}
|
||||
|
||||
private _hide(): void {
|
||||
this._primaryCursor.hide();
|
||||
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
|
||||
this._secondaryCursors[i].hide();
|
||||
}
|
||||
this._isVisible = false;
|
||||
}
|
||||
|
||||
// ---- IViewPart implementation
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
this._primaryCursor.prepareRender(ctx);
|
||||
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
|
||||
this._secondaryCursors[i].prepareRender(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
let renderData: IViewCursorRenderData[] = [], renderDataLen = 0;
|
||||
|
||||
const primaryRenderData = this._primaryCursor.render(ctx);
|
||||
if (primaryRenderData) {
|
||||
renderData[renderDataLen++] = primaryRenderData;
|
||||
}
|
||||
|
||||
for (let i = 0, len = this._secondaryCursors.length; i < len; i++) {
|
||||
const secondaryRenderData = this._secondaryCursors[i].render(ctx);
|
||||
if (secondaryRenderData) {
|
||||
renderData[renderDataLen++] = secondaryRenderData;
|
||||
}
|
||||
}
|
||||
|
||||
this._renderData = renderData;
|
||||
}
|
||||
|
||||
public getLastRenderData(): IViewCursorRenderData[] {
|
||||
return this._renderData;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let caret = theme.getColor(editorCursorForeground);
|
||||
if (caret) {
|
||||
let caretBackground = theme.getColor(editorCursorBackground);
|
||||
if (!caretBackground) {
|
||||
caretBackground = caret.opposite();
|
||||
}
|
||||
collector.addRule(`.monaco-editor .cursor { background-color: ${caret}; border-color: ${caret}; color: ${caretBackground}; }`);
|
||||
if (theme.type === 'hc') {
|
||||
collector.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${caretBackground}; border-right: 1px solid ${caretBackground}; }`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
353
src/vs/editor/browser/viewParts/viewZones/viewZones.ts
Normal file
353
src/vs/editor/browser/viewParts/viewZones/viewZones.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
|
||||
import { IViewZone } from 'vs/editor/browser/editorBrowser';
|
||||
import { ViewPart } from 'vs/editor/browser/view/viewPart';
|
||||
import { ViewContext } from 'vs/editor/common/view/viewContext';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext';
|
||||
import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel';
|
||||
import * as viewEvents from 'vs/editor/common/view/viewEvents';
|
||||
|
||||
export interface IMyViewZone {
|
||||
whitespaceId: number;
|
||||
delegate: IViewZone;
|
||||
isVisible: boolean;
|
||||
domNode: FastDomNode<HTMLElement>;
|
||||
marginDomNode: FastDomNode<HTMLElement>;
|
||||
}
|
||||
|
||||
export interface IMyRenderData {
|
||||
data: IViewWhitespaceViewportData[];
|
||||
}
|
||||
|
||||
interface IComputedViewZoneProps {
|
||||
afterViewLineNumber: number;
|
||||
heightInPx: number;
|
||||
}
|
||||
|
||||
export class ViewZones extends ViewPart {
|
||||
|
||||
private _zones: { [id: string]: IMyViewZone; };
|
||||
private _lineHeight: number;
|
||||
private _contentWidth: number;
|
||||
private _contentLeft: number;
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
|
||||
public marginDomNode: FastDomNode<HTMLElement>;
|
||||
|
||||
constructor(context: ViewContext) {
|
||||
super(context);
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
|
||||
this.domNode = createFastDomNode(document.createElement('div'));
|
||||
this.domNode.setClassName('view-zones');
|
||||
this.domNode.setPosition('absolute');
|
||||
this.domNode.setAttribute('role', 'presentation');
|
||||
this.domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this.marginDomNode = createFastDomNode(document.createElement('div'));
|
||||
this.marginDomNode.setClassName('margin-view-zones');
|
||||
this.marginDomNode.setPosition('absolute');
|
||||
this.marginDomNode.setAttribute('role', 'presentation');
|
||||
this.marginDomNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._zones = {};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._zones = {};
|
||||
}
|
||||
|
||||
// ---- begin view event handlers
|
||||
|
||||
private _recomputeWhitespacesProps(): boolean {
|
||||
let hadAChange = false;
|
||||
|
||||
let keys = Object.keys(this._zones);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
let id = keys[i];
|
||||
let zone = this._zones[id];
|
||||
let props = this._computeWhitespaceProps(zone.delegate);
|
||||
if (this._context.viewLayout.changeWhitespace(parseInt(id, 10), props.afterViewLineNumber, props.heightInPx)) {
|
||||
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
|
||||
hadAChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hadAChange;
|
||||
}
|
||||
|
||||
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
|
||||
|
||||
if (e.lineHeight) {
|
||||
this._lineHeight = this._context.configuration.editor.lineHeight;
|
||||
return this._recomputeWhitespacesProps();
|
||||
}
|
||||
|
||||
if (e.layoutInfo) {
|
||||
this._contentWidth = this._context.configuration.editor.layoutInfo.contentWidth;
|
||||
this._contentLeft = this._context.configuration.editor.layoutInfo.contentLeft;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean {
|
||||
return this._recomputeWhitespacesProps();
|
||||
}
|
||||
|
||||
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
|
||||
return e.scrollTopChanged || e.scrollWidthChanged;
|
||||
}
|
||||
|
||||
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- end view event handlers
|
||||
|
||||
private _getZoneOrdinal(zone: IViewZone): number {
|
||||
|
||||
if (typeof zone.afterColumn !== 'undefined') {
|
||||
return zone.afterColumn;
|
||||
}
|
||||
|
||||
return 10000;
|
||||
}
|
||||
|
||||
|
||||
private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps {
|
||||
if (zone.afterLineNumber === 0) {
|
||||
return {
|
||||
afterViewLineNumber: 0,
|
||||
heightInPx: this._heightInPixels(zone)
|
||||
};
|
||||
}
|
||||
|
||||
let zoneAfterModelPosition: Position;
|
||||
if (typeof zone.afterColumn !== 'undefined') {
|
||||
zoneAfterModelPosition = this._context.model.validateModelPosition({
|
||||
lineNumber: zone.afterLineNumber,
|
||||
column: zone.afterColumn
|
||||
});
|
||||
} else {
|
||||
let validAfterLineNumber = this._context.model.validateModelPosition({
|
||||
lineNumber: zone.afterLineNumber,
|
||||
column: 1
|
||||
}).lineNumber;
|
||||
|
||||
zoneAfterModelPosition = new Position(
|
||||
validAfterLineNumber,
|
||||
this._context.model.getModelLineMaxColumn(validAfterLineNumber)
|
||||
);
|
||||
}
|
||||
|
||||
let zoneBeforeModelPosition: Position;
|
||||
if (zoneAfterModelPosition.column === this._context.model.getModelLineMaxColumn(zoneAfterModelPosition.lineNumber)) {
|
||||
zoneBeforeModelPosition = this._context.model.validateModelPosition({
|
||||
lineNumber: zoneAfterModelPosition.lineNumber + 1,
|
||||
column: 1
|
||||
});
|
||||
} else {
|
||||
zoneBeforeModelPosition = this._context.model.validateModelPosition({
|
||||
lineNumber: zoneAfterModelPosition.lineNumber,
|
||||
column: zoneAfterModelPosition.column + 1
|
||||
});
|
||||
}
|
||||
|
||||
let viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(zoneAfterModelPosition);
|
||||
let isVisible = this._context.model.coordinatesConverter.modelPositionIsVisible(zoneBeforeModelPosition);
|
||||
return {
|
||||
afterViewLineNumber: viewPosition.lineNumber,
|
||||
heightInPx: (isVisible ? this._heightInPixels(zone) : 0)
|
||||
};
|
||||
}
|
||||
|
||||
public addZone(zone: IViewZone): number {
|
||||
let props = this._computeWhitespaceProps(zone);
|
||||
let whitespaceId = this._context.viewLayout.addWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx);
|
||||
|
||||
let myZone: IMyViewZone = {
|
||||
whitespaceId: whitespaceId,
|
||||
delegate: zone,
|
||||
isVisible: false,
|
||||
domNode: createFastDomNode(zone.domNode),
|
||||
marginDomNode: zone.marginDomNode ? createFastDomNode(zone.marginDomNode) : null
|
||||
};
|
||||
|
||||
this._safeCallOnComputedHeight(myZone.delegate, props.heightInPx);
|
||||
|
||||
myZone.domNode.setPosition('absolute');
|
||||
myZone.domNode.domNode.style.width = '100%';
|
||||
myZone.domNode.setDisplay('none');
|
||||
myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString());
|
||||
this.domNode.appendChild(myZone.domNode);
|
||||
|
||||
if (myZone.marginDomNode) {
|
||||
myZone.marginDomNode.setPosition('absolute');
|
||||
myZone.marginDomNode.domNode.style.width = '100%';
|
||||
myZone.marginDomNode.setDisplay('none');
|
||||
myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString());
|
||||
this.marginDomNode.appendChild(myZone.marginDomNode);
|
||||
}
|
||||
|
||||
this._zones[myZone.whitespaceId.toString()] = myZone;
|
||||
|
||||
|
||||
this.setShouldRender();
|
||||
|
||||
return myZone.whitespaceId;
|
||||
}
|
||||
|
||||
public removeZone(id: number): boolean {
|
||||
if (this._zones.hasOwnProperty(id.toString())) {
|
||||
let zone = this._zones[id.toString()];
|
||||
delete this._zones[id.toString()];
|
||||
this._context.viewLayout.removeWhitespace(zone.whitespaceId);
|
||||
|
||||
zone.domNode.removeAttribute('monaco-visible-view-zone');
|
||||
zone.domNode.removeAttribute('monaco-view-zone');
|
||||
zone.domNode.domNode.parentNode.removeChild(zone.domNode.domNode);
|
||||
|
||||
if (zone.marginDomNode) {
|
||||
zone.marginDomNode.removeAttribute('monaco-visible-view-zone');
|
||||
zone.marginDomNode.removeAttribute('monaco-view-zone');
|
||||
zone.marginDomNode.domNode.parentNode.removeChild(zone.marginDomNode.domNode);
|
||||
}
|
||||
|
||||
this.setShouldRender();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public layoutZone(id: number): boolean {
|
||||
let changed = false;
|
||||
if (this._zones.hasOwnProperty(id.toString())) {
|
||||
let zone = this._zones[id.toString()];
|
||||
let props = this._computeWhitespaceProps(zone.delegate);
|
||||
// let newOrdinal = this._getZoneOrdinal(zone.delegate);
|
||||
changed = this._context.viewLayout.changeWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx) || changed;
|
||||
// TODO@Alex: change `newOrdinal` too
|
||||
|
||||
if (changed) {
|
||||
this._safeCallOnComputedHeight(zone.delegate, props.heightInPx);
|
||||
this.setShouldRender();
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public shouldSuppressMouseDownOnViewZone(id: number): boolean {
|
||||
if (this._zones.hasOwnProperty(id.toString())) {
|
||||
let zone = this._zones[id.toString()];
|
||||
return zone.delegate.suppressMouseDown;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _heightInPixels(zone: IViewZone): number {
|
||||
if (typeof zone.heightInPx === 'number') {
|
||||
return zone.heightInPx;
|
||||
}
|
||||
if (typeof zone.heightInLines === 'number') {
|
||||
return this._lineHeight * zone.heightInLines;
|
||||
}
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
private _safeCallOnComputedHeight(zone: IViewZone, height: number): void {
|
||||
if (typeof zone.onComputedHeight === 'function') {
|
||||
try {
|
||||
zone.onComputedHeight(height);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _safeCallOnDomNodeTop(zone: IViewZone, top: number): void {
|
||||
if (typeof zone.onDomNodeTop === 'function') {
|
||||
try {
|
||||
zone.onDomNodeTop(top);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public prepareRender(ctx: RenderingContext): void {
|
||||
// Nothing to read
|
||||
}
|
||||
|
||||
public render(ctx: RestrictedRenderingContext): void {
|
||||
const visibleWhitespaces = ctx.viewportData.whitespaceViewportData;
|
||||
let visibleZones: { [id: string]: IViewWhitespaceViewportData; } = {};
|
||||
|
||||
let hasVisibleZone = false;
|
||||
for (let i = 0, len = visibleWhitespaces.length; i < len; i++) {
|
||||
visibleZones[visibleWhitespaces[i].id.toString()] = visibleWhitespaces[i];
|
||||
hasVisibleZone = true;
|
||||
}
|
||||
|
||||
let keys = Object.keys(this._zones);
|
||||
for (let i = 0, len = keys.length; i < len; i++) {
|
||||
let id = keys[i];
|
||||
let zone = this._zones[id];
|
||||
|
||||
let newTop = 0;
|
||||
let newHeight = 0;
|
||||
let newDisplay = 'none';
|
||||
if (visibleZones.hasOwnProperty(id)) {
|
||||
newTop = visibleZones[id].verticalOffset - ctx.bigNumbersDelta;
|
||||
newHeight = visibleZones[id].height;
|
||||
newDisplay = 'block';
|
||||
// zone is visible
|
||||
if (!zone.isVisible) {
|
||||
zone.domNode.setAttribute('monaco-visible-view-zone', 'true');
|
||||
zone.isVisible = true;
|
||||
}
|
||||
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(visibleZones[id].verticalOffset));
|
||||
} else {
|
||||
if (zone.isVisible) {
|
||||
zone.domNode.removeAttribute('monaco-visible-view-zone');
|
||||
zone.isVisible = false;
|
||||
}
|
||||
this._safeCallOnDomNodeTop(zone.delegate, ctx.getScrolledTopFromAbsoluteTop(-1000000));
|
||||
}
|
||||
zone.domNode.setTop(newTop);
|
||||
zone.domNode.setHeight(newHeight);
|
||||
zone.domNode.setDisplay(newDisplay);
|
||||
|
||||
if (zone.marginDomNode) {
|
||||
zone.marginDomNode.setTop(newTop);
|
||||
zone.marginDomNode.setHeight(newHeight);
|
||||
zone.marginDomNode.setDisplay(newDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasVisibleZone) {
|
||||
this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));
|
||||
this.marginDomNode.setWidth(this._contentLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user