@@ -590,21 +590,23 @@ export function selectionMutationWrapper(
590590 // document state without mutating anything.
591591 const compiled = compilePlan ( editor , [ step ] ) ;
592592
593- // Insert requires a collapsed (point) target inside a textblock.
594- // Reject spans, non-collapsed ranges, and positions that resolve to
595- // block boundaries (e.g., refs to images/tables) so executeTextInsert()
596- // never attempts tr.insert() outside a textblock.
593+ // Insert validation: reject multi-segment spans and non-textblock targets.
594+ // Single-block range refs (absFrom < absTo) are valid — executeTextInsert()
595+ // inserts at position: 'before' (absFrom), so the range width is irrelevant.
597596 if ( request . kind === 'insert' ) {
598597 const compiledStep = compiled . mutationSteps . find ( ( s ) => s . step . id === stepId ) ;
599598 const target = compiledStep ?. targets [ 0 ] ;
600599 if ( target ) {
601- const isInvalidInsertTarget = target . kind === 'span' || target . absFrom !== target . absTo ;
602- if ( isInvalidInsertTarget ) {
600+ // Multi-segment refs (span) are never valid for point inserts.
601+ if ( target . kind === 'span' ) {
603602 const resolution = buildSelectionResolutionFromCompiled ( compiled , stepId ) ;
604603 return {
605604 success : false ,
606605 resolution,
607- failure : { code : 'INVALID_TARGET' , message : 'Insert operations require a collapsed target range.' } ,
606+ failure : {
607+ code : 'INVALID_TARGET' ,
608+ message : 'Insert operations require a single-block target, not a multi-segment span.' ,
609+ } ,
608610 } ;
609611 }
610612
@@ -893,7 +895,9 @@ function insertStructuredInner(editor: Editor, input: InsertInput, options?: Mut
893895 { ref } ,
894896 ) ;
895897 }
896- resolvedRange = { from : compiledTarget . absFrom , to : compiledTarget . absTo } ;
898+ // Collapse to the start position — refs resolve to non-collapsed ranges
899+ // but insert semantics is "insert before", not "replace range".
900+ resolvedRange = { from : compiledTarget . absFrom , to : compiledTarget . absFrom } ;
897901 const resolution = buildSelectionResolutionFromCompiled ( compiled , dummyStepId ) ;
898902 effectiveTarget = resolution . target ;
899903 } else {
@@ -923,8 +927,10 @@ function insertStructuredInner(editor: Editor, input: InsertInput, options?: Mut
923927
924928 const { from, to } = resolvedRange ;
925929
926- // Insert semantics are point-only for doc.insert, regardless of content type.
927- if ( from !== to ) {
930+ // Explicit targets with a non-collapsed range indicate a text selection —
931+ // that's a replace operation, not an insert. Refs are already collapsed
932+ // to their start position in the ref branch above.
933+ if ( target && from !== to ) {
928934 return {
929935 success : false ,
930936 resolution,
0 commit comments