Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
19 kB · 471 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472// src/editor/extensions/__tests__/widget-pointer-select-guard.test.ts/** * Unit tests for the shared widget pointer-select guard helper. * These test the helper function in isolation — no full EditorView needed. */import { describe, it, expect } from 'vitest'import { EditorState } from '@codemirror/state'import { EditorView, type ViewUpdate } from '@codemirror/view'import { detectPointerSelectInRanges, detectPointerSelectOnVisibleWidget, type WidgetMarkerRange,} from '../widget-pointer-select-guard'
/** * Create a minimal ViewUpdate with the given selection and userEvent. * This is a test-only utility that constructs just enough state for * detectPointerSelectInRanges to inspect. */function createMockUpdate( docText: string, anchorPos: number, options: { userEvent?: string; empty?: boolean } = {},): ViewUpdate { const { userEvent, empty = true } = options const headPos = empty ? anchorPos : anchorPos + 5
const startState = EditorState.create({ doc: docText, selection: { anchor: 0 } }) const endState = EditorState.create({ doc: docText, selection: { anchor: anchorPos, head: headPos }, })
const tr = startState.update({ selection: { anchor: anchorPos, head: headPos }, ...(userEvent ? { userEvent } : {}), })
// Construct a minimal ViewUpdate-like object return { state: endState, startState, transactions: [tr], selectionSet: true, docChanged: false, viewportChanged: false, view: null as unknown as EditorView, changes: tr.changes, get heightChanged() { return false }, } as unknown as ViewUpdate}
describe('detectPointerSelectInRanges', () => { const sampleRanges: WidgetMarkerRange[] = [ { from: 10, to: 15 }, { from: 20, to: 25 }, ]
describe('basic detection', () => { it('returns null for empty ranges array', () => { const update = createMockUpdate('some text here', 10, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, []) expect(result).toBeNull() })
it('returns null when selectionSet is false', () => { const update = createMockUpdate('some text here', 10, { userEvent: 'select.pointer' }) ;(update as any).selectionSet = false const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toBeNull() })
it('returns null for non-pointer userEvent', () => { const update = createMockUpdate('some text here', 10, { userEvent: 'input' }) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toBeNull() })
it('returns null for no userEvent (keyboard selection)', () => { const update = createMockUpdate('some text here', 10) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toBeNull() })
it('returns null for pointer-select outside any range', () => { const update = createMockUpdate('some text here', 5, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toBeNull() })
it('returns null for non-empty selection (drag)', () => { const update = createMockUpdate('some text here plus more', 10, { userEvent: 'select.pointer', empty: false, }) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toBeNull() }) })
describe('boundary cases — single range', () => { const singleRange: WidgetMarkerRange[] = [{ from: 10, to: 15 }]
it('returns the range when cursor is at from', () => { const update = createMockUpdate('some text here ok', 10, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, singleRange) expect(result).toEqual({ from: 10, to: 15 }) })
it('returns the range when cursor is at to', () => { const update = createMockUpdate('some text here ok', 15, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, singleRange) expect(result).toEqual({ from: 10, to: 15 }) })
it('returns null when cursor is just before from', () => { const update = createMockUpdate('some text here ok', 9, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, singleRange) expect(result).toBeNull() })
it('returns null when cursor is just after to', () => { const update = createMockUpdate('some text here ok', 16, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, singleRange) expect(result).toBeNull() }) })
describe('multiple ranges — first match wins', () => { it('returns first range when cursor matches first range', () => { const update = createMockUpdate('some text here and more', 10, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toEqual({ from: 10, to: 15 }) })
it('returns second range when cursor matches second range only', () => { const update = createMockUpdate('some text here and more', 20, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, sampleRanges) expect(result).toEqual({ from: 20, to: 25 }) })
it('adjacent ranges (chip1.to === chip2.from): first range wins', () => { // Cursor at 15, which is both range1.to AND range2.from const adjacentRanges: WidgetMarkerRange[] = [ { from: 10, to: 15 }, { from: 15, to: 20 }, ] const update = createMockUpdate('some text here and more', 15, { userEvent: 'select.pointer' }) const result = detectPointerSelectInRanges(update, adjacentRanges) // First range wins (chip1.to === 15 matches before chip2.from === 15) expect(result).toEqual({ from: 10, to: 15 }) }) })
describe('integration with select.pointer.* sub-events', () => { it('matches select.pointer.word (double-click variant)', () => { // CM6 may dispatch select.pointer.word for double-click // isUserEvent('select.pointer') matches both const update = createMockUpdate('some text here', 10, { userEvent: 'select.pointer.word' }) const result = detectPointerSelectInRanges(update, [{ from: 10, to: 15 }]) expect(result).toEqual({ from: 10, to: 15 }) }) })})
// ---------------------------------------------------------------------------// detectPointerSelectOnVisibleWidget — visibility-aware detection (#323)// ---------------------------------------------------------------------------
describe('detectPointerSelectOnVisibleWidget', () => { const PROXIMITY_CHARS = 2
/** * Create a mock update with a specific startState cursor position. * This tests the visibility-aware detection that needs to know where * the cursor was BEFORE the select.pointer transaction. */ function createMockUpdateWithPreviousCursor( docText: string, newAnchorPos: number, previousCursorPos: number, options: { userEvent?: string; empty?: boolean } = {}, ): ViewUpdate { const { userEvent, empty = true } = options const headPos = empty ? newAnchorPos : newAnchorPos + 5
const startState = EditorState.create({ doc: docText, selection: { anchor: previousCursorPos }, }) const endState = EditorState.create({ doc: docText, selection: { anchor: newAnchorPos, head: headPos }, })
const tr = startState.update({ selection: { anchor: newAnchorPos, head: headPos }, ...(userEvent ? { userEvent } : {}), })
return { state: endState, startState, transactions: [tr], selectionSet: true, docChanged: false, viewportChanged: false, view: null as unknown as EditorView, changes: tr.changes, get heightChanged() { return false }, } as unknown as ViewUpdate }
describe('widget was VISIBLE (cursor was far) — should engage guard', () => { it('returns null when selectionSet is false', () => { const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 10, 0, { userEvent: 'select.pointer' }, ) ;(update as any).selectionSet = false expect(detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS)).toBeNull() })
it('returns range when widget was visible and click lands at boundary', () => { // Widget at 10-15, PROXIMITY=2 → visible if cursor outside [8, 17] // Previous cursor at 0 (far) → widget WAS visible // New cursor at 10 (widget.from) → click detected, guard engages const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 10, // new cursor at widget.from 0, // previous cursor far away { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toEqual({ from: 10, to: 15 }) })
it('returns range when cursor was just outside proximity and click lands at widget.to', () => { // Widget at 10-15, PROXIMITY=2 → visible if cursor outside [8, 17] // Previous cursor at 18 (far) → widget WAS visible // New cursor at 15 (widget.to) → click detected, guard engages const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 15, // new cursor at widget.to 18, // previous cursor far away { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toEqual({ from: 10, to: 15 }) }) })
describe('widget was HIDDEN (cursor was near) — should NOT engage guard', () => { it('returns null when widget was hidden and click lands at boundary', () => { // Widget at 10-15, PROXIMITY=2 → hidden if cursor inside [8, 17] // Previous cursor at 12 (inside widget range) → widget WAS hidden // New cursor at 10 (widget.from) → NO guard, user wants to edit const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 10, // new cursor at widget.from 12, // previous cursor inside widget (near) → widget hidden { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() })
it('returns null when cursor was at proximity boundary (just inside)', () => { // Widget at 10-15, PROXIMITY=2 → hidden if cursor inside [8, 17] // Previous cursor at 8 (exactly at from - PROXIMITY) → widget WAS hidden const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 10, // new cursor at widget.from 8, // previous cursor at proximity boundary → widget hidden { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() })
it('returns null when cursor was at proximity boundary after widget', () => { // Widget at 10-15, PROXIMITY=2 → hidden if cursor inside [8, 17] // Previous cursor at 17 (exactly at to + PROXIMITY) → widget WAS hidden const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 15, // new cursor at widget.to 17, // previous cursor at proximity boundary → widget hidden { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() }) })
describe('boundary cases', () => { it('returns null for non-pointer userEvent even when widget was visible', () => { const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 10, 0, { userEvent: 'input' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() })
it('returns null when new cursor is not at widget boundary', () => { const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more', 5, // not at widget boundary 0, // far away { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() })
it('returns null for non-empty selection (drag)', () => { const ranges: WidgetMarkerRange[] = [{ from: 10, to: 15 }] const update = createMockUpdateWithPreviousCursor( 'some text [[link]] more text', 10, 0, { userEvent: 'select.pointer', empty: false }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() })
it('returns null for empty ranges array', () => { const update = createMockUpdateWithPreviousCursor( 'some text', 5, 0, { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, [], PROXIMITY_CHARS) expect(result).toBeNull() })
it('handles widget at document start (from=0)', () => { // Widget at 0-5, PROXIMITY=2 → visible if cursor outside [-2, 7] = [0, 7] // Previous cursor at 10 (far) → widget WAS visible const ranges: WidgetMarkerRange[] = [{ from: 0, to: 5 }] const update = createMockUpdateWithPreviousCursor( '[[link]] more text', 0, // new cursor at widget.from 10, // previous cursor far { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toEqual({ from: 0, to: 5 }) })
it('returns null for widget at doc start when cursor was near', () => { // Widget at 0-5, PROXIMITY=2 → hidden if cursor inside [0, 7] // Previous cursor at 6 → widget WAS hidden const ranges: WidgetMarkerRange[] = [{ from: 0, to: 5 }] const update = createMockUpdateWithPreviousCursor( '[[link]] more text', 0, 6, // previous cursor near widget end { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() }) })
describe('multiple widgets', () => { it('only engages guard for the visible widget that was clicked', () => { // Two widgets: one at 0-5 (cursor near), one at 20-25 (cursor far) // Previous cursor at 3 → first widget hidden, second widget visible // Click at widget 2's boundary should engage guard const ranges: WidgetMarkerRange[] = [ { from: 0, to: 5 }, { from: 20, to: 25 }, ] const update = createMockUpdateWithPreviousCursor( '[[a]] some text [[b]] more', 20, // click at second widget 3, // previous cursor near first widget { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) // Second widget was visible (cursor at 3 is far from 20-25) expect(result).toEqual({ from: 20, to: 25 }) })
it('returns null when clicking hidden widget even if another is visible', () => { const ranges: WidgetMarkerRange[] = [ { from: 0, to: 5 }, { from: 20, to: 25 }, ] const update = createMockUpdateWithPreviousCursor( '[[a]] some text [[b]] more', 0, // click at first widget 3, // previous cursor near first widget → first hidden { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toBeNull() }) })
describe('same-line gate (adversary review fix)', () => { it('engages guard when cursor is near but on DIFFERENT line — widget stays visible', () => { // Widget at end of line 1: positions 10-15 (e.g., "line 1 [[a]]") // Line 2 starts at position 17 (after "line 1 [[a]]\n") // Cursor at 17 (start of line 2) is within distance (17 <= 15+2=17) // BUT cursor is on a different line, so widget is VISIBLE (same-line gate) // Click should engage guard because widget was visible // // Document: "line 1 [[a]]\nline 2" // 0123456789... // Line 1: 0-13 (includes newline at 13) // Line 2: 14-20 // Widget: 7-12 ([[a]] = 5 chars) const docText = 'line 1 [[a]]\nline 2' const ranges: WidgetMarkerRange[] = [{ from: 7, to: 12 }] const update = createMockUpdateWithPreviousCursor( docText, 7, // new cursor at widget.from (click on widget) 14, // previous cursor at start of line 2 — within distance (14 <= 12+2=14) but different line { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) // Widget WAS visible because cursor was on different line → guard engages expect(result).toEqual({ from: 7, to: 12 }) })
it('returns null when cursor is near AND on same line — widget was hidden', () => { // Same document but cursor on same line within proximity const docText = 'line 1 [[a]]\nline 2' const ranges: WidgetMarkerRange[] = [{ from: 7, to: 12 }] const update = createMockUpdateWithPreviousCursor( docText, 7, // new cursor at widget.from 5, // previous cursor at position 5 (same line, near widget) → widget hidden { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) // Widget was hidden → guard does NOT engage expect(result).toBeNull() })
it('engages guard when cursor is far even on same line', () => { // Cursor is far from widget on same line — widget visible const docText = 'start [[a]] end of a very long line here' const ranges: WidgetMarkerRange[] = [{ from: 6, to: 11 }] const update = createMockUpdateWithPreviousCursor( docText, 6, // new cursor at widget.from 30, // previous cursor far away on same line → widget visible { userEvent: 'select.pointer' }, ) const result = detectPointerSelectOnVisibleWidget(update, ranges, PROXIMITY_CHARS) expect(result).toEqual({ from: 6, to: 11 }) }) })})