@@ -14,6 +14,7 @@ let _syncId = 0;
1414let _lastReceivedSyncId = - 1 ;
1515let _suppressContentChange = false ;
1616let _scrollFromCM = false ;
17+ let _scrollFromViewer = false ;
1718let _baseURL = "" ;
1819let _cursorPosBeforeEdit = null ; // cursor position before current edit batch
1920let _cursorPosDirty = false ; // true after content changes, reset when emitted
@@ -426,6 +427,8 @@ export function initBridge() {
426427 const sourceLine = _getSourceLineFromElement ( e . target ) ;
427428 if ( getState ( ) . editMode ) {
428429 if ( sourceLine != null ) {
430+ _scrollFromViewer = true ;
431+ setTimeout ( ( ) => { _scrollFromViewer = false ; } , 500 ) ;
429432 sendToParent ( "mdviewrScrollSync" , { sourceLine } ) ;
430433 }
431434 return ;
@@ -798,10 +801,11 @@ function handleReloadFile(data) {
798801// --- Theme, edit mode, locale ---
799802
800803function handleSetTheme ( data ) {
801- const { theme } = data ;
802804 // Force light theme for a paper-like appearance regardless of editor theme.
803805 // The theme infrastructure is preserved for future use.
804806 const appliedTheme = "light" ;
807+ // Skip if already applied to avoid reflows that can reset scroll position
808+ if ( document . documentElement . getAttribute ( "data-theme" ) === appliedTheme ) return ;
805809 document . documentElement . setAttribute ( "data-theme" , appliedTheme ) ;
806810 document . documentElement . style . colorScheme = "light" ;
807811 setState ( { theme : appliedTheme } ) ;
@@ -975,18 +979,14 @@ function handleScrollToLine(data) {
975979 const { line, fromScroll, tableCol } = data ;
976980 if ( line == null ) return ;
977981
978- // In edit mode, ignore scroll-based sync from CM to prevent feedback
979- // loops (click in viewer → CM scroll → scroll sync back → viewer jumps ).
980- if ( fromScroll && getState ( ) . editMode ) return ;
982+ // In edit mode, ignore scroll-based sync that originated from the viewer
983+ // itself (feedback loop: viewer click → CM scroll → scroll sync back).
984+ if ( fromScroll && getState ( ) . editMode && _scrollFromViewer ) return ;
981985
982986 const viewer = document . getElementById ( "viewer-content" ) ;
983987 if ( ! viewer ) return ;
984988
985- // In edit mode, skip CM cursor sync when the viewer has focus — the user
986- // is actively editing and highlight span creation/removal would displace
987- // the cursor.
988- if ( getState ( ) . editMode && viewer . contains ( document . activeElement ) ) return ;
989- if ( ! viewer ) return ;
989+ const skipHighlight = getState ( ) . editMode && viewer . contains ( document . activeElement ) ;
990990
991991 const elements = viewer . querySelectorAll ( "[data-source-line]" ) ;
992992 let bestEl = null ;
@@ -1089,7 +1089,8 @@ function handleScrollToLine(data) {
10891089 setTimeout ( ( ) => { _scrollFromCM = false ; } , 200 ) ;
10901090
10911091 // Persistent highlight on the element corresponding to the CM cursor.
1092- // Only show when CM has focus (not when viewer has focus).
1092+ // Skip highlight when viewer has focus to avoid cursor displacement.
1093+ if ( skipHighlight ) return ;
10931094 _removeCursorHighlight ( viewer ) ;
10941095
10951096 // For <br> paragraphs, wrap only the specific line's content in a
0 commit comments