Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
11 kB · 276 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277// src/editor/extensions/__tests__/selection-validation.test.tsimport { describe, test, expect } from 'vitest'import { EditorState, EditorSelection } from '@codemirror/state'import { markdown, markdownLanguage } from '@codemirror/lang-markdown'import { markdownFrontmatterField } from '../markdown-context'import { isPositionInsideFrontmatterWidget, adjustRangeOutsideFrontmatter, adjustSelectionOutsideFrontmatter, isSelectionSafeToRestore, getSafeSelectionForRestore, getInitialCursorPosition, createInitialSelection,} from '../selection-validation'
/** * Create an EditorState with markdown language and frontmatter field support. */function createState(doc: string, selection?: EditorSelection): EditorState { return EditorState.create({ doc, selection, extensions: [ markdown({ base: markdownLanguage }), markdownFrontmatterField, ], })}
const DOC_WITH_FRONTMATTER = `---title: Testtags: [a, b]---
# Hello
This is content after frontmatter.`
const DOC_WITHOUT_FRONTMATTER = `# Hello
This is a document without frontmatter.`
describe('selection-validation', () => { describe('isPositionInsideFrontmatterWidget', () => { test('returns false for document without frontmatter', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) expect(isPositionInsideFrontmatterWidget(state, 0)).toBe(false) expect(isPositionInsideFrontmatterWidget(state, 5)).toBe(false) })
test('returns true for positions inside frontmatter range', () => { const state = createState(DOC_WITH_FRONTMATTER) // Frontmatter is "---\ntitle: Test\ntags: [a, b]\n---\n" // which is 33 characters (endOffset = 33) expect(isPositionInsideFrontmatterWidget(state, 0)).toBe(true) expect(isPositionInsideFrontmatterWidget(state, 10)).toBe(true) expect(isPositionInsideFrontmatterWidget(state, 32)).toBe(true) })
test('returns false for positions at or after frontmatter endOffset', () => { const state = createState(DOC_WITH_FRONTMATTER) // endOffset is 33 (just past the closing ---\n) expect(isPositionInsideFrontmatterWidget(state, 33)).toBe(false) expect(isPositionInsideFrontmatterWidget(state, 34)).toBe(false) expect(isPositionInsideFrontmatterWidget(state, 50)).toBe(false) }) })
describe('adjustRangeOutsideFrontmatter', () => { test('returns original range for document without frontmatter', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) const range = EditorSelection.range(0, 5) const adjusted = adjustRangeOutsideFrontmatter(state, range) expect(adjusted.anchor).toBe(0) expect(adjusted.head).toBe(5) })
test('adjusts anchor inside frontmatter to endOffset', () => { const state = createState(DOC_WITH_FRONTMATTER) const range = EditorSelection.range(0, 50) const adjusted = adjustRangeOutsideFrontmatter(state, range) expect(adjusted.anchor).toBe(33) // frontmatter endOffset expect(adjusted.head).toBe(50) })
test('adjusts head inside frontmatter to endOffset', () => { const state = createState(DOC_WITH_FRONTMATTER) const range = EditorSelection.range(50, 10) const adjusted = adjustRangeOutsideFrontmatter(state, range) expect(adjusted.anchor).toBe(50) expect(adjusted.head).toBe(33) // frontmatter endOffset })
test('adjusts both anchor and head when both inside frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) const range = EditorSelection.range(0, 10) const adjusted = adjustRangeOutsideFrontmatter(state, range) expect(adjusted.anchor).toBe(33) expect(adjusted.head).toBe(33) })
test('returns original range when both outside frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) const range = EditorSelection.range(40, 50) const adjusted = adjustRangeOutsideFrontmatter(state, range) expect(adjusted.anchor).toBe(40) expect(adjusted.head).toBe(50) }) })
describe('adjustSelectionOutsideFrontmatter', () => { test('returns original selection when no adjustment needed', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.single(40) const adjusted = adjustSelectionOutsideFrontmatter(state, selection) expect(adjusted.main.anchor).toBe(40) expect(adjusted.main.head).toBe(40) })
test('adjusts all ranges in multi-range selection', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.create([ EditorSelection.range(0, 0), EditorSelection.range(10, 20), EditorSelection.range(40, 50), ], 0) const adjusted = adjustSelectionOutsideFrontmatter(state, selection) // When multiple ranges adjust to the same position (33, 33), // EditorSelection.create merges them into a single range. // So we end up with 2 ranges instead of 3. expect(adjusted.ranges.length).toBe(2) expect(adjusted.ranges[0].anchor).toBe(33) expect(adjusted.ranges[0].head).toBe(33) expect(adjusted.ranges[1].anchor).toBe(40) expect(adjusted.ranges[1].head).toBe(50) })
test('preserves mainIndex', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.create([ EditorSelection.range(0, 0), EditorSelection.range(40, 50), ], 1) const adjusted = adjustSelectionOutsideFrontmatter(state, selection) expect(adjusted.mainIndex).toBe(1) })
test('returns the selection unchanged for a document without frontmatter', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) const selection = EditorSelection.create([ EditorSelection.range(2, 4), EditorSelection.range(10, 20), ], 1) const adjusted = adjustSelectionOutsideFrontmatter(state, selection) expect(adjusted).toBe(selection) }) })
describe('isSelectionSafeToRestore', () => { test('returns true for safe selection', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.single(40) expect(isSelectionSafeToRestore(state, selection)).toBe(true) })
test('returns false for selection inside frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.single(10) expect(isSelectionSafeToRestore(state, selection)).toBe(false) })
test('returns false for selection with out-of-bounds positions', () => { const state = createState(DOC_WITH_FRONTMATTER) const docLen = state.doc.length const selection = EditorSelection.single(docLen + 100) expect(isSelectionSafeToRestore(state, selection)).toBe(false) })
test('returns false for negative positions', () => { const state = createState(DOC_WITH_FRONTMATTER) // Create a mock selection with negative position (shouldn't happen normally) const selection = EditorSelection.create([ EditorSelection.range(-1, 10), ]) expect(isSelectionSafeToRestore(state, selection)).toBe(false) })
test('treats a document without frontmatter as safe from offset zero', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) const selection = EditorSelection.single(5) expect(isSelectionSafeToRestore(state, selection)).toBe(true) }) })
describe('getSafeSelectionForRestore', () => { test('returns original selection when already safe', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.single(40) const safe = getSafeSelectionForRestore(state, selection) expect(safe).not.toBeNull() expect(safe!.main.anchor).toBe(40) expect(safe!.main.head).toBe(40) })
test('adjusts selection inside frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = EditorSelection.single(0) const safe = getSafeSelectionForRestore(state, selection) expect(safe).not.toBeNull() expect(safe!.main.anchor).toBe(33) expect(safe!.main.head).toBe(33) })
test('clamps out-of-bounds selection to document length', () => { const state = createState(DOC_WITH_FRONTMATTER) const docLen = state.doc.length const selection = EditorSelection.single(docLen + 100) const safe = getSafeSelectionForRestore(state, selection) expect(safe).not.toBeNull() expect(safe!.main.anchor).toBe(docLen) expect(safe!.main.head).toBe(docLen) })
test('handles selection that needs both clamping and frontmatter adjustment', () => { const state = createState(DOC_WITH_FRONTMATTER) // Create a selection with one range in frontmatter, one out of bounds const docLen = state.doc.length const selection = EditorSelection.create([ EditorSelection.range(5, 10), EditorSelection.range(docLen + 50, docLen + 100), ], 0) const safe = getSafeSelectionForRestore(state, selection) expect(safe).not.toBeNull() expect(safe!.ranges[0].anchor).toBe(33) // adjusted for frontmatter expect(safe!.ranges[0].head).toBe(33) expect(safe!.ranges[1].anchor).toBe(docLen) // clamped to doc length expect(safe!.ranges[1].head).toBe(docLen) }) })
describe('getInitialCursorPosition', () => { test('returns 0 for document without frontmatter', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) expect(getInitialCursorPosition(state)).toBe(0) })
test('returns endOffset for document with frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) expect(getInitialCursorPosition(state)).toBe(33) }) })
describe('createInitialSelection', () => { test('creates cursor at 0 for document without frontmatter', () => { const state = createState(DOC_WITHOUT_FRONTMATTER) const selection = createInitialSelection(state) expect(selection.main.anchor).toBe(0) expect(selection.main.head).toBe(0) })
test('creates cursor after frontmatter for document with frontmatter', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = createInitialSelection(state) expect(selection.main.anchor).toBe(33) expect(selection.main.head).toBe(33) })
test('creates collapsed selection (cursor, not range)', () => { const state = createState(DOC_WITH_FRONTMATTER) const selection = createInitialSelection(state) expect(selection.main.empty).toBe(true) }) })})