From e0619cb43c401330390fab7fecf00d43d718e998 Mon Sep 17 00:00:00 2001
From: Mingtong Lin <mt.oss@fastmail.com>
Date: Fri, 28 Nov 2025 14:55:58 -0500
Subject: [PATCH 4/6] lisp/ob-core.el: Enrich the Noweb comments genereated by
 org-babel-expand-noweb-references. lisp/ob-tangle.el: Add additional field
 %extra in Noweb comments, for extra information. testing/lisp/test-ob.el: Add
 test.

* ob-core.el (org-babel-expand-noweb-references): Generate Noweb
  comments for prose/library/multi/evaluation references as well.
  Then, provide necessary information for %extra that will help
  detangling.

* ob-tangle.el (org-babel-tangle-comment-format-beg,
  org-babel-tangle-comment-links, org-babel-spec-to-string):  Add and
  handle the new %extra field.  It is a plist of auxiliary information:

  :type       if the expansion is from `org-babel-library-of-babel' or is an
              evaluation reference, the value is 'eval-or-lib; if the expansion
              is a Org header content, the value is 'prose; nil otherwise.
  :ref        the Noweb reference string.
  :multi      'first or 'last, if the expansion is the first/last source block
              referred by a multi reference.
  :no-prefix  if the expansion excludes Noweb prefix (i.e., :noweb-prefix is
              "no").

  They are necessary for parsing tangled results when detangling.  The
  plist will be pruned to ensure that we include only the minimal
  information needed in Noweb comments.

This is the first commit of the rewrite of Org Babel detangle.

Link: https://list.orgmode.org/f43360bb-dc8f-41bb-b40e-dfdd38ebb87b@app.fastmail.com/
---
 lisp/ob-core.el         | 73 +++++++++++++++++++++++++++++++----------
 lisp/ob-tangle.el       | 60 ++++++++++++++++++++++++++-------
 testing/lisp/test-ob.el | 61 +++++++++++++++++++++++++++++++++-
 3 files changed, 164 insertions(+), 30 deletions(-)

diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index c0ff8659c..fc04aabb2 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -3281,31 +3281,49 @@ block but are passed literally to the \"example-block\"."
 		         (comment-region (point)
 				         (progn (insert ,s) (point)))
 		         (org-trim (buffer-string)))))
+		  (add-comment
+		   (b i extra)
+		   `(let ((cs (org-babel-tangle-comment-links ,i ,extra)))
+		      (concat (c-wrap (car cs)) "\n"
+			      ,b "\n"
+			      (c-wrap (cadr cs)))))
 	          (expand-body
-	            (i)
+	            (i extra)
 	            ;; Expand body of code represented by block info I.
 	            `(let ((b (if (org-babel-noweb-p (nth 2 ,i) context)
 			          (org-babel-expand-noweb-references
 			           ,i parent-buffer context)
 		                (nth 1 ,i))))
 	               (if (not comment) b
-		         (let ((cs (org-babel-tangle-comment-links ,i)))
-		           (concat (c-wrap (car cs)) "\n"
-			           b "\n"
-			           (c-wrap (cadr cs)))))))
+		         (add-comment b ,i ,extra))))
 	          (expand-references
 	            (ref)
 	            `(pcase (gethash ,ref org-babel-expand-noweb-references--cache)
 	               (`(,last . ,previous)
 	                ;; Ignore separator for last block.
-	                (let ((strings (list (expand-body last))))
-		          (dolist (i previous)
-		            (let ((parameters (nth 2 i)))
-		              ;; Since we're operating in reverse order, first
-		              ;; push separator, then body.
-		              (push (or (cdr (assq :noweb-sep parameters)) "\n")
-			            strings)
-		              (push (expand-body i) strings)))
+			(let* ((multi? (not (null previous)))
+			       (strings
+				(list (expand-body
+				       last
+				       (list
+					:ref ,ref
+					:multi (if multi? 'last nil)
+					:no-prefix (not noweb-prefix))))))
+			  (cl-loop for tail on previous
+				   for i = (car tail)
+				   for first? = (null (cdr tail))
+				   do (let ((parameters (nth 2 i)))
+					;; Since we're operating in reverse order, first
+					;; push separator, then body.
+					(push (or (cdr (assq :noweb-sep parameters)) "\n")
+					      strings)
+					(push
+					 (expand-body
+					  i
+					  (list :ref ,ref
+						:multi (if first? 'first nil)
+						:no-prefix (not noweb-prefix)))
+					 strings)))
 		          (mapconcat #'identity strings "")))
 	               ;; Raise an error about missing reference, or return the
 	               ;; empty string.
@@ -3322,12 +3340,18 @@ block but are passed literally to the \"example-block\"."
 	     (let* ((prefix (match-string 1 m))
 		    (id (match-string 3 m))
 		    (evaluate (string-match-p "(.*)" id))
+		    (lib-blk (nth 2 (assoc-string id org-babel-library-of-babel)))
 		    (expansion
 		     (cond
 		      (evaluate
                        (prog1
-		           (let ((raw (org-babel-ref-resolve id)))
-		             (if (stringp raw) raw (format "%S" raw)))
+			   (let* ((raw (org-babel-ref-resolve id))
+				  (res (if (stringp raw) raw (format "%S" raw))))
+			     (if (not comment) res
+			       (add-comment res nil
+					    (list :type 'eval-or-lib
+						  :ref id
+						  :no-prefix (not noweb-prefix)))))
                          ;; Evaluation can potentially modify the buffer
 		         ;; and invalidate the cache: reset it.
                          (unless (equal org-babel-expand-noweb-references--cache-buffer
@@ -3344,7 +3368,13 @@ block but are passed literally to the \"example-block\"."
                        (expand-references id))
 		      ;; Return the contents of headlines literally.
 		      ((org-babel-ref-goto-headline-id id)
-		       (org-babel-ref-headline-body))
+		       (let ((res (org-babel-ref-headline-body)))
+			 (if (not comment) res
+			   (add-comment res nil
+					(list
+					 :type 'prose
+					 :ref id
+					 :no-prefix (not noweb-prefix))))))
 		      ;; Look for a source block named SOURCE-NAME.  If
 		      ;; found, assume it is unique; do not look after
 		      ;; `:noweb-ref' header argument.
@@ -3356,9 +3386,16 @@ block but are passed literally to the \"example-block\"."
                                   (unless (hash-table-p org-babel-expand-noweb-references--cache)
                                     (setq org-babel-expand-noweb-references--cache (make-hash-table :test #'equal)))
                                   (push info (gethash id  org-babel-expand-noweb-references--cache))
-			          (expand-body info))))))
+			          (expand-body info (list :ref id
+							  :no-prefix (not noweb-prefix))))))))
 		      ;; Retrieve from the Library of Babel.
-		      ((nth 2 (assoc-string id org-babel-library-of-babel)))
+		      (lib-blk
+		       (if (not comment) lib-blk
+			 (add-comment lib-blk nil
+				      (list
+				       :type 'eval-or-lib
+				       :ref id
+				       :no-prefix (not noweb-prefix)))))
 		      ;; All Noweb references were cached in a previous
 		      ;; run.  Yet, ID is not in cache (see the above
 		      ;; condition).  Process missing reference in
diff --git a/lisp/ob-tangle.el b/lisp/ob-tangle.el
index 6e71800c7..1287efb55 100644
--- a/lisp/ob-tangle.el
+++ b/lisp/ob-tangle.el
@@ -108,6 +108,11 @@ information into the output using `org-fill-template'.
 %file --------- the file from which the code block was tangled
 %link --------- Org style link to the code block
 %source-name -- name of the code block
+%extra -------- additional auxiliary information for `org-babel-detangle`
+
+To detangle Noweb blocks, the Noweb comments should be tangled with
+the %extra field.  Also, the fields need to be in the order of %link >
+%source-name > %extra for the regexp to parse.
 
 Upon insertion the formatted comment will be commented out, and
 followed by a newline.  To inhibit this post-insertion processing
@@ -429,7 +434,10 @@ that the appropriate major-mode is set.  SPEC has the form:
        (link-data `(("start-line" . ,(number-to-string start))
 		    ("file" . ,file)
 		    ("link" . ,link)
-		    ("source-name" . ,source)))
+		    ("source-name" . ,source)
+		    ;; Outermost blocks do not need to provide extra.
+		    ;; It is used for Noweb-referred blocks only.
+		    ("extra" . "")))
        (insert-comment (lambda (text)
 			 (when (and comments
 				    (not (string= comments "no"))
@@ -600,18 +608,48 @@ non-nil, return the full association list to be used by
           (list (cons file-name (list (cons src-lang result)))))
       result)))
 
-(defun org-babel-tangle-comment-links (&optional info)
+(defun org-babel-tangle-comment-links (&optional info extra)
   "Return a list of begin and end link comments for the code block at point.
 INFO, when non nil, is the source block information, as returned
-by `org-babel-get-src-block-info'."
-  (let ((link-data (pcase (or info (org-babel-get-src-block-info 'no-eval))
-		     (`(,_ ,_ ,params ,_ ,name ,start ,_)
-		      `(("start-line" . ,(org-with-point-at start
-					   (number-to-string
-					    (line-number-at-pos))))
-			("file" . ,(buffer-file-name))
-			("link" . ,(org-babel-tangle--unbracketed-link params))
-			("source-name" . ,name))))))
+by `org-babel-get-src-block-info'.
+
+EXTRA is an plist of additional information, which may include the following:
+- :ref    the Noweb reference string used to include this block.
+- :type   the type of the Noweb reference, one of:
+	  - 'eval-or-lib   if the block is used as either an evaluation block or
+			   is from `org-babel-library-of-babel'.
+	  - nil            otherwise
+- :multi  a symbol denoting if the source block is the first/last one in the
+	  sequence of expanded content from a multi reference.  This is useful
+	  for distinguishing two consecutive uses of the same multi reference."
+  (let* ((ref (plist-get extra :ref))
+	 (type (plist-get extra :type))
+	 (extra-clean (cl-loop for (key val) on extra by #'cddr
+			       when val
+			       append (list key val)))
+	 (link-data
+	  (pcase type
+	    ((or 'eval-or-lib 'prose)
+	     `(("start-line" . "")
+	       ("file" . "")
+	       ("link" . "")
+	       ;; We need to resolve the source block name from NOWEB.
+	       ("source-name" . ,(if (string-match (rx (+ (not "("))) ref)
+				     (match-string 0 ref)
+				   ""))
+	       ("extra" . ,(prin1-to-string extra-clean))))
+	    ('nil
+	     (pcase (or info (org-babel-get-src-block-info 'no-eval))
+	       (`(,_ ,_ ,params ,_ ,name ,start ,_)
+		`(("start-line" . ,(org-with-point-at start
+						      (number-to-string
+						       (line-number-at-pos))))
+		  ("file" . ,(buffer-file-name))
+		  ("link" . ,(org-babel-tangle--unbracketed-link params))
+		  ("source-name" . ,name)
+		  ("extra" . ,(prin1-to-string extra-clean))))))
+	    (_
+	     (error "Invalid block type")))))
     (list (org-fill-template org-babel-tangle-comment-format-beg link-data)
 	  (org-fill-template org-babel-tangle-comment-format-end link-data))))
 
diff --git a/testing/lisp/test-ob.el b/testing/lisp/test-ob.el
index bdb8d9c21..38f9491c0 100644
--- a/testing/lisp/test-ob.el
+++ b/testing/lisp/test-ob.el
@@ -1005,7 +1005,66 @@ prefix<<inner>>
 prefix1
 prefix;; inner ends here"
             file file)
-    (org-babel-expand-noweb-references nil nil :eval))))))
+    (org-babel-expand-noweb-references nil nil :eval)))))
+  ;; Test extended Noweb comments.
+  (should
+   (org-test-with-temp-text-in-file
+       "* H1
+:PROPERTIES:
+:CUSTOM_ID: myid
+:END:
+Something important.
+* H
+#+name: inner
+#+begin_src emacs-lisp
+0
+#+end_src
+#+begin_src emacs-lisp :noweb-ref inners
+1
+#+end_src
+#+name: comp
+#+begin_src emacs-lisp
+(message \"3\")
+#+end_src
+#+begin_src emacs-lisp :comments noweb :noweb yes<point>
+<<inner>>
+<<myid>>
+<<inners>>
+<<lib-blk>>
+<<comp()>>
+#+end_src
+"
+     (let ((file (file-name-nondirectory (buffer-file-name)))
+	   (org-babel-library-of-babel
+	    '((lib-blk
+	       "emacs-lisp" "2"
+	       ((:results . "replace") (:exports . "code") (:lexical . "no")
+		(:tangle . "no") (:hlines . "no") (:noweb . "no") (:cache . "no")
+		(:session . "none"))
+	       "" "lib-blk" 33 "(ref:%s)")))
+           (org-babel-tangle-comment-format-beg "[[%link][%source-name]]%extra"))
+       (equal
+	(format
+	 ";; [[file:%s::inner][inner]](:ref \"inner\")
+0
+;; inner ends here
+
+;; [[][myid]](:type prose :ref \"myid\")
+Something important.
+;; myid ends here
+
+;; [[file:%s::#myid][*H:2]](:ref \"inners\")
+1
+;; *H:2 ends here
+
+;; [[][lib-blk]](:type eval-or-lib :ref \"lib-blk\")
+2
+;; lib-blk ends here
+
+;; [[][comp]](:type eval-or-lib :ref \"comp()\")
+3
+;; comp ends here" file file)
+	(org-babel-expand-noweb-references nil nil :eval))))))
 
 (ert-deftest test-ob/splitting-variable-lists-in-references ()
   (org-test-with-temp-text ""
-- 
2.39.5 (Apple Git-154)

