mirror of
https://github.com/convergencelabs/monaco-collab-ext.git
synced 2024-10-27 20:34:17 +00:00
222 lines
6.0 KiB
TypeScript
222 lines
6.0 KiB
TypeScript
import {editor, IDisposable, IPosition} from "monaco-editor";
|
|
import {EditorContentManager} from "./EditorContentManager";
|
|
import {OnDisposed} from "./OnDisposed";
|
|
import {Validation} from "./Validation";
|
|
|
|
/**
|
|
* This class implements a Monaco Content Widget to render a remote user's
|
|
* cursor, and an optional tooltip.
|
|
*
|
|
* @internal
|
|
*/
|
|
export class RemoteCursorWidget implements editor.IContentWidget, IDisposable {
|
|
|
|
private readonly _id: string;
|
|
private readonly _editor: editor.ICodeEditor;
|
|
private readonly _domNode: HTMLDivElement;
|
|
private readonly _tooltipNode: HTMLDivElement | null;
|
|
private readonly _tooltipDuration: number;
|
|
private readonly _scrollListener: IDisposable | null;
|
|
private readonly _onDisposed: OnDisposed;
|
|
private readonly _contentManager: EditorContentManager;
|
|
|
|
private _position: editor.IContentWidgetPosition | null;
|
|
private _offset: number;
|
|
private _hideTimer: any;
|
|
private _disposed: boolean;
|
|
|
|
constructor(codeEditor: editor.ICodeEditor,
|
|
widgetId: string,
|
|
color: string,
|
|
label: string,
|
|
tooltipEnabled: boolean,
|
|
tooltipDuration: number,
|
|
onDisposed: OnDisposed) {
|
|
this._editor = codeEditor;
|
|
this._tooltipDuration = tooltipDuration;
|
|
this._id = `monaco-remote-cursor-${widgetId}`;
|
|
this._onDisposed = onDisposed;
|
|
|
|
// Create the main node for the cursor element.
|
|
const {lineHeight} = this._editor.getConfiguration();
|
|
this._domNode = document.createElement("div");
|
|
this._domNode.className = "monaco-remote-cursor";
|
|
this._domNode.style.background = color;
|
|
this._domNode.style.height = `${lineHeight}px`;
|
|
|
|
// Create the tooltip element if the tooltip is enabled.
|
|
if (tooltipEnabled) {
|
|
this._tooltipNode = document.createElement("div");
|
|
this._tooltipNode.className = "monaco-remote-cursor-tooltip";
|
|
this._tooltipNode.style.background = color;
|
|
this._tooltipNode.innerHTML = label;
|
|
this._domNode.appendChild(this._tooltipNode);
|
|
|
|
// we only need to listen to scroll positions to update the
|
|
// tooltip location on scrolling.
|
|
this._scrollListener = this._editor.onDidScrollChange(() => {
|
|
this._updateTooltipPosition();
|
|
});
|
|
} else {
|
|
this._tooltipNode = null;
|
|
this._scrollListener = null;
|
|
}
|
|
|
|
this._contentManager = new EditorContentManager({
|
|
editor: this._editor,
|
|
onInsert: this._onInsert,
|
|
onReplace: this._onReplace,
|
|
onDelete: this._onDelete
|
|
});
|
|
|
|
this._hideTimer = null;
|
|
this._editor.addContentWidget(this);
|
|
|
|
this._offset = -1;
|
|
|
|
this._disposed = false;
|
|
}
|
|
|
|
public hide(): void {
|
|
this._domNode.style.display = "none";
|
|
}
|
|
|
|
public show(): void {
|
|
this._domNode.style.display = "inherit";
|
|
}
|
|
|
|
public setOffset(offset: number): void {
|
|
Validation.assertNumber(offset, "offset");
|
|
|
|
const position = this._editor.getModel().getPositionAt(offset);
|
|
this.setPosition(position);
|
|
}
|
|
|
|
public setPosition(position: IPosition): void {
|
|
Validation.assertPosition(position, "position");
|
|
|
|
this._updatePosition(position);
|
|
|
|
if (this._tooltipNode !== null) {
|
|
setTimeout(() => this._showTooltip(), 0);
|
|
}
|
|
}
|
|
|
|
public isDisposed(): boolean {
|
|
return this._disposed;
|
|
}
|
|
|
|
public dispose(): void {
|
|
if (this._disposed) {
|
|
return;
|
|
}
|
|
|
|
this._editor.removeContentWidget(this);
|
|
if (this._scrollListener !== null) {
|
|
this._scrollListener.dispose();
|
|
}
|
|
|
|
this._contentManager.dispose();
|
|
|
|
this._disposed = true;
|
|
|
|
this._onDisposed();
|
|
}
|
|
|
|
public getId(): string {
|
|
return this._id;
|
|
}
|
|
|
|
public getDomNode(): HTMLElement {
|
|
return this._domNode;
|
|
}
|
|
|
|
public getPosition(): editor.IContentWidgetPosition | null {
|
|
return this._position;
|
|
}
|
|
|
|
private _updatePosition(position: IPosition): void {
|
|
this._position = {
|
|
position: {...position},
|
|
preference: [editor.ContentWidgetPositionPreference.EXACT]
|
|
};
|
|
|
|
this._offset = this._editor.getModel().getOffsetAt(position);
|
|
|
|
this._editor.layoutContentWidget(this);
|
|
}
|
|
|
|
private _showTooltip(): void {
|
|
this._updateTooltipPosition();
|
|
|
|
if (this._hideTimer !== null) {
|
|
clearTimeout(this._hideTimer);
|
|
} else {
|
|
this._setTooltipVisible(true);
|
|
}
|
|
|
|
this._hideTimer = setTimeout(() => {
|
|
this._setTooltipVisible(false);
|
|
this._hideTimer = null;
|
|
}, this._tooltipDuration);
|
|
}
|
|
|
|
private _updateTooltipPosition(): void {
|
|
const distanceFromTop = this._domNode.offsetTop - this._editor.getScrollTop();
|
|
if (distanceFromTop - this._tooltipNode.offsetHeight < 5) {
|
|
this._tooltipNode.style.top = `${this._tooltipNode.offsetHeight + 2}px`;
|
|
} else {
|
|
this._tooltipNode.style.top = `-${this._tooltipNode.offsetHeight}px`;
|
|
}
|
|
|
|
this._tooltipNode.style.left = "0";
|
|
}
|
|
|
|
private _setTooltipVisible(visible: boolean): void {
|
|
if (visible) {
|
|
this._tooltipNode.style.opacity = "1.0";
|
|
} else {
|
|
this._tooltipNode.style.opacity = "0";
|
|
}
|
|
}
|
|
|
|
private _onInsert = (index: number, text: string) => {
|
|
if (this._position === null) {
|
|
return;
|
|
}
|
|
|
|
const offset = this._offset;
|
|
if (index <= offset) {
|
|
const newOffset = offset + text.length;
|
|
const position = this._editor.getModel().getPositionAt(newOffset);
|
|
this._updatePosition(position);
|
|
}
|
|
}
|
|
|
|
private _onReplace = (index: number, length: number, text: string) => {
|
|
if (this._position === null) {
|
|
return;
|
|
}
|
|
|
|
const offset = this._offset;
|
|
if (index <= offset) {
|
|
const newOffset = (offset - Math.min(offset - index, length)) + text.length;
|
|
const position = this._editor.getModel().getPositionAt(newOffset);
|
|
this._updatePosition(position);
|
|
}
|
|
}
|
|
|
|
private _onDelete = (index: number, length: number) => {
|
|
if (this._position === null) {
|
|
return;
|
|
}
|
|
|
|
const offset = this._offset;
|
|
if (index <= offset) {
|
|
const newOffset = offset - Math.min(offset - index, length);
|
|
const position = this._editor.getModel().getPositionAt(newOffset);
|
|
this._updatePosition(position);
|
|
}
|
|
}
|
|
}
|