mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-17 13:27:50 +08:00
cd80bd7804
* Implement user cursor and selection tracking * Separate jump-to-user and follow
168 lines
4.2 KiB
JavaScript
168 lines
4.2 KiB
JavaScript
import monaco from "./monaco";
|
|
import { randomId } from "../../lib/utils";
|
|
|
|
/**
|
|
* Remote user visual indicators within the editor.
|
|
*
|
|
* Consists of a cursor widget and a selection highlight.
|
|
* Both elements have the user's hex color of choice.
|
|
*/
|
|
export default class RemoteUser {
|
|
constructor(editor, selection, hexColor, label) {
|
|
this._cursorWidget = new CursorWidget(
|
|
editor,
|
|
selection.getPosition(),
|
|
hexColor,
|
|
label
|
|
);
|
|
|
|
this._selectionDecoration = new SelectionDecoration(
|
|
editor,
|
|
selection,
|
|
hexColor
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates indicators to match the given selection.
|
|
*/
|
|
update(selection) {
|
|
this._cursorWidget.update(selection.getPosition());
|
|
this._selectionDecoration.update(selection);
|
|
}
|
|
|
|
/**
|
|
* Performs necessary cleanup actions.
|
|
*/
|
|
dispose() {
|
|
this._cursorWidget.dispose();
|
|
this._selectionDecoration.dispose();
|
|
}
|
|
}
|
|
|
|
class CursorWidget {
|
|
constructor(editor, position, hexColor, label) {
|
|
this._id = randomId();
|
|
this._editor = editor;
|
|
this._position = position;
|
|
this._isPositionValid = this.__checkPositionValidity(position);
|
|
|
|
this.__buildDomNode(hexColor, label);
|
|
|
|
this._editor.addContentWidget(this);
|
|
|
|
this._onDidChangeModelContentDisposable = this._editor.onDidChangeModelContent(
|
|
(event) => {
|
|
// We may receive new cursor position before content update,
|
|
// and the position may be invalid (e.g. column 10, even though the line has currently length 9).
|
|
// If that's the case then we want to update the cursor once the content is updated.
|
|
if (!this._isPositionValid) {
|
|
this.update(this._position);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
getId() {
|
|
return this._id;
|
|
}
|
|
|
|
getPosition() {
|
|
return {
|
|
position: this._position,
|
|
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
|
|
};
|
|
}
|
|
|
|
update(position) {
|
|
this._position = position;
|
|
this._isPositionValid = this.__checkPositionValidity(position);
|
|
this.__updateDomNode();
|
|
this._editor.layoutContentWidget(this);
|
|
}
|
|
|
|
getDomNode() {
|
|
return this._domNode;
|
|
}
|
|
|
|
dispose() {
|
|
this._editor.removeContentWidget(this);
|
|
this._onDidChangeModelContentDisposable.dispose();
|
|
}
|
|
|
|
__checkPositionValidity(position) {
|
|
const validPosition = this._editor.getModel().validatePosition(position);
|
|
return position.equals(validPosition);
|
|
}
|
|
|
|
__buildDomNode(hexColor, label) {
|
|
const lineHeight = this._editor.getOption(
|
|
monaco.editor.EditorOption.lineHeight
|
|
);
|
|
|
|
const node = document.createElement("div");
|
|
node.classList.add("monaco-cursor-widget-container");
|
|
|
|
const cursorNode = document.createElement("div");
|
|
cursorNode.classList.add("monaco-cursor-widget-cursor");
|
|
cursorNode.style.background = hexColor;
|
|
cursorNode.style.height = `${lineHeight}px`;
|
|
|
|
const labelNode = document.createElement("div");
|
|
labelNode.classList.add("monaco-cursor-widget-label");
|
|
labelNode.style.height = `${lineHeight}px`;
|
|
labelNode.innerText = label;
|
|
labelNode.style.background = hexColor;
|
|
|
|
node.appendChild(cursorNode);
|
|
node.appendChild(labelNode);
|
|
|
|
this._domNode = node;
|
|
this.__updateDomNode();
|
|
}
|
|
|
|
__updateDomNode() {
|
|
const isFirstLine = this._position.lineNumber === 1;
|
|
this._domNode.classList.toggle("inline", isFirstLine);
|
|
}
|
|
}
|
|
|
|
class SelectionDecoration {
|
|
constructor(editor, selection, hexColor) {
|
|
this._editor = editor;
|
|
this._decorations = [];
|
|
|
|
// Dynamically create CSS class for the given hex color
|
|
|
|
this._className = `user-selection-${hexColor.replace("#", "")}`;
|
|
|
|
this._styleElement = document.createElement("style");
|
|
this._styleElement.innerHTML = `
|
|
.${this._className} {
|
|
background-color: ${hexColor}30;
|
|
}
|
|
`;
|
|
document.body.appendChild(this._styleElement);
|
|
|
|
this.update(selection);
|
|
}
|
|
|
|
update(selection) {
|
|
const newDecorations = [
|
|
{
|
|
range: selection,
|
|
options: { className: this._className },
|
|
},
|
|
];
|
|
|
|
this._decorations = this._editor.deltaDecorations(
|
|
this._decorations,
|
|
newDecorations
|
|
);
|
|
}
|
|
|
|
dispose() {
|
|
this._editor.deltaDecorations(this._decorations, []);
|
|
this._styleElement.remove();
|
|
}
|
|
}
|