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.