branch: externals/org-transclusion
commit 009a44235a4e5d9b30110cb8a9a69bc70510d7a4
Author: Noboru Ota <[email protected]>
Commit: Noboru Ota <[email protected]>

    feat: transient-menu and org-transclusion-insert
---
 org-transclusion-transient.el | 320 ++++++++++++++++++++++++------------------
 org-transclusion.el           |  80 +++++++++--
 2 files changed, 250 insertions(+), 150 deletions(-)

diff --git a/org-transclusion-transient.el b/org-transclusion-transient.el
index 636ce71f75..8a84086b6e 100644
--- a/org-transclusion-transient.el
+++ b/org-transclusion-transient.el
@@ -1,105 +1,78 @@
-;; -*- lexical-binding: t; -*-
+;;; org-transclusion-transient.el --- Transient menu for org-transclusion -*- 
lexical-binding: t; -*-
 
-;;    https://github.com/nobiot/org-transclusion/issues/169
+;; Copyright (C) 2021-2025  Free Software Foundation, Inc.
+
+;; This program is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation, either version 3 of the License, or (at your
+;; option) any later version.
+
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License along
+;; with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;; Author: Noboru Ota <[email protected]>
+;; Created: 1 January 2025
+;; Last modified: 19 December 2025
+
+;;; Commentary:
+
+;;  Transient menu for `org-transclusion'. To use the menu, simply call
+;;  `org-transclusion-transient-menu'. This command will call a different menu
+;;  depending on whether the point is at a transcluded content.
 
 (require 'org-transclusion)
 (require 'transient) ; Need more recent than that comes with 29.4; tested on
-                    ; transient-20241224.2234
+                     ; transient-20241224.2234
+
+;;; Code:
+
+;; Variables
+
+(defvar org-transclusion-transient-repeat-mode-was-active-p nil)
+
+(defvar org-transclusion-transient-src-languaes '(python emacs-lisp)
+  "History for `completing-read' when selecting a src language.")
 
-;; Utilities
+(defvar org-transclusion-transient-src-history nil
+  "History for `completing-read' when selecting a src language.")
 
-(defmacro hydra-org-transclusion--detect-transclude-at-point-wrapper (body)
-  `(let ((line-text (buffer-substring-no-properties
-                     (line-beginning-position) (line-end-position)))
-         (position (point))
+
+;; Helper macro
+
+;; This macro is originally shared by GitHub user stardiviner via
+;; https://github.com/nobiot/org-transclusion/issues/169
+(defmacro org-transclusion-transient--detect-transclude-at-point-wrapper (body)
+  "Evaluate BODY at the end of #+transclude: keyword.
+This macro checks if the current line is a transclude keyword line. If
+it is not, it emits a user error."
+  `(let ((position (point))
          (end-of-line (line-end-position)))
-     (if (string-match-p "#\\+transclude:" line-text)
+     (if (org-transclusion-at-keyword-p)
          (save-excursion
            (unless (eq position end-of-line) (end-of-line))
            (insert " ")
            ,body
            (pulse-momentary-highlight-region end-of-line (line-end-position)
                                              'pulse-highlight-start-face))
-       (user-error "You'r not on #+transclude: [[link]] line."))))
+       (user-error "You're not on #+transclude: [[link]] line.?"))))
 
-(defun org-transclusion--transient-read-level (&rest _)
-  "Read a string from the minibuffer, restricted to the range 1 to 9 or an 
empty value."
-  (cl-loop for result =
-           (read-string "Enter org-transclusion content headline\
-level (1-9) or leave empty: ")
-           if (or (string= result "")
-                  (string-match-p "^[1-9]$" result))
-           return result
-           else do (progn
-                     (message "Invalid input. Number 1-9 or leave empty")
-                     (sit-for 1))))
+;; Transient menus
 
-(defun org-transclusion--transient-read-lines (&rest _)
-  "Read a string from the minibuffer, restricted to eg 5-10, 6-, -6."
-  (cl-loop for result =
-           (read-string "Enter :lines option values (eg 5-10, 6-, -6): ")
-           if (string-match-p "\"?[0-9]*-[0-9]*\"?" result)
-           return result
-           else do (progn
-                     (message "Invalid input. The format must be eg 5-10, 6-, 
-6")
-                     (sit-for 1))))
-
-;; You can add `org-roam-node-insert' as an example.
-(defvar org-transclusion-insert-link-functions '(org-insert-link))
-
-(defun org-transclusion-insert-org-link ()
-  (let ((function (if (length> org-transclusion-insert-link-functions 1)
-                      (intern (completing-read
-                               "Choose a function: "
-                               org-transclusion-insert-link-functions))
-                    (car org-transclusion-insert-link-functions))))
-    (when function
-      (with-temp-buffer
-        (funcall function)
-        (buffer-string)))))
-
-(defun org-transclusion-insert-from-link (&optional insert-below)
-  "Insert #+TRANSCLUDE: keyword from a link.
-If you pass a `universal-argument' via \\[universal-argument]
- \(INSERT-BELOW is non-nil\), the keyword is added to the line
- below current one. Otherwise, to the line above."
-  (interactive "P")
-  (let* ((link-elem-at-pt
-          (or (org-element-lineage (org-element-context) 'link t) ; at-point
-              ;; if not at-point, find the first one in the current line
-              (save-excursion
-                (beginning-of-line)
-                (re-search-forward org-link-bracket-re (line-end-position) t)
-                (org-element-lineage (org-element-context) 'link t))))
-         (blank-line-p (save-excursion
-                         (beginning-of-line)
-                         (looking-at-p "^[ \t]*$")))
-         (link-string (cond
-                       (link-elem-at-pt
-                        (buffer-substring (org-element-begin link-elem-at-pt)
-                                          (org-element-end link-elem-at-pt)))
-                       (blank-line-p
-                        (org-transclusion-insert-org-link)))))
-    (when link-string
-      ;; When the current line is not blank, open a line above or below the
-      ;; current.
-      (unless blank-line-p
-        (when insert-below (progn (forward-line 1) (unless (bolp) (insert 
"\n"))))
-        (beginning-of-line)
-        (open-line 1))
-      (insert (format "#+transclude: %s" link-string))
-      (beginning-of-line)
-      (pulse-momentary-highlight-region
-       (point) (line-end-position) 'pulse-highlight-start-face))))
-
-(transient-define-prefix org-transclusion--buffer-transient ()
-  "Prefix that waves at the user"
+(transient-define-prefix org-transclusion-transient--buffer-menu ()
+  "Transient menu when point is not at transcluded content.
+In general, users should use command `org-transclusion-transient-menu',
+which automatically calls the appropriate transient-prefix."
   [[:description "Add/Remove"
-                 ("a" "Add at point"
-                  org-transclusion-add
-                  :transient transient--do-return)
-                  ("A" "Add all in buffer" org-transclusion-add-all)
-                  ("R" "Remove all in buffer" org-transclusion-remove-all)]
+                 ("a" "Add at point" org-transclusion-transient--add
+                  :inapt-if-not org-transclusion-at-keyword-p)
+                 ("A" "Add all in buffer" org-transclusion-add-all)
+                 ("R" "Remove all in buffer" org-transclusion-remove-all)]
+
     [:description "Options for #+TRANSCLUDE keyword"
                   (:info "Select options. Keep adding")
                   ("i" "insert from link at point or current line"
@@ -114,6 +87,7 @@ If you pass a `universal-argument' via \\[universal-argument]
                    :inapt-if-not org-transclusion-at-keyword-p)
                   ("el" "expand-links" org-transclusion-transient--expand-links
                    :inapt-if-not org-transclusion-at-keyword-p)]
+
     [:description "Addiitonal options: :src and :lines"
                   ;; TODO check the extension to be active
                   :inapt-if-not org-transclusion-at-keyword-p
@@ -128,131 +102,201 @@ If you pass a `universal-argument' via 
\\[universal-argument]
                   ("sl" "lines (eg 3-5, 6-, -6)"
                    org-transclusion-transient--lines
                    :inapt-if-not org-transclusion-at-keyword-p)
-                  ("sl" "end"
+                  ("se" "end"
                    org-transclusion-transient--end
                    :inapt-if-not org-transclusion-at-keyword-p)
                   ("st" "thing-at-point"
                    org-transclusion-transient--thingatpt
                    :inapt-if-not org-transclusion-at-keyword-p)]]
-  [:description "Setting"
-                (:info ".")
-                ("-m" "Show more" test/set-level)
-                ("-l" "show less" test/set-level-less)])
-
-(transient-define-suffix test/set-level ()
-  :transient t
+  [[:description "Undo / Redo"
+                 ("<left>" "Undo" undo-only :transient t)
+                 ("<right>" "Redo" undo-redo :transient t)]
+   [:description "Return"
+                 ("<RET>" "Quit" transient-quit-all)]]
   (interactive)
-  (transient-set-level 'org-transclusion--buffer-transient
-                       4)
-  (org-transclusion-transient-menu))
+  (org-transclusion-transient--setup)
+  (transient-setup 'org-transclusion-transient--buffer-menu))
 
-(transient-define-suffix test/set-level-less ()
-  :transient t
-  (interactive)
-  (transient-set-level 'org-transclusion--buffer-transient
-                       3)
-  (org-transclusion-transient-menu))
-
-(transient-define-prefix org-transclusion--at-point-transient ()
-  "Prefix that waves at the user"
-  [:description
-    "Operation on Transclusion at Point"
+(transient-define-prefix org-transclusion-transient--at-point-menu ()
+  "Transient menu when point is at transcluded content.
+In general, users should use command `org-transclusion-transient-menu',
+which automatically calls the appropriate transient-prefix."
+  [:description "Operation on Transclusion at Point"
     [:description "Remove"
-                  ("r" "Remove at point"   org-transclusion-remove)
-                  ("d" "Detach at point"   org-transclusion-detach)
+                  ("d" "Remove at point"   org-transclusion-remove)
+                  ("C" "Detach at point"   org-transclusion-detach)
                   ("R" "Remove all in buffer" org-transclusion-remove-all)]
     [:description "Other at-point functions"
-                  ("P" "Promote" org-transclusion-promote-subtree)
-                  ("D" "Demote"  org-transclusion-demote-subtree)
+                  ("P" "Promote" org-transclusion-promote-subtree :transient t)
+                  ("D" "Demote"  org-transclusion-demote-subtree :transient t)
                   ("o" "Open the source buffer" org-transclusion-open-source)
-                  ("O" "Move to the source buffer" 
org-transclusion-move-to-source)]]
-  [:description ""
-                (:info ".")])
+                  ("O" "Move to the source buffer" 
org-transclusion-move-to-source)]])
+
+;;;###autoload
+(defun org-transclusion-transient-menu ()
+  "Call a transient menu for `org-transclusion'.
+It calls different menu depending on whether the point is at a
+transcluded content or not."
+  (interactive)
+  (unless (derived-mode-p 'org-mode)
+    (user-error "`org-transclusion' works only in `org' buffer"))
+  (let ((org-transclusion-buffer (current-buffer)))
+    (if (org-transclusion-within-transclusion-p)
+        (org-transclusion-transient--at-point-menu)
+      (org-transclusion-transient--buffer-menu))))
+
+;; Private functions
+
+(defun org-transclusion-transient--setup ()
+  "Temporarily deactivate Repeat mode, which interferes with transient.
+Only when `repeat-mode' is active when calling the transient menu. This
+function also sets `org-transclusion-transient--teardown' to
+`transient-exit-hook' to automatically turn `repeat-mode' back on."
+  (when (and (require 'repeat nil t) repeat-mode)
+    (message "Temporarily deactivating Repeat mode")
+    (setq org-transclusion-transient-repeat-mode-was-active-p t)
+    (repeat-mode -1)
+    (add-hook 'transient-exit-hook #'org-transclusion-transient--teardown)))
+
+(defun org-transclusion-transient--teardown ()
+  "Turn `repeat-mode' on in `transient-exit-hook'.
+See `org-transclusion-transient--setup'"
+  (when org-transclusion-transient-repeat-mode-was-active-p
+    (repeat-mode +1)
+    (setq org-transclusion-transient-repeat-mode-was-active-p nil)))
+
+(defun org-transclusion-transient--read-level (&rest _)
+  "Read a string from the minibuffer, restricted to the range 1 to 9 or an 
empty value."
+  (cl-loop for result =
+           (read-string "Enter org-transclusion content headline\
+level (1-9) or leave empty: ")
+           if (or (string= result "")
+                  (string-match-p "^[1-9]$" result))
+           return result
+           else do (progn
+                     (message "Invalid input. Number 1-9 or leave empty")
+                     (sit-for 1))))
+
+(defun org-transclusion-transient--read-lines (&rest _)
+  "Read a string from the minibuffer, restricted to eg 5-10, 6-, -6."
+  (cl-loop for result =
+           (read-string "Enter :lines option values (eg 5-10, 6-, -6): ")
+           if (string-match-p "\"?[0-9]*-[0-9]*\"?" result)
+           return result
+           else do (progn
+                     (message "Invalid input. The format must be eg 5-10, 6-, 
-6")
+                     (sit-for 1))))
+
+;; Transient suffix
 
 (transient-define-suffix org-transclusion-transient--insert ()
-  :transient 'transient--do-return
+  "Call `org-transclusion-insert', which see."
+  :transient 'transient--do-stay
   (interactive)
-  (org-transclusion-insert-from-link)
-  (org-transclusion--buffer-transient))
+  (org-transclusion-insert)
+  (org-transclusion-transient--buffer-menu))
 
 (transient-define-suffix org-transclusion-transient--level ()
+  "Add :level property to transclude keyword.
+The command prompts for a number or empty without a number,
+which automatically adjust headline levels."
   :transient 'transient--do-stay
   (interactive)
-  (let ((level-string (org-transclusion--transient-read-level)))
-    (hydra-org-transclusion--detect-transclude-at-point-wrapper
+  (let ((level-string (org-transclusion-transient--read-level)))
+    (org-transclusion-transient--detect-transclude-at-point-wrapper
      (insert (if (string-empty-p level-string)
                  ":level"
                (format ":level %s" level-string))))))
 
 (transient-define-suffix org-transclusion-transient--only-contents ()
+  ":only-content will exclude titles of headlines of a subtree (headline).
+With this property, transclude only the contents."
   :transient 'transient--do-stay
   (interactive)
-  (hydra-org-transclusion--detect-transclude-at-point-wrapper
+  (org-transclusion-transient--detect-transclude-at-point-wrapper
    (insert ":only-contents")))
 
 (transient-define-suffix org-transclusion-transient--expand-links ()
+  ":expand-links expand the file names in links to absolute file names."
   :transient 'transient--do-stay
   (interactive)
-  (hydra-org-transclusion--detect-transclude-at-point-wrapper
+  (org-transclusion-transient--detect-transclude-at-point-wrapper
    (insert ":expand-links")))
 
 (transient-define-suffix org-transclusion-transient--exclude-elements ()
+  "Add org-elements to be excluded.
+The command prompts for elements and lets you select multiple items when
+you type a certain character (typically a comma). See `crm-separator'."
   :transient 'transient--do-stay
   (interactive)
   (and-let* ((list-elements (completing-read-multiple
                              "Select elements to exclude: "
                              org-element-all-elements))
              (elements-string (mapconcat #'identity list-elements "\s")))
-    (hydra-org-transclusion--detect-transclude-at-point-wrapper
+    (org-transclusion-transient--detect-transclude-at-point-wrapper
      (insert (format ":exclude-elements %S" elements-string)))))
 
 (transient-define-suffix org-transclusion-transient--src ()
+  ":src property lets you wrap the content in a src-block.
+Choose a language from items in
+`org-transclusion-transient-src-languaes' or type a language."
   :transient 'transient--do-stay
   (interactive)
-  (let ((string (read-string "Enter language for :src option: ")))
+  (let ((string (completing-read "Enter language for :src option: "
+                                 org-transclusion-transient-src-languaes
+                                 nil nil nil
+                                 org-transclusion-transient-src-history)))
     (when string
-      (hydra-org-transclusion--detect-transclude-at-point-wrapper
+      (org-transclusion-transient--detect-transclude-at-point-wrapper
        (insert (format ":src %s" string))))))
 
 (transient-define-suffix org-transclusion-transient--rest ()
+  ":rest for additional properties for the src-block."
   :transient 'transient--do-stay
   (interactive)
   (let ((string (read-string "Enter :rest option values: ")))
     (when string
-      (hydra-org-transclusion--detect-transclude-at-point-wrapper
+      (org-transclusion-transient--detect-transclude-at-point-wrapper
        (insert (format ":rest %S" string))))))
 
 (transient-define-suffix org-transclusion-transient--lines ()
+  ":lines for range of lines to transclude from a source and text file."
   :transient 'transient--do-stay
   (interactive)
-  (let ((string (org-transclusion--transient-read-lines)))
+  (let ((string (org-transclusion-transient--read-lines)))
     (when string
-      (hydra-org-transclusion--detect-transclude-at-point-wrapper
+      (org-transclusion-transient--detect-transclude-at-point-wrapper
        (insert (format ":lines %s" string))))))
 
 (transient-define-suffix org-transclusion-transient--end ()
+  ":end for a search term as the end of content to be transcluded."
   :transient 'transient--do-stay
   (interactive)
   (let ((string (read-string "Enter :end option value: ")))
     (when string
-      (hydra-org-transclusion--detect-transclude-at-point-wrapper
+      (org-transclusion-transient--detect-transclude-at-point-wrapper
        (insert (format ":end %S" string))))))
 
 (transient-define-suffix org-transclusion-transient--thingatpt ()
+  ":thingatpt to specify a \"thing\" to transclude from the source.
+Choose one of the things \"sentence\" \"paragraph\" \"defun\" \"sexp\"."
   :transient 'transient--do-stay
   (interactive)
   (let ((string (completing-read "Enter :thingatpt option value: "
                                  '("sentence" "paragraph" "defun" "sexp"))))
     (when string
-      (hydra-org-transclusion--detect-transclude-at-point-wrapper
+      (org-transclusion-transient--detect-transclude-at-point-wrapper
        (insert (format ":thingatpt %s" string))))))
 
-;;;###autoload
-(defun org-transclusion-transient-menu ()
+(transient-define-suffix org-transclusion-transient--add ()
+  "Call `org-transclusion-add'.
+This will not exit the transient menu. You will navigate to another menu
+for the transcluded content."
+  :transient 'transient--do-stay
   (interactive)
-  (unless (derived-mode-p 'org-mode)
-    (user-error "`org-transclusion' works only in `org' buffer"))
-  (let ((org-transclusion-buffer (current-buffer)))
-    (if (org-transclusion-within-transclusion-p)
-        (org-transclusion--at-point-transient)
-      (org-transclusion--buffer-transient))))
+  (org-transclusion-add)
+  (org-transclusion-transient--at-point-menu))
+
+(provide 'org-transclusion-transient)
+
+;;; org-transclusion-transient.el ends here
diff --git a/org-transclusion.el b/org-transclusion.el
index f4fec85553..cc18be1ee0 100644
--- a/org-transclusion.el
+++ b/org-transclusion.el
@@ -17,7 +17,7 @@
 
 ;; Author:        Noboru Ota <[email protected]>
 ;; Created:       10 October 2020
-;; Last modified: 18 December 2025
+;; Last modified: 19 December 2025
 
 ;; URL: https://github.com/nobiot/org-transclusion
 ;; Keywords: org-mode, transclusion, writing
@@ -170,6 +170,8 @@ The default is no color specification (transparent).")
 
 ;;;; Variables
 
+(defvar org-transclusion-insert-link-functions '(org-insert-link))
+
 (defvar org-transclusion-extensions-loaded nil
   "Have the extensions been loaded already?")
 
@@ -414,6 +416,46 @@ transclusion keyword."
           (insert (format " :level %d" arg)))
         (when auto-transclude-p (org-transclusion-add))))))
 
+(defun org-transclusion-insert (&optional insert-above)
+  "Insert #+TRANSCLUDE: keyword from a link at point or to a blank line.
+If the point is not at a link but has text, this command will try to
+find the first link in the current line. If the point is at a blank
+line, this command will call a function in set to variable
+`org-transclusion-insert-link-functions' and prompt for the user to find
+a link.
+
+If you pass a `universal-argument' via \\[universal-argument]
+\(INSERT-ABOVE is non-nil\), the keyword is added to the line above
+current one. Otherwise, to the line below."
+  (interactive "P")
+  (let* ((link-elem-at-pt
+          (or (org-element-lineage (org-element-context) 'link t) ; at-point
+              ;; if not at-point, find the first one in the current line
+              (save-excursion
+                (beginning-of-line)
+                (re-search-forward org-link-bracket-re (line-end-position) t)
+                (org-element-lineage (org-element-context) 'link t))))
+         (blank-line-p (save-excursion
+                         (beginning-of-line)
+                         (looking-at-p "^[ \t]*$")))
+         (link-string (cond
+                       (link-elem-at-pt
+                        (buffer-substring (org-element-begin link-elem-at-pt)
+                                          (org-element-end link-elem-at-pt)))
+                       (blank-line-p
+                        (org-transclusion-insert-org-link)))))
+    (when link-string
+      ;; When the current line is not blank, open a line above or below the
+      ;; current.
+      (unless blank-line-p
+        (when insert-above (forward-line -1))
+        (end-of-line)
+        (unless (bolp) (newline nil t)))
+      (org-indent-line) (insert (format "#+transclude: %s" link-string))
+      (beginning-of-line)
+      (pulse-momentary-highlight-region
+       (point) (line-end-position) 'pulse-highlight-start-face))))
+
 ;;;###autoload
 (defun org-transclusion-add (&optional copy)
   "Transclude text content for the #+transclude at point.
@@ -1627,6 +1669,18 @@ dynamic updates."
          (overlay-buffer ov) ov-beg ov-end 'org-transclusion-source-fringe)))))
 
 ;;;; Utility Functions
+
+(defun org-transclusion-insert-org-link ()
+  (let ((function (if (length> org-transclusion-insert-link-functions 1)
+                      (intern (completing-read
+                               "Choose a function: "
+                               org-transclusion-insert-link-functions))
+                    (car org-transclusion-insert-link-functions))))
+    (when function
+      (with-temp-buffer
+        (funcall function)
+        (buffer-string)))))
+
 (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
@@ -1801,17 +1855,19 @@ used."
   ;;   #+transclude: [[link]]
   ;;   |
   ;;   New paragraph starts
-  (let ((edge-case-p
-         (save-excursion
-           (and (looking-at-p "$")
-                (not (bobp))
-                (progn (forward-char -1)
-                       (looking-at-p "$")))))
-        (element (org-element-at-point)))
-    ;; If edge-case, do not transclude.
-    (unless edge-case-p
-      (and (string-equal "keyword" (org-element-type element))
-           (string-equal "TRANSCLUDE" (org-element-property :key element))))))
+  (and (equal (derived-mode-p major-mode) 'org-mode)
+       (let ((edge-case-p
+              (save-excursion
+                (and (looking-at-p "$")
+                     (not (bobp))
+                     (progn (forward-char -1)
+                            (looking-at-p "$")))))
+             (element (org-element-at-point)))
+         ;; If edge-case, do not transclude.
+         (unless edge-case-p
+           (and (string-equal "keyword" (org-element-type element))
+                (string-equal "TRANSCLUDE"
+                              (org-element-property :key element)))))))
 
 (defun org-transclusion-within-transclusion-p ()
   "Return t if the current point is within a transclusion region."

Reply via email to