From 6d5767ed6d18eaf4cf662569a10fa0e0d75b6d09 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] ob-tangle.el: Rewrite detangling

* lisp/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.org: New test example file,
based on the test file attached in the mailing list.  It is
comprehensive and covers all the cases of Noweb tangles.

* testing/examples/babel-detangle-noweb.el: New tangled output file
corresponding to the above test.

* testing/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/lisp/test-ob-tangle.el (ob-tangle/detangle-false-positive,
ob-tangle/detangle-noweb): Fix tests to not use `org-test-in-example-file'
for tangled source files.

`org-test-in-example-file' forces `org-mode' on every buffer it opens
(testing/org-test.el:161).  Tangled source files (e.g. "babel.el",
"babel-detangle-noweb.el") need their natural major mode for
`comment-start' to be correct; otherwise the tangle comment regex ---
which is derived from `comment-start' --- never matches the actual
comments, and `org-babel-detangle' silently detangles 0 blocks.

The old upstream detangle searched for `org-link-bracket-re' directly
and did not depend on `comment-start', so the wrong major mode was
harmless.  With our rewrite, which builds the comment regex from
`comment-start' (via `org-babel-tangle--comment-start-re'), the tests
must open tangled files in the correct mode.

The `detangle-false-positive' test binds `org-src-content-indentation'
to 0.  The `detangle-noweb' test binds `org-src-preserve-indentation'
to t and `org-babel-tangle-comment-format-beg' to include %extra,
matching the settings used by tests/run.sh when the .el file was
generated.  Previously these were invisible because the detangle never
actually ran.

* 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) commit of the rewrite of Org Babel
detangle.

Link: https://list.orgmode.org/f43360bb-dc8f-41bb-b40e-dfdd38ebb87b@app.fastmail.com/
---
 lisp/ob-tangle.el                         | 243 ++++++++++++++++++++--
 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            |  53 +++--
 5 files changed, 589 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 f3fdc9e26..fb5a95707 100644
--- a/lisp/ob-tangle.el
+++ b/lisp/ob-tangle.el
@@ -984,6 +984,210 @@ 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 nil
+	  :documentation
+	  "The text on the line after the inner block's end comment, or nil.
+
+- nil: the next line is another comment (refs were on the same line
+  in the source).  No padline.
+- \"\": the next line is empty.  This empty line is the padline,
+  meaning the ref was on its own line in the source.
+- \"text\": non-empty text that followed the ref on the same line
+  in the source (e.g., from `<<ref>> text`)."))
+
+(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?
+	      (let ((end-c (org-babel-tangle--find-associated-end beg-comment)))
+		;; Check if there's a suffix on the line after the end comment,
+		;; mirroring the suffix-checking in the normal (invertible) path.
+		(when end-c
+		  (save-excursion
+		    (goto-char (org-babel-tangle--comment-end end-c))
+		    (forward-line 1)
+		    (let ((suffix-line-end (line-end-position)))
+		      (if (org-babel-tangle--comment-line-search
+			   nil nil nil suffix-line-end t)
+			  ;; Next line is a comment: no suffix, but the block
+			  ;; was on its own line.
+			  (setq ret-suffix "")
+			;; Next line is text: it's a suffix.
+			(setq ret-suffix
+			      (buffer-substring-no-properties
+			       (line-beginning-position)
+			       (min (point-max) suffix-line-end)))
+			(setq ret-end suffix-line-end)))))
+		end-c)
+	    (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))
+			  ;; Any non-nil suffix (even "") means the ref was
+			  ;; on its own line and needs a padline before the
+			  ;; next body segment.  nil means same-line refs.
+			  (setq need-padline? (not (null 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)
+				   (goto-char (point-max))
+				   (when (bolp) (forward-line -1))
+				   (beginning-of-line)
+				   (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
@@ -995,20 +1199,31 @@ 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
+		   (let ((tangle-dir default-directory))
+		     (maphash
+		      (pcase-lambda (`(,link ,source-name) body)
+			(let ((default-directory tangle-dir))
+			  (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..311c8a8db
--- /dev/null
+++ b/testing/examples/babel-detangle-noweb.el
@@ -0,0 +1,170 @@
+;; [[file:babel-detangle-noweb.org::*Without Noweb (compatibility)][*Without Noweb (compatibility):1]]
+A line
+;; *Without Noweb (compatibility):1 ends here
+
+;; [[file:babel-detangle-noweb.org::*Without Noweb (compatibility)][*Without Noweb (compatibility):2]]
+Another line
+;; *Without Noweb (compatibility):2 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:2]]
+;; [[file:babel-detangle-noweb.org::a string][a string]](:ref "a string")
+"a string"
+;; a string ends here
+;; *Single reference:2 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:3]]
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+ suffix
+;; *Single reference:3 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:4]]
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+;; *Single reference:4 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:5]]
+;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+"a string"
+;; a string ends here
+ suffix
+;; *Single reference:5 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:6]]
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+ "a string"
+ ;; a string ends here
+ suffix
+;; *Single reference:6 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:7]]
+before
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+ "a string"
+ ;; a string ends here
+ suffix
+;; *Single reference:7 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:8]]
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Single reference:8 ends here
+
+;; [[file:babel-detangle-noweb.org::*Single reference][*Single reference:9]]
+before
+prefix ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+ ;; [[file:babel-detangle-noweb.org::*Single reference][a string]](:ref "a string")
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Single reference:9 ends here
+
+;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:3]]
+;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "strings" :multi first)
+"a string"
+;; a string ends here
+;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:1]](:ref "strings")
+"another string"
+;; *Multi reference:1 ends here
+;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:2]](:ref "strings" :multi last)
+;; [[file:babel-detangle-noweb.org::a string][a string]](:ref "a string")
+"a string"
+;; a string ends here
+ "and yet another string"
+;; *Multi reference:2 ends here
+;; *Multi reference:3 ends here
+
+;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:4]]
+before
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "strings" :multi first)
+prefix "a string"
+prefix ;; a string ends here
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:1]](:ref "strings")
+prefix "another string"
+prefix ;; *Multi reference:1 ends here
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:2]](:ref "strings" :multi last)
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "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
+
+;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:5]]
+before
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "strings" :multi first)
+prefix "a string"
+prefix ;; a string ends here
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:1]](:ref "strings")
+prefix "another string"
+prefix ;; *Multi reference:1 ends here
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][*Multi reference:2]](:ref "strings" :multi last)
+prefix ;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "a string")
+prefix "a string"
+prefix ;; a string ends here
+prefix  "and yet another string"
+prefix ;; *Multi reference:2 ends here
+ ;; [[file:babel-detangle-noweb.org::*Multi reference][a string]](:ref "a string")
+ "a string"
+ ;; a string ends here
+ suffix
+after
+;; *Multi reference:5 ends here
+
+;; [[file:babel-detangle-noweb.org::*Evaluation reference][*Evaluation reference:2]]
+;; [[][a computation]](:type eval-or-lib :ref "a computation(str=\"hello\")")
+"hello"
+;; a computation ends here
+;; *Evaluation reference:2 ends here
+
+;; [[file:babel-detangle-noweb.org::*Evaluation reference][*Evaluation reference:3]]
+before
+prefix ;; [[][a computation]](:type eval-or-lib :ref "a computation(str=\"hello\")")
+prefix "hello"
+prefix ;; a computation ends here
+ suffix
+after
+;; *Evaluation reference:3 ends here
+
+;; [[file:babel-detangle-noweb.org::*Lib reference][*Lib reference:2]]
+;; [[][lib-blk]](:type eval-or-lib :ref "lib-blk")
+2
+;; lib-blk ends here
+;; *Lib reference:2 ends here
+
+;; [[file:babel-detangle-noweb.org::*Lib reference][*Lib reference:3]]
+before
+prefix ;; [[][lib-blk]](:type eval-or-lib :ref "lib-blk")
+prefix 2
+prefix ;; lib-blk ends here
+ suffix
+after
+;; *Lib reference:3 ends here
+
+;; [[file:babel-detangle-noweb.org::#same name 2][#same name 2:2]]
+;; [[file:babel-detangle-noweb.org::#same name 2][#same name 1:1]](:ref "other-strings" :multi first)
+"other string1"
+;; #same name 1:1 ends here
+;; [[file:babel-detangle-noweb.org::#same name 2][#same name 2:1]](:ref "other-strings" :multi last)
+"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 80964cf11..036b9709f 100644
--- a/testing/lisp/test-ob-tangle.el
+++ b/testing/lisp/test-ob-tangle.el
@@ -1023,21 +1023,44 @@ 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 ((org-src-content-indentation 0)
+	buffer)
+    (unwind-protect
+	(progn
+	  (find-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 ((org-src-preserve-indentation t)
+	(org-babel-tangle-comment-format-beg "[[%link][%source-name]]%extra")
+	buffer prev-bufstr)
+    (org-test-at-id
+     "651D7F1E-B5E4-4406-82A5-AABBC2A27BCF"
+     (setq prev-bufstr (buffer-string)))
+    (unwind-protect
+	(progn
+	  (find-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))))
 
 (defun ob-tangle/tangle-targets ()
   "Return tangle targets for testing."
-- 
2.39.5 (Apple Git-154)

