mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-14 19:11:37 -04:00
533 lines
15 KiB
TypeScript
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;
|
|
}
|
|
}
|