Files
azuredatastudio/src/vs/editor/contrib/zoneWidget/zoneWidget.ts

533 lines
15 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./zoneWidget';
import * as dom from 'vs/base/browser/dom';
import { IHorizontalSashLayoutProvider, ISashEvent, Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash';
import { Color, RGBA } from 'vs/base/common/color';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import * as objects from 'vs/base/common/objects';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
export interface IOptions {
showFrame?: boolean;
showArrow?: boolean;
frameWidth?: number;
className?: string;
isAccessible?: boolean;
isResizeable?: boolean;
frameColor?: Color;
arrowColor?: Color;
keepEditorSelection?: boolean;
}
export interface IStyles {
frameColor?: Color | null;
arrowColor?: Color | null;
}
const defaultColor = new Color(new RGBA(0, 122, 204));
const defaultOptions: IOptions = {
showArrow: true,
showFrame: true,
className: '',
frameColor: defaultColor,
arrowColor: defaultColor,
keepEditorSelection: false
};
const WIDGET_ID = 'vs.editor.contrib.zoneWidget';
export class ViewZoneDelegate implements IViewZone {
public domNode: HTMLElement;
public id: string = ''; // A valid zone id should be greater than 0
public afterLineNumber: number;
public afterColumn: number;
public heightInLines: number;
private readonly _onDomNodeTop: (top: number) => void;
private readonly _onComputedHeight: (height: number) => void;
constructor(domNode: HTMLElement, afterLineNumber: number, afterColumn: number, heightInLines: number,
onDomNodeTop: (top: number) => void,
onComputedHeight: (height: number) => void
) {
this.domNode = domNode;
this.afterLineNumber = afterLineNumber;
this.afterColumn = afterColumn;
this.heightInLines = heightInLines;
this._onDomNodeTop = onDomNodeTop;
this._onComputedHeight = onComputedHeight;
}
public onDomNodeTop(top: number): void {
this._onDomNodeTop(top);
}
public onComputedHeight(height: number): void {
this._onComputedHeight(height);
}
}
export class OverlayWidgetDelegate implements IOverlayWidget {
private readonly _id: string;
private readonly _domNode: HTMLElement;
constructor(id: string, domNode: HTMLElement) {
this._id = id;
this._domNode = domNode;
}
public getId(): string {
return this._id;
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition(): IOverlayWidgetPosition | null {
return null;
}
}
class Arrow {
private static readonly _IdGenerator = new IdGenerator('.arrow-decoration-');
private readonly _ruleName = Arrow._IdGenerator.nextId();
private _decorations: string[] = [];
private _color: string | null = null;
private _height: number = -1;
constructor(
private readonly _editor: ICodeEditor
) {
//
}
dispose(): void {
this.hide();
dom.removeCSSRulesContainingSelector(this._ruleName);
}
set color(value: string) {
if (this._color !== value) {
this._color = value;
this._updateStyle();
}
}
set height(value: number) {
if (this._height !== value) {
this._height = value;
this._updateStyle();
}
}
private _updateStyle(): void {
dom.removeCSSRulesContainingSelector(this._ruleName);
dom.createCSSRule(
`.monaco-editor ${this._ruleName}`,
`border-style: solid; border-color: transparent; border-bottom-color: ${this._color}; border-width: ${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; `
);
}
show(where: IPosition): void {
this._decorations = this._editor.deltaDecorations(
this._decorations,
[{ range: Range.fromPositions(where), options: { className: this._ruleName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }]
);
}
hide(): void {
this._editor.deltaDecorations(this._decorations, []);
}
}
export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
private _arrow: Arrow | null = null;
private _overlayWidget: OverlayWidgetDelegate | null = null;
private _resizeSash: Sash | null = null;
private _positionMarkerId: string[] = [];
protected _viewZone: ViewZoneDelegate | null = null;
protected readonly _disposables = new DisposableStore();
public container: HTMLElement | null = null;
public domNode: HTMLElement;
public editor: ICodeEditor;
public options: IOptions;
constructor(editor: ICodeEditor, options: IOptions = {}) {
this.editor = editor;
this.options = objects.deepClone(options);
objects.mixin(this.options, defaultOptions, false);
this.domNode = document.createElement('div');
if (!this.options.isAccessible) {
this.domNode.setAttribute('aria-hidden', 'true');
this.domNode.setAttribute('role', 'presentation');
}
this._disposables.add(this.editor.onDidLayoutChange((info: EditorLayoutInfo) => {
const width = this._getWidth(info);
this.domNode.style.width = width + 'px';
this.domNode.style.left = this._getLeft(info) + 'px';
this._onWidth(width);
}));
}
public dispose(): void {
this._disposables.dispose();
if (this._overlayWidget) {
this.editor.removeOverlayWidget(this._overlayWidget);
this._overlayWidget = null;
}
if (this._viewZone) {
this.editor.changeViewZones(accessor => {
if (this._viewZone) {
accessor.removeZone(this._viewZone.id);
}
this._viewZone = null;
});
}
this.editor.deltaDecorations(this._positionMarkerId, []);
this._positionMarkerId = [];
}
public create(): void {
dom.addClass(this.domNode, 'zone-widget');
if (this.options.className) {
dom.addClass(this.domNode, this.options.className);
}
this.container = document.createElement('div');
dom.addClass(this.container, 'zone-widget-container');
this.domNode.appendChild(this.container);
if (this.options.showArrow) {
this._arrow = new Arrow(this.editor);
this._disposables.add(this._arrow);
}
this._fillContainer(this.container);
this._initSash();
this._applyStyles();
}
public style(styles: IStyles): void {
if (styles.frameColor) {
this.options.frameColor = styles.frameColor;
}
if (styles.arrowColor) {
this.options.arrowColor = styles.arrowColor;
}
this._applyStyles();
}
protected _applyStyles(): void {
if (this.container && this.options.frameColor) {
let frameColor = this.options.frameColor.toString();
this.container.style.borderTopColor = frameColor;
this.container.style.borderBottomColor = frameColor;
}
if (this._arrow && this.options.arrowColor) {
let arrowColor = this.options.arrowColor.toString();
this._arrow.color = arrowColor;
}
}
private _getWidth(info: EditorLayoutInfo): number {
return info.width - info.minimapWidth - info.verticalScrollbarWidth;
}
private _getLeft(info: EditorLayoutInfo): number {
// If minimap is to the left, we move beyond it
if (info.minimapWidth > 0 && info.minimapLeft === 0) {
return info.minimapWidth;
}
return 0;
}
private _onViewZoneTop(top: number): void {
this.domNode.style.top = top + 'px';
}
private _onViewZoneHeight(height: number): void {
this.domNode.style.height = `${height}px`;
if (this.container) {
let containerHeight = height - this._decoratingElementsHeight();
this.container.style.height = `${containerHeight}px`;
const layoutInfo = this.editor.getLayoutInfo();
this._doLayout(containerHeight, this._getWidth(layoutInfo));
}
if (this._resizeSash) {
this._resizeSash.layout();
}
}
public get position(): Position | undefined {
const [id] = this._positionMarkerId;
if (!id) {
return undefined;
}
const model = this.editor.getModel();
if (!model) {
return undefined;
}
const range = model.getDecorationRange(id);
if (!range) {
return undefined;
}
return range.getStartPosition();
}
protected _isShowing: boolean = false;
public show(rangeOrPos: IRange | IPosition, heightInLines: number): void {
const range = Range.isIRange(rangeOrPos)
? rangeOrPos
: new Range(rangeOrPos.lineNumber, rangeOrPos.column, rangeOrPos.lineNumber, rangeOrPos.column);
this._isShowing = true;
this._showImpl(range, heightInLines);
this._isShowing = false;
this._positionMarkerId = this.editor.deltaDecorations(this._positionMarkerId, [{ range, options: ModelDecorationOptions.EMPTY }]);
}
public hide(): void {
if (this._viewZone) {
this.editor.changeViewZones(accessor => {
if (this._viewZone) {
accessor.removeZone(this._viewZone.id);
}
});
this._viewZone = null;
}
if (this._overlayWidget) {
this.editor.removeOverlayWidget(this._overlayWidget);
this._overlayWidget = null;
}
if (this._arrow) {
this._arrow.hide();
}
}
private _decoratingElementsHeight(): number {
let lineHeight = this.editor.getConfiguration().lineHeight;
let result = 0;
if (this.options.showArrow) {
let arrowHeight = Math.round(lineHeight / 3);
result += 2 * arrowHeight;
}
if (this.options.showFrame) {
let frameThickness = Math.round(lineHeight / 9);
result += 2 * frameThickness;
}
return result;
}
private _showImpl(where: IRange, heightInLines: number): void {
const position = {
lineNumber: where.startLineNumber,
column: where.startColumn
};
const layoutInfo = this.editor.getLayoutInfo();
const width = this._getWidth(layoutInfo);
this.domNode.style.width = `${width}px`;
this.domNode.style.left = this._getLeft(layoutInfo) + 'px';
// Render the widget as zone (rendering) and widget (lifecycle)
const viewZoneDomNode = document.createElement('div');
viewZoneDomNode.style.overflow = 'hidden';
const lineHeight = this.editor.getConfiguration().lineHeight;
// adjust heightInLines to viewport
const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * 0.8;
if (heightInLines >= maxHeightInLines) {
heightInLines = maxHeightInLines;
}
let arrowHeight = 0;
let frameThickness = 0;
// Render the arrow one 1/3 of an editor line height
if (this._arrow && this.options.showArrow) {
arrowHeight = Math.round(lineHeight / 3);
this._arrow.height = arrowHeight;
this._arrow.show(position);
}
// Render the frame as 1/9 of an editor line height
if (this.options.showFrame) {
frameThickness = Math.round(lineHeight / 9);
}
// insert zone widget
this.editor.changeViewZones((accessor: IViewZoneChangeAccessor) => {
if (this._viewZone) {
accessor.removeZone(this._viewZone.id);
}
if (this._overlayWidget) {
this.editor.removeOverlayWidget(this._overlayWidget);
this._overlayWidget = null;
}
this.domNode.style.top = '-1000px';
this._viewZone = new ViewZoneDelegate(
viewZoneDomNode,
position.lineNumber,
position.column,
heightInLines,
(top: number) => this._onViewZoneTop(top),
(height: number) => this._onViewZoneHeight(height)
);
this._viewZone.id = accessor.addZone(this._viewZone);
this._overlayWidget = new OverlayWidgetDelegate(WIDGET_ID + this._viewZone.id, this.domNode);
this.editor.addOverlayWidget(this._overlayWidget);
});
if (this.container && this.options.showFrame) {
const width = this.options.frameWidth ? this.options.frameWidth : frameThickness;
this.container.style.borderTopWidth = width + 'px';
this.container.style.borderBottomWidth = width + 'px';
}
let containerHeight = heightInLines * lineHeight - this._decoratingElementsHeight();
if (this.container) {
this.container.style.top = arrowHeight + 'px';
this.container.style.height = containerHeight + 'px';
this.container.style.overflow = 'hidden';
}
this._doLayout(containerHeight, width);
if (!this.options.keepEditorSelection) {
this.editor.setSelection(where);
}
const model = this.editor.getModel();
if (model) {
// Reveal the line above or below the zone widget, to get the zone widget in the viewport
const revealLineNumber = Math.min(model.getLineCount(), Math.max(1, where.endLineNumber + 1));
this.revealLine(revealLineNumber);
}
}
protected revealLine(lineNumber: number) {
this.editor.revealLine(lineNumber, ScrollType.Smooth);
}
protected setCssClass(className: string, classToReplace?: string): void {
if (!this.container) {
return;
}
if (classToReplace) {
this.container.classList.remove(classToReplace);
}
dom.addClass(this.container, className);
}
protected abstract _fillContainer(container: HTMLElement): void;
protected _onWidth(widthInPixel: number): void {
// implement in subclass
}
protected _doLayout(heightInPixel: number, widthInPixel: number): void {
// implement in subclass
}
protected _relayout(newHeightInLines: number): void {
if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) {
this.editor.changeViewZones(accessor => {
if (this._viewZone) {
this._viewZone.heightInLines = newHeightInLines;
accessor.layoutZone(this._viewZone.id);
}
});
}
}
// --- sash
private _initSash(): void {
if (this._resizeSash) {
return;
}
this._resizeSash = this._disposables.add(new Sash(this.domNode, this, { orientation: Orientation.HORIZONTAL }));
if (!this.options.isResizeable) {
this._resizeSash.hide();
this._resizeSash.state = SashState.Disabled;
}
let data: { startY: number; heightInLines: number; } | undefined;
this._disposables.add(this._resizeSash.onDidStart((e: ISashEvent) => {
if (this._viewZone) {
data = {
startY: e.startY,
heightInLines: this._viewZone.heightInLines,
};
}
}));
this._disposables.add(this._resizeSash.onDidEnd(() => {
data = undefined;
}));
this._disposables.add(this._resizeSash.onDidChange((evt: ISashEvent) => {
if (data) {
let lineDelta = (evt.currentY - data.startY) / this.editor.getConfiguration().lineHeight;
let roundedLineDelta = lineDelta < 0 ? Math.ceil(lineDelta) : Math.floor(lineDelta);
let newHeightInLines = data.heightInLines + roundedLineDelta;
if (newHeightInLines > 5 && newHeightInLines < 35) {
this._relayout(newHeightInLines);
}
}
}));
}
getHorizontalSashLeft() {
return 0;
}
getHorizontalSashTop() {
return (this.domNode.style.height === null ? 0 : parseInt(this.domNode.style.height)) - (this._decoratingElementsHeight() / 2);
}
getHorizontalSashWidth() {
const layoutInfo = this.editor.getLayoutInfo();
return layoutInfo.width - layoutInfo.minimapWidth;
}
}