| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- import {
- MapMode,
- RangeSet,
- RangeValue,
- StateEffect,
- StateField,
- Transaction,
- TransactionSpec,
- } from '@codemirror/state'
- import {
- EditorView,
- hoverTooltip,
- layer,
- RectangleMarker,
- Tooltip,
- } from '@codemirror/view'
- import { findValidPosition } from '../utils/position'
- import { Highlight } from '../../../../../types/highlight'
- import { fullHeightCoordsAtPos, getBase } from '../utils/layer'
- import { debugConsole } from '@/utils/debugging'
- /**
- * A custom extension that displays collaborator cursors in a separate layer.
- */
- export const cursorHighlights = () => {
- return [
- cursorHighlightsState,
- cursorHighlightsLayer,
- hoverTooltip(cursorTooltip, {
- hoverTime: 1,
- }),
- EditorView.theme({
- '.ol-cm-cursorHighlightsLayer': {
- zIndex: 100,
- contain: 'size style',
- pointerEvents: 'none',
- },
- '.ol-cm-cursorHighlight': {
- color: 'hsl(var(--hue), 70%, 50%)',
- borderLeft: '2px solid hsl(var(--hue), 70%, 50%)',
- display: 'inline-block',
- height: '1.6em',
- position: 'absolute',
- pointerEvents: 'none',
- },
- '.ol-cm-cursorHighlight:before': {
- content: "''",
- position: 'absolute',
- left: '-2px',
- top: '-5px',
- height: '5px',
- width: '5px',
- borderWidth: '3px 3px 2px 2px',
- borderStyle: 'solid',
- borderColor: 'inherit',
- },
- '.ol-cm-cursorHighlightLabel': {
- lineHeight: 1,
- backgroundColor: 'hsl(var(--hue), 70%, 50%)',
- padding: '1em 1em',
- fontSize: '0.8rem',
- fontFamily: 'Lato, sans-serif',
- color: 'white',
- fontWeight: 700,
- whiteSpace: 'nowrap',
- pointerEvents: 'none',
- },
- }),
- ]
- }
- class HighlightRangeValue extends RangeValue {
- mapMode = MapMode.Simple
- constructor(public highlight: Highlight) {
- super()
- }
- eq(other: HighlightRangeValue) {
- return other.highlight === this.highlight
- }
- }
- const cursorHighlightsState = StateField.define<RangeSet<HighlightRangeValue>>({
- create() {
- return RangeSet.empty
- },
- update(value, tr) {
- for (const effect of tr.effects) {
- if (effect.is(setCursorHighlightsEffect)) {
- const highlightRanges = []
- for (const highlight of effect.value) {
- // NOTE: other highlight types could be handled here
- if ('cursor' in highlight) {
- try {
- const { row, column } = highlight.cursor
- const pos = findValidPosition(tr.state.doc, row + 1, column)
- highlightRanges.push(
- new HighlightRangeValue(highlight).range(pos)
- )
- } catch (error) {
- // ignore invalid highlights
- debugConsole.debug('invalid highlight position', error)
- }
- }
- }
- return RangeSet.of(highlightRanges, true)
- }
- }
- if (tr.docChanged && !tr.annotation(Transaction.remote)) {
- value = value.map(tr.changes)
- }
- return value
- },
- })
- const cursorTooltip = (view: EditorView, pos: number): Tooltip | null => {
- const highlights: Highlight[] = []
- view.state
- .field(cursorHighlightsState)
- .between(pos, pos, (from, to, value) => {
- highlights.push(value.highlight)
- })
- if (highlights.length === 0) {
- return null
- }
- return {
- pos,
- end: pos,
- above: true,
- create: () => {
- const dom = document.createElement('div')
- dom.classList.add('ol-cm-cursorTooltip')
- for (const highlight of highlights) {
- const label = document.createElement('div')
- label.classList.add('ol-cm-cursorHighlightLabel')
- label.style.setProperty('--hue', highlight.hue)
- label.textContent = highlight.label
- dom.appendChild(label)
- }
- return { dom }
- },
- }
- }
- const setCursorHighlightsEffect = StateEffect.define<Highlight[]>()
- export const setCursorHighlights = (
- cursorHighlights: Highlight[] = []
- ): TransactionSpec => {
- return {
- effects: setCursorHighlightsEffect.of(cursorHighlights),
- }
- }
- class CursorMarker extends RectangleMarker {
- constructor(
- private highlight: Highlight,
- className: string,
- left: number,
- top: number,
- width: number | null,
- height: number
- ) {
- super(className, left, top, width, height)
- }
- draw(): HTMLDivElement {
- const element = super.draw()
- element.style.setProperty('--hue', this.highlight.hue)
- return element
- }
- }
- // draw the collaborator cursors in a separate layer, so they don't affect word wrapping
- const cursorHighlightsLayer = layer({
- above: true,
- class: 'ol-cm-cursorHighlightsLayer',
- update: (update, layer) => {
- return (
- update.docChanged ||
- update.selectionSet ||
- update.transactions.some(tr =>
- tr.effects.some(effect => effect.is(setCursorHighlightsEffect))
- )
- )
- },
- markers(view) {
- const markers: CursorMarker[] = []
- const highlightRanges = view.state.field(cursorHighlightsState)
- const base = getBase(view)
- const { from, to } = view.viewport
- highlightRanges.between(from, to, (from, to, { highlight }) => {
- const pos = fullHeightCoordsAtPos(view, from)
- if (pos) {
- markers.push(
- new CursorMarker(
- highlight,
- 'ol-cm-cursorHighlight',
- pos.left - base.left,
- pos.top - base.top,
- null,
- pos.bottom - pos.top
- )
- )
- }
- })
- return markers
- },
- })
|