From 4200a1dfadf301c8a257dcdfb2fdbf183e9f7956 Mon Sep 17 00:00:00 2001
From: Mingtong Lin <mt.oss@fastmail.com>
Date: Fri, 28 Nov 2025 21:27:37 -0500
Subject: [PATCH 6/6] lisp/ob-tangle.el: Rewrite detangling.
 testing/examples/babel-detangle-noweb.org: New test example file.
 testing/examples/babel-detangle-noweb.el: New test example file.
 test/lisp/test-ob-tangle.el: Add/update tests. testing/examples/babel.el:
 Update the test example file.

* ob-tangle.el (org-babel-detangle and its auxiliaries): Rewrite the
  detangling functions to handle Noweb blocks, using the new Noweb
  comment parsing facilities.

* testing/examples/babel-detangle-noweb.el and
  testing/examples/babel-detangle-noweb.el: New test example file, based
  on the test file attached in the mailing list.  They are comprehensive
  and covers all the cases of Noweb tangles.

* test/lisp/test-ob-tangle.el: The ob-tangle/detangle-false-positive
  test case, commented in previous commit, is now back.  Additionally,
  we have a new test case for detangling Noweb blocks.

* testing/examples/babel.el: This is the example file for the
  ob-tangle/detangle-false-positive test case.  It was malformed - the
  source name in the end comment does not match that in the begin comment.
  This is not allowed when Noweb parsing is desired (the old
  implementation does not check against the end comment, which is one of
  the reasons why it fails to detangle Noweb blocks, since it does not
  know where the nested blocks end).

This is the third (and the last) comit of the rewrite of Org Babel
detangle.
Link: https://list.orgmode.org/f43360bb-dc8f-41bb-b40e-dfdd38ebb87b@app.fastmail.com/
---
 lisp/ob-tangle.el                         | 208 ++++++++++++++++++++--
 testing/examples/babel-detangle-noweb.el  | 170 ++++++++++++++++++
 testing/examples/babel-detangle-noweb.org | 151 ++++++++++++++++
 testing/examples/babel.el                 |   2 +-
 testing/lisp/test-ob-tangle.el            |  48 +++--
 5 files changed, 549 insertions(+), 30 deletions(-)
 create mode 100644 testing/examples/babel-detangle-noweb.el
 create mode 100644 testing/examples/babel-detangle-noweb.org

diff --git a/lisp/ob-tangle.el b/lisp/ob-tangle.el
index 0161c770b..efdcba918 100644
--- a/lisp/ob-tangle.el
+++ b/lisp/ob-tangle.el
@@ -923,6 +923,177 @@ If LINK and SOURCE-NAME are non-nil, jump accordingly."
       (error "The comments do not contain enough location information"))))
 
 ;; de-tangling functions
+
+(cl-defstruct (org-babel-detangle--info
+	       (:copier nil)
+	       (:constructor nil)
+	       (:constructor org-babel-detangle--make-info))
+  updates end prefix noweb-ref suffix)
+
+(defun org-babel-detangle--process-block (beg-comment)
+  "Process the expanded content wrapped by the BEG-COMMENT.
+Return an instance of `org-babel-detangle--info'."
+  (let* ((link (org-babel-tangle--comment-link beg-comment))
+	 (source-name (org-babel-tangle--comment-source-name beg-comment))
+	 (ret-prefix (org-babel-tangle--comment-prefix beg-comment))
+	 (extra (org-babel-tangle--comment-extra beg-comment))
+	 (type (plist-get extra :type))
+	 (no-update? (or (not (org-babel-tangle--string-nonempty link))
+			 (not (org-babel-tangle--string-nonempty source-name))
+			 (eq 'eval-or-lib type)
+			 ;; NOTE: we may want to think about detangling
+			 ;; prose references in future, but not now.
+			 (eq 'prose type)))
+	 (ret-updates (if no-update? nil (make-hash-table :test #'equal)))
+	 ret-suffix ret-end
+	 (end-comment
+	  (if no-update?
+	      (org-babel-tangle--find-associated-end beg-comment)
+	    (cl-loop with body = ""
+		     with body-start = (1+ (org-babel-tangle--comment-end beg-comment))
+		     with need-padline? = nil
+		     with match = nil
+		     while (setq match (org-babel-tangle--comment-line-search
+					nil nil nil nil t
+					`(("source-name" . ,source-name))))
+		     do
+		     ;; First collect everything up to the matched comment.
+		     (let ((body-end
+			    (1- (org-babel-tangle--comment-start match))))
+		       ;; If two comments are on consecutive lines, there's
+		       ;; nothing to collect.
+		       (when (> body-end body-start)
+			 (setq was-expansion nil)
+			 (setq body
+			       (concat body
+				       (when need-padline? "\n")
+				       (buffer-substring-no-properties
+					body-start body-end)))
+			 (setq need-padline? t)))
+		     if (org-babel-tangle--comment-beg? match) do
+		     (let* ((match-extra
+			     (org-babel-tangle--comment-extra match))
+			    (multi-first?
+			     (eq 'first (plist-get match-extra :multi))))
+		       (pcase (if multi-first?
+				  (org-babel-detangle--process-multi-reference match)
+				(org-babel-detangle--process-block match))
+			 ((cl-struct org-babel-detangle--info
+				     updates end prefix noweb-ref suffix)
+			  (setq ret-updates
+				(map-merge 'hash-table ret-updates updates))
+			  ;; If the prefix is from the outer block (i.e., the
+			  ;; current block), the inner block will not able to
+			  ;; clean the interpose of its suffix.  The outer block
+			  ;; must do the job.
+			  (when (and (org-babel-tangle--string-nonempty suffix)
+				     (org-babel-tangle--string-nonempty ret-prefix)
+				     (string-prefix-p ret-prefix suffix))
+			    (setq suffix (substring suffix (length ret-prefix))))
+			  (setq body
+				(concat body
+					(when need-padline? "\n")
+					prefix noweb-ref suffix))
+			  (setq need-padline?
+				(org-babel-tangle--string-nonempty suffix))
+			  (goto-char (1+ end))
+			  (setq body-start (1+ end)))))
+		     else return
+		     (prog1 match
+		       ;; Check if there's a suffix, on the next line.
+		       (forward-line 1)
+		       (let* ((suffix-line-start (line-beginning-position))
+			      (suffix-line-end (line-end-position)))
+			 (save-excursion
+			   (unless (org-babel-tangle--comment-line-search
+				    nil nil nil suffix-line-end t)
+			     (setq ret-suffix
+				   (buffer-substring-no-properties
+				    suffix-line-start
+				    (min (point-max) suffix-line-end)))
+			     (setq ret-end suffix-line-end))))
+		       ;; Cleanup interposed prefix.
+		       (unless (plist-get extra :no-prefix)
+			 (let ((prefix-length (length ret-prefix)))
+			   (setq body
+				 (with-temp-buffer
+				   (insert body)
+				   (forward-line -1)
+				   (forward-char prefix-length)
+				   (delete-extract-rectangle (point-min) (point))
+				   (buffer-string)))))
+		       ;; Links in Noweb comments point to the outer block, for
+		       ;; the use of `org-babel-tangle-jump-to-org', so we need
+		       ;; to create the real link here.
+		       (let* ((link (org-babel-tangle--comment-link beg-comment))
+			      (org-babel-tangle--comment-source-name beg-comment)
+			      (file (if (string-match "::" link)
+					(substring link 0 (match-beginning 0))
+				      link))
+			      real-link)
+			 (setq real-link
+			       (if (string-match org-babel-tangle--unnamed-block-re
+						 source-name)
+				   (let ((search-option
+					  (substring source-name 0
+						     (1- (match-beginning 1)))))
+				     (if (string= "No heading" search-option)
+					 file
+				       (concat file "::" search-option)))
+				 (concat file "::" source-name)))
+			 (puthash (list real-link source-name) body
+				  ret-updates)))))))
+    (unless end-comment (error "Not in tangled code"))
+    (org-babel-detangle--make-info
+     :updates ret-updates
+     :end (or ret-end (org-babel-tangle--comment-end end-comment))
+     :prefix ret-prefix
+     :noweb-ref
+     (let ((ref (plist-get extra :ref)))
+       (if ref (concat org-babel-noweb-wrap-start
+		       ref
+		       org-babel-noweb-wrap-end)
+	 nil))
+     :suffix ret-suffix)))
+
+(defun org-babel-detangle--process-multi-reference (beg-comment)
+  "Process the expanded content of a multi reference.
+Return an instance of `org-babel-detangle--info' with the combined information
+of all source blocks from first to last in the sequence of source blocks
+referred by the multi reference.
+
+BEG-COMMENT the begin comment of the first source block in the sequence."
+  (unless (eq (plist-get (org-babel-tangle--comment-extra beg-comment) :multi)
+	      'first)
+    (error "Not a begin comment of the first source block in a multi reference"))
+  (let* ((first-info (org-babel-detangle--process-block beg-comment))
+	 (ret-updates (org-babel-detangle--info-updates first-info))
+	 (last-info
+	  (cl-loop with match = nil
+		   while (setq match (org-babel-tangle--comment-line-search
+				      'beg nil nil nil t))
+		   do
+		   (let ((match-info (org-babel-detangle--process-block match)))
+		     (pcase match-info
+		       ((cl-struct org-babel-detangle--info
+				   updates end)
+			(setq ret-updates
+			      (map-merge 'hash-table ret-updates updates))
+			(when (eq
+			       (plist-get
+				(org-babel-tangle--comment-extra match) :multi)
+			       'last)
+			  (cl-return match-info))
+			(goto-char end)))))))
+    (unless last-info
+      (error "Not in tangled code"))
+    (org-babel-detangle--make-info
+     :updates ret-updates
+     :end (org-babel-detangle--info-end last-info)
+     :prefix (org-babel-detangle--info-prefix first-info)
+     :noweb-ref (org-babel-detangle--info-noweb-ref first-info)
+     :suffix (org-babel-detangle--info-suffix last-info))))
+
 (defun org-babel-detangle (&optional source-code-file)
   "Propagate changes from current source buffer back to the original Org file.
 This requires that code blocks were tangled with link comments
@@ -934,20 +1105,29 @@ of the current buffer."
   (save-excursion
     (when source-code-file (find-file source-code-file))
     (goto-char (point-min))
-    (let ((counter 0) new-body end)
-      (while (re-search-forward org-link-bracket-re nil t)
-        (if (and (match-string 2)
-		 (re-search-forward
-		  (concat " " (regexp-quote (match-string 2)) " ends here") nil t))
-	    (progn (setq end (match-end 0))
-		   (forward-line -1)
-		   (save-excursion
-		     (when (setq new-body (org-babel-tangle-jump-to-org))
-		       (org-babel-update-block-body new-body)))
-		   (setq counter (+ 1 counter)))
-	  (setq end (point)))
-        (goto-char end))
-      (prog1 counter (message "Detangled %d code blocks" counter)))))
+    (cl-loop with beg = nil
+	     with all-updates = nil
+	     while (setq beg (org-babel-tangle--comment-line-search
+			      'beg nil nil nil t))
+	     do
+	     (pcase (org-babel-detangle--process-block beg)
+	       ((cl-struct org-babel-detangle--info
+			   updates end)
+		(setq all-updates
+		      (map-merge 'hash-table all-updates updates))
+		(goto-char end)))
+	     finally return
+	     (progn
+	       (when all-updates
+		 (save-excursion
+		   (maphash
+		    (pcase-lambda (`(,link ,source-name) body)
+		      (org-babel-tangle-jump-to-org link source-name)
+		      (org-babel-update-block-body body))
+		    all-updates)))
+	       (let ((count (if all-updates (hash-table-count all-updates) 0)))
+		 (message "Detangled %d code blocks" count)
+		 count)))))
 
 (provide 'ob-tangle)
 
diff --git a/testing/examples/babel-detangle-noweb.el b/testing/examples/babel-detangle-noweb.el
new file mode 100644
index 000000000..89b2effcc
--- /dev/null
+++ b/testing/examples/babel-detangle-noweb.el
@@ -0,0 +1,170 @@
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Without Noweb (compatibility)][*Without Noweb (compatibility):1]]
+A line
+;; *Without Noweb (compatibility):1 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Without Noweb (compatibility)][*Without Noweb (compatibility):2]]
+Another line
+;; *Without Noweb (compatibility):2 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:2]]
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::a string][a string]]
+"a string"
+;; a string ends here
+;; *Single reference:2 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:3]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+ suffix
+;; *Single reference:3 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:4]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+;; *Single reference:4 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:5]]
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+"a string"
+;; a string ends here
+ suffix
+;; *Single reference:5 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:6]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+ "a string"
+ ;; a string ends here
+ suffix
+;; *Single reference:6 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:7]]
+before
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+ "a string"
+ ;; a string ends here
+ suffix
+;; *Single reference:7 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:8]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Single reference:8 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][*Single reference:9]]
+before
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Single reference][a string]]
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Single reference:9 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:3]]
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+"a string"
+;; a string ends here
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:1]]
+"another string"
+;; *Multi reference:1 ends here
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:2]]
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::a string][a string]]
+"a string"
+;; a string ends here
+ "and yet another string"
+;; *Multi reference:2 ends here
+;; *Multi reference:3 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:4]]
+before
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:1]]
+prefix "another string"
+prefix ;; *Multi reference:1 ends here
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:2]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+prefix  "and yet another string"
+prefix ;; *Multi reference:2 ends here
+ suffix
+after
+;; *Multi reference:4 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:5]]
+before
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:1]]
+prefix "another string"
+prefix ;; *Multi reference:1 ends here
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][*Multi reference:2]]
+prefix ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+prefix "a string"
+prefix ;; a string ends here
+prefix  "and yet another string"
+prefix ;; *Multi reference:2 ends here
+ ;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Multi reference][a string]]
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Multi reference:5 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Evaluation reference][*Evaluation reference:2]]
+;; [[][a computation]]
+"hello"
+;; a computation ends here
+;; *Evaluation reference:2 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Evaluation reference][*Evaluation reference:3]]
+before
+prefix ;; [[][a computation]]
+prefix "hello"
+prefix ;; a computation ends here
+ suffix
+after
+;; *Evaluation reference:3 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Lib reference][*Lib reference:2]]
+;; [[][lib-blk]]
+2
+;; lib-blk ends here
+;; *Lib reference:2 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::*Lib reference][*Lib reference:3]]
+before
+prefix ;; [[][lib-blk]]
+prefix 2
+prefix ;; lib-blk ends here
+ suffix
+after
+;; *Lib reference:3 ends here
+
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::#same name 2][#same name 2:2]]
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::#same name 2][#same name 1:1]]
+"other string1"
+;; #same name 1:1 ends here
+;; [[id:651D7F1E-B5E4-4406-82A5-AABBC2A27BCF::#same name 2][#same name 2:1]]
+"other string2"
+;; #same name 2:1 ends here
+;; #same name 2:2 ends here
diff --git a/testing/examples/babel-detangle-noweb.org b/testing/examples/babel-detangle-noweb.org
new file mode 100644
index 000000000..0edf4c034
--- /dev/null
+++ b/testing/examples/babel-detangle-noweb.org
@@ -0,0 +1,151 @@
+:PROPERTIES:
+:ID:       651D7F1E-B5E4-4406-82A5-AABBC2A27BCF
+:END:
+
+* Without Noweb (compatibility)
+
+The following blocks don't contain Noweb references.  They tangle/detangle in the same way for compatbility.
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :comments noweb
+A line
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :comments noweb
+Another line
+#+end_src
+
+* Single reference
+
+#+name: a string
+#+begin_src emacs-lisp :noweb-ref strings
+"a string"
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<a string>>
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+prefix <<a string>> suffix
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+prefix <<a string>>
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<a string>> suffix
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+prefix <<a string>> <<a string>> suffix
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<a string>> <<a string>> suffix
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+prefix <<a string>> <<a string>> suffix
+after
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<a string>> <<a string>> suffix
+after
+#+end_src
+
+* Multi reference
+
+#+begin_src emacs-lisp :noweb-ref strings
+"another string"
+#+end_src
+
+Recursive case:
+
+#+begin_src emacs-lisp :noweb-ref strings :noweb yes :comments noweb
+<<a string>> "and yet another string"
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<strings>>
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<strings>> suffix
+after
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<strings>> <<a string>> suffix
+after
+#+end_src
+
+* Evaluation reference
+
+#+name: a computation
+#+begin_src emacs-lisp :eval yes :var str=""
+(message "%S" str)
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<a computation(str="hello")>>
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<a computation(str="hello")>> suffix
+after
+#+end_src
+
+* Lib reference
+
+Run this block before tangling.
+
+#+begin_src emacs-lisp :eval yes :results silent
+(setq 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)")))
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<lib-blk>>
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+before
+prefix <<lib-blk>> suffix
+after
+#+end_src
+
+* Improved name generation
+
+** Same name headline
+:PROPERTIES:
+:CUSTOM_ID: same name 1
+:END:
+
+#+begin_src emacs-lisp :noweb-ref other-strings
+"other string1"
+#+end_src
+
+** Same name headline
+:PROPERTIES:
+:CUSTOM_ID: same name 2
+:END:
+
+#+begin_src emacs-lisp :noweb-ref other-strings
+"other string2"
+#+end_src
+
+#+begin_src emacs-lisp :tangle babel-detangle-noweb.el :noweb yes :comments noweb
+<<other-strings>>
+#+end_src
diff --git a/testing/examples/babel.el b/testing/examples/babel.el
index cbd522e24..16a0fe308 100644
--- a/testing/examples/babel.el
+++ b/testing/examples/babel.el
@@ -3,4 +3,4 @@
 
 ;; [[id:73115FB0-6565-442B-BB95-50195A499EF4][detangle:1]]
 ;; detangle changes
-;; linked content to detangle:1 ends here
+;; detangle:1 ends here
diff --git a/testing/lisp/test-ob-tangle.el b/testing/lisp/test-ob-tangle.el
index 172ff3125..7f5565c4d 100644
--- a/testing/lisp/test-ob-tangle.el
+++ b/testing/lisp/test-ob-tangle.el
@@ -1021,21 +1021,39 @@ suffix5
     (insert buffer-file-name)
     (should-error (org-babel-tangle))))
 
-;; (ert-deftest ob-tangle/detangle-false-positive ()
-;;   "Test handling of false positive link during detangle."
-;;   (let (buffer)
-;;     (unwind-protect
-;; 	(org-test-in-example-file (expand-file-name "babel.el" org-test-example-dir)
-;; 	  (org-babel-detangle)
-;; 	  (org-test-at-id "73115FB0-6565-442B-BB95-50195A499EF4"
-;; 	    (setq buffer (current-buffer))
-;; 	    (org-babel-next-src-block)
-;; 	    (should (equal (string-trim (org-element-property
-;; 					 :value (org-element-at-point)))
-;; 			   ";; detangle changes"))))
-;;       (with-current-buffer buffer
-;;         (set-buffer-modified-p nil))
-;;       (kill-buffer buffer))))
+(ert-deftest ob-tangle/detangle-false-positive ()
+  "Test handling of false positive link during detangle."
+  (let (buffer)
+    (unwind-protect
+	(org-test-in-example-file (expand-file-name "babel.el" org-test-example-dir)
+	  (org-babel-detangle)
+	  (org-test-at-id "73115FB0-6565-442B-BB95-50195A499EF4"
+	    (setq buffer (current-buffer))
+	    (org-babel-next-src-block)
+	    (should (equal (string-trim (org-element-property
+					 :value (org-element-at-point)))
+			   ";; detangle changes"))))
+      (with-current-buffer buffer
+        (set-buffer-modified-p nil))
+      (kill-buffer buffer))))
+
+(ert-deftest ob-tangle/detangle-noweb ()
+  "Test detangling Noweb references."
+  (let (buffer prev-bufstr)
+    (org-test-at-id
+     "651D7F1E-B5E4-4406-82A5-AABBC2A27BCF"
+     (setq prev-bufstr (buffer-string)))
+    (unwind-protect
+	(org-test-in-example-file
+	 (expand-file-name "babel-detangle-noweb.el" org-test-example-dir)
+	 (org-babel-detangle)
+	 (org-test-at-id
+	  "651D7F1E-B5E4-4406-82A5-AABBC2A27BCF"
+	  (setq buffer (current-buffer))
+	  (should (equal (buffer-string) prev-bufstr))))
+      (with-current-buffer buffer
+	(set-buffer-modified-p nil))
+      (kill-buffer buffer))))
 
 (ert-deftest ob-tangle/collect-blocks ()
   "Test block collection into groups for tangling."
-- 
2.39.5 (Apple Git-154)

