@theia/monaco
Version:
Theia - Monaco Extension
251 lines (216 loc) • 9.94 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation and others. All rights reserved.
* Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableCollection, Event, Emitter } from '@theia/core';
import { TrackedRangeStickiness } from '@theia/editor/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
export interface MonacoEditorViewZone extends monaco.editor.IViewZone {
id: string;
}
export class MonacoEditorZoneWidget implements Disposable {
private arrow: Arrow | undefined;
readonly zoneNode = document.createElement('div');
readonly containerNode = document.createElement('div');
protected readonly onDidLayoutChangeEmitter = new Emitter<monaco.editor.IDimension>();
readonly onDidLayoutChange: Event<monaco.editor.IDimension> = this.onDidLayoutChangeEmitter.event;
protected viewZone: MonacoEditorViewZone | undefined;
protected readonly toHide = new DisposableCollection();
protected readonly toDispose = new DisposableCollection(
this.onDidLayoutChangeEmitter,
this.toHide
);
editor: monaco.editor.ICodeEditor;
constructor(
editorInstance: monaco.editor.ICodeEditor, readonly showArrow: boolean = true
) {
this.editor = editorInstance;
this.zoneNode.classList.add('zone-widget');
this.containerNode.classList.add('zone-widget-container');
this.zoneNode.appendChild(this.containerNode);
this.updateWidth();
this.toDispose.push(this.editor.onDidLayoutChange(info => this.updateWidth(info)));
}
dispose(): void {
this.toDispose.dispose();
this.hide();
}
protected _options: MonacoEditorZoneWidget.Options | undefined;
get options(): MonacoEditorZoneWidget.Options | undefined {
return this.viewZone ? this._options : undefined;
}
hide(): void {
this.toHide.dispose();
}
show(options: MonacoEditorZoneWidget.Options): void {
let { afterLineNumber, afterColumn, heightInLines } = this._options = { showFrame: true, ...options };
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
// adjust heightInLines to viewport
const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8);
heightInLines = Math.min(heightInLines, maxHeightInLines);
let arrowHeight = 0;
this.toHide.dispose();
this.editor.changeViewZones(accessor => {
this.zoneNode.style.top = '-1000px';
const domNode = document.createElement('div');
domNode.style.overflow = 'hidden';
const zone: monaco.editor.IViewZone = {
domNode,
afterLineNumber,
afterColumn,
heightInLines,
onDomNodeTop: zoneTop => this.updateTop(zoneTop),
onComputedHeight: zoneHeight => this.updateHeight(zoneHeight)
};
this.viewZone = Object.assign(zone, {
id: accessor.addZone(zone)
});
const id = this.viewZone.id;
this.toHide.push(Disposable.create(() => {
this.editor.changeViewZones(a => a.removeZone(id));
this.viewZone = undefined;
}));
if (this.showArrow) {
this.arrow = new Arrow(this.editor);
arrowHeight = Math.round(lineHeight / 3);
this.arrow.height = arrowHeight;
this.arrow.show({ lineNumber: options.afterLineNumber, column: 0 });
this.toHide.push(this.arrow);
}
const widget: monaco.editor.IOverlayWidget = {
getId: () => 'editor-zone-widget-' + id,
getDomNode: () => this.zoneNode,
// eslint-disable-next-line no-null/no-null
getPosition: () => null!
};
this.editor.addOverlayWidget(widget);
this.toHide.push(Disposable.create(() => this.editor.removeOverlayWidget(widget)));
});
this.containerNode.style.overflow = 'hidden';
this.updateContainerHeight(heightInLines * lineHeight);
const model = this.editor.getModel();
if (model) {
const revealLineNumber = Math.min(model.getLineCount(), Math.max(1, afterLineNumber + 1));
this.editor.revealLine(revealLineNumber, monaco.editor.ScrollType.Smooth);
}
}
layout(heightInLines: number): void {
if (this.viewZone && this.viewZone.heightInLines !== heightInLines) {
this.viewZone.heightInLines = heightInLines;
const id = this.viewZone.id;
this.editor.changeViewZones(accessor => accessor.layoutZone(id));
}
}
protected updateTop(top: number): void {
this.zoneNode.style.top = top + (this.showArrow ? 6 : 0) + 'px';
}
protected updateHeight(zoneHeight: number): void {
this.zoneNode.style.height = zoneHeight + 'px';
this.updateContainerHeight(zoneHeight);
}
protected updateContainerHeight(zoneHeight: number): void {
const { frameWidth, height } = this.computeContainerHeight(zoneHeight);
this.containerNode.style.height = height + 'px';
this.containerNode.style.borderTopWidth = frameWidth + 'px';
this.containerNode.style.borderBottomWidth = frameWidth + 'px';
const width = this.computeWidth();
this.onDidLayoutChangeEmitter.fire({ height, width });
}
protected computeContainerHeight(zoneHeight: number): {
height: number,
frameWidth: number
} {
const lineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
const frameWidth = this._options && this._options.frameWidth;
const frameThickness = this._options && this._options.showFrame ? Math.round(lineHeight / 9) : 0;
return {
frameWidth: frameWidth !== undefined ? frameWidth : frameThickness,
height: zoneHeight - 2 * frameThickness
};
}
protected updateWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): void {
const width = this.computeWidth(info);
this.zoneNode.style.width = width + 'px';
this.zoneNode.style.left = this.computeLeft(info) + 'px';
}
protected computeWidth(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number {
return info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth;
}
protected computeLeft(info: monaco.editor.EditorLayoutInfo = this.editor.getLayoutInfo()): number {
// If minimap is to the left, we move beyond it
if (info.minimap.minimapWidth > 0 && info.minimap.minimapLeft === 0) {
return info.minimap.minimapWidth;
}
return 0;
}
}
class IdGenerator {
private lastId: number;
constructor(private prefix: string) {
this.lastId = 0;
}
nextId(): string {
return this.prefix + (++this.lastId);
}
}
class Arrow implements Disposable {
private readonly idGenerator = new IdGenerator('.arrow-decoration-');
private readonly ruleName = this.idGenerator.nextId();
private decorations: string[] = [];
private _height: number = -1;
constructor(
private readonly _editor: monaco.editor.ICodeEditor
) { }
dispose(): void {
this.hide();
}
set height(value: number) {
if (this._height !== value) {
this._height = value;
this._updateStyle();
}
}
private _updateStyle(): void {
const style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
document.getElementsByTagName('head')[0].appendChild(style);
const selector = `.monaco-editor ${this.ruleName}`;
const cssText = `border-style: solid; border-color: transparent transparent var(--theia-peekView-border); border-width:
${this._height}px; bottom: -${this._height}px; margin-left: -${this._height}px; `;
(<CSSStyleSheet>style.sheet).insertRule(selector + '{' + cssText + '}', 0);
}
show(where: monaco.IPosition): void {
this.decorations = this._editor.deltaDecorations(
this.decorations,
[{ range: monaco.Range.fromPositions(where), options: { className: this.ruleName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }]
);
}
hide(): void {
this._editor.deltaDecorations(this.decorations, []);
}
}
export namespace MonacoEditorZoneWidget {
export interface Options {
afterLineNumber: number,
afterColumn?: number,
heightInLines: number,
showFrame?: boolean,
frameWidth?: number
}
}