import type { ModelPrivate } from './model-private' ; import { LatexAtom } from '../core-atoms/latex' ; import { TextAtom } from '../core-atoms/text' ; import { LETTER_AND_DIGITS } from '../core-definitions/definitions' ; import { Offset } from '../public/mathfield' ; import { getCommandSuggestionRange } from '../editor-mathfield/mode-editor-latex' ; export function wordBoundaryOffset ( model , offset , direction ) { if ( model . at ( offset ) . mode !== 'text' ) return offset ; const dir = direction === 'backward' ? - 1 : + 1 ; let result ; if ( LETTER_AND_DIGITS . test ( model . at ( offset ) . value ) ) { let i = offset ; let match ; do { match = model . at ( i ) . mode === 'text' && LETTER_AND_DIGITS . test ( model . at ( i ) . value ) ; i += dir ; } while ( model . at ( i ) && match ) ; result = model . at ( i ) ? i - 2 * dir : i - dir ; } else if ( /\s/ . test ( model . at ( offset ) . value ) ) { let i = offset ; while ( model . at ( i ) && model . at ( i ) . mode === 'text' && /\s/ . test ( model . at ( i ) . value ) ) { i += dir ; } if ( ! model . at ( i ) ) { result = i - dir ; } else { let match = true ; do { match = model . at ( i ) . mode === 'text' && ! /\s/ . test ( model . at ( i ) . value ) ; i += dir ; } while ( model . at ( i ) && match ) ; result = model . at ( i ) ? i - 2 * dir : i - dir ; } } else { let i = offset ; while ( model . at ( i ) && model . at ( i ) . mode === 'text' && ! /\s/ . test ( model . at ( i ) . value ) ) { i += dir ; } result = model . at ( i ) ? i : i - dir ; let match = true ; while ( model . at ( i ) && match ) { match = model . at ( i ) . mode === 'text' && /\s/ . test ( model . at ( i ) . value ) ; if ( match ) result = i ; i += dir ; } result = model . at ( i ) ? i - 2 * dir : i - dir ; } return result - ( dir > 0 ? 0 : 1 ) ; } todo export function skip ( model , direction , options ? ) { const previousPosition = model . position ; if ( ! ( options ?. extend ?? false ) ) { model . collapseSelection ( direction ) ; } let atom = model . at ( model . position ) ; if ( direction === 'forward' ) { if ( atom . type === 'msubsup' ) { atom = atom . rightSibling ; if ( ! atom ) { atom = model . at ( model . position + 1 ) ; } } else { atom = model . at ( model . position + 1 ) ; } } if ( ! atom ) { model . announce ( 'plonk' ) ; return false ; } let offset = model . offsetOf ( atom ) ; if ( atom instanceof TextAtom ) { offset = wordBoundaryOffset ( model , offset , direction ) ; } else if ( atom instanceof LatexAtom ) { if ( atom . isSuggestion ) { console . assert ( direction === 'forward' ) ; while ( atom && atom instanceof LatexAtom ) { atom . isSuggestion = false ; offset = model . offsetOf ( atom ) ; atom = atom . rightSibling ; } } else if ( direction === 'forward' ) { atom = atom . rightSibling ; if ( ! atom || ! ( atom instanceof LatexAtom ) ) { model . announce ( 'plonk' ) ; return false ; } while ( atom && atom instanceof LatexAtom && /[a-zA-Z*]/ . test ( atom . value ) ) { offset = model . offsetOf ( atom ) ; atom = atom . rightSibling ; } } else { atom = atom . leftSibling ; if ( ! atom || ! ( atom instanceof LatexAtom ) ) { model . announce ( 'plonk' ) ; return false ; } while ( atom && atom instanceof LatexAtom && /[a-zA-Z*]/ . test ( atom . value ) ) { offset = model . offsetOf ( atom ) ; atom = atom . leftSibling ; } } } else if ( direction === 'forward' && atom . type === 'mopen' ) { let level = 0 ; do { if ( atom . type === 'mopen' ) { level += 1 ; } else if ( atom . type === 'mclose' ) { level -= 1 ; } atom = atom . rightSibling ; } while ( ! atom . isLastSibling && level !== 0 ) ; offset = model . offsetOf ( atom . leftSibling ) ; } else if ( direction === 'backward' && atom . type === 'mclose' ) { let level = 0 ; do { if ( atom . type === 'mopen' ) { level += 1 ; } else if ( atom . type === 'mclose' ) { level -= 1 ; } atom = atom . leftSibling ; } while ( ! atom . isFirstSibling && level !== 0 ) ; offset = model . offsetOf ( atom ) ; } else if ( direction === 'backward' ) { if ( atom . type === 'first' ) { while ( offset > 0 && atom . type === 'first' ) { offset -= 1 ; atom = model . at ( offset ) ; } } else { const type = atom . type ; if ( atom . type === 'msubsup' ) { offset = model . offsetOf ( model . at ( offset ) . leftSibling ) ; } offset -= 1 ; let nextType = model . at ( offset ) ?. type ; while ( offset >= 0 && nextType === type ) { if ( model . at ( offset ) ?. type === 'msubsup' ) { offset = model . offsetOf ( model . at ( offset ) . leftSibling ) ; } else { offset -= 1 ; } nextType = model . at ( offset ) . type ; } } } else { const { type } = atom ; let nextType = model . at ( offset ) ?. type ; const { lastOffset } = model ; while ( offset <= lastOffset && ( nextType === type || nextType === 'msubsup' ) ) { while ( model . at ( offset ) . rightSibling ?. type === 'msubsup' ) { offset = model . offsetOf ( model . at ( offset ) . rightSibling ) ; } offset += 1 ; nextType = model . at ( offset ) ?. type ; } offset -= 1 ; } if ( options ?. extend ?? false ) { if ( ! model . setSelection ( model . anchor , offset ) ) { model . announce ( 'plonk' ) ; return false ; } } else { if ( offset === model . position ) { model . announce ( 'plonk' ) ; return false ; } model . position = offset ; } model . announce ( 'move' , previousPosition ) ; return true ; } /**
 * Handle keyboard navigation (arrow keys)
 */ export function move ( model , direction , options ? ) { options = options ?? { extend : false } ; if ( direction !== 'forward' ) { const [ from , to ] = getCommandSuggestionRange ( model ) ; if ( from !== undefined && to !== undefined ) { model . deleteAtoms ( [ from , to ] ) ; } } if ( direction === 'upward' ) return moveUpward ( model , options ) ; if ( direction === 'downward' ) return moveDownward ( model , options ) ; const previousPosition = model . position ; if ( options . extend ) { return model . extendSelection ( direction ) ; } if ( model . selectionIsPlaceholder ) { model . collapseSelection ( direction ) ; return move ( model , direction ) ; } if ( ! model . collapseSelection ( direction ) ) { let pos = model . position + ( direction === 'forward' ? + 1 : - 1 ) ; if ( direction === 'forward' ) { let atom = model . at ( pos ) ; if ( atom ?. inCaptureSelection ) { while ( ! atom . captureSelection ) atom = atom . parent ! ; pos = model . offsetOf ( atom ) ; } else if ( ! atom ?. isFirstSibling && atom ?. isLastSibling && atom . parent ?. skipBoundary ) { if ( pos + 1 === model . lastOffset ) { pos = pos + 1 ; } else { model . position = pos + 1 ; return move ( model , 'forward' , options ) ; } } else if ( atom instanceof LatexAtom && atom . isSuggestion ) { atom . isSuggestion = false ; } } else if ( direction === 'backward' ) { let atom = model . at ( pos ) ; if ( atom ?. inCaptureSelection ) { while ( ! atom . captureSelection ) atom = atom . parent ! ; pos = Math . max ( 0 , model . offsetOf ( atom . leftSibling ) ) ; } else if ( ! atom ?. isLastSibling && atom ?. isFirstSibling && atom . parent ?. skipBoundary ) { pos = Math . max ( 0 , pos - 1 ) ; } } if ( pos < 0 || pos > model . lastOffset ) { let result = true ; if ( ! model . suppressChangeNotifications ) { result = model . hooks ?. moveOut ( model , direction ) ; } if ( result ) model . announce ( 'plonk' ) ; return result ; } setPositionHandlingPlaceholder ( model , pos ) ; } model . announce ( 'move' , previousPosition ) ; return true ; } function setPositionHandlingPlaceholder ( model , pos ) { if ( model . at ( pos ) ?. type === 'placeholder' ) { model . setSelection ( pos - 1 , pos ) ; } else if ( model . at ( pos ) ?. rightSibling ?. type === 'placeholder' ) { model . setSelection ( pos , pos + 1 ) ; } else { model . position = pos ; } } function moveUpward ( model , options ? ) { const extend = options ?. extend ?? false ; model . collapseSelection ( 'backward' ) ; let atom = model . at ( model . position ) ; while ( atom && atom . treeBranch !== 'below' ) { atom = atom . parent ! ; } if ( atom ) { if ( extend ) { model . setSelection ( model . offsetOf ( atom . parent ! . leftSibling ) , model . offsetOf ( atom . parent ! ) ) ; } else { const branch = atom . parent ! . branch ( 'above' ) ?? atom . parent ! . createBranch ( 'above' ) ; setPositionHandlingPlaceholder ( model , model . offsetOf ( branch [ branch . length - 1 ] ) ) ; } model . announce ( 'move up' ) ; } else { let result = true ; if ( ! model . suppressChangeNotifications ) { result = model . hooks ?. moveOut ( model , 'upward' ) ; } model . announce ( result ? 'plonk' : 'line' ) ; return result ; } return true ; } function moveDownward ( model , options ? ) { const extend = options ?. extend ?? false ; model . collapseSelection ( 'forward' ) ; let atom = model . at ( model . position ) ; while ( atom && atom . treeBranch !== 'above' ) { atom = atom . parent ! ; } if ( atom ) { if ( extend ) { model . setSelection ( model . offsetOf ( atom . parent ! . leftSibling ) , model . offsetOf ( atom . parent ! ) ) ; } else { const branch = atom . parent ! . branch ( 'below' ) ?? atom . parent ! . createBranch ( 'below' ) ; setPositionHandlingPlaceholder ( model , model . offsetOf ( branch [ branch . length - 1 ] ) ) ; } model . announce ( 'move down' ) ; } else { let result = true ; if ( ! model . suppressChangeNotifications ) { result = model . hooks ?. moveOut ( model , 'downward' ) ; } model . announce ( result ? 'plonk' : 'line' ) ; return result ; } return true ; }