Skip to main content
Glama
y-codemirror.cjs20.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Y = require('yjs'); var cmView = require('@codemirror/view'); var cmState = require('@codemirror/state'); var dom = require('lib0/dist/dom.cjs'); var pair = require('lib0/dist/pair.cjs'); var math = require('lib0/dist/math.cjs'); var mutex = require('lib0/dist/mutex.cjs'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var Y__namespace = /*#__PURE__*/_interopNamespace(Y); var cmView__namespace = /*#__PURE__*/_interopNamespace(cmView); var cmState__namespace = /*#__PURE__*/_interopNamespace(cmState); var dom__namespace = /*#__PURE__*/_interopNamespace(dom); var pair__namespace = /*#__PURE__*/_interopNamespace(pair); var math__namespace = /*#__PURE__*/_interopNamespace(math); /** * Defines a range on text using relative positions that can be transformed back to * absolute positions. (https://docs.yjs.dev/api/relative-positions) */ class YRange { /** * @param {Y.RelativePosition} yanchor * @param {Y.RelativePosition} yhead */ constructor (yanchor, yhead) { this.yanchor = yanchor; this.yhead = yhead; } /** * @returns {any} */ toJSON () { return { yanchor: Y__namespace.relativePositionToJSON(this.yanchor), yhead: Y__namespace.relativePositionToJSON(this.yhead) } } /** * @param {any} json * @return {YRange} */ static fromJSON (json) { return new YRange(Y__namespace.createRelativePositionFromJSON(json.yanchor), Y__namespace.createRelativePositionFromJSON(json.yhead)) } } class YSyncConfig { constructor (ytext, awareness, getUserInfo) { this.ytext = ytext; this.awareness = awareness; this.undoManager = new Y__namespace.UndoManager(ytext); this.getUserInfo = getUserInfo; } /** * Helper function to transform an absolute index position to a Yjs-based relative position * (https://docs.yjs.dev/api/relative-positions). * * A relative position can be transformed back to an absolute position even after the document has changed. The position is * automatically adapted. This does not require any position transformations. Relative positions are computed based on * the internal Yjs document model. Peers that share content through Yjs are guaranteed that their positions will always * synced up when using relatve positions. * * ```js * import { ySyncFacet } from 'y-codemirror' * * .. * const ysync = view.state.facet(ySyncFacet) * // transform an absolute index position to a ypos * const ypos = ysync.getYPos(3) * // transform the ypos back to an absolute position * ysync.fromYPos(ypos) // => 3 * ``` * * It cannot be guaranteed that absolute index positions can be synced up between peers. * This might lead to undesired behavior when implementing features that require that all peers see the * same marked range (e.g. a comment plugin). * * @param {number} pos * @param {number} [assoc] */ toYPos (pos, assoc = 0) { return Y__namespace.createRelativePositionFromTypeIndex(this.ytext, pos, assoc) } /** * @param {Y.RelativePosition | Object} rpos */ fromYPos (rpos) { const pos = Y__namespace.createAbsolutePositionFromRelativePosition(Y__namespace.createRelativePositionFromJSON(rpos), this.ytext.doc); if (pos == null || pos.type !== this.ytext) { throw new Error('[y-codemirror] The position you want to retrieve was created by a different document') } return { pos: pos.index, assoc: pos.assoc } } /** * @param {cmState.SelectionRange} range * @return {YRange} */ toYRange (range) { const assoc = range.assoc; const yanchor = this.toYPos(range.anchor, assoc); const yhead = this.toYPos(range.head, assoc); return new YRange(yanchor, yhead) } /** * @param {YRange} yrange */ fromYRange (yrange) { const anchor = this.fromYPos(yrange.yanchor); const head = this.fromYPos(yrange.yhead); if (anchor.pos === head.pos) { return cmState__namespace.EditorSelection.cursor(head.pos, head.assoc) } return cmState__namespace.EditorSelection.range(anchor.pos, head.pos) } } /** * @type {cmState.Facet<YSyncConfig, YSyncConfig>} */ const ySyncFacet = cmState__namespace.Facet.define({ combine (inputs) { return inputs[inputs.length - 1] } }); /** * @type {cmState.AnnotationType<YSyncConfig>} */ const ySyncAnnotation = cmState__namespace.Annotation.define(); /** * @extends {PluginValue} */ class YSyncPluginValue { /** * @param {cmView.EditorView} view */ constructor (view) { this.view = view; this.conf = view.state.facet(ySyncFacet); this._observer = (event, tr) => { if (tr.origin !== this.conf) { const delta = event.delta; const changes = []; let pos = 0; for (let i = 0; i < delta.length; i++) { const d = delta[i]; if (d.insert != null) { changes.push({ from: pos, to: pos, insert: d.insert }); } else if (d.delete != null) { changes.push({ from: pos, to: pos + d.delete, insert: '' }); pos += d.delete; } else { pos += d.retain; } } view.dispatch({ changes, annotations: [ySyncAnnotation.of(this.conf)] }); } }; this._ytext = this.conf.ytext; this._ytext.observe(this._observer); } /** * @param {cmView.ViewUpdate} update */ update (update) { if (!update.docChanged || (update.transactions.length > 0 && update.transactions[0].annotation(ySyncAnnotation) === this.conf)) { return } const ytext = this.conf.ytext; ytext.doc.transact(() => { /** * This variable adjusts the fromA position to the current position in the Y.Text type. */ let adj = 0; update.changes.iterChanges((fromA, toA, fromB, toB, insert) => { const insertText = insert.sliceString(0, insert.length, '\n'); if (fromA !== toA) { ytext.delete(fromA + adj, toA - fromA); } if (insertText.length > 0) { ytext.insert(fromA + adj, insertText); } adj += insertText.length - (toA - fromA); }); }, this.conf); } destroy () { this._ytext.unobserve(this._observer); } } const ySync = cmView__namespace.ViewPlugin.fromClass(YSyncPluginValue); const yRemoteSelectionsTheme = cmView__namespace.EditorView.baseTheme({ '.cm-ySelection': { }, '.cm-yLineSelection': { padding: 0, margin: '0px 2px 0px 4px' }, '.cm-ySelectionCaret': { position: 'relative', borderLeft: '1px solid black', borderRight: '1px solid black', marginLeft: '-1px', marginRight: '-1px', boxSizing: 'border-box', display: 'inline' }, '.cm-ySelectionCaretDot': { borderRadius: '50%', position: 'absolute', width: '.4em', height: '.4em', top: '-.2em', left: '-.2em', backgroundColor: 'inherit', transition: 'transform .3s ease-in-out', boxSizing: 'border-box' }, '.cm-ySelectionCaret:hover > .cm-ySelectionCaretDot': { transformOrigin: 'bottom center', transform: 'scale(0)' }, '.cm-ySelectionInfo': { position: 'absolute', top: '-1.05em', left: '-1px', fontSize: '.75em', fontFamily: 'serif', fontStyle: 'normal', fontWeight: 'normal', lineHeight: 'normal', userSelect: 'none', color: 'white', paddingLeft: '2px', paddingRight: '2px', zIndex: 101, transition: 'opacity .3s ease-in-out', backgroundColor: 'inherit', // these should be separate opacity: 0, transitionDelay: '0s', whiteSpace: 'nowrap' }, '.cm-ySelectionCaret:hover > .cm-ySelectionInfo': { opacity: 1, transitionDelay: '0s' } }); /** * @todo specify the users that actually changed. Currently, we recalculate positions for every user. * @type {cmState.AnnotationType<Array<number>>} */ const yRemoteSelectionsAnnotation = cmState__namespace.Annotation.define(); class YRemoteCaretWidget extends cmView__namespace.WidgetType { /** * @param {string} color * @param {string} name */ constructor (color, name) { super(); this.color = color; this.name = name; } toDOM () { return /** @type {HTMLElement} */ (dom__namespace.element('span', [pair__namespace.create('class', 'cm-ySelectionCaret'), pair__namespace.create('style', `background-color: ${this.color}; border-color: ${this.color}`)], [ dom__namespace.text('\u2060'), dom__namespace.element('div', [ pair__namespace.create('class', 'cm-ySelectionCaretDot') ]), dom__namespace.text('\u2060'), dom__namespace.element('div', [ pair__namespace.create('class', 'cm-ySelectionInfo') ], [ dom__namespace.text(this.name) ]), dom__namespace.text('\u2060') ])) } eq (widget) { return widget.color === this.color } compare (widget) { return widget.color === this.color } updateDOM () { return false } get estimatedHeight () { return -1 } ignoreEvent () { return true } } class YRemoteSelectionsPluginValue { /** * @param {cmView.EditorView} view */ constructor (view) { this.conf = view.state.facet(ySyncFacet); this._listener = ({ added, updated, removed }, s, t) => { const clients = added.concat(updated).concat(removed); if (clients.findIndex(id => id !== this.conf.awareness.doc.clientID) >= 0) { view.dispatch({ annotations: [yRemoteSelectionsAnnotation.of([])] }); } }; this._awareness = this.conf.awareness; this._awareness.on('change', this._listener); /** * @type {cmView.DecorationSet} */ this.decorations = cmState__namespace.RangeSet.of([]); } destroy () { this._awareness.off('change', this._listener); } /** * @param {cmView.ViewUpdate} update */ update (update) { const ytext = this.conf.ytext; const ydoc = /** @type {Y.Doc} */ (ytext.doc); const awareness = this.conf.awareness; /** * @type {Array<cmState.Range<cmView.Decoration>>} */ const decorations = []; const localAwarenessState = this.conf.awareness.getLocalState(); // set local awareness state (update cursors) if (localAwarenessState != null) { const hasFocus = update.view.hasFocus && update.view.dom.ownerDocument.hasFocus(); const sel = hasFocus ? update.state.selection.main : null; const currentAnchor = localAwarenessState.cursor == null ? null : Y__namespace.createRelativePositionFromJSON(localAwarenessState.cursor.anchor); const currentHead = localAwarenessState.cursor == null ? null : Y__namespace.createRelativePositionFromJSON(localAwarenessState.cursor.head); if (sel != null) { const anchor = Y__namespace.createRelativePositionFromTypeIndex(ytext, sel.anchor); const head = Y__namespace.createRelativePositionFromTypeIndex(ytext, sel.head); if (localAwarenessState.cursor == null || !Y__namespace.compareRelativePositions(currentAnchor, anchor) || !Y__namespace.compareRelativePositions(currentHead, head)) { awareness.setLocalStateField('cursor', { anchor, head }); } } else if (localAwarenessState.cursor != null && hasFocus) { awareness.setLocalStateField('cursor', null); } } // update decorations (remote selections) awareness.getStates().forEach((state, clientid) => { if (clientid === awareness.doc.clientID) { return } const cursor = state.cursor; if (cursor == null || cursor.anchor == null || cursor.head == null) { return } const anchor = Y__namespace.createAbsolutePositionFromRelativePosition(cursor.anchor, ydoc); const head = Y__namespace.createAbsolutePositionFromRelativePosition(cursor.head, ydoc); if (anchor == null || head == null || anchor.type !== ytext || head.type !== ytext) { return } const userColorAndName = this.conf.getUserInfo ? this.conf.getUserInfo(state.user) : state.user; const color = userColorAndName?.color || '#30bced'; const name = userColorAndName?.name || 'Anonymous'; const colorLight = userColorAndName.colorLight || color + '33'; const start = math__namespace.min(anchor.index, head.index); const end = math__namespace.max(anchor.index, head.index); const startLine = update.view.state.doc.lineAt(start); const endLine = update.view.state.doc.lineAt(end); if (startLine.number === endLine.number) { // selected content in a single line. decorations.push({ from: start, to: end, value: cmView__namespace.Decoration.mark({ attributes: { style: `background-color: ${colorLight}` }, class: 'cm-ySelection' }) }); } else { // selected content in multiple lines // first, render text-selection in the first line decorations.push({ from: start, to: startLine.from + startLine.length, value: cmView__namespace.Decoration.mark({ attributes: { style: `background-color: ${colorLight}` }, class: 'cm-ySelection' }) }); // render text-selection in the last line decorations.push({ from: endLine.from, to: end, value: cmView__namespace.Decoration.mark({ attributes: { style: `background-color: ${colorLight}` }, class: 'cm-ySelection' }) }); for (let i = startLine.number + 1; i < endLine.number; i++) { const linePos = update.view.state.doc.line(i).from; decorations.push({ from: linePos, to: linePos, value: cmView__namespace.Decoration.line({ attributes: { style: `background-color: ${colorLight}`, class: 'cm-yLineSelection' } }) }); } } decorations.push({ from: head.index, to: head.index, value: cmView__namespace.Decoration.widget({ side: head.index - anchor.index > 0 ? -1 : 1, // the local cursor should be rendered outside the remote selection block: false, widget: new YRemoteCaretWidget(color, name) }) }); }); this.decorations = cmView__namespace.Decoration.set(decorations, true); } } const yRemoteSelections = cmView__namespace.ViewPlugin.fromClass(YRemoteSelectionsPluginValue, { decorations: v => v.decorations }); class YUndoManagerConfig { /** * @param {Y.UndoManager} undoManager */ constructor (undoManager) { this.undoManager = undoManager; } /** * @param {any} origin */ addTrackedOrigin (origin) { this.undoManager.addTrackedOrigin(origin); } /** * @param {any} origin */ removeTrackedOrigin (origin) { this.undoManager.removeTrackedOrigin(origin); } /** * @return {boolean} Whether a change was undone. */ undo () { return this.undoManager.undo() != null } /** * @return {boolean} Whether a change was redone. */ redo () { return this.undoManager.redo() != null } } /** * @type {cmState.Facet<YUndoManagerConfig, YUndoManagerConfig>} */ const yUndoManagerFacet = cmState__namespace.Facet.define({ combine (inputs) { return inputs[inputs.length - 1] } }); /** * @type {cmState.AnnotationType<YUndoManagerConfig>} */ cmState__namespace.Annotation.define(); /** * @extends {PluginValue} */ class YUndoManagerPluginValue { /** * @param {cmView.EditorView} view */ constructor (view) { this.view = view; this.conf = view.state.facet(yUndoManagerFacet); this._undoManager = this.conf.undoManager; this.syncConf = view.state.facet(ySyncFacet); /** * @type {null | YRange} */ this._beforeChangeSelection = null; this._mux = mutex.createMutex(); this._onStackItemAdded = ({ stackItem, changedParentTypes }) => { // only store metadata if this type was affected if (changedParentTypes.has(this.syncConf.ytext) && this._beforeChangeSelection && !stackItem.meta.has(this)) { // do not overwrite previous stored selection stackItem.meta.set(this, this._beforeChangeSelection); } }; this._onStackItemPopped = ({ stackItem }) => { const sel = stackItem.meta.get(this); if (sel) { const selection = this.syncConf.fromYRange(sel); view.dispatch(view.state.update({ selection })); this._storeSelection(); } }; /** * Do this without mutex, simply use the sync annotation */ this._storeSelection = () => { // store the selection before the change is applied so we can restore it with the undo manager. this._beforeChangeSelection = this.syncConf.toYRange(this.view.state.selection.main); }; this._undoManager.on('stack-item-added', this._onStackItemAdded); this._undoManager.on('stack-item-popped', this._onStackItemPopped); this._undoManager.addTrackedOrigin(this.syncConf); } /** * @param {cmView.ViewUpdate} update */ update (update) { if (update.selectionSet && (update.transactions.length === 0 || update.transactions[0].annotation(ySyncAnnotation) !== this.syncConf)) { // This only works when YUndoManagerPlugin is included before the sync plugin this._storeSelection(); } } destroy () { this._undoManager.off('stack-item-added', this._onStackItemAdded); this._undoManager.off('stack-item-popped', this._onStackItemPopped); this._undoManager.removeTrackedOrigin(this.syncConf); } } const yUndoManager = cmView__namespace.ViewPlugin.fromClass(YUndoManagerPluginValue); /** * @type {cmState.StateCommand} */ const undo = ({ state, dispatch }) => state.facet(yUndoManagerFacet).undo() || true; /** * @type {cmState.StateCommand} */ const redo = ({ state, dispatch }) => state.facet(yUndoManagerFacet).redo() || true; /** * Default key bindigs for the undo manager. * @type {Array<cmView.KeyBinding>} */ const yUndoManagerKeymap = [ { key: 'Mod-z', run: undo, preventDefault: true }, { key: 'Mod-y', mac: 'Mod-Shift-z', run: redo, preventDefault: true }, { key: 'Mod-Shift-z', run: redo, preventDefault: true } ]; /** * @param {Y.Text} ytext * @param {any} awareness * @param {Object} [opts] * @param {Y.UndoManager | false} [opts.undoManager] Set undoManager to false to disable the undo-redo plugin * @param {((u: any) => { name?: String, colorLight?: String, color?: String })} [opts.getUserInfo] Optional fn to take user awareness state info and transform into name + color(s) * @return {cmState.Extension} */ const yCollab = (ytext, awareness, opts) => { // types not quite right... the jsdoc type stuff is awkward const undoManager = opts?.undoManager ?? new Y__namespace.UndoManager(ytext); const getUserInfo = opts?.getUserInfo; const ySyncConfig = new YSyncConfig(ytext, awareness, getUserInfo); const plugins = [ ySyncFacet.of(ySyncConfig), ySync ]; if (awareness) { plugins.push( yRemoteSelectionsTheme, yRemoteSelections ); } if (undoManager !== false) { // By default, only track changes that are produced by the sync plugin (local edits) plugins.push( yUndoManagerFacet.of(new YUndoManagerConfig(undoManager)), yUndoManager, cmView__namespace.EditorView.domEventHandlers({ beforeinput (e, view) { if (e.inputType === 'historyUndo') return undo(view) if (e.inputType === 'historyRedo') return redo(view) return false } }) ); } return plugins }; exports.YRange = YRange; exports.YSyncConfig = YSyncConfig; exports.yCollab = yCollab; exports.yRemoteSelections = yRemoteSelections; exports.yRemoteSelectionsTheme = yRemoteSelectionsTheme; exports.ySync = ySync; exports.ySyncFacet = ySyncFacet; exports.yUndoManagerKeymap = yUndoManagerKeymap; //# sourceMappingURL=y-codemirror.cjs.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server