From a1ae54e9353378345f73a43e0cc495d9a3705dcc Mon Sep 17 00:00:00 2001
From: Mingtong Lin <mt.oss@fastmail.com>
Date: Fri, 28 Nov 2025 17:38:42 -0500
Subject: [PATCH 5/6] lisp/ob-tangle.el: Rewrite Noweb comment parsing and
 org-babel-tangle-jump-to-org. testing/lisp/test-ob-tangle.el: Add tests.

* ob-tangle.el (org-babel-tangle-jump-to-org, and added a buncn of new
constructs): A new and robust Noweb comment parsing mechanism that
handles Noweb blocks correctly and jump from tangled file to Org source
file with the new mechanism.

* test-ob-tangle.el: Add new tests on complex Noweb tangles, and
  temporarily commented out a test on org-babel-detangle (which will be
  rewritten immediately in the next commit, using the new
  org-babel-tangle-jump-to-org).

This is the second 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              | 321 ++++++++++++++++++++----
 testing/lisp/test-ob-tangle.el | 439 ++++++++++++++++++++++++++++++---
 2 files changed, 680 insertions(+), 80 deletions(-)

diff --git a/lisp/ob-tangle.el b/lisp/ob-tangle.el
index 1287efb55..0161c770b 100644
--- a/lisp/ob-tangle.el
+++ b/lisp/ob-tangle.el
@@ -653,6 +653,275 @@ EXTRA is an plist of additional information, which may include the following:
     (list (org-fill-template org-babel-tangle-comment-format-beg link-data)
 	  (org-fill-template org-babel-tangle-comment-format-end link-data))))
 
+(cl-defstruct (org-babel-tangle--comment
+	       (:copier org-babel-tangle--copy-comment)
+	       (:constructor nil)
+	       (:constructor org-babel-tangle--make-comment))
+  beg? full prefix link source-name
+  (extra nil
+	 :documentation
+	 "A plist of additional information.  Entries may include:
+: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\").")
+  start end)
+
+(defun org-babel-tangle--comment-start-re ()
+  "Compute the comment start regexp used by `comment-region' in current buffer."
+  (comment-normalize-vars)
+  (let* ((arg nil)
+	 (multi-char (/= (string-match "[ \t]*\\'" comment-start) 1))
+	 (numarg (if (and (null arg) (not multi-char))
+		     (* comment-add 1)
+		   0))
+	 (s (comment-padright comment-start numarg)))
+    (concat
+     (if (string-match comment-start-skip s) s
+       (comment-padright comment-start))
+     "?")))
+
+(defun org-babel-tangle--comment-line-re (&optional end alist)
+  "Return a regexp string which matches a line containing Org Babel tangle comment.
+
+END  if nil, the regexp will match a line with begin comment; if non-nil, the
+     regexp will match a line with end comment.
+ALIST will be consulted for:
+  source-name  the pattern that matches the string as the source block name
+  link         the pattern that matches the string as the link
+
+To match for comments, the regexp respects `org-babel-tangle-comment-format-beg'
+and `org-babel-tangle-comment-format-end', and fill the template values with
+ALIST, for subpatterns that match each part of them."
+  (let* ((template (if end
+		       org-babel-tangle-comment-format-end
+		     org-babel-tangle-comment-format-beg))
+	 (link-pat (or (alist-get "link" alist nil nil #'string=)
+		       (rx (group
+			    (*?
+			     (or (not (any "[]\\"))
+				 (and "\\" (zero-or-more "\\\\") (any "[]"))
+				 (and (one-or-more "\\") (not (any "[]")))))))))
+	 (source-name-pat (or (alist-get "source-name" alist nil nil #'string=)
+			      (rx (group (* nonl)))))
+	 (extra-pat (or (alist-get "extra" alist nil nil #'string=)
+			(rx (group
+			     (? (seq "(" (+ nonl) ")"))))))
+	 (filled (org-fill-template (regexp-quote template)
+				    `(("link" . ,link-pat)
+				      ("source-name" . ,source-name-pat)
+				      ("extra" . ,extra-pat)))))
+    (rx-to-string
+     `(seq bol
+	   (group (* nonl))
+	   ;; `comment-start-skip' is not reliable, so we need our custom
+	   ;; function instead.  For example, its value in Elisp mode is ";+ *",
+	   ;; which may not reliably match ";; " when the string has multiple
+	   ;; comment starter, e.g.: ";; ;; xxx".
+	   (regexp ,(org-babel-tangle--comment-start-re))
+	   (regexp ,filled)
+	   ;; Some languages only support block comments
+	   ;; (e.g., OCaml uses (* ... *)), so we need to match them optionally.
+	   ;; However, `comment-end-skip' is NOT reliable for this, since some
+	   ;; modes (e.g., Elisp) have it matching "\n".
+	   (*? nonl)
+	   eol))))
+
+(defun org-babel-tangle--comment-line-search (&optional pattern backward alist bound noerror alist2)
+  "Search for a line that contains Org Babel tangle comment.
+
+PATTERN   if nil, look for either begin or end comment; if 'beg, look for begin
+	  comment only; if 'end, look for end comment only.
+BACKWARD  if non-nil, use `re-search-backward'
+ALIST     check `org-babel-tangle--comment-line-re'
+BOUND and NOERROR are the arguments for `re-search-forward' and
+`re-search-backward', check their respective docstring.
+ALIST2    same as ALIST, but will be used for end comment separately if non-nil.
+
+Return nil if no match found, or an instance of `org-babel-tangle--comment'.
+If PATTERN is nil, the search only works if `org-babel-tangle-comment-format-beg'
+places fields in the order of %link then %source-name %extra, otherwise the
+regexp group won't match."
+  (cl-flet ((do-search (regexp)
+		       (funcall (if backward #'re-search-backward #'re-search-forward)
+				regexp bound noerror)))
+    (pcase pattern
+      ('beg
+       (when (and (do-search (org-babel-tangle--comment-line-re nil alist))
+		  (match-string 0))
+	 (org-babel-tangle--make-comment
+	  :beg? t
+	  :full (match-string-no-properties 0)
+	  :prefix (match-string-no-properties 1)
+	  :link (match-string-no-properties 2)
+	  :source-name (match-string-no-properties 3)
+	  :extra
+	  (let ((extra (match-string-no-properties 4)))
+	    (if (and extra (not (string-empty-p extra)))
+		(read extra)
+	      nil))
+	  :start (line-beginning-position)
+	  :end (line-end-position))))
+      ('end
+       (when (and (do-search (org-babel-tangle--comment-line-re t alist))
+		  (match-string 0))
+	 (org-babel-tangle--make-comment
+	  :beg? nil
+	  :full (match-string-no-properties 0)
+	  :prefix (match-string-no-properties 1)
+	  :link nil
+	  :source-name (match-string-no-properties 2)
+	  :extra nil
+	  :start (line-beginning-position)
+	  :end (line-end-position))))
+      ('nil
+       (when (and (do-search
+		   (rx (or
+			(group (regexp
+				(org-babel-tangle--comment-line-re nil alist)))
+			(group (regexp
+				(org-babel-tangle--comment-line-re
+				 t (or alist2 alist)))))))
+		  (match-string 0))
+	 (let* ((extra? (string-match-p "%extra"
+					org-babel-tangle-comment-format-beg))
+		(beg? (match-string 1))
+		(prefix (match-string-no-properties (if beg? 2
+						      (if extra? 7 6))))
+		(link (if beg? (match-string-no-properties 3) nil))
+		(source-name (match-string-no-properties (if beg? 4
+							   (if extra?
+							       8 7))))
+		(extra (if beg? (match-string-no-properties 5) nil)))
+	   (org-babel-tangle--make-comment
+	    :beg? (if beg? t nil)
+	    :full (match-string-no-properties 0)
+	    :prefix prefix
+	    :link link
+	    :source-name source-name
+	    :extra (if (and extra (not (string-empty-p extra)))
+		       (read extra)
+		     nil)
+	    :start (line-beginning-position)
+	    :end (line-end-position))))))))
+
+(defun org-babel-tangle--find-associated-end (beg-comment &optional start-point bound)
+  "Find the associated end comment with BEG-COMMENT.
+Return nil if not found, or the Noweb comments are corrupted (i.e., there are
+dangling Noweb comments that break the structure of the file).
+
+The search will be done between START-POINT and BOUND, if provided.  Otherwise,
+search from the end field of BEG-COMMENT to end of buffer."
+  (save-excursion
+    (if start-point
+	(goto-char start-point)
+      (goto-char (org-babel-tangle--comment-end beg-comment)))
+    (let ((end-comment
+	   (cl-loop with beg-stack = '()
+		    with match = nil
+		    while (setq match (org-babel-tangle--comment-line-search
+				       nil nil nil bound t))
+		    for beg? = (org-babel-tangle--comment-beg? match)
+		    if (and (not beg?) (null beg-stack))
+		    return match
+		    else do
+		    (if beg?
+			(setq beg-stack (cons match beg-stack))
+		      (let ((top (car beg-stack)))
+			(if (string=
+			     (org-babel-tangle--comment-source-name top)
+			     (org-babel-tangle--comment-source-name match))
+			    (pop beg-stack)
+			  (cl-return nil)))))))
+      (when (and end-comment
+		 (string= (org-babel-tangle--comment-source-name beg-comment)
+			  (org-babel-tangle--comment-source-name end-comment)))
+	end-comment))))
+
+(defun org-babel-tangle--string-nonempty (s)
+  "Return S if S is an non-empty string, else nil."
+  (and (not (string-empty-p s)) s))
+
+(defun org-babel-tangle--find-beg-comment (&optional cur-point)
+  "Find the associated begin comment at CUR-POINT.
+Return nil if not inside a valid tangled block."
+  (when cur-point (goto-char cur-point))
+  (let ((beg-comment
+	 (save-excursion
+	   (cl-loop with end-stack = '()
+		    with match = nil
+		    while (setq match (org-babel-tangle--comment-line-search
+				       nil t nil nil t))
+		    for beg? = (org-babel-tangle--comment-beg? match)
+		    if (and beg? (null end-stack))
+		    return match
+		    else do
+		    (if (not beg?)
+			(setq end-stack (cons match end-stack))
+		      (let ((top (car end-stack)))
+			(if (string=
+			     (org-babel-tangle--comment-source-name top)
+			     (org-babel-tangle--comment-source-name match))
+			    (pop end-stack)
+			  (cl-return nil))))))))
+    ;; Verify that beg-comment is not dangling by trying to find its friend.
+    (when (and beg-comment
+	       (org-babel-tangle--find-associated-end beg-comment))
+      beg-comment)))
+
+(defconst org-babel-tangle--unnamed-block-re
+  "[^ \t\n\r]:\\([[:digit:]]+\\)"
+  "Regexp matching the generated name for unnamed tangled source block.")
+
+(defun org-babel-tangle-jump-to-org (&optional link source-name)
+  "Jump from a tangled code file to the related Org mode file.
+If LINK and SOURCE-NAME are non-nil, jump accordingly."
+  (interactive)
+  (let* ((beg (unless (and (org-babel-tangle--string-nonempty link)
+			   (org-babel-tangle--string-nonempty source-name))
+		(or (org-babel-tangle--find-beg-comment)
+		    (error "Not in tangled code"))))
+	 (link  (or (org-babel-tangle--string-nonempty link)
+		    (org-babel-tangle--comment-link beg)))
+	 (source-name (or (org-babel-tangle--string-nonempty source-name)
+			  (org-babel-tangle--comment-source-name beg))))
+    (if (and (org-babel-tangle--string-nonempty link)
+	     (org-babel-tangle--string-nonempty source-name))
+	(let ((org-link (org-fill-template
+			 "[[%link][%source-name]]"
+			 `(("link" . ,link)
+			   ("source-name" . ,source-name))))
+	      target-buffer target-char)
+	  (condition-case err
+	      (progn
+		(save-window-excursion
+		  (let ((org-link-search-must-match-exact-headline))
+		    (org-link-open-from-string org-link))
+		  (setq target-buffer (current-buffer))
+		  (if (string-match org-babel-tangle--unnamed-block-re source-name)
+		      (let ((n (string-to-number (match-string 1 source-name))))
+			(if (org-before-first-heading-p) (goto-char (point-min))
+			  (org-back-to-heading t))
+			;; Do not skip the first block if it begins at point min.
+			(cond ((or (org-at-heading-p)
+				   (not (org-element-type-p (org-element-at-point) 'src-block)))
+			       (org-babel-next-src-block n))
+			      ((= n 1))
+			      (t (org-babel-next-src-block (1- n)))))
+		    (org-babel-goto-named-src-block source-name))
+		  (goto-char (org-babel-where-is-src-block-head))
+		  (forward-line 1)
+		  (setq target-char (point)))
+		(org-src-switch-to-buffer target-buffer t)
+		(goto-char target-char))
+	    (error
+	     (error "Failed to jump back, maybe the original Org file has been changed"))))
+      (error "The comments do not contain enough location information"))))
+
 ;; de-tangling functions
 (defun org-babel-detangle (&optional source-code-file)
   "Propagate changes from current source buffer back to the original Org file.
@@ -680,58 +949,6 @@ of the current buffer."
         (goto-char end))
       (prog1 counter (message "Detangled %d code blocks" counter)))))
 
-(defun org-babel-tangle-jump-to-org ()
-  "Jump from a tangled code file to the related Org mode file."
-  (interactive)
-  (let ((mid (point))
-	start body-start end target-buffer target-char link block-name body)
-    (save-window-excursion
-      (save-excursion
-	(while (and (re-search-backward org-link-bracket-re nil t)
-		    (not ; ever wider searches until matching block comments
-		     (and (setq start (line-beginning-position))
-			  (setq body-start (line-beginning-position 2))
-			  (setq link (match-string 0))
-			  (setq block-name (match-string 2))
-			  (save-excursion
-			    (save-match-data
-			      (re-search-forward
-			       (concat " " (regexp-quote block-name)
-				       " ends here")
-			       nil t)
-			      (setq end (line-beginning-position))))))))
-	(unless (and start (< start mid) (< mid end))
-	  (error "Not in tangled code"))
-        (setq body (buffer-substring body-start end)))
-      ;; Go to the beginning of the relative block in Org file.
-      ;; Explicitly allow fuzzy search even if user customized
-      ;; otherwise.
-      (let (org-link-search-must-match-exact-headline)
-        (org-link-open-from-string link))
-      (setq target-buffer (current-buffer))
-      (if (string-match "[^ \t\n\r]:\\([[:digit:]]+\\)" block-name)
-          (let ((n (string-to-number (match-string 1 block-name))))
-	    (if (org-before-first-heading-p) (goto-char (point-min))
-	      (org-back-to-heading t))
-	    ;; Do not skip the first block if it begins at point min.
-	    (cond ((or (org-at-heading-p)
-		       (not (org-element-type-p (org-element-at-point) 'src-block)))
-		   (org-babel-next-src-block n))
-		  ((= n 1))
-		  (t (org-babel-next-src-block (1- n)))))
-        (org-babel-goto-named-src-block block-name))
-      (goto-char (org-babel-where-is-src-block-head))
-      (forward-line 1)
-      ;; Try to preserve location of point within the source code in
-      ;; tangled code file.
-      (let ((offset (- mid body-start)))
-	(when (> end (+ offset (point)))
-	  (forward-char offset)))
-      (setq target-char (point)))
-    (org-src-switch-to-buffer target-buffer t)
-    (goto-char target-char)
-    body))
-
 (provide 'ob-tangle)
 
 ;; Local variables:
diff --git a/testing/lisp/test-ob-tangle.el b/testing/lisp/test-ob-tangle.el
index 91b965b4a..172ff3125 100644
--- a/testing/lisp/test-ob-tangle.el
+++ b/testing/lisp/test-ob-tangle.el
@@ -317,10 +317,12 @@ they are referred by :noweb-ref."
       "* H\n#+begin_src emacs-lisp\n1\n#+end_src"
       (org-test-with-temp-text-in-file
           "* H\n#+begin_src emacs-lisp\n1\n#+end_src"
+        (org-mode)
 	(let ((file (buffer-file-name)))
           (org-test-with-temp-text
               (format ";; [[file:%s][H:1]]\n<point>1\n;; H:1 ends here\n"
                       (file-name-nondirectory file))
+            (emacs-lisp-mode)
             (org-babel-tangle-jump-to-org)
             (buffer-string))))))
     ;; Multiple blocks in the same section.
@@ -342,35 +344,27 @@ another block
 2
 #+end_src
 "
+        (org-mode)
 	(let ((file (buffer-file-name)))
           (org-test-with-temp-text
               (format ";; [[file:%s][H:2]]\n<point>2\n;; H:2 ends here\n"
                       (file-name-nondirectory file))
+            (emacs-lisp-mode)
             (org-babel-tangle-jump-to-org)
             (buffer-substring (line-beginning-position)
                               (line-end-position)))))))
-    ;; Preserve position within the source code.
-    (should
-     (equal
-      "1)"
-      (org-test-with-temp-text-in-file
-          "* H\n#+begin_src emacs-lisp\n(+ 1 1)\n#+end_src"
-	(let ((file (buffer-file-name)))
-          (org-test-with-temp-text
-              (format ";; [[file:%s][H:1]]\n(+ 1 <point>1)\n;; H:1 ends here\n"
-                      (file-name-nondirectory file))
-            (org-babel-tangle-jump-to-org)
-            (buffer-substring-no-properties (point) (line-end-position)))))))
     ;; Blocks before first heading.
     (should
      (equal
       "Buffer start\n#+begin_src emacs-lisp\n1\n#+end_src\n* H"
       (org-test-with-temp-text-in-file
           "Buffer start\n#+begin_src emacs-lisp\n1\n#+end_src\n* H"
+        (org-mode)
 	(let ((file (buffer-file-name)))
           (org-test-with-temp-text
               (format ";; [[file:%s][H:1]]\n<point>1\n;; H:1 ends here\n"
                       (file-name-nondirectory file))
+            (emacs-lisp-mode)
             (org-babel-tangle-jump-to-org)
             (buffer-string))))))
     ;; Special case: buffer starts with a source block.
@@ -379,12 +373,401 @@ another block
       "#+begin_src emacs-lisp\n1\n#+end_src\n* H"
       (org-test-with-temp-text-in-file
           "#+begin_src emacs-lisp\n1\n#+end_src\n* H"
+        (org-mode)
 	(let ((file (buffer-file-name)))
           (org-test-with-temp-text
               (format ";; [[file:%s][H:1]]\n<point>1\n;; H:1 ends here\n"
                       (file-name-nondirectory file))
+            (emacs-lisp-mode)
             (org-babel-tangle-jump-to-org)
-            (buffer-string))))))))
+            (buffer-string))))))
+    ;; Noweb: single reference
+    (should
+     (equal
+      "* H
+#+name: inner
+#+begin_src emacs-lisp
+1
+#+end_src
+
+#+header: :comments noweb :noweb yes
+#+begin_src emacs-lisp
+prefix<<inner>>
+#+end_src"
+      (org-test-with-temp-text-in-file
+          "* H
+#+name: inner
+#+begin_src emacs-lisp
+1
+#+end_src
+
+#+header: :comments noweb :noweb yes
+#+begin_src emacs-lisp
+prefix<<inner>>
+#+end_src"
+        (org-mode)
+        (let ((file (buffer-file-name)))
+          (org-test-with-temp-text
+              (format "prefix;; [[file:%s::inner][inner]]
+prefix1<point>
+prefix;; inner ends here"
+                      file file)
+            (emacs-lisp-mode)
+            (org-babel-tangle-jump-to-org)
+            (buffer-string))))))
+    ;; Noweb: noweb-ref
+    (should
+     (org-test-with-temp-text-in-file
+         "* H
+#+begin_src emacs-lisp :noweb-ref ints
+1
+#+end_src
+#+begin_src emacs-lisp :noweb-ref ints
+2
+#+end_src
+#+begin_src emacs-lisp :noweb yes
+<<ints>>
+#+end_src"
+       (org-mode)
+       (let ((file (buffer-file-name)))
+     (and
+      (equal "1"
+             (org-test-with-temp-text
+                 (format ";; [[file:%s::*H][*H:3]]
+;; [[file:%s::*H][*H:1]]
+1<point>
+;; *H:1 ends here
+;; [[file:%s::*H][*H:2]]
+2
+;; *H:2 ends here
+;; *H:3 ends here" file file file)
+               (emacs-lisp-mode)
+               (org-babel-tangle-jump-to-org)
+               (buffer-substring (line-beginning-position)
+                                 (line-end-position))))
+      (equal "2"
+             (org-test-with-temp-text
+                 (format ";; [[file:%s::*H][*H:3]]
+;; [[file:%s::*H][*H:1]]
+1
+;; *H:1 ends here
+;; [[file:%s::*H][*H:2]]
+2<point>
+;; *H:2 ends here
+;; *H:3 ends here" file file file)
+               (emacs-lisp-mode)
+               (org-babel-tangle-jump-to-org)
+               (buffer-substring (line-beginning-position)
+                                 (line-end-position))))))))
+    ;; Noweb: able to jump back to outer block when inner ones are not jumpable.
+    (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
+"
+   (org-mode)
+   (let ((file (buffer-file-name)))
+     (equal "<<inner>>"
+            (org-test-with-temp-text
+             (format
+              ";; [[file:%s::*H][*H:4]]
+;; [[file:%s::inner][inner]]
+0
+;; inner ends here
+
+;; [[][myid]]
+Something important.
+;; myid ends here
+
+;; [[file:%s::#myid][*H:2]]
+1
+;; *H:2 ends here
+
+;; [[][lib-blk]]
+2
+;; lib-blk ends here
+
+;; [[][comp]]
+3
+;; comp ends here
+;; <point>*H:4 ends here
+" file file file)
+             (emacs-lisp-mode)
+             (org-babel-tangle-jump-to-org)
+             (buffer-substring (line-beginning-position)
+                               (line-end-position)))))))
+    ;; Noweb: complex.
+    (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
+"
+      (org-mode)
+      (let ((file (buffer-file-name)))
+        (and
+         (equal "<<inner>>"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+;; [[file:%s::inner][inner]]
+0
+;; inner ends here
+
+;; [[][myid]]
+Something important.
+;; myid ends here
+
+;; [[file:%s::#myid][*H:2]]
+1
+;; *H:2 ends here
+
+;; [[][lib-blk]]
+2
+;; lib-blk ends here
+
+;; [[][comp]]
+3
+;; comp ends here
+;; <point>*H:4 ends here
+" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position))))
+         (equal "1"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+;; [[file:%s::inner][inner]]
+0
+;; inner ends here
+
+;; [[][myid]]
+Something important.
+;; myid ends here
+
+;; [[file:%s::#myid][*H:2]]
+1
+;; <point>*H:2 ends here
+
+;; [[][lib-blk]]
+2
+;; lib-blk ends here
+
+;; [[][comp]]
+3
+;; comp ends here
+;; *H:4 ends here
+" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position))))
+         (equal "0"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+;; [[file:%s::inner][inner]]
+0
+;; <point>inner ends here
+
+;; [[][myid]]
+Something important.
+;; myid ends here
+
+;; [[file:%s::#myid][*H:2]]
+1
+;; *H:2 ends here
+
+;; [[][lib-blk]]
+2
+;; lib-blk ends here
+
+;; [[][comp]]
+3
+;; comp ends here
+;; *H:4 ends here
+" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position)))))))
+    ;; Noweb: complex with Noweb prefixes
+    (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 :tangle test.el
+prefix1<<inner>>suffix1
+---
+prefix2<<myid>>suffix2
+---
+prefix3<<inners>>suffix3
+---
+prefix4<<lib-blk>>suffix4
+---
+prefix5<<comp()>>suffix5
+#+end_src
+"
+      (org-mode)
+      (let ((file (buffer-file-name)))
+        (and
+         (equal "prefix1<<inner>>suffix1"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+prefix1;; [[file:%s::inner][inner]]
+prefix10
+prefix1;; inner ends here
+suffix1
+---
+prefix2;; [[][myid]]
+prefix2Something important.
+prefix2;; myid ends here
+suffix2
+---
+prefix3;; [[file:%s::#myid][*H:2]]
+prefix31
+prefix3;; *H:2 ends here
+suffix3
+---
+prefix4;; [[][lib-blk]]
+prefix42
+prefix4;; lib-blk ends here
+suffix4
+---
+prefix5;; [[][comp]]
+prefix53
+prefix5;; comp ends here
+suffix5<point>
+;; *H:4 ends here" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position))))
+         (equal "1"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+prefix1;; [[file:%s::inner][inner]]
+prefix10
+prefix1;; inner ends here
+suffix1
+---
+prefix2;; [[][myid]]
+prefix2Something important.
+prefix2;; myid ends here
+suffix2
+---
+prefix3;; [[file:%s::#myid][*H:2]]
+prefix31<point>
+prefix3;; *H:2 ends here
+suffix3
+---
+prefix4;; [[][lib-blk]]
+prefix42
+prefix4;; lib-blk ends here
+suffix4
+---
+prefix5;; [[][comp]]
+prefix53
+prefix5;; comp ends here
+suffix5
+;; *H:4 ends here" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position))))
+         (equal "0"
+                (org-test-with-temp-text
+                    (format
+                     ";; [[file:%s::*H][*H:4]]
+prefix1;; [[file:%s::inner][inner]]
+prefix10<point>
+prefix1;; inner ends here
+suffix1
+---
+prefix2;; [[][myid]]
+prefix2Something important.
+prefix2;; myid ends here
+suffix2
+---
+prefix3;; [[file:%s::#myid][*H:2]]
+prefix31
+prefix3;; *H:2 ends here
+suffix3
+---
+prefix4;; [[][lib-blk]]
+prefix42
+prefix4;; lib-blk ends here
+suffix4
+---
+prefix5;; [[][comp]]
+prefix53
+prefix5;; comp ends here
+suffix5
+;; *H:4 ends here" file file file)
+                  (emacs-lisp-mode)
+                  (org-babel-tangle-jump-to-org)
+                  (buffer-substring (line-beginning-position)
+                                    (line-end-position)))))))))
 
 (ert-deftest ob-tangle/nested-block ()
   "Test tangling of org file with nested block."
@@ -638,21 +1021,21 @@ another block
     (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/collect-blocks ()
   "Test block collection into groups for tangling."
-- 
2.39.5 (Apple Git-154)

