@@ -62,7 +62,6 @@ import {
6262} from "@codemirror/view" ;
6363import {
6464 ChangeDesc ,
65- Compartment ,
6665 EditorState ,
6766 Extension ,
6867 StateField ,
@@ -102,15 +101,8 @@ import { show_toast } from "./show_toast.mjs";
102101// -----------------------------------------------------------------------------
103102let current_view : EditorView ;
104103let tinymce_singleton : Editor | undefined ;
105- // True if the change was produce by a user edit, not an IDE update.
106- let is_user_change = false ;
107104// This indicates that a call to `on_dirty` is scheduled, but hasn't run yet.
108105let on_dirty_scheduled = false ;
109- // True to ignore the next text selection change, since updates to the cursor or
110- // scroll position from the Client trigged this change.
111- let ignore_selection_change = false ;
112- // The compartment used to enable and disable the autosave extension.
113- const autosaveCompartment = new Compartment ( ) ;
114106
115107// Options used when creating a `Decoration`.
116108const decorationOptions = {
@@ -124,8 +116,12 @@ declare global {
124116 }
125117}
126118
119+ // When this is included in a transaction, don't update from/to of doc blocks.
127120const docBlockFreezeAnnotation = Annotation . define < boolean > ( ) ;
128121
122+ // When this is included in a transaction, don't send autosave scroll/cursor location updates.
123+ const noAutosaveAnnotation = Annotation . define < boolean > ( ) ;
124+
129125// Doc blocks in CodeMirror
130126// -----------------------------------------------------------------------------
131127//
@@ -193,6 +189,7 @@ export const docBlockField = StateField.define<DecorationSet>({
193189 effect . value . indent ,
194190 effect . value . delimiter ,
195191 effect . value . content ,
192+ false ,
196193 ) ,
197194 ...decorationOptions ,
198195 } ) . range ( effect . value . from , effect . value . to ) ,
@@ -279,6 +276,8 @@ export const docBlockField = StateField.define<DecorationSet>({
279276 prev . spec . widget . contents ,
280277 effect . value . contents ,
281278 ) ,
279+ // Assume this isn't a user change unless it's specified.
280+ effect . value . is_user_change ?? false ,
282281 ) ,
283282 ...decorationOptions ,
284283 } ) . range ( from , to ) ,
@@ -329,7 +328,12 @@ export const docBlockField = StateField.define<DecorationSet>({
329328 contents ,
330329 ] : CodeMirrorDocBlockTuple ) =>
331330 Decoration . replace ( {
332- widget : new DocBlockWidget ( indent , delimiter , contents ) ,
331+ widget : new DocBlockWidget (
332+ indent ,
333+ delimiter ,
334+ contents ,
335+ false ,
336+ ) ,
333337 ...decorationOptions ,
334338 } ) . range ( from , to ) ,
335339 ) ,
@@ -369,6 +373,8 @@ type updateDocBlockType = {
369373 indent ?: string ;
370374 delimiter ?: string ;
371375 contents : string | StringDiff [ ] ;
376+ // True if this update comes from a user change, as opposed to an update received from the IDE.
377+ is_user_change ?: boolean ;
372378} ;
373379
374380// Define an update.
@@ -411,6 +417,7 @@ class DocBlockWidget extends WidgetType {
411417 readonly indent : string ,
412418 readonly delimiter : string ,
413419 readonly contents : string ,
420+ readonly is_user_change : boolean ,
414421 ) {
415422 // TODO: I don't understand why I don't need to store the provided
416423 // parameters in the object: `this.indent = indent;`, etc.
@@ -449,10 +456,10 @@ class DocBlockWidget extends WidgetType {
449456 // [docs](https://codemirror.net/docs/ref/#view.WidgetType.updateDOM),
450457 // "Update a DOM element created by a widget of the same type (but
451458 // different, non-eq content) to reflect this widget."
452- updateDOM ( dom : HTMLElement , view : EditorView ) : boolean {
459+ updateDOM ( dom : HTMLElement , _view : EditorView ) : boolean {
453460 // If this change was produced by a user edit, then the DOM was already updated. Stop here.
454- if ( is_user_change ) {
455- is_user_change = false ;
461+ if ( this . is_user_change ) {
462+ console . log ( "user change -- skipping DOM update." ) ;
456463 return true ;
457464 }
458465 ( dom . childNodes [ 0 ] as HTMLDivElement ) . innerHTML = this . indent ;
@@ -462,10 +469,14 @@ class DocBlockWidget extends WidgetType {
462469 const [ contents_div , is_tinymce ] = get_contents ( dom ) ;
463470 window . MathJax . typesetClear ( contents_div ) ;
464471 if ( is_tinymce ) {
465- // Save the cursor location before the update, then restore it afterwards.
466- const sel = saveSelection ( ) ;
472+ // Save the cursor location before the update, then restore it afterwards, if TinyMCE has focus.
473+ const sel = tinymce_singleton ! . hasFocus ( )
474+ ? saveSelection ( )
475+ : undefined ;
467476 tinymce_singleton ! . setContent ( this . contents ) ;
468- restoreSelection ( sel ) ;
477+ if ( sel !== undefined ) {
478+ restoreSelection ( sel ) ;
479+ }
469480 } else {
470481 contents_div . innerHTML = this . contents ;
471482 }
@@ -552,7 +563,7 @@ const saveSelection = () => {
552563 return { selection_path, selection_offset } ;
553564} ;
554565
555- // Restore the selection produced by `saveSelection`.
566+ // Restore the selection produced by `saveSelection` to the active TinyMCE instance .
556567const restoreSelection = ( {
557568 selection_path,
558569 selection_offset,
@@ -651,24 +662,22 @@ const element_is_in_doc_block = (
651662// untypeset, then the dirty ignored.
652663// 3. When MathJax typesets math on a TinyMCE focus out event, the dirty flag
653664// gets set. This should be ignored. However, typesetting is an async
654- // operation, so we assume it's OK to await the typeset completion, then
655- // clear the `ignore_next_dirty flag`. This will lead to nasty bugs at some
656- // point.
665+ // operation, so we assume it's OK to await the typeset completion.
666+ // This will lead to nasty bugs at some point.
657667// 4. When an HTML doc block is assigned to the TinyMCE instance for editing,
658668// the dirty flag is set. This must be ignored.
659669const on_dirty = (
660670 // The div that's dirty. It must be a child of the doc block div.
661671 event_target : HTMLElement ,
662672) => {
663- is_user_change = true ;
664-
665673 if ( on_dirty_scheduled ) {
666674 return ;
667675 }
668676 on_dirty_scheduled = true ;
669677
670678 // Only run this after typesetting is done.
671- window . MathJax . whenReady ( ( ) => {
679+ window . MathJax . whenReady ( async ( ) => {
680+ console . log ( "Starting update for user change." ) ;
672681 on_dirty_scheduled = false ;
673682 // Find the doc block parent div.
674683 const target = ( event_target as HTMLDivElement ) . closest (
@@ -694,18 +703,17 @@ const on_dirty = (
694703 const contents = is_tinymce
695704 ? tinymce_singleton ! . save ( )
696705 : contents_div . innerHTML ;
697- // Although this is async, nothing following this call depends on its completion.
698- mathJaxTypeset ( contents_div ) ;
699- let effects : StateEffect < updateDocBlockType > [ ] = [
700- updateDocBlock . of ( {
701- from,
702- indent,
703- delimiter,
704- contents,
705- } ) ,
706- ] ;
707-
708- current_view . dispatch ( { effects } ) ;
706+ await mathJaxTypeset ( contents_div ) ;
707+ current_view . dispatch ( {
708+ effects : [
709+ updateDocBlock . of ( {
710+ from,
711+ indent,
712+ delimiter,
713+ contents,
714+ } ) ,
715+ ] ,
716+ } ) ;
709717 } ) ;
710718} ;
711719
@@ -819,7 +827,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
819827 // Wait until the focus event completes; this causes the
820828 // cursor position (the selection) to be set in the
821829 // contenteditable div. Then, save that location.
822- setTimeout ( ( ) => {
830+ setTimeout ( async ( ) => {
823831 // Untypeset math in the old doc block and the current doc block before moving its contents around.
824832 const tinymce_div =
825833 document . getElementById ( "TinyMCE-inst" ) ! ;
@@ -847,7 +855,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
847855 null ,
848856 ) ;
849857 // The previous content edited by TinyMCE is now a div. Retypeset this after the transition.
850- mathJaxTypeset ( old_contents_div ) ;
858+ await mathJaxTypeset ( old_contents_div ) ;
851859 // Move TinyMCE to the new location, then remove the old
852860 // div it will replace.
853861 target . insertBefore ( tinymce_div , null ) ;
@@ -857,7 +865,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
857865 tinymce_singleton ! . setContent ( contents_div . innerHTML ) ;
858866 contents_div . remove ( ) ;
859867 // The new div is now a TinyMCE editor. Retypeset this.
860- mathJaxTypeset ( tinymce_div ) ;
868+ await mathJaxTypeset ( tinymce_div ) ;
861869
862870 // This process causes TinyMCE to lose focus. Restore
863871 // that. However, this causes TinyMCE to lose the
@@ -895,6 +903,15 @@ const autosaveExtension = EditorView.updateListener.of(
895903 // [ViewUpdate](https://codemirror.net/docs/ref/#view.ViewUpdate) which
896904 // describes a change being made to the document.
897905 ( v : ViewUpdate ) => {
906+ // Ignore any transaction group marked with a `noAutosaveAnnotation`.
907+ if (
908+ v . transactions . some (
909+ ( tr ) => tr . annotation ( noAutosaveAnnotation ) === true ,
910+ )
911+ ) {
912+ return true ;
913+ }
914+
898915 // The
899916 // [docChanged](https://codemirror.net/docs/ref/#view.ViewUpdate.docChanged)
900917 // flag is the relevant part of this change description. However, this
@@ -919,10 +936,6 @@ const autosaveExtension = EditorView.updateListener.of(
919936 set_is_dirty ( ) ;
920937 startAutosaveTimer ( ) ;
921938 } else if ( v . selectionSet ) {
922- if ( ignore_selection_change ) {
923- ignore_selection_change = false ;
924- return ;
925- }
926939 // Send an update if only the selection changed.
927940 startAutosaveTimer ( ) ;
928941 }
@@ -1047,7 +1060,7 @@ export const CodeMirror_load = async (
10471060 parser ,
10481061 basicSetup ,
10491062 EditorView . lineWrapping ,
1050- autosaveCompartment . of ( autosaveExtension ) ,
1063+ autosaveExtension ,
10511064 // Make tab an indent per the
10521065 // [docs](https://codemirror.net/examples/tab/). TODO:
10531066 // document a way to escape the tab key per the same docs.
@@ -1093,16 +1106,17 @@ export const CodeMirror_load = async (
10931106 } )
10941107 ) [ 0 ] ;
10951108 } else {
1096- // Disable autosave when performing these updates.
1097- current_view . dispatch ( { effects : autosaveCompartment . reconfigure ( [ ] ) } ) ;
10981109 // This contains a diff, instead of plain text. Apply the text diff.
10991110 //
11001111 // First, apply just the text edits. Use an annotation so that the doc
11011112 // blocks aren't changed; without this, the diff won't work (since
11021113 // from/to values of doc blocks are changed by unfrozen text edits).
11031114 current_view . dispatch ( {
11041115 changes : codechat_for_web . source . Diff . doc ,
1105- annotations : docBlockFreezeAnnotation . of ( true ) ,
1116+ annotations : [
1117+ docBlockFreezeAnnotation . of ( true ) ,
1118+ noAutosaveAnnotation . of ( true ) ,
1119+ ] ,
11061120 } ) ;
11071121 // Now, apply the diff in a separate transaction. Applying them in the
11081122 // same transaction causes the text edits to modify from/to values in
@@ -1130,10 +1144,9 @@ export const CodeMirror_load = async (
11301144 }
11311145 }
11321146 // Update the view with these changes to the state.
1133- current_view . dispatch ( { effects : stateEffects } ) ;
1134- // Resume autosave.
11351147 current_view . dispatch ( {
1136- effects : autosaveCompartment . reconfigure ( autosaveExtension ) ,
1148+ effects : stateEffects ,
1149+ annotations : noAutosaveAnnotation . of ( true ) ,
11371150 } ) ;
11381151 }
11391152 scroll_to_line ( cursor_line , scroll_line ) ;
@@ -1145,8 +1158,10 @@ export const scroll_to_line = (cursor_line?: number, scroll_line?: number) => {
11451158 return ;
11461159 }
11471160
1148- // Create a transaction to set the cursor and scroll position.
1149- const dispatch_data : TransactionSpec = { } ;
1161+ // Create a transaction to set the cursor and scroll position. Avoid an autosave that sends updated cursor/scroll positions produced by this transaction.
1162+ const dispatch_data : TransactionSpec = {
1163+ annotations : noAutosaveAnnotation . of ( true ) ,
1164+ } ;
11501165 if ( cursor_line !== undefined ) {
11511166 // Translate the line numbers to a position.
11521167 const cursor_pos = current_view ?. state . doc . line ( cursor_line ) . from ;
@@ -1169,7 +1184,6 @@ export const scroll_to_line = (cursor_line?: number, scroll_line?: number) => {
11691184 }
11701185
11711186 // Run it.
1172- ignore_selection_change = true ;
11731187 current_view ?. dispatch ( dispatch_data ) ;
11741188} ;
11751189
0 commit comments