Skip to main content
Glama
y-remote-selections.js8.11 kB
import * as cmView from '@codemirror/view' import * as cmState from '@codemirror/state' import * as dom from 'lib0/dom' import * as pair from 'lib0/pair' import * as math from 'lib0/math' import * as Y from 'yjs' import { ySyncFacet } from './y-sync.js' export const yRemoteSelectionsTheme = cmView.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.Annotation.define() class YRemoteCaretWidget extends cmView.WidgetType { /** * @param {string} color * @param {string} name */ constructor (color, name) { super() this.color = color this.name = name } toDOM () { return /** @type {HTMLElement} */ (dom.element('span', [pair.create('class', 'cm-ySelectionCaret'), pair.create('style', `background-color: ${this.color}; border-color: ${this.color}`)], [ dom.text('\u2060'), dom.element('div', [ pair.create('class', 'cm-ySelectionCaretDot') ]), dom.text('\u2060'), dom.element('div', [ pair.create('class', 'cm-ySelectionInfo') ], [ dom.text(this.name) ]), dom.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 } } export 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.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.createRelativePositionFromJSON(localAwarenessState.cursor.anchor) const currentHead = localAwarenessState.cursor == null ? null : Y.createRelativePositionFromJSON(localAwarenessState.cursor.head) if (sel != null) { const anchor = Y.createRelativePositionFromTypeIndex(ytext, sel.anchor) const head = Y.createRelativePositionFromTypeIndex(ytext, sel.head) if (localAwarenessState.cursor == null || !Y.compareRelativePositions(currentAnchor, anchor) || !Y.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.createAbsolutePositionFromRelativePosition(cursor.anchor, ydoc) const head = Y.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.min(anchor.index, head.index) const end = math.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.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.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.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.Decoration.line({ attributes: { style: `background-color: ${colorLight}`, class: 'cm-yLineSelection' } }) }) } } decorations.push({ from: head.index, to: head.index, value: cmView.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.Decoration.set(decorations, true) } } export const yRemoteSelections = cmView.ViewPlugin.fromClass(YRemoteSelectionsPluginValue, { decorations: v => v.decorations })

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