|
| 1 | +import * as Y from 'yjs' |
| 2 | +import * as monaco from 'monaco-editor' |
| 3 | +import * as error from 'lib0/error' |
| 4 | +import { createMutex } from 'lib0/mutex' |
| 5 | +import { Awareness } from 'y-protocols/awareness' // eslint-disable-line |
| 6 | + |
| 7 | +class RelativeSelection { |
| 8 | + /** |
| 9 | + * @param {Y.RelativePosition} start |
| 10 | + * @param {Y.RelativePosition} end |
| 11 | + * @param {monaco.SelectionDirection} direction |
| 12 | + */ |
| 13 | + constructor (start, end, direction) { |
| 14 | + this.start = start |
| 15 | + this.end = end |
| 16 | + this.direction = direction |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +/** |
| 21 | + * @param {monaco.editor.IStandaloneCodeEditor} editor |
| 22 | + * @param {monaco.editor.ITextModel} monacoModel |
| 23 | + * @param {Y.Text} type |
| 24 | + */ |
| 25 | +const createRelativeSelection = (editor, monacoModel, type) => { |
| 26 | + const sel = editor.getSelection() |
| 27 | + if (sel !== null) { |
| 28 | + const startPos = sel.getStartPosition() |
| 29 | + const endPos = sel.getEndPosition() |
| 30 | + const start = Y.createRelativePositionFromTypeIndex(type, monacoModel.getOffsetAt(startPos)) |
| 31 | + const end = Y.createRelativePositionFromTypeIndex(type, monacoModel.getOffsetAt(endPos)) |
| 32 | + return new RelativeSelection(start, end, sel.getDirection()) |
| 33 | + } |
| 34 | + return null |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * @param {monaco.editor.IEditor} editor |
| 39 | + * @param {Y.Text} type |
| 40 | + * @param {RelativeSelection} relSel |
| 41 | + * @param {Y.Doc} doc |
| 42 | + * @return {null|monaco.Selection} |
| 43 | + */ |
| 44 | +const createMonacoSelectionFromRelativeSelection = (editor, type, relSel, doc) => { |
| 45 | + const start = Y.createAbsolutePositionFromRelativePosition(relSel.start, doc) |
| 46 | + const end = Y.createAbsolutePositionFromRelativePosition(relSel.end, doc) |
| 47 | + if (start !== null && end !== null && start.type === type && end.type === type) { |
| 48 | + const model = /** @type {monaco.editor.ITextModel} */ (editor.getModel()) |
| 49 | + const startPos = model.getPositionAt(start.index) |
| 50 | + const endPos = model.getPositionAt(end.index) |
| 51 | + return monaco.Selection.createWithDirection(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column, relSel.direction) |
| 52 | + } |
| 53 | + return null |
| 54 | +} |
| 55 | + |
| 56 | +export class MonacoBinding { |
| 57 | + /** |
| 58 | + * @param {Y.Text} ytext |
| 59 | + * @param {monaco.editor.ITextModel} monacoModel |
| 60 | + * @param {Set<monaco.editor.IStandaloneCodeEditor>} [editors] |
| 61 | + * @param {Awareness?} [awareness] |
| 62 | + */ |
| 63 | + constructor (ytext, monacoModel, editors = new Set(), awareness = null) { |
| 64 | + this.doc = /** @type {Y.Doc} */ (ytext.doc) |
| 65 | + this.ytext = ytext |
| 66 | + this.monacoModel = monacoModel |
| 67 | + this.editors = editors |
| 68 | + this.mux = createMutex() |
| 69 | + /** |
| 70 | + * @type {Map<monaco.editor.IStandaloneCodeEditor, RelativeSelection>} |
| 71 | + */ |
| 72 | + this._savedSelections = new Map() |
| 73 | + this._beforeTransaction = () => { |
| 74 | + this.mux(() => { |
| 75 | + this._savedSelections = new Map() |
| 76 | + editors.forEach(editor => { |
| 77 | + if (editor.getModel() === monacoModel) { |
| 78 | + const rsel = createRelativeSelection(editor, monacoModel, ytext) |
| 79 | + if (rsel !== null) { |
| 80 | + this._savedSelections.set(editor, rsel) |
| 81 | + } |
| 82 | + } |
| 83 | + }) |
| 84 | + }) |
| 85 | + } |
| 86 | + this.doc.on('beforeAllTransactions', this._beforeTransaction) |
| 87 | + this._decorations = new Map() |
| 88 | + this._rerenderDecorations = () => { |
| 89 | + editors.forEach(editor => { |
| 90 | + if (awareness && editor.getModel() === monacoModel) { |
| 91 | + // render decorations |
| 92 | + const currentDecorations = this._decorations.get(editor) || [] |
| 93 | + /** |
| 94 | + * @type {Array<monaco.editor.IModelDeltaDecoration>} |
| 95 | + */ |
| 96 | + const newDecorations = [] |
| 97 | + awareness.getStates().forEach((state, clientID) => { |
| 98 | + if (clientID !== this.doc.clientID && state.selection != null && state.selection.anchor != null && state.selection.head != null) { |
| 99 | + const anchorAbs = Y.createAbsolutePositionFromRelativePosition(state.selection.anchor, this.doc) |
| 100 | + const headAbs = Y.createAbsolutePositionFromRelativePosition(state.selection.head, this.doc) |
| 101 | + if (anchorAbs !== null && headAbs !== null && anchorAbs.type === ytext && headAbs.type === ytext) { |
| 102 | + let start, end, afterContentClassName, beforeContentClassName |
| 103 | + if (anchorAbs.index < headAbs.index) { |
| 104 | + start = monacoModel.getPositionAt(anchorAbs.index) |
| 105 | + end = monacoModel.getPositionAt(headAbs.index) |
| 106 | + afterContentClassName = 'yRemoteSelectionHead yRemoteSelectionHead-' + clientID |
| 107 | + beforeContentClassName = null |
| 108 | + } else { |
| 109 | + start = monacoModel.getPositionAt(headAbs.index) |
| 110 | + end = monacoModel.getPositionAt(anchorAbs.index) |
| 111 | + afterContentClassName = null |
| 112 | + beforeContentClassName = 'yRemoteSelectionHead yRemoteSelectionHead-' + clientID |
| 113 | + } |
| 114 | + newDecorations.push({ |
| 115 | + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), |
| 116 | + options: { |
| 117 | + className: 'yRemoteSelection yRemoteSelection-' + clientID, |
| 118 | + afterContentClassName, |
| 119 | + beforeContentClassName |
| 120 | + } |
| 121 | + }) |
| 122 | + } |
| 123 | + } |
| 124 | + }) |
| 125 | + this._decorations.set(editor, editor.deltaDecorations(currentDecorations, newDecorations)) |
| 126 | + } else { |
| 127 | + // ignore decorations |
| 128 | + this._decorations.delete(editor) |
| 129 | + } |
| 130 | + }) |
| 131 | + } |
| 132 | + /** |
| 133 | + * @param {Y.YTextEvent} event |
| 134 | + */ |
| 135 | + this._ytextObserver = event => { |
| 136 | + this.mux(() => { |
| 137 | + let index = 0 |
| 138 | + event.delta.forEach(op => { |
| 139 | + if (op.retain !== undefined) { |
| 140 | + index += op.retain |
| 141 | + } else if (op.insert !== undefined) { |
| 142 | + const pos = monacoModel.getPositionAt(index) |
| 143 | + const range = new monaco.Selection(pos.lineNumber, pos.column, pos.lineNumber, pos.column) |
| 144 | + const insert = /** @type {string} */ (op.insert) |
| 145 | + monacoModel.applyEdits([{ range, text: insert }]) |
| 146 | + index += insert.length |
| 147 | + } else if (op.delete !== undefined) { |
| 148 | + const pos = monacoModel.getPositionAt(index) |
| 149 | + const endPos = monacoModel.getPositionAt(index + op.delete) |
| 150 | + const range = new monaco.Selection(pos.lineNumber, pos.column, endPos.lineNumber, endPos.column) |
| 151 | + monacoModel.applyEdits([{ range, text: '' }]) |
| 152 | + } else { |
| 153 | + throw error.unexpectedCase() |
| 154 | + } |
| 155 | + }) |
| 156 | + this._savedSelections.forEach((rsel, editor) => { |
| 157 | + const sel = createMonacoSelectionFromRelativeSelection(editor, ytext, rsel, this.doc) |
| 158 | + if (sel !== null) { |
| 159 | + editor.setSelection(sel) |
| 160 | + } |
| 161 | + }) |
| 162 | + }) |
| 163 | + this._rerenderDecorations() |
| 164 | + } |
| 165 | + ytext.observe(this._ytextObserver) |
| 166 | + { |
| 167 | + const ytextValue = ytext.toString() |
| 168 | + if (monacoModel.getValue() !== ytextValue) { |
| 169 | + const eol = monacoModel.getEndOfLineSequence(); |
| 170 | + monacoModel.setValue(ytextValue) |
| 171 | + monacoModel.setEOL(eol); |
| 172 | + } |
| 173 | + } |
| 174 | + this._monacoChangeHandler = monacoModel.onDidChangeContent(event => { |
| 175 | + // apply changes from right to left |
| 176 | + this.mux(() => { |
| 177 | + this.doc.transact(() => { |
| 178 | + event.changes.sort((change1, change2) => change2.rangeOffset - change1.rangeOffset).forEach(change => { |
| 179 | + ytext.delete(change.rangeOffset, change.rangeLength) |
| 180 | + ytext.insert(change.rangeOffset, change.text) |
| 181 | + }) |
| 182 | + }, this) |
| 183 | + }) |
| 184 | + }) |
| 185 | + this._monacoDisposeHandler = monacoModel.onWillDispose(() => { |
| 186 | + this.destroy() |
| 187 | + }) |
| 188 | + /** |
| 189 | + * @param {monaco.editor.IStandaloneCodeEditor} editor |
| 190 | + */ |
| 191 | + this._monacoSelectionChangeHandler = (editor) => { |
| 192 | + if(!awareness) return |
| 193 | + const sel = editor.getSelection() |
| 194 | + if (sel === null) { |
| 195 | + return |
| 196 | + } |
| 197 | + let anchor = monacoModel.getOffsetAt(sel.getStartPosition()) |
| 198 | + let head = monacoModel.getOffsetAt(sel.getEndPosition()) |
| 199 | + if (sel.getDirection() === monaco.SelectionDirection.RTL) { |
| 200 | + const tmp = anchor |
| 201 | + anchor = head |
| 202 | + head = tmp |
| 203 | + } |
| 204 | + awareness.setLocalStateField('selection', { |
| 205 | + anchor: Y.createRelativePositionFromTypeIndex(ytext, anchor), |
| 206 | + head: Y.createRelativePositionFromTypeIndex(ytext, head) |
| 207 | + }) |
| 208 | + } |
| 209 | + if (awareness) { |
| 210 | + editors.forEach(editor => { |
| 211 | + editor.onDidChangeCursorSelection((e) => { |
| 212 | + if (editor.getModel() !== monacoModel) return |
| 213 | + // Workaround for the wrong event order bug in monaco-editor, ref: https://github.com/microsoft/monaco-editor/issues/2774 |
| 214 | + if ( |
| 215 | + e.reason === /** @type {monaco.editor.CursorChangeReason.Undo} */ (5) || |
| 216 | + e.reason === /** @type {monaco.editor.CursorChangeReason.Redo} */ (6) |
| 217 | + ) { |
| 218 | + setTimeout(() => this._monacoSelectionChangeHandler(editor)) |
| 219 | + } else { |
| 220 | + this._monacoSelectionChangeHandler(editor) |
| 221 | + } |
| 222 | + }) |
| 223 | + awareness.on('change', this._rerenderDecorations) |
| 224 | + }) |
| 225 | + this.awareness = awareness |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + destroy () { |
| 230 | + this._monacoChangeHandler.dispose() |
| 231 | + this._monacoDisposeHandler.dispose() |
| 232 | + this.ytext.unobserve(this._ytextObserver) |
| 233 | + this.doc.off('beforeAllTransactions', this._beforeTransaction) |
| 234 | + if (this.awareness) { |
| 235 | + this.awareness.off('change', this._rerenderDecorations) |
| 236 | + } |
| 237 | + } |
| 238 | +} |
0 commit comments