branch: externals/org-transclusion
commit d0e43fa5e0a0014df6de3817bfce0f2f7ffda378
Merge: e9d7e24ce0 74321bba9a
Author: nobiot <[email protected]>
Commit: GitHub <[email protected]>

    Merge pull request #281 from 
gggion/refactor/propertization-management-and-indentation
---
 org-transclusion-indent-mode.el       | 328 ++++++++++++++++++++++++++++------
 org-transclusion.el                   | 277 ++++++++++++++++++++++++----
 test/test-indent-mode-destination.org |  85 +++++++++
 test/test-indent-mode-source.org      |  74 ++++++++
 4 files changed, 673 insertions(+), 91 deletions(-)

diff --git a/org-transclusion-indent-mode.el b/org-transclusion-indent-mode.el
index 2b3ba26a48..532439bfd3 100644
--- a/org-transclusion-indent-mode.el
+++ b/org-transclusion-indent-mode.el
@@ -22,70 +22,284 @@
 ;;; Commentary:
 ;;  This file is part of Org-transclusion
 ;;  URL: https://github.com/nobiot/org-transclusion
+;;
+;;  This extension ensures org-indent-mode properties are correctly
+;;  applied to transcluded content and refreshed after transclusion
+;;  removal. It also preserves fringe indicators in both source and
+;;  destination buffers when org-indent-mode regenerates line-prefix
+;;  properties.
+;;
+;;  The timing mechanism for synchronizing with org-indent's asynchronous
+;;  initialization is copied from org-modern-indent.
 
 ;;; Code:
 
 (require 'org-indent)
-(declare-function org-transclusion-within-transclusion-p
-                  "org-transclusion")
 
-(add-hook 'org-transclusion-after-add-functions
-          #'org-translusion-indent-add-properties)
+;;;; Variables
+
+(defvar-local org-transclusion-indent--timer nil
+  "Timer for debounced fringe re-application.")
+
+(defvar-local org-transclusion-indent--last-change-tick nil
+  "Buffer modification tick at last fringe application.")
+
+(defvar-local org-transclusion-indent--has-overlays nil
+  "Non-nil if buffer has ever had source overlays.
+Used to prevent premature mode deactivation during buffer refresh.")
+
+(defvar-local org-transclusion-indent--init nil
+  "Initialization state for waiting on org-indent.
+Either nil, t (initialized), or (TIMER ATTEMPT-COUNT).")
+
+;;;; Forward Declarations
+
+;; Silence byte-compiler warnings for functions defined in org-transclusion.el
+(declare-function org-transclusion-prefix-has-fringe-p "org-transclusion" 
(prefix))
+(declare-function org-transclusion-add-fringe-to-region "org-transclusion" 
(buffer beg end face))
+(declare-function org-transclusion-remove-fringe-from-region 
"org-transclusion" (buffer beg end))
+
+
+;; Variable defined by define-minor-mode later in this file
+(defvar org-transclusion-indent-mode)
+
+(defun org-transclusion-indent--find-source-overlays ()
+  "Return list of all transclusion source overlays in current buffer."
+  (seq-filter
+   (lambda (ov) (overlay-get ov 'org-transclusion-by))
+   (overlays-in (point-min) (point-max))))
+
+(defun org-transclusion-indent--reapply-all-fringes ()
+  "Re-apply fringe indicators to all transcluded regions in buffer.
+This function is called after any change that might have removed
+`line-prefix' or `wrap-prefix' properties.
+
+In graphical mode, optimizes by checking only the first line of each
+overlay region, since org-indent regenerates entire subtrees at once.
+
+In terminal mode, always re-applies fringes to all lines, since
+org-indent may regenerate individual lines during typing."
+  (when (buffer-live-p (current-buffer))
+    (let ((current-tick (buffer-modified-tick))
+          (overlays (org-transclusion-indent--find-source-overlays)))
+      ;; Track if we have overlays
+      (when overlays
+        (setq org-transclusion-indent--has-overlays t))
+
+      ;; Only re-apply if buffer actually changed since last application
+      (unless (eq current-tick org-transclusion-indent--last-change-tick)
+        (setq org-transclusion-indent--last-change-tick current-tick)
+        (dolist (ov overlays)
+          (let ((ov-beg (overlay-start ov))
+                (ov-end (overlay-end ov)))
+            (when (and ov-beg ov-end)
+              (if (display-graphic-p)
+                  ;; Graphical mode: optimize by checking only first line
+                  (save-excursion
+                    (goto-char ov-beg)
+                    (let* ((line-beg (line-beginning-position))
+                           (line-prefix (get-text-property line-beg 
'line-prefix)))
+                      (when (and line-prefix
+                                 (not (org-transclusion-prefix-has-fringe-p 
line-prefix)))
+                        (org-transclusion-add-fringe-to-region
+                         (current-buffer) ov-beg ov-end
+                         'org-transclusion-source-fringe))))
+                ;; Terminal mode: always re-apply to all lines
+                (org-transclusion-add-fringe-to-region
+                 (current-buffer) ov-beg ov-end
+                 'org-transclusion-source-fringe)))))))))
+
+(defun org-transclusion-indent--schedule-reapply ()
+  "Schedule fringe re-application after a short delay.
+This debounces rapid changes to avoid excessive processing.
+
+In graphical mode, uses a shorter delay (0.2s) since bitmap rendering
+is fast and flicker-free.  In terminal mode, uses a longer delay (0.7s)
+to reduce visible flicker of ASCII fringe indicators during rapid typing."
+  (when org-transclusion-indent--timer
+    (cancel-timer org-transclusion-indent--timer))
+  (setq org-transclusion-indent--timer
+        (run-with-idle-timer
+         (if (display-graphic-p) 0.2 0.7)  ; Shorter delay for graphical, 
longer for terminal
+         nil
+         (lambda (buf)
+           (when (buffer-live-p buf)
+             (with-current-buffer buf
+               (org-transclusion-indent--reapply-all-fringes))))
+         (current-buffer))))
+
+(defun org-transclusion-indent--after-change (_beg _end _len)
+  "Schedule fringe re-application after buffer change.
+Added to `after-change-functions' in source buffers."
+  (org-transclusion-indent--schedule-reapply))
+
+(defun org-transclusion-indent--check-and-disable ()
+  "Disable mode if no source overlays remain in buffer.
+Only disables if overlays have been checked and confirmed absent,
+not during temporary states like buffer refresh."
+  (when (and org-transclusion-indent--has-overlays
+             (not (org-transclusion-indent--find-source-overlays)))
+    ;; Wait a bit to ensure this isn't just a temporary state
+    (run-with-idle-timer
+     0.2 nil
+     (lambda (buf)
+       (when (buffer-live-p buf)
+         (with-current-buffer buf
+           (unless (org-transclusion-indent--find-source-overlays)
+             (org-transclusion-indent-mode -1)))))
+     (current-buffer))))
+
+(defun org-transclusion-indent--wait-and-init (buf)
+  "Wait for org-indent to finish initializing BUF, then apply fringes.
+Copied from org-modern-indent's timing mechanism."
+  (if (or (not (bound-and-true-p org-indent-agentized-buffers))
+          (memq buf org-indent-agentized-buffers))
+      ;; org-indent is ready
+      (org-transclusion-indent--init buf)
+    ;; Still waiting
+    (when (buffer-live-p buf)
+      (with-current-buffer buf
+        (if org-transclusion-indent--init
+            (let ((cnt (cl-incf (cadr org-transclusion-indent--init))))
+              (if (> cnt 5)
+                  (progn
+                    (message "org-transclusion-indent-mode: Gave up waiting 
for %s to initialize" buf)
+                    (setq org-transclusion-indent--init t))
+                (timer-activate
+                 (timer-set-time (car org-transclusion-indent--init)
+                                 (time-add (current-time) 0.2)))))
+          (setq org-transclusion-indent--init
+                (list (run-at-time 0.1 nil 
#'org-transclusion-indent--wait-and-init buf)
+                      1)))))))
 
-(defun org-translusion-indent-add-properties (beg end)
-  "BEG END."
+(defun org-transclusion-indent--init (buf)
+  "Initialize indent mode in BUF after org-indent completes.
+To be added to `org-indent-post-buffer-init-functions'."
+  (when (buffer-live-p buf)
+    (with-current-buffer buf
+      (setq org-transclusion-indent--init t)
+      (org-transclusion-indent--reapply-all-fringes))))
+
+(defun org-transclusion-indent--auto-enable-maybe ()
+  "Auto-enable indent mode if source overlays are detected.
+Added to `post-command-hook' in `org-mode' buffers with `org-indent-mode'."
+  (when (and (not org-transclusion-indent-mode)
+             (org-transclusion-indent--find-source-overlays))
+    (org-transclusion-indent-mode +1)))
+
+;;;; Destination Buffer Support
+
+(defun org-transclusion-indent--add-properties-and-fringes (beg __end)
+  "Ensure org-indent properties and fringe indicators in transcluded region.
+BEG and END are approximate bounds; we find actual bounds from text properties.
+
+When org-indent-mode is active, `org-indent-add-properties' overwrites
+the uniform `line-prefix' and `wrap-prefix' properties set by the main
+package, removing fringe indicators. This function re-applies fringes
+by appending them to org-indent's indentation prefixes."
   (when org-indent-mode
-    (advice-add #'org-indent-set-line-properties
-                :override
-                #'org-transclusion-indent-set-line-properties-ad)
-    (org-indent-add-properties beg end)
-    (advice-remove #'org-indent-set-line-properties
-                   #'org-transclusion-indent-set-line-properties-ad)))
-
-(defun org-transclusion-indent-set-line-properties-ad (level indentation 
&optional heading)
-  "Set prefix properties on current line an move to next one.
-
-LEVEL is the current level of heading.  INDENTATION is the
-expected indentation when wrapping line.
-
-When optional argument HEADING is non-nil, assume line is at
-a heading.  Moreover, if it is `inlinetask', the first star will
-have `org-warning' face."
-
-  (let* ((line (aref (pcase heading
-                       (`nil org-indent--text-line-prefixes)
-                       (`inlinetask org-indent--inlinetask-line-prefixes)
-                       (_ org-indent--heading-line-prefixes))
-                     level))
-         (wrap
-          (org-add-props
-              (concat line
-                      (if heading (concat (make-string level ?*) " ")
-                        (make-string indentation ?\s)))
-              nil 'face 'org-indent)))
-
-    ;; Org-transclusion's addition begin
-    (when (org-transclusion-within-transclusion-p)
-      (setq line
-            (concat line
-                    (propertize
-                     "x"
-                     'display
-                     '(left-fringe org-transclusion-fringe-bitmap
-                                   org-transclusion-fringe))))
-      (setq wrap
-            (concat line
-                    (propertize
-                     "x"
-                     'display
-                     '(left-fringe org-transclusion-fringe-bitmap
-                                   org-transclusion-fringe)))))
-    ;; Org-transclusion's addition end
-
-    ;; Add properties down to the next line to indent empty lines.
-    (add-text-properties (line-beginning-position) (line-beginning-position 2)
-                         `(line-prefix ,line wrap-prefix ,wrap)))
-  (forward-line))
+    ;; Find actual transclusion bounds using text properties
+    ;; The transclusion that was just added should be at or near BEG
+    (save-excursion
+      (goto-char beg)
+      ;; Search forward for org-transclusion-type property
+      (when-let* ((match (text-property-search-forward 'org-transclusion-type))
+                  (actual-beg (prop-match-beginning match))
+                  (actual-end (prop-match-end match)))
+        ;; Apply org-indent properties and fringes to actual bounds
+        (org-indent-add-properties actual-beg actual-end)
+        (org-transclusion-add-fringe-to-region
+         (current-buffer) actual-beg actual-end 'org-transclusion-fringe)))))
+
+(defun org-transclusion-indent--refresh-source-region (src-buf src-beg src-end)
+  "Refresh org-indent properties in source region after transclusion removal.
+SRC-BUF is the source buffer, SRC-BEG and SRC-END are the region bounds.
+
+For `org-mode' buffers with `org-indent-mode', refreshes indentation 
properties.
+For non-org buffers, removes fringe indicators that were added during
+transclusion."
+  (with-current-buffer src-buf
+    (if (buffer-local-value 'org-indent-mode src-buf)
+        ;; Org buffer with indent-mode: refresh properties
+        (progn
+          (org-indent-add-properties src-beg src-end)
+          (when (and (boundp 'org-transclusion-indent-mode)
+                     org-transclusion-indent-mode)
+            (org-transclusion-indent--check-and-disable)))
+      ;; Non-org buffer or org buffer without indent-mode: just remove fringes
+      (org-transclusion-remove-fringe-from-region src-buf src-beg src-end))))
+
+;;;; Minor Mode Definition
+
+;;;###autoload
+(define-minor-mode org-transclusion-indent-mode
+  "Minor mode for org-indent-mode support in org-transclusion.
+
+This mode serves two purposes:
+
+1. In destination buffers: ensures org-indent properties are applied
+   and fringe indicators are preserved when org-indent overwrites them.
+
+2. In source buffers: preserves fringe indicators when org-indent-mode
+   regenerates `line-prefix' properties.
+
+The mode auto-activates in source buffers when transclusion source
+overlays are detected, and auto-deactivates when all transclusions
+are removed."
+  :init-value nil
+  :lighter " OT-Indent"
+  :group 'org-transclusion
+  (if org-transclusion-indent-mode
+      (progn
+        ;; Install hooks for source buffer fringe preservation
+        (add-hook 'after-change-functions
+                  #'org-transclusion-indent--after-change nil t)
+
+        ;; Register with org-indent or wait for it
+        (cond
+         ;; Already initialized before, just toggle
+         ((or (called-interactively-p 'any) org-transclusion-indent--init)
+          (org-transclusion-indent--init (current-buffer)))
+         ;; Register with buffer init hook if available
+         ((boundp 'org-indent-post-buffer-init-functions)
+          (add-hook 'org-indent-post-buffer-init-functions
+                    #'org-transclusion-indent--init nil t))
+         ;; Fallback: wait for org-indent
+         (t (org-transclusion-indent--wait-and-init (current-buffer)))))
+
+    ;; Cleanup
+    (remove-hook 'after-change-functions
+                 #'org-transclusion-indent--after-change t)
+    (when (boundp 'org-indent-post-buffer-init-functions)
+      (remove-hook 'org-indent-post-buffer-init-functions
+                   #'org-transclusion-indent--init t))
+    (when org-transclusion-indent--timer
+      (cancel-timer org-transclusion-indent--timer)
+      (setq org-transclusion-indent--timer nil))
+    (when (and (listp org-transclusion-indent--init)
+               (timerp (car org-transclusion-indent--init)))
+      (cancel-timer (car org-transclusion-indent--init)))
+    (setq org-transclusion-indent--has-overlays nil
+          org-transclusion-indent--init nil)))
+
+;;;###autoload
+(defun org-transclusion-indent-mode-setup ()
+  "Set up auto-activation of `indent mode' in `org-mode' buffers.
+Adds `post-command-hook' to detect when source overlays appear."
+  (when (and (derived-mode-p 'org-mode)
+             (bound-and-true-p org-indent-mode))
+    (add-hook 'post-command-hook
+              #'org-transclusion-indent--auto-enable-maybe nil t)))
+
+;; Auto-setup in org-mode buffers - add late to hook like org-modern-indent
+(add-hook 'org-mode-hook #'org-transclusion-indent-mode-setup 90)
+
+;;;; Hook Registration
+
+(add-hook 'org-transclusion-after-add-functions
+          #'org-transclusion-indent--add-properties-and-fringes)
+(add-hook 'org-transclusion-after-remove-functions
+          #'org-transclusion-indent--refresh-source-region)
 
 (provide 'org-transclusion-indent-mode)
 
diff --git a/org-transclusion.el b/org-transclusion.el
index 0b97a62680..6a922477d1 100644
--- a/org-transclusion.el
+++ b/org-transclusion.el
@@ -116,6 +116,17 @@ and end, pointing to the beginning and end of the 
transcluded
 content."
   :type '(repeat function))
 
+(defcustom org-transclusion-after-remove-functions nil
+  "Functions to be called after a transclusion has been removed.
+The hook runs after the transclusion overlay has been deleted and
+the #+transclude keyword has been re-inserted. It is intended for
+cleanup operations in the source buffer. For example, it is used by
+the `org-transclusion-indent-mode' extension to refresh org-indent
+properties after transclusion removal. The functions are called with
+arguments (src-buf src-beg src-end), pointing to the source buffer
+and the region that was transcluded."
+  :type '(repeat function))
+
 ;;;; Faces
 
 (defface org-transclusion-source-fringe
@@ -549,6 +560,18 @@ When success, return the beginning point of the keyword 
re-inserted."
           beg
         (when (org-transclusion-within-live-sync-p)
           (org-transclusion-live-sync-exit))
+
+        ;; Clean up source buffer fringe indicators before deleting overlay
+        (when (overlay-buffer tc-pair-ov)
+          (let ((src-buf (overlay-buffer tc-pair-ov))
+                (src-beg (overlay-start tc-pair-ov))
+                (src-end (overlay-end tc-pair-ov)))
+            ;; Remove our fringe indicators from source
+            (org-transclusion-remove-fringe-from-region src-buf src-beg 
src-end)
+            ;; Run hooks for extensions to do additional cleanup
+            (run-hook-with-args 'org-transclusion-after-remove-functions
+                                src-buf src-beg src-end)))
+
         (delete-overlay tc-pair-ov)
         (org-transclusion-with-inhibit-read-only
           (save-excursion
@@ -977,8 +1000,8 @@ Return nil if not found."
 ;;-----------------------------------------------------------------------------
 ;;;; Functions for inserting content
 
-(defun org-transclusion-content-insert ( keyword-values type content
-                                         sbuf sbeg send copy)
+(defun org-transclusion-content-insert (keyword-values type content
+                                                       sbuf sbeg send copy)
   "Insert CONTENT at point and put source overlay in SBUF.
 Return t when successful.
 
@@ -1034,22 +1057,21 @@ based on the following arguments:
       type content (plist-get keyword-values :current-indentation)))
     (setq end (point))
     (unless copy
+      ;; Add uniform fringe indicator to transcluded content
       (add-text-properties
        beg end
        `( local-map ,org-transclusion-map
           read-only t
           front-sticky t
-          ;; rear-nonticky seems better for
-          ;; src-lines to add "#+result" after C-c
-          ;; C-c
           rear-nonsticky t
           org-transclusion-id ,id
           org-transclusion-type ,type
           org-transclusion-pair ,tc-pair
           org-transclusion-orig-keyword ,keyword-values
-          ;; TODO Fringe is not supported for terminal
-          line-prefix ,(org-transclusion-propertize-transclusion)
-          wrap-prefix ,(org-transclusion-propertize-transclusion)))
+          line-prefix ,(org-transclusion--make-fringe-indicator
+                        'org-transclusion-fringe)
+          wrap-prefix ,(org-transclusion--make-fringe-indicator
+                        'org-transclusion-fringe)))
       ;; Put the transclusion overlay
       (let ((ov-tc (text-clone-make-overlay beg end)))
         (overlay-put ov-tc 'evaporate t)
@@ -1060,12 +1082,14 @@ based on the following arguments:
       (overlay-put ov-src 'org-transclusion-buffer tc-buffer)
       (overlay-put ov-src 'evaporate t)
       (overlay-put ov-src 'face 'org-transclusion-source)
-      (overlay-put ov-src 'line-prefix (org-transclusion-propertize-source))
-      (overlay-put ov-src 'wrap-prefix (org-transclusion-propertize-source))
       (overlay-put ov-src 'priority -60)
-      ;; TODO this should not be necessary, but it is at the moment
-      ;; live-sync-enclosing-element fails without tc-pair on source overlay
-      (overlay-put ov-src 'org-transclusion-pair tc-pair))
+      (overlay-put ov-src 'org-transclusion-pair tc-pair)
+      ;; Add modification hook to source overlay
+      (overlay-put ov-src 'modification-hooks
+                   '(org-transclusion-source-overlay-modified))
+      ;; Add per-line fringe indicators to source buffer only
+      (org-transclusion-add-fringe-to-region
+       sbuf sbeg send 'org-transclusion-source-fringe))
     t))
 
 (defun org-transclusion-content-highest-org-headline ()
@@ -1303,8 +1327,213 @@ is non-nil."
              :src-end (point-max))))))
 
 ;;-----------------------------------------------------------------------------
-;;; Utility Functions
+;;; Helper Functions
+(defun org-transclusion--fringe-spec-p (prop-value)
+  "Return non-nil if PROP-VALUE represents a transclusion fringe indicator.
+Checks both graphical fringe (display property) and
+terminal fringe (face property)."
+  (or
+   ;; Graphical: (left-fringe BITMAP FACE)
+   ;; BITMAP can be 'empty-line (source) or 'org-transclusion-fringe-bitmap 
(destination)
+   (and (listp prop-value)
+        (eq (car prop-value) 'left-fringe)
+        (memq (cadr prop-value) '(empty-line org-transclusion-fringe-bitmap))
+        (memq (nth 2 prop-value)
+              '(org-transclusion-source-fringe
+                org-transclusion-fringe)))
+   ;; Terminal: face property with our face names
+   (memq prop-value
+         '(org-transclusion-source-fringe
+           org-transclusion-fringe
+           org-transclusion-source
+           org-transclusion))))
+
+(defun org-transclusion--make-fringe-indicator (face)
+  "Create fringe indicator string for FACE.
+Handles both graphical and terminal display modes.
+Uses empty-line bitmap for source fringe, org-transclusion-fringe-bitmap
+for transclusion fringe to match original overlay-based appearance."
+  (if (display-graphic-p)
+      (let ((bitmap (if (eq face 'org-transclusion-source-fringe)
+                        'empty-line
+                      'org-transclusion-fringe-bitmap)))
+        (propertize "x" 'display `(left-fringe ,bitmap ,face)))
+    (propertize "| " 'face face)))
+
+(defun org-transclusion--update-line-prefix (line-beg line-end prop-name 
new-value)
+  "Update text property PROP-NAME to NEW-VALUE for line at LINE-BEG.
+LINE-END is the end of the region to update.
+If NEW-VALUE is nil, removes the property entirely."
+  (if new-value
+      (put-text-property line-beg line-end prop-name new-value)
+    (remove-text-properties line-beg line-end (list prop-name nil))))
+
+;;; Fringe Management
+
+;;;; Fringe Detection
+
+(defun org-transclusion-prefix-has-fringe-p (prefix)
+  "Return non-nil if PREFIX string contains a transclusion fringe indicator.
+Checks for both graphical fringe (display property) and terminal
+fringe (face property within the string)."
+  (when (stringp prefix)
+    (let ((pos 0))
+      (catch 'found
+        ;; Check display properties (graphical fringe)
+        (while (setq pos (next-single-property-change pos 'display prefix))
+          (when (org-transclusion--fringe-spec-p
+                 (get-text-property pos 'display prefix))
+            (throw 'found t)))
+
+        ;; Check face properties within the string (terminal fringe only)
+        ;; Terminal fringes are strings like "| " with face property
+        (unless (display-graphic-p)
+          (setq pos 0)
+          (while (< pos (length prefix))
+            (let ((face (get-text-property pos 'face prefix)))
+              (when (memq face '(org-transclusion-source-fringe
+                                 org-transclusion-fringe))
+                (throw 'found t)))
+            (setq pos (1+ pos))))
 
+        nil))))
+
+;;;; Fringe Creation
+(defun org-transclusion-append-fringe-to-prefix (existing-prefix face)
+  "Append fringe indicator to EXISTING-PREFIX, preserving it.
+FACE determines the fringe color (org-transclusion-source-fringe or
+org-transclusion-fringe).
+Returns concatenated string suitable for `line-prefix' or `wrap-prefix'.
+
+In terminal mode, prepends fringe to place it at the left margin.
+In graphical mode, appends fringe to preserve indentation alignment."
+  (let ((fringe-indicator (org-transclusion--make-fringe-indicator face)))
+    (if existing-prefix
+        (if (display-graphic-p)
+            ;; Graphical: append fringe (invisible in fringe area)
+            (concat existing-prefix fringe-indicator)
+          ;; Terminal: prepend fringe to show at left margin
+          (concat fringe-indicator existing-prefix))
+      fringe-indicator)))
+
+(defun org-transclusion-add-fringe-to-region (buffer beg end face)
+  "Add fringe indicator to each line in BUFFER between BEG and END.
+FACE determines the fringe color.
+
+When org-indent-mode is active (`line-prefix'/`wrap-prefix' properties exist),
+appends fringe to existing indentation. When org-indent-mode is inactive,
+adds fringe-only prefix."
+  (with-current-buffer buffer
+    (with-silent-modifications
+      (save-excursion
+        (goto-char beg)
+        (catch 'done
+          (while (< (point) end)
+            (let* ((line-beg (line-beginning-position))
+                   (line-end (min (line-end-position) end))
+                   (line-prefix (get-text-property line-beg 'line-prefix))
+                   (wrap-prefix (get-text-property line-beg 'wrap-prefix))
+                   (fringe-only (org-transclusion--make-fringe-indicator 
face)))
+
+              ;; Handle line-prefix
+              (if line-prefix
+                  ;; org-indent-mode case: append to existing prefix
+                  (unless (org-transclusion-prefix-has-fringe-p line-prefix)
+                    (org-transclusion--update-line-prefix
+                     line-beg line-end 'line-prefix
+                     (org-transclusion-append-fringe-to-prefix line-prefix 
face)))
+                ;; Non-indent case: add fringe-only prefix
+                (org-transclusion--update-line-prefix
+                 line-beg line-end 'line-prefix fringe-only))
+
+              ;; Handle wrap-prefix
+              (if wrap-prefix
+                  ;; org-indent-mode case: append to existing prefix
+                  (unless (org-transclusion-prefix-has-fringe-p wrap-prefix)
+                    (org-transclusion--update-line-prefix
+                     line-beg line-end 'wrap-prefix
+                     (org-transclusion-append-fringe-to-prefix wrap-prefix 
face)))
+                ;; Non-indent case: add fringe-only prefix
+                (org-transclusion--update-line-prefix
+                 line-beg line-end 'wrap-prefix fringe-only)))
+
+            ;; Try to advance to next line; if we can't, we're done
+            (when (not (zerop (forward-line 1)))
+              (throw 'done nil))))))))
+
+;;;; Fringe Removal
+(defun org-transclusion-remove-fringe-from-prefix (prefix)
+  "Remove fringe indicator from PREFIX string.
+Returns the cleaned prefix, or nil if prefix was only the fringe indicator."
+  (when (stringp prefix)
+    (let ((cleaned prefix)
+          (pos 0))
+      ;; Remove all fringe indicators (both graphical and terminal)
+      (while (setq pos (next-single-property-change pos nil cleaned))
+        (let ((display-prop (get-text-property pos 'display cleaned))
+              (face-prop (get-text-property pos 'face cleaned)))
+          (when (or (org-transclusion--fringe-spec-p display-prop)
+                    (org-transclusion--fringe-spec-p face-prop))
+            ;; Found a fringe indicator, remove it
+            (setq cleaned (concat (substring cleaned 0 pos)
+                                  (substring cleaned (1+ pos))))
+            ;; Adjust position since we removed a character
+            (setq pos (max 0 (1- pos))))))
+      ;; Return nil if nothing left, otherwise return cleaned prefix
+      (if (string-empty-p cleaned) nil cleaned))))
+
+(defun org-transclusion-remove-fringe-from-region (buffer beg end)
+  "Remove fringe indicators from each line in BUFFER between BEG and END.
+This restores `line-prefix' and `wrap-prefix' to their state before
+`org-transclusion-add-fringe-to-region' was called.
+
+In `org-mode' buffers, removes only the fringe portion while preserving
+org-indent indentation.  In non-org buffers, removes the properties
+entirely since they were added solely for fringe display."
+  (with-current-buffer buffer
+    (with-silent-modifications
+      (save-excursion
+        (goto-char beg)
+        (let ((is-org-buffer (derived-mode-p 'org-mode)))
+          (while (< (point) end)
+            (let* ((line-beg (line-beginning-position))
+                   (line-end (min (1+ line-beg) end))
+                   (line-prefix (get-text-property line-beg 'line-prefix))
+                   (wrap-prefix (get-text-property line-beg 'wrap-prefix)))
+
+              (if is-org-buffer
+                  ;; Org buffer: strip fringes, preserve org-indent content
+                  (progn
+                    (when line-prefix
+                      (org-transclusion--update-line-prefix
+                       line-beg line-end 'line-prefix
+                       (org-transclusion-remove-fringe-from-prefix 
line-prefix)))
+                    (when wrap-prefix
+                      (org-transclusion--update-line-prefix
+                       line-beg line-end 'wrap-prefix
+                       (org-transclusion-remove-fringe-from-prefix 
wrap-prefix))))
+                ;; Non-org buffer: remove properties entirely
+                (progn
+                  (org-transclusion--update-line-prefix line-beg line-end 
'line-prefix nil)
+                  (org-transclusion--update-line-prefix line-beg line-end 
'wrap-prefix nil))))
+            (forward-line 1)))))))
+
+;;;; Hook
+(defun org-transclusion-source-overlay-modified (ov after-p _beg _end 
&optional _len)
+  "Update source overlay OV indentation after modification.
+Called by overlay modification hooks. AFTER-P is t after modification.
+This ensures fringe indicators stay synchronized with org-indent-mode's
+dynamic updates."
+  (when (and after-p (overlay-buffer ov))
+    (let ((ov-beg (overlay-start ov))
+          (ov-end (overlay-end ov)))
+      ;; Only re-apply fringes if org-transclusion-indent-mode is NOT active
+      ;; When indent-mode is active, its after-change-function handles this
+      (unless (buffer-local-value 'org-transclusion-indent-mode 
(overlay-buffer ov))
+        (org-transclusion-add-fringe-to-region
+         (overlay-buffer ov) ov-beg ov-end 'org-transclusion-source-fringe)))))
+
+;;;; Utility Functions
 (defun org-transclusion-find-source-marker (beg end)
   "Return marker that points to source begin point for transclusion.
 It works on the transclusion region at point.  BEG and END are
@@ -1501,26 +1730,6 @@ used."
              (get-char-property (point) 'text-clones))
     t))
 
-(defun org-transclusion-propertize-transclusion ()
-  "."
-  (if (not (display-graphic-p))
-      (propertize "| " 'face 'org-transclusion)
-    (propertize
-     "x"
-     'display
-     '(left-fringe org-transclusion-fringe-bitmap
-                   org-transclusion-fringe))))
-
-(defun org-transclusion-propertize-source ()
-  "."
-  (if (not (display-graphic-p))
-      (propertize "| " 'face 'org-transclusion-source)
-    (propertize
-     "x"
-     `display
-     `(left-fringe empty-line
-                   org-transclusion-source-fringe))))
-
 (defun org-transclusion-type-is-org (type)
   "Return non-nil if TYPE begins with \"org\".
 TYPE is assumed to be a text-property \"org-transclusion-type\"
diff --git a/test/test-indent-mode-destination.org 
b/test/test-indent-mode-destination.org
new file mode 100644
index 0000000000..dd13d60261
--- /dev/null
+++ b/test/test-indent-mode-destination.org
@@ -0,0 +1,85 @@
+#+title: Test Destination for org-transclusion-indent-mode
+#+startup: show2levels
+
+* Test Case 1: Short transclusion with following content
+Test that fringes don't extend beyond actual transcluded content.
+
+#+transclude: [[file:test-indent-mode-source.org::*testing - 1][testing - 1]]
+
+- line 1 - should NOT have fringe
+- line 2 - should NOT have fringe
+- line 3 - should NOT have fringe
+- line 4 - should NOT have fringe
+- line 5 - should NOT have fringe
+- line 6 - should NOT have fringe
+- line 7 - should NOT have fringe
+- line 8 - should NOT have fringe
+- line 9 - should NOT have fringe
+- line 10 - should NOT have fringe
+
+* Test Case 2: Full subtree transclusion
+Test fringe placement on complex nested structure.
+
+#+transclude: [[file:test-indent-mode-source.org::*TEST 2 - Complex nested 
structure][TEST 2]]
+
+* Test Case 3: Mid-level heading transclusion
+Test transclusion of heading with nested content.
+
+#+transclude: [[file:test-indent-mode-source.org::*HEADING 2.1][HEADING 2.1]]
+
+* Test Case 4: Multiple transclusions in sequence
+Test that multiple transclusions maintain correct fringe boundaries.
+
+#+transclude: [[file:test-indent-mode-source.org::*testing - 1][testing - 1]]
+
+Some text between transclusions.
+
+#+transclude: [[file:test-indent-mode-source.org::*TEST 3 - Single 
paragraph][TEST 3]]
+
+More text between transclusions.
+
+#+transclude: [[file:test-indent-mode-source.org::*Table example][Table 
example]]
+
+ Test Case 5: Transclusion within list
+Test fringe behavior when transclusion is nested in a list.
+
+- List item 1
+  - Nested item with transclusion below:
+    #+transclude: [[file:test-indent-mode-source.org::*testing - 1][testing - 
1]]
+  - Another nested item after transclusion
+
+* Test Case 6: Top-level heading transclusion
+Test transclusion of a top-level heading.
+
+#+transclude: [[file:test-indent-mode-source.org::*Heading test 1 - edit this 
to test fringe persistence][Heading test 1]]
+
+* Test Case 7: Special blocks
+Test transclusion of quote blocks and code blocks.
+
+#+transclude: [[file:test-indent-mode-source.org::*Quote block][Quote block]]
+
+* Instructions for Testing
+
+1. Open both files in Emacs with org-indent-mode enabled
+2. Enable org-transclusion-mode in destination buffer
+3. Run M-x org-transclusion-add-all
+
+** Expected Behavior - Graphical Mode
+- Fringe bitmaps appear in left fringe area
+- Fringes only on transcluded content, not following lines
+- Editing source content preserves fringes
+- Editing headings preserves fringes on all child content
+
+** Expected Behavior - Terminal Mode (emacs -nw)
+- ASCII "| " appears at left margin before indentation
+- Fringes only on transcluded content, not following lines
+- Editing source content preserves fringes (may flicker briefly)
+- Longer idle delay (0.7s) reduces flicker during rapid typing
+
+** Common Issues to Verify Fixed
+- [ ] Fringes don't extend beyond transclusion (Test Case 1)
+- [ ] Fringes don't accumulate on repeated edits
+- [ ] Fringes persist when editing list items
+- [ ] Fringes persist when editing headings
+- [ ] Terminal fringes appear at left margin, not after indent
+- [ ] Multiple transclusions maintain separate boundaries (Test Case 4)
diff --git a/test/test-indent-mode-source.org b/test/test-indent-mode-source.org
new file mode 100644
index 0000000000..d8aa477e1d
--- /dev/null
+++ b/test/test-indent-mode-source.org
@@ -0,0 +1,74 @@
+#+title: Test Source for org-transclusion-indent-mode
+
+* TEST 1
+** Heading test 1 - edit this to test fringe persistence
+*** testing - 1
+This is a short transclusion target for testing basic fringe placement.
+Edit this text to verify fringes remain visible during typing.
+
+*** Heading test 2
+- line item - edit this to test fringe on list items
+  - line item 2 - nested list item for testing indentation
+
+* TEST 2 - Complex nested structure
+** HEADING 1
+*** HEADING 2
+- list item 1 in heading 2
+  - list item 2 in heading 2 under list item 1
+    - deeply nested item for testing indentation levels
+
+Aliquam erat volutpat. Nunc eleifend leo vitae magna. In id erat non
+orci commodo lobortis. Proin neque massa, cursus ut, gravida ut,
+lobortis eget, lacus. Sed diam. Praesent fermentum tempor tellus.
+Nullam tempus. Mauris ac felis vel velit tristique imperdiet. Donec at
+pede. Etiam vel neque nec dui dignissim bibendum. Vivamus id enim.
+Phasellus neque orci, porta a, aliquet quis, semper a, massa.
+Phasellus purus. Pellentesque tristique imperdiet tortor. Nam euismod
+tellus id erat.
+
+**** HEADING 2.1
+Proin quam nisl, tincidunt et, mattis eget, convallis nec, purus.
+Edit this paragraph to test fringe behavior on heading content.
+
+***** HEADING 2.1.1
+- Proin neque massa, cursus ut, gravida ut, lobortis eget, lacus.
+- Nunc rutrum turpis sed pede.
+  - Nested item under second list item
+  - Another nested item for testing
+
+**** HEADING 2.2
+Aliquam erat volutpat. Nunc eleifend leo vitae magna. In id erat non orci
+commodo lobortis. Proin neque massa, cursus ut, gravida ut, lobortis eget,
+lacus. Sed diam. Praesent fermentum tempor tellus. Nullam tempus. Mauris ac
+felis vel velit tristique imperdiet. Donec at pede. Etiam vel neque nec dui
+dignissim bibendum. Vivamus id enim. Phasellus neque orci, porta a, aliquet
+quis, semper a, massa. Phasellus purus. Pellentesque tristique imperdiet 
tortor.
+Nam euismod tellus id erat.
+
+* TEST 3 - Single paragraph
+This is a single paragraph target for testing transclusion of simple content.
+Edit this text to verify fringe persistence on paragraph-level transclusions.
+
+* TEST 4 - Table and code
+** Table example
+| Column 1 | Column 2 | Column 3 |
+|----------+----------+----------|
+| Data 1   | Data 2   | Data 3   |
+| Data 4   | Data 5   | Data 6   |
+
+** Code block example
+#+begin_src elisp
+(defun test-function ()
+  "A test function for transclusion."
+  (message "Testing transclusion with code blocks"))
+#+end_src
+
+* TEST 5 - Mixed content with quotes
+** Quote block
+#+begin_quote
+This is a quote block to test transclusion of special blocks.
+Edit this to verify fringe behavior on quoted content.
+#+end_quote
+
+** Regular text after quote
+This paragraph follows the quote block to test mixed content transclusion.


Reply via email to