Fix link search

* lisp/org.el (org-open-link-marker): Remove variable.
(org-open-at-point): Apply removal.
(org-link-search): Sanitize function.  Fix issue with internal link
encoding.
(org-search-not-self): Remove function.

* lisp/ox.el (org-export--inclusion-absolute-lines): Ensure INCLUDE
  keywords with locations can find named elements and targets.

* testing/lisp/test-org.el (test-org/return):
(test-org/coderef):
(test-org/custom-id): Update tests.
(test-org/fuzzy-links): Add tests.

Reported-by: Ivanov Dmitry <usr345@gmail.com>
<http://permalink.gmane.org/gmane.emacs.orgmode/99340>
This commit is contained in:
Nicolas Goaziou 2015-08-03 01:06:32 +02:00
parent 8094d01a68
commit cfe5bc97f8
4 changed files with 220 additions and 204 deletions

View File

@ -10603,9 +10603,6 @@ See the docstring of `org-open-file' for details."
"The window configuration before following a link.
This is saved in case the need arises to restore it.")
(defvar org-open-link-marker (make-marker)
"Marker pointing to the location where `org-open-at-point' was called.")
;;;###autoload
(defun org-open-at-point-global ()
"Follow a link like Org-mode does.
@ -10670,7 +10667,6 @@ link in a property drawer line."
;; On a code block, open block's results.
(unless (call-interactively 'org-babel-open-src-block-result)
(org-load-modules-maybe)
(move-marker org-open-link-marker (point))
(setq org-window-config-before-follow-link (current-window-configuration))
(org-remove-occur-highlights nil nil t)
(unless (run-hook-with-args-until-success 'org-open-at-point-functions)
@ -10858,7 +10854,6 @@ link in a property drawer line."
(line-beginning-position)))))))
(org-footnote-action))
(t (user-error "No link found")))))
(move-marker org-open-link-marker nil)
(run-hook-with-args 'org-follow-link-hook)))
(defun org-offer-links-in-entry (buffer marker &optional nth zero)
@ -10977,170 +10972,178 @@ the window configuration before `org-open-at-point' was called using:
(set-window-configuration org-window-config-before-follow-link)")
(defun org-link-search (s &optional type avoid-pos stealth)
"Search for a link search option.
If S is surrounded by forward slashes, it is interpreted as a
regular expression. In org-mode files, this will create an `org-occur'
sparse tree. In ordinary files, `occur' will be used to list matches.
If the current buffer is in `dired-mode', grep will be used to search
in all files. If AVOID-POS is given, ignore matches near that position.
"Search for a search string S.
If S starts with \"#\", it triggers a custom ID search.
If S is enclosed within parenthesis, it initiates a coderef
search.
If S is surrounded by forward slashes, it is interpreted as
a regular expression. In Org mode files, this will create an
`org-occur' sparse tree. In ordinary files, `occur' will be used
to list matches. If the current buffer is in `dired-mode', grep
will be used to search in all files.
When optional argument TYPE is `org-occur', call this function on
a regexp matching S. If it is `occur', call Emacs' `occur'
function instead.
When AVOID-POS is given, ignore matches near that position.
When optional argument STEALTH is non-nil, do not modify
visibility around point, thus ignoring `org-show-context-detail'
variable."
(let ((case-fold-search t)
(s0 (mapconcat 'identity (org-split-string s "[ \t\r\n]+") " "))
(markers (concat "\\(?:" (mapconcat (lambda (x) (regexp-quote (car x)))
(append '(("") (" ") ("\t") ("\n"))
org-emphasis-alist)
"\\|") "\\)"))
(pos (point))
words re0 re2 re4_ re4 re5 re2a re2a_ reall)
variable.
Search is case-insensitive and ignores white spaces."
(unless (org-string-nw-p s) (error "Invalid search string \"%s\"" s))
(let* ((case-fold-search t)
(origin (point))
(normalized (replace-regexp-in-string "\n[ \t]*" " " s))
(words (org-split-string s "[ \t\n]+"))
(s-multi-re (mapconcat #'regexp-quote words "[ \t]+\\(?:\n[ \t]*\\)?"))
(s-single-re (mapconcat #'regexp-quote words "[ \t]+")))
(cond
;; First check if there are any special search functions
;; Check if there are any special search functions.
((run-hook-with-args-until-success 'org-execute-file-search-functions s))
;; Now try the builtin stuff
((and (eq (aref s0 0) ?#)
(> (length s0) 1)
(let ((match (org-find-property "CUSTOM_ID" (substring s0 1))))
(and match (goto-char match) (setf type 'dedicated)))))
((save-excursion
((eq (string-to-char s) ?#)
;; Look for a custom ID S if S starts with "#".
(let* ((id (substring normalized 1))
(match (org-find-property "CUSTOM_ID" id)))
(if match (progn (goto-char match) (setf type 'dedicated))
(error "No match for custom ID: %s" id))))
((string-match "\\`(\\(.*\\))\\'" normalized)
;; Look for coderef targets if S is enclosed within parenthesis.
(let ((coderef (match-string-no-properties 1 normalized))
(re (substring s-single-re 1 -1)))
(goto-char (point-min))
(and
(re-search-forward
(concat "<<" (regexp-quote s0) ">>") nil t)
(setq type 'dedicated
pos (match-beginning 0))))
;; There is an exact target for this
(goto-char pos))
((save-excursion
(goto-char (point-min))
(and
(re-search-forward
(format "^[ \t]*#\\+NAME: %s" (regexp-quote s0)) nil t)
(setq type 'dedicated pos (match-beginning 0))))
;; Found an element with a matching #+name affiliated keyword.
(goto-char pos))
((and (string-match "^(\\(.*\\))$" s0)
(save-excursion
(goto-char (point-min))
(and
(re-search-forward
(concat "[^[]" (regexp-quote
(format org-coderef-label-format
(match-string 1 s0))))
nil t)
(setq type 'dedicated
pos (1+ (match-beginning 0))))))
;; There is a coderef target for this
(goto-char pos))
((string-match "^/\\(.*\\)/$" s)
;; A regular expression
(catch :coderef-match
(while (re-search-forward re nil t)
(let ((element (org-element-at-point)))
(when (and (memq (org-element-type element)
'(example-block src-block))
;; Build proper regexp according to current
;; block's label format.
(let ((label-fmt
(regexp-quote
(or (org-element-property :label-fmt element)
org-coderef-label-format))))
(save-excursion
(beginning-of-line)
(looking-at (format ".*?\\(%s\\)[ \t]*$"
(format label-fmt coderef))))))
(setq type 'dedicated)
(goto-char (match-beginning 1))
(throw :coderef-match nil))))
(goto-char origin)
(error "No match for coderef: %s" coderef))))
((string-match "\\`/\\(.*\\)/\\'" normalized)
;; Look for a regular expression.
(cond
((derived-mode-p 'org-mode)
(org-occur (match-string 1 s)))
(t (org-do-occur (match-string 1 s)))))
((and (derived-mode-p 'org-mode)
(or (and (equal (string-to-char s) ?*) (setq s (substring s 1)))
org-link-search-must-match-exact-headline))
;; Headline search.
(goto-char (point-min))
(cond
((let (case-fold-search)
(re-search-forward
(let* ((wspace "[ \t]")
(wspaceopt (concat wspace "*"))
(cookie (concat "\\(?:"
wspaceopt
"\\[[0-9]*\\(?:%\\|/[0-9]*\\)\\]"
wspaceopt
"\\)"))
(sep (concat "\\(?:" wspace "+\\|" cookie "+\\)")))
(concat
org-outline-regexp-bol
"\\(?:" org-todo-regexp "[ \t]+\\)?"
"\\(?:\\[#.\\][ \t]+\\)?"
"\\(?:" org-comment-string "[ \t]+\\)?"
sep "*"
(mapconcat #'identity
(org-split-string (regexp-quote s))
(concat sep "+"))
sep "*"
(org-re "\\(?:[ \t]+:[[:alnum:]_@#%%:]+:\\)?")
"[ \t]*$"))
nil t))
;; OK, found a match
(setq type 'dedicated)
(goto-char (match-beginning 0)))
((and (not org-link-search-inhibit-query)
(eq org-link-search-must-match-exact-headline 'query-to-create)
(y-or-n-p "No match - create this as a new heading? "))
(goto-char (point-max))
(unless (bolp) (newline))
(org-insert-heading nil t t)
(insert s "\n")
(beginning-of-line 0))
(t
(goto-char pos)
(error "No match"))))
;; Fuzzy links.
(t
;; A normal search string
(remove-text-properties
0 (length s)
'(face nil mouse-face nil keymap nil fontified nil) s)
;; Make a series of regular expressions to find a match
(setq words (org-split-string s "[ \n\r\t]+")
re0 (concat "\\(<<" (regexp-quote s0) ">>\\)")
re2 (concat markers "\\(" (mapconcat 'downcase words "[ \t]+")
"\\)" markers)
re2a_ (concat "\\(" (mapconcat 'downcase words
"[ \t\r\n]+") "\\)[ \t\r\n]")
re2a (concat "[ \t\r\n]" re2a_)
re4_ (concat "\\(" (mapconcat 'downcase words
"[^a-zA-Z_\r\n]+") "\\)[^a-zA-Z_]")
re4 (concat "[^a-zA-Z_]" re4_)
re5 (concat ".*" re4)
reall (concat "\\(" re0 "\\)\\|\\(" re2 "\\)\\|\\(" re4
"\\)\\|\\(" re5 "\\)"))
(cond
((eq type 'org-occur) (org-occur reall))
((eq type 'occur) (org-do-occur (downcase reall) 'cleanup))
(t (goto-char (point-min))
(setq type 'fuzzy)
(if (or (and (org-search-not-self 1 re0 nil t)
(setq type 'dedicated))
(org-search-not-self 1 re2 nil t)
(org-search-not-self 1 re2a nil t)
(org-search-not-self 1 re4 nil t)
(org-search-not-self 1 re5 nil t))
(goto-char (match-beginning 1))
(goto-char pos)
(error "No match"))))))
(and (derived-mode-p 'org-mode)
(not stealth)
(org-show-context 'link-search))
(let* ((starred (eq (string-to-char normalized) ?*))
(headline-search (and (derived-mode-p 'org-mode)
(or org-link-search-must-match-exact-headline
starred))))
(cond
;; Look for targets, only if not in a headline search.
((and (not headline-search)
(let ((target (format "<<%s>>" s-multi-re)))
(catch :target-match
(goto-char (point-min))
(while (re-search-forward target nil t)
(backward-char)
(let ((context (org-element-context)))
(when (eq (org-element-type context) 'target)
(setq type 'dedicated)
(goto-char (org-element-property :begin context))
(throw :target-match t))))
nil))))
;; Look for elements named after S, only if not in a headline
;; search.
((and (not headline-search)
(let ((name (format "^[ \t]*#\\+NAME: +%s[ \t]*$" s-single-re)))
(catch :name-match
(goto-char (point-min))
(while (re-search-forward name nil t)
(let ((element (org-element-at-point)))
(when (equal (org-split-string
(org-element-property :name element)
"[ \t]+")
words)
(setq type 'dedicated)
(beginning-of-line)
(throw :name-match t))))
nil))))
;; Regular text search. Prefer headlines in Org mode
;; buffers.
((and (derived-mode-p 'org-mode)
(let* ((wspace "[ \t]")
(wspaceopt (concat wspace "*"))
(cookie (concat "\\(?:"
wspaceopt
"\\[[0-9]*\\(?:%\\|/[0-9]*\\)\\]"
wspaceopt
"\\)"))
(sep (concat "\\(?:" wspace "+\\|" cookie "+\\)"))
(re (concat
org-outline-regexp-bol
"\\(?:" org-todo-regexp "[ \t]+\\)?"
"\\(?:\\[#.\\][ \t]+\\)?"
"\\(?:" org-comment-string "[ \t]+\\)?"
sep "*"
(let ((title (mapconcat #'regexp-quote
words
(concat sep "+"))))
(if starred (substring title 1) title))
sep "*"
(org-re "\\(?:[ \t]+:[[:alnum:]_@#%%:]+:\\)?")
"[ \t]*$")))
(goto-char (point-min))
(re-search-forward re nil t)))
(goto-char (match-beginning 0))
(setq type 'dedicated))
;; Offer to create non-existent headline depending on
;; `org-link-search-must-match-exact-headline'.
((and headline-search
(not org-link-search-inhibit-query)
(eq org-link-search-must-match-exact-headline 'query-to-create)
(yes-or-no-p "No match - create this as a new heading? "))
(goto-char (point-max))
(unless (bolp) (newline))
(org-insert-heading nil t t)
(insert s "\n")
(beginning-of-line 0))
;; Only headlines are looked after. No need to process
;; further: throw an error.
(headline-search
(goto-char origin)
(error "No match for fuzzy expression: %s" normalized))
;; Regular text search or occur, depending on TYPE.
((eq type 'org-occur) (org-occur s-multi-re))
((eq type 'occur) (org-do-occur s-multi-re 'cleanup))
((catch :fuzzy-match
(goto-char (point-min))
(while (re-search-forward s-multi-re nil t)
(unless (and avoid-pos
(>= (match-beginning 0) avoid-pos)
(< (match-end 0) avoid-pos))
(goto-char (match-beginning 0))
(setq type 'fuzzy)
(throw :fuzzy-match t)))
nil))
;; All failed. Throw an error.
(t (goto-char origin)
(error "No match for fuzzy expression: %s" normalized))))))
;; Disclose surroundings of match, if appropriate.
(when (and (derived-mode-p 'org-mode) (not stealth))
(org-show-context 'link-search))
type))
(defun org-search-not-self (group &rest args)
"Execute `re-search-forward', but only accept matches that do not
enclose the position of `org-open-link-marker'."
(let ((m org-open-link-marker))
(catch 'exit
(while (apply 're-search-forward args)
(unless (get-text-property (match-end group) 'intangible) ; Emacs 21
(goto-char (match-end group))
(if (and (or (not (eq (marker-buffer m) (current-buffer)))
(> (match-beginning 0) (marker-position m))
(< (match-end 0) (marker-position m)))
(save-match-data
(or (not (org-in-regexp
org-bracket-link-analytic-regexp 1))
(not (match-end 4)) ; no description
(and (<= (match-beginning 4) (point))
(>= (match-end 4) (point))))))
(throw 'exit (point))))))))
(defun org-get-buffer-for-internal-link (buffer)
"Return a buffer to be used for displaying the link target of internal links."
(cond

View File

@ -3297,7 +3297,7 @@ Return a string of lines to be included in the format expected by
(let ((org-inhibit-startup t)) (org-mode)))
(condition-case err
;; Enforce consistent search.
(let ((org-link-search-must-match-exact-headline t))
(let ((org-link-search-must-match-exact-headline nil))
(org-link-search location))
(error
(error "%s for %s::%s" (error-message-string err) file location)))

View File

@ -886,7 +886,8 @@
;; is non-nil.
(should
(org-test-with-temp-text "Link [[target<point>]] <<target>>"
(let ((org-return-follows-link t)) (org-return))
(let ((org-return-follows-link t)
(org-link-search-must-match-exact-headline nil)) (org-return))
(org-looking-at-p "<<target>>")))
(should-not
(org-test-with-temp-text "Link [[target<point>]] <<target>>"
@ -1422,10 +1423,18 @@
#+BEGIN_SRC emacs-lisp
\(+ 1 1) (ref:sc)
#+END_SRC
\[[(sc)]]"
(goto-char (point-max))
\[[(sc)]]<point>"
(org-open-at-point)
(looking-at "(ref:sc)"))))
(looking-at "(ref:sc)")))
;; Find coderef even with alternate label format.
(should
(org-test-with-temp-text "
#+BEGIN_SRC emacs-lisp -l \"{ref:%s}\"
\(+ 1 1) {ref:sc}
#+END_SRC
\[[(sc)]]<point>"
(org-open-at-point)
(looking-at "{ref:sc}"))))
;;;; Custom ID
@ -1433,16 +1442,14 @@
"Test custom ID links specifications."
(should
(org-test-with-temp-text
"* H1\n:PROPERTIES:\n:CUSTOM_ID: custom\n:END:\n* H2\n[[#custom]]"
(goto-char (point-max))
"* H1\n:PROPERTIES:\n:CUSTOM_ID: custom\n:END:\n* H2\n[[#custom]]<point>"
(org-open-at-point)
(org-looking-at-p "\\* H1")))
;; Ignore false positives.
(should-not
;; Throw an error on false positives.
(should-error
(org-test-with-temp-text
"* H1\n:DRAWER:\n:CUSTOM_ID: custom\n:END:\n* H2\n[[#custom]]<point>"
(goto-char (point-max))
(let (org-link-search-must-match-exact-headline) (org-open-at-point))
(org-open-at-point)
(org-looking-at-p "\\* H1"))))
;;;; Fuzzy Links
@ -1454,27 +1461,24 @@
"Test fuzzy links specifications."
;; Fuzzy link goes in priority to a matching target.
(should
(org-test-with-temp-text "#+NAME: Test\n|a|b|\n<<Test>>\n* Test\n[[Test]]"
(goto-line 5)
(org-open-at-point)
(org-test-with-temp-text
"#+NAME: Test\n|a|b|\n<<Test>>\n* Test\n<point>[[Test]]"
(let ((org-link-search-must-match-exact-headline nil)) (org-open-at-point))
(looking-at "<<Test>>")))
;; Then fuzzy link points to an element with a given name.
(should
(org-test-with-temp-text "Test\n#+NAME: Test\n|a|b|\n* Test\n[[Test]]"
(goto-line 5)
(org-open-at-point)
(org-test-with-temp-text "Test\n#+NAME: Test\n|a|b|\n* Test\n<point>[[Test]]"
(let ((org-link-search-must-match-exact-headline nil)) (org-open-at-point))
(looking-at "#\\+NAME: Test")))
;; A target still lead to a matching headline otherwise.
(should
(org-test-with-temp-text "* Head1\n* Head2\n*Head3\n[[Head2]]"
(goto-line 4)
(org-open-at-point)
(org-test-with-temp-text "* Head1\n* Head2\n*Head3\n<point>[[Head2]]"
(let ((org-link-search-must-match-exact-headline nil)) (org-open-at-point))
(looking-at "\\* Head2")))
;; With a leading star in link, enforce heading match.
(should
(org-test-with-temp-text "* Test\n<<Test>>\n[[*Test]]"
(goto-line 3)
(org-open-at-point)
(org-test-with-temp-text "* Test\n<<Test>>\n<point>[[*Test]]"
(let ((org-link-search-must-match-exact-headline nil)) (org-open-at-point))
(looking-at "\\* Test")))
;; With a leading star in link, enforce exact heading match, even
;; with `org-link-search-must-match-exact-headline' set to nil.
@ -1482,7 +1486,16 @@
(org-test-with-temp-text "* Test 1\nFoo Bar\n<point>[[*Test]]"
(let ((org-link-search-must-match-exact-headline nil))
(org-open-at-point))))
;; Heading match should not care about spaces, cookies, todo
;; Handle non-nil `org-link-search-must-match-exact-headline'.
(should
(org-test-with-temp-text "* Test\nFoo Bar\n<point>[[Test]]"
(let ((org-link-search-must-match-exact-headline t)) (org-open-at-point))
(looking-at "\\* Test")))
(should
(org-test-with-temp-text "* Test\nFoo Bar\n<point>[[*Test]]"
(let ((org-link-search-must-match-exact-headline t)) (org-open-at-point))
(looking-at "\\* Test")))
;; Heading match should not care about spaces, cookies, TODO
;; keywords, priorities, and tags.
(should
(let ((first-line

View File

@ -911,40 +911,40 @@ text
(equal
"#+BEGIN_EXAMPLE\nSmall Org file with an include keyword.\n#+END_EXAMPLE\n"
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org\" :lines \"1-2\" EXAMPLE"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
(format "#+INCLUDE: \"%s/examples/include.org\" :lines \"1-2\" EXAMPLE"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Inclusion within a src-block.
(should
(equal
"#+BEGIN_SRC emacs-lisp\n(+ 2 1)\n#+END_SRC\n"
(org-test-with-temp-text
(format
"#+INCLUDE: \"%s/examples/include.org\" :lines \"4-5\" SRC emacs-lisp"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
(format
"#+INCLUDE: \"%s/examples/include.org\" :lines \"4-5\" SRC emacs-lisp"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Inclusion within an html export-block.
(should
(equal
"#+BEGIN_HTML\n<p>HTML!</p>\n#+END_HTML\n"
(org-test-with-temp-text
(format
"#+INCLUDE: \"%s/examples/include.html\" HTML"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
(format
"#+INCLUDE: \"%s/examples/include.html\" HTML"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Inclusion within an center paragraph
(should
(equal
"#+BEGIN_CENTER\nSuccess!\n#+END_CENTER\n"
(org-test-with-temp-text
(format
"#+INCLUDE: \"%s/examples/include2.org\" CENTER"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
(format
"#+INCLUDE: \"%s/examples/include2.org\" CENTER"
org-test-dir)
(org-export-expand-include-keyword)
(buffer-string))))
;; Footnotes labels are local to each included file.
(should
(= 6
@ -1016,15 +1016,15 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
(equal
"body\n"
(org-test-with-temp-text
(concat
(format "#+INCLUDE: \"%s/examples/include.org::*Heading\" " org-test-dir)
":only-contents t")
(concat
(format "#+INCLUDE: \"%s/examples/include.org::*Heading\" " org-test-dir)
":only-contents t")
(org-export-expand-include-keyword)
(buffer-string))))
;; Headings can be included via CUSTOM_ID.
(should
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::#ah\"" org-test-dir)
(format "#+INCLUDE: \"%s/examples/include.org::#ah\"" org-test-dir)
(org-export-expand-include-keyword)
(goto-char (point-min))
(looking-at "* Another heading")))
@ -1039,7 +1039,7 @@ Footnotes[fn:2], foot[fn:test], digit only[3], and [fn:inline:anonymous footnote
;; Including non-existing elements should result in an error.
(should-error
(org-test-with-temp-text
(format "#+INCLUDE: \"%s/examples/include.org::*non-existing heading\"" org-test-dir)
(format "#+INCLUDE: \"%s/examples/include.org::*non-existing heading\"" org-test-dir)
(org-export-expand-include-keyword)))
;; Lines work relatively to an included element.
(should