nobiot / org-transclusion

Emacs package to enable transclusion with Org Mode
https://nobiot.github.io/org-transclusion/
GNU General Public License v3.0
902 stars 43 forks source link

bug: infinite loop / org-element--cache corruption during live sync #243

Open akashpal-21 opened 2 months ago

akashpal-21 commented 2 months ago

org-element-cache is corrupted sometimes during live sync which causes emacs to infinitely generate errors

since #' org-transclusion-content-org-buffer-or-element uses #'org-element-context this causes org-transclusion to go into infinite recursion.

Adding video to show the problem.

https://github.com/nobiot/org-transclusion/assets/46517170/6e095d89-e9af-478b-ba91-3f2e067062d7

nobiot commented 2 months ago

What is the Org version you use? I will need to look at the other (older) infinite loop issue you investigated extensively first, but if the built-in org-element has an influence here, the version info may be relevant.

nobiot commented 2 months ago

org-element-cache-reset opens the pandora's box for more bugs

Where is this done? I don't recall me doing it explicitly.

akashpal-21 commented 2 months ago

One Solution

Improvements are requested. With async clone propagation org-element--cache-sync works appropriately.

Solution is yet hacky - will require refinement from someone more experienced.

(defun patch/text-clone-live-sync (ol1 after beg end &optional _len)
    (when (and after                                     
           (not text-clone-live-sync-in-progress)   
           (overlay-start ol1)                      
           (<= beg end))                            

      (save-excursion

    ;; Now go ahead and update the clones.
    (let ((head (- beg (overlay-start ol1)))       
          (tail (- (overlay-end ol1) end))         
          (str (buffer-substring-no-properties beg end))
          (text-clone-live-sync-in-progress t))   

      ;; Iterate over each clone of the overlay ol1.
      (dolist (ol2 (overlay-get ol1 'text-clones))
        ;; Call the helper function to perform actions within the context of the buffer of the clone ol2.
        (text-clone-live-sync--async-clone ol1 ol2 head tail str))))))

(defun text-clone-live-sync--async-clone (ol1 ol2 head tail str)
  "Update a clone with synchronized text asynchronously."
  ;; Set `run-clone` to t
  (overlay-put ol2 'run-clone t)
  ;; Initiate timer to run asynchronously
  (run-with-timer 0 nil
          (lambda (ol1 ol2 head tail str)
            (with-current-buffer (overlay-buffer ol2)
              ;; Check if `run-clone` is set to t
              (when (overlay-get ol2 'run-clone)
            ;; Set `run-clone` to nil before proceeding to prevent infinite loop
            (overlay-put ol2 'run-clone nil)
            (save-restriction
              (widen)
              (let ((oe (overlay-end ol2)))
                (unless (or (eq ol1 ol2) (null oe)) 
                  (let ((mod-beg (+ (overlay-start ol2) head)))
                (goto-char (- oe tail))
                (if (not (> mod-beg (point)))  
                    (progn                     
                      (save-excursion (insert str)) 
                      (delete-region mod-beg (point))) 
                  (user-error "No live-sync done.  \
The text strings in the overlays are not identical"))
                )))))))
          ol1 ol2 head tail str))

(advice-add 'text-clone-live-sync :override #'patch/text-clone-live-sync)
akashpal-21 commented 2 months ago

Radchenko also suggested a more trivial way to ensure that before-change-functions run appropriately within the context of the second buffer -- it is to set inhibit-modification-hooks to nil before proceeding -- but we are this way out maneuvering a limitation that has been placed by emacs - I do not know why they have done this, so for the sake of caution I had resorted to using a run-with-timer 0 but also the following works. Please check documentation of before-change-functions for more information about it.

(defun patch/text-clone-live-sync (ol1 after beg end &optional _len)
  (when (and after
             (not text-clone-live-sync-in-progress)
             (overlay-start ol1)
             (<= beg end))
    (save-excursion
      ;; Now go ahead and update the clones.
      (let ((head (- beg (overlay-start ol1)))
            (tail (- (overlay-end ol1) end))
            (str (buffer-substring-no-properties beg end)) ;changed to no-properties
            (text-clone-live-sync-in-progress t)
        ;; patch: set inhibit-modification-hooks to nil
        ;; to ensure befre-change-functions run appropriately
        ;; within the context of clone buffer.
        (inhibit-modification-hooks nil))
        (dolist (ol2 (overlay-get ol1 'text-clones))
          (with-current-buffer (overlay-buffer ol2)
            (save-restriction
              (widen)
              (let ((oe (overlay-end ol2)))
                (unless (or (eq ol1 ol2) (null oe))
                  (let ((mod-beg (+ (overlay-start ol2) head)))
                    (goto-char (- oe tail))
                    (if (not (> mod-beg (point)))
                        (progn
                          (save-excursion (insert str))
                          (delete-region mod-beg (point)))
                      (user-error "No live-sync done.  \
The text strings in the overlays are not identical"))))))))))))

-- or more simply

(defun patch/text-clone-live-sync (fn &rest args)
  (let ((inhibit-modification-hooks nil))
    (apply fn args)))
(advice-add 'text-clone-live-sync :around #'patch/text-clone-live-sync)