yjs / y-monaco

Monaco editor bindings for Yjs
MIT License
168 stars 33 forks source link

Allow providing monaco as a parameter to MonacoBinding, remove it as a dependency #13

Closed jperl closed 3 years ago

jperl commented 3 years ago

When trying to install this package with NextJS it will complain since MonacoEditor imports monaco-editor which has global css imports that NextJS does not support out of the box.

We use @monaco-editor/react to avoid bundling monaco, since that is slow and difficult to do with NextJS.

Describe the solution you'd like

I would like to be able to provide monaco as a parameter to the MonacoBinding constructor instead of importing it. Ex: new MonacoBinding(monaco, ...).

jperl commented 3 years ago

I hacked this together locally and it works well. I would be happy to clean it up and submit a PR if that is something you would accept?

Example ``` import * as Y from "yjs"; import * as error from "lib0/error.js"; import { createMutex } from "lib0/mutex.js"; class RelativeSelection { /** * @param {Y.RelativePosition} start * @param {Y.RelativePosition} end * @param {monaco.SelectionDirection} direction */ constructor(start, end, direction) { this.start = start; this.end = end; this.direction = direction; } } /** * @param {monaco.editor.IStandaloneCodeEditor} editor * @param {monaco.editor.ITextModel} monacoModel * @param {Y.Text} type */ const createRelativeSelection = (editor, monacoModel, type) => { const sel = editor.getSelection(); if (sel !== null) { const startPos = sel.getStartPosition(); const endPos = sel.getEndPosition(); const start = Y.createRelativePositionFromTypeIndex( type, monacoModel.getOffsetAt(startPos) ); const end = Y.createRelativePositionFromTypeIndex( type, monacoModel.getOffsetAt(endPos) ); return new RelativeSelection(start, end, sel.getDirection()); } return null; }; /** * @param {monaco.editor.IEditor} editor * @param {Y.Text} type * @param {RelativeSelection} relSel * @param {Y.Doc} doc * @return {null|monaco.Selection} */ const createMonacoSelectionFromRelativeSelection = ( monaco, editor, type, relSel, doc ) => { const start = Y.createAbsolutePositionFromRelativePosition(relSel.start, doc); const end = Y.createAbsolutePositionFromRelativePosition(relSel.end, doc); if ( start !== null && end !== null && start.type === type && end.type === type ) { const model = /** @type {monaco.editor.ITextModel} */ editor.getModel(); const startPos = model.getPositionAt(start.index); const endPos = model.getPositionAt(end.index); return monaco.Selection.createWithDirection( startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column, relSel.direction ); } return null; }; export class MonacoBinding { /** * @param {Y.Text} ytext * @param {monaco.editor.ITextModel} monacoModel * @param {Set} [editors] * @param {Awareness?} [awareness] */ constructor( monaco, ytext, monacoModel, editors = new Set(), awareness = null ) { this.monaco = monaco; this.doc = /** @type {Y.Doc} */ ytext.doc; this.ytext = ytext; this.monacoModel = monacoModel; this.editors = editors; this.mux = createMutex(); /** * @type {Map} */ this._savedSelections = new Map(); this._beforeTransaction = () => { this.mux(() => { this._savedSelections = new Map(); editors.forEach((editor) => { if (editor.getModel() === monacoModel) { const rsel = createRelativeSelection(editor, monacoModel, ytext); if (rsel !== null) { this._savedSelections.set(editor, rsel); } } }); }); }; this.doc.on("beforeAllTransactions", this._beforeTransaction); this._decorations = new Map(); this._rerenderDecorations = () => { editors.forEach((editor) => { if (awareness && editor.getModel() === monacoModel) { // render decorations const currentDecorations = this._decorations.get(editor) || []; const newDecorations = []; awareness.getStates().forEach((state, clientID) => { if ( clientID !== this.doc.clientID && state.selection != null && state.selection.anchor != null && state.selection.head != null ) { const anchorAbs = Y.createAbsolutePositionFromRelativePosition( state.selection.anchor, this.doc ); const headAbs = Y.createAbsolutePositionFromRelativePosition( state.selection.head, this.doc ); if ( anchorAbs !== null && headAbs !== null && anchorAbs.type === ytext && headAbs.type === ytext ) { let start, end, afterContentClassName, beforeContentClassName; if (anchorAbs.index < headAbs.index) { start = monacoModel.getPositionAt(anchorAbs.index); end = monacoModel.getPositionAt(headAbs.index); afterContentClassName = "yRemoteSelectionHead"; beforeContentClassName = null; } else { start = monacoModel.getPositionAt(headAbs.index); end = monacoModel.getPositionAt(anchorAbs.index); afterContentClassName = null; beforeContentClassName = "yRemoteSelectionHead"; } newDecorations.push({ range: new monaco.Range( start.lineNumber, start.column, end.lineNumber, end.column ), options: { className: "yRemoteSelection", afterContentClassName, beforeContentClassName, }, }); } } }); this._decorations.set( editor, editor.deltaDecorations(currentDecorations, newDecorations) ); } else { // ignore decorations this._decorations.delete(editor); } }); }; this._ytextObserver = (event) => { this.mux(() => { let index = 0; event.delta.forEach((op) => { if (op.retain !== undefined) { index += op.retain; } else if (op.insert !== undefined) { const pos = monacoModel.getPositionAt(index); const range = new monaco.Selection( pos.lineNumber, pos.column, pos.lineNumber, pos.column ); monacoModel.applyEdits([{ range, text: op.insert }]); index += op.insert.length; } else if (op.delete !== undefined) { const pos = monacoModel.getPositionAt(index); const endPos = monacoModel.getPositionAt(index + op.delete); const range = new monaco.Selection( pos.lineNumber, pos.column, endPos.lineNumber, endPos.column ); monacoModel.applyEdits([{ range, text: "" }]); } else { throw error.unexpectedCase(); } }); this._savedSelections.forEach((rsel, editor) => { const sel = createMonacoSelectionFromRelativeSelection( monaco, editor, ytext, rsel, this.doc ); if (sel !== null) { editor.setSelection(sel); } }); }); this._rerenderDecorations(); }; ytext.observe(this._ytextObserver); monacoModel.setValue(ytext.toString()); this._monacoChangeHandler = monacoModel.onDidChangeContent((event) => { // apply changes from right to left this.mux(() => { this.doc.transact(() => { event.changes .sort( (change1, change2) => change2.rangeOffset - change1.rangeOffset ) .forEach((change) => { ytext.delete(change.rangeOffset, change.rangeLength); ytext.insert(change.rangeOffset, change.text); }); }, this); }); }); monacoModel.onWillDispose(() => { this.destroy(); }); if (awareness) { editors.forEach((editor) => { editor.onDidChangeCursorSelection(() => { if (editor.getModel() === monacoModel) { const sel = editor.getSelection(); if (sel === null) { return; } let anchor = monacoModel.getOffsetAt(sel.getStartPosition()); let head = monacoModel.getOffsetAt(sel.getEndPosition()); if (sel.getDirection() === monaco.SelectionDirection.RTL) { const tmp = anchor; anchor = head; head = tmp; } awareness.setLocalStateField("selection", { anchor: Y.createRelativePositionFromTypeIndex(ytext, anchor), head: Y.createRelativePositionFromTypeIndex(ytext, head), }); } }); awareness.on("change", this._rerenderDecorations); }); this.awareness = awareness; } } destroy() { this._monacoChangeHandler.dispose(); this.ytext.unobserve(this._ytextObserver); this.doc.off("beforeAllTransactions", this._beforeTransaction); if (this.awareness !== null) { this.awareness.off("change", this._rerenderDecorations); } } } ```
jperl commented 3 years ago

Closing for now, going to try rewriting the bundle.