@@ -491,7 +491,7 @@ export function deleteRange(
491491 const result = model . getAtoms ( range ) ;
492492 if ( result . length > 0 && result [ 0 ] . parent ) {
493493 //
494- // multiline environment (`\displaylines`, `multline`, `split`, `gather`, etc...)
494+ // multiline environment (`\\ displaylines`, `multline`, `split`, `gather`, etc...)
495495 //
496496 let parent : Atom | undefined = result [ 0 ] ;
497497 while ( parent && ! ( parent instanceof ArrayAtom ) ) parent = parent . parent ;
@@ -547,7 +547,7 @@ export function deleteRange(
547547 const lastSelected = result [ result . length - 1 ] ;
548548
549549 // If we're deleting all the children, also delete the parent
550- // (for example for surd/\sqrt)
550+ // (for example for surd/\\ sqrt)
551551 if ( firstSelected === firstChild && lastSelected === lastChild ) {
552552 const parent = result [ 0 ] . parent ! ;
553553 if ( parent . parent && parent . type !== 'prompt' )
@@ -582,7 +582,178 @@ export function deleteRange(
582582 }
583583 return model . deferNotifications (
584584 { content : true , selection : true , type } ,
585- ( ) => model . deleteAtoms ( range )
585+ ( ) => {
586+ // Track special atom parents and their relationship to the selection
587+ const specialAtomInfo = new Map < Atom , {
588+ containsStart : boolean ;
589+ containsEnd : boolean ;
590+ } > ( ) ;
591+
592+ if ( result . length > 0 ) {
593+ // Check if the start and end of the selection are in different contexts
594+ const startAtom = result [ 0 ] ;
595+ const endAtom = result [ result . length - 1 ] ;
596+
597+ // Find all special atom ancestors for both start and end
598+ const startAncestors = new Set < Atom > ( ) ;
599+ let p = startAtom . parent ;
600+ while ( p ) {
601+ if (
602+ ( p . type === 'surd' ||
603+ p . type === 'box' ||
604+ p . type === 'enclose' ||
605+ p . type === 'leftright' ||
606+ p . type === 'genfrac' ||
607+ p . type === 'overunder' ) &&
608+ p . parent
609+ ) {
610+ startAncestors . add ( p ) ;
611+ }
612+ p = p . parent ;
613+ }
614+
615+ const endAncestors = new Set < Atom > ( ) ;
616+ p = endAtom . parent ;
617+ while ( p ) {
618+ if (
619+ ( p . type === 'surd' ||
620+ p . type === 'box' ||
621+ p . type === 'enclose' ||
622+ p . type === 'leftright' ||
623+ p . type === 'genfrac' ||
624+ p . type === 'overunder' ) &&
625+ p . parent
626+ ) {
627+ endAncestors . add ( p ) ;
628+ }
629+ p = p . parent ;
630+ }
631+
632+ // Collect all special atoms that are ancestors of any atom in the selection
633+ for ( const atom of result ) {
634+ p = atom . parent ;
635+ while ( p ) {
636+ if (
637+ ( p . type === 'surd' ||
638+ p . type === 'box' ||
639+ p . type === 'enclose' ||
640+ p . type === 'leftright' ||
641+ p . type === 'genfrac' ||
642+ p . type === 'overunder' ) &&
643+ p . parent
644+ ) {
645+ if ( ! specialAtomInfo . has ( p ) ) {
646+ specialAtomInfo . set ( p , {
647+ containsStart : startAncestors . has ( p ) ,
648+ containsEnd : endAncestors . has ( p ) ,
649+ } ) ;
650+ }
651+ break ; // Only track the innermost special atom
652+ }
653+ p = p . parent ;
654+ }
655+ }
656+ }
657+
658+ // Delete the atoms
659+ const startPos = Math . min ( ...range ) ;
660+ model . deleteAtoms ( range ) ;
661+
662+ // After deletion, check if any special atoms should be removed or hoisted
663+ for ( const [ specialAtom , info ] of specialAtomInfo ) {
664+ // Check if the atom still exists (it might have been deleted)
665+ if ( ! specialAtom . parent ) continue ;
666+
667+ // Check if this was a boundary-crossing deletion
668+ // (selection started outside and ended inside, or vice versa)
669+ const isBoundaryCrossing = info . containsStart !== info . containsEnd ;
670+
671+ // For surd/box/enclose/leftright: handle both empty and boundary-crossing cases
672+ if (
673+ specialAtom . type === 'surd' ||
674+ specialAtom . type === 'box' ||
675+ specialAtom . type === 'enclose' ||
676+ specialAtom . type === 'leftright'
677+ ) {
678+ const body = specialAtom . branch ( 'body' ) ;
679+ const bodyEmpty = ! body || body . length === 0 ||
680+ ( body . length === 1 && body [ 0 ] . type === 'placeholder' ) ;
681+
682+ if ( bodyEmpty ) {
683+ // Body is empty, remove the special atom
684+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
685+ specialAtom . parent . removeChild ( specialAtom ) ;
686+ model . position = Math . max ( 0 , pos ) ;
687+ } else if ( isBoundaryCrossing && ! info . containsStart && info . containsEnd ) {
688+ // Selection started outside and ended inside - hoist remaining content
689+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
690+ const content = specialAtom . removeBranch ( 'body' ) ;
691+ if ( content && content . length > 0 ) {
692+ specialAtom . parent . addChildrenAfter ( content , specialAtom ) ;
693+ }
694+ specialAtom . parent . removeChild ( specialAtom ) ;
695+ model . position = Math . max ( 0 , pos ) ;
696+ }
697+ }
698+
699+ // For genfrac: if both branches are empty/placeholder, remove it
700+ // If one branch is empty, hoist the other
701+ // Also handle boundary-crossing deletions
702+ if ( specialAtom . type === 'genfrac' ) {
703+ const above = specialAtom . branch ( 'above' ) ;
704+ const below = specialAtom . branch ( 'below' ) ;
705+
706+ const aboveEmpty = ! above || above . length === 0 ||
707+ ( above . length === 1 && above [ 0 ] . type === 'placeholder' ) ;
708+ const belowEmpty = ! below || below . length === 0 ||
709+ ( below . length === 1 && below [ 0 ] . type === 'placeholder' ) ;
710+
711+ if ( aboveEmpty && belowEmpty ) {
712+ // Both empty, remove the fraction
713+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
714+ specialAtom . parent . removeChild ( specialAtom ) ;
715+ model . position = Math . max ( 0 , pos ) ;
716+ } else if ( aboveEmpty && ! belowEmpty ) {
717+ // Hoist the denominator
718+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
719+ const content = specialAtom . removeBranch ( 'below' ) ;
720+ if ( content && content . length > 0 ) {
721+ specialAtom . parent . addChildrenAfter ( content , specialAtom ) ;
722+ }
723+ specialAtom . parent . removeChild ( specialAtom ) ;
724+ model . position = Math . max ( 0 , pos ) ;
725+ } else if ( ! aboveEmpty && belowEmpty ) {
726+ // Hoist the numerator
727+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
728+ const content = specialAtom . removeBranch ( 'above' ) ;
729+ if ( content && content . length > 0 ) {
730+ specialAtom . parent . addChildrenAfter ( content , specialAtom ) ;
731+ }
732+ specialAtom . parent . removeChild ( specialAtom ) ;
733+ model . position = Math . max ( 0 , pos ) ;
734+ } else if ( isBoundaryCrossing && ! info . containsStart && info . containsEnd ) {
735+ // Selection started outside and ended inside - hoist whatever remains
736+ // For fractions, we need to check which branch has content and hoist it
737+ if ( ! aboveEmpty && ! belowEmpty ) {
738+ // Both have content - this is a more complex case
739+ // For now, just hoist the numerator
740+ const pos = model . offsetOf ( specialAtom . leftSibling ) ;
741+ const content = specialAtom . removeBranch ( 'above' ) ;
742+ if ( content && content . length > 0 ) {
743+ specialAtom . parent . addChildrenAfter ( content , specialAtom ) ;
744+ }
745+ specialAtom . parent . removeChild ( specialAtom ) ;
746+ model . position = Math . max ( 0 , pos ) ;
747+ }
748+ }
749+ }
750+ }
751+
752+ // Set position to where deletion started if not already set
753+ if ( specialAtomInfo . size === 0 ) {
754+ model . position = startPos ;
755+ }
756+ }
586757 ) ;
587758}
588759
0 commit comments