org-ctags.el (19845B)
1 ;;; org-ctags.el --- Integrate Emacs "tags" Facility with Org -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2007-2023 Free Software Foundation, Inc. 4 5 ;; Author: Paul Sexton <eeeickythump@gmail.com> 6 ;; Keywords: org, wp 7 8 ;; This file is part of GNU Emacs. 9 ;; 10 ;; GNU Emacs is free software: you can redistribute it and/or modify 11 ;; it under the terms of the GNU General Public License as published by 12 ;; the Free Software Foundation, either version 3 of the License, or 13 ;; (at your option) any later version. 14 15 ;; GNU Emacs is distributed in the hope that it will be useful, 16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 ;; GNU General Public License for more details. 19 20 ;; You should have received a copy of the GNU General Public License 21 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 22 23 ;;; Commentary: 24 25 ;; 26 ;; Synopsis 27 ;; ======== 28 ;; 29 ;; Allows Org mode to make use of the Emacs `etags' system. Defines 30 ;; tag destinations in Org files as any text between <<double angled 31 ;; brackets>>. This allows the tags-generation program `exuberant 32 ;; ctags' to parse these files and create tag tables that record where 33 ;; these destinations are found. Plain [[links]] in org mode files 34 ;; which do not have <<matching destinations>> within the same file 35 ;; will then be interpreted as links to these 'tagged' destinations, 36 ;; allowing seamless navigation between multiple Org files. Topics 37 ;; can be created in any org mode file and will always be found by 38 ;; plain links from other files. Other file types recognized by ctags 39 ;; (source code files, latex files, etc) will also be available as 40 ;; destinations for plain links, and similarly, Org links will be 41 ;; available as tags from source files. Finally, the function 42 ;; `org-ctags-find-tag-interactive' lets you choose any known tag, 43 ;; using autocompletion, and quickly jump to it. 44 ;; 45 ;; Installation 46 ;; ============ 47 ;; 48 ;; Download and install Exuberant ctags -- "https://ctags.sourceforge.net/" 49 ;; Edit your .emacs file (see next section) and load emacs. 50 51 ;; To put in your init file (.emacs): 52 ;; ================================== 53 ;; 54 ;; Assuming you already have org mode installed and set up: 55 ;; 56 ;; (setq org-ctags-path-to-ctags "/path/to/ctags/executable") 57 ;; (add-hook 'org-mode-hook 58 ;; (lambda () 59 ;; (define-key org-mode-map "\C-co" 'org-ctags-find-tag-interactive))) 60 ;; 61 ;; By default, with org-ctags loaded, org will first try and visit the tag 62 ;; with the same name as the link; then, if unsuccessful, ask the user if 63 ;; he/she wants to rebuild the 'TAGS' database and try again; then ask if 64 ;; the user wishes to append 'tag' as a new toplevel heading at the end of 65 ;; the buffer; and finally, defer to org's default behavior which is to 66 ;; search the entire text of the current buffer for 'tag'. 67 ;; 68 ;; This behavior can be modified by changing the value of 69 ;; ORG-CTAGS-OPEN-LINK-FUNCTIONS. For example I have the following in my 70 ;; .emacs, which describes the same behavior as the above paragraph with 71 ;; one difference: 72 ;; 73 ;; (setq org-ctags-open-link-functions 74 ;; '(org-ctags-find-tag 75 ;; org-ctags-ask-rebuild-tags-file-then-find-tag 76 ;; org-ctags-ask-append-topic 77 ;; org-ctags-fail-silently)) ; <-- prevents org default behavior 78 ;; 79 ;; 80 ;; Usage 81 ;; ===== 82 ;; 83 ;; When you click on a link "[[foo]]" and org cannot find a matching "<<foo>>" 84 ;; in the current buffer, the tags facility will take over. The file TAGS in 85 ;; the active directory is examined to see if the tags facility knows about 86 ;; "<<foo>>" in any other files. If it does, the matching file will be opened 87 ;; and the cursor will jump to the position of "<<foo>>" in that file. 88 ;; 89 ;; User-visible functions: 90 ;; - `org-ctags-find-tag-interactive': type a tag (plain link) name and visit 91 ;; it. With autocompletion. Bound to ctrl-O in the above setup. 92 ;; - All the etags functions should work. These include: 93 ;; 94 ;; M-. `find-tag' -- finds the tag at point 95 ;; 96 ;; C-M-. find-tag based on regular expression 97 ;; 98 ;; M-x tags-search RET -- like C-M-. but searches through ENTIRE TEXT 99 ;; of ALL the files referenced in the TAGS file. A quick way to 100 ;; search through an entire 'project'. 101 ;; 102 ;; M-* "go back" from a tag jump. Like `org-mark-ring-goto'. 103 ;; You may need to bind this key yourself with (eg) 104 ;; (global-set-key (kbd "<M-kp-multiply>") 'pop-tag-mark) 105 ;; 106 ;; (see etags chapter in Emacs manual for more) 107 ;; 108 ;; 109 ;; Keeping the TAGS file up to date 110 ;; ================================ 111 ;; 112 ;; Tags mode has no way of knowing that you have created new tags by 113 ;; typing in your Org buffer. New tags make it into the TAGS file in 114 ;; 3 ways: 115 ;; 116 ;; 1. You re-run (org-ctags-create-tags "directory") to rebuild the file. 117 ;; 2. You put the function `org-ctags-ask-rebuild-tags-file-then-find-tag' in 118 ;; your `org-open-link-functions' list, as is done in the setup 119 ;; above. This will cause the TAGS file to be rebuilt whenever a link 120 ;; cannot be found. This may be slow with large file collections however. 121 ;; 3. You run the following from the command line (all 1 line): 122 ;; 123 ;; ctags --langdef=orgmode --langmap=orgmode:.org 124 ;; --regex-orgmode="/<<([^>]+)>>/\1/d,definition/" 125 ;; -f /your/path/TAGS -e -R /your/path/*.org 126 ;; 127 ;; If you are paranoid, you might want to run (org-ctags-create-tags 128 ;; "/path/to/org/files") at startup, by including the following toplevel form 129 ;; in .emacs. However this can cause a pause of several seconds if ctags has 130 ;; to scan lots of files. 131 ;; 132 ;; (progn 133 ;; (message "-- rebuilding tags tables...") 134 ;; (mapc 'org-ctags-create-tags tags-table-list)) 135 136 ;;; Code: 137 138 (require 'org-macs) 139 (org-assert-version) 140 141 (eval-when-compile (require 'cl-lib)) 142 (require 'org) 143 144 (defgroup org-ctags nil 145 "Options concerning use of ctags within org mode." 146 :tag "Org-Ctags" 147 :group 'org-link) 148 149 (defvar org-ctags-enabled-p t 150 "Activate ctags support in org mode?") 151 152 (defvar org-ctags-tag-regexp "/<<([^>]+)>>/\\1/d,definition/" 153 "Regexp expression used by ctags external program. 154 The regexp matches tag destinations in Org files. 155 Format is: /REGEXP/TAGNAME/FLAGS,TAGTYPE/ 156 See the ctags documentation for more information.") 157 158 (defcustom org-ctags-path-to-ctags 159 (if (executable-find "ctags-exuberant") "ctags-exuberant" "ctags") 160 "Name of the ctags executable file." 161 :version "24.1" 162 :type 'file) 163 164 (defcustom org-ctags-open-link-functions 165 '(org-ctags-find-tag 166 org-ctags-ask-rebuild-tags-file-then-find-tag 167 org-ctags-ask-append-topic) 168 "List of functions to be prepended to ORG-OPEN-LINK-FUNCTIONS by ORG-CTAGS." 169 :version "24.1" 170 :type 'hook 171 :options '(org-ctags-find-tag 172 org-ctags-ask-rebuild-tags-file-then-find-tag 173 org-ctags-rebuild-tags-file-then-find-tag 174 org-ctags-ask-append-topic 175 org-ctags-append-topic 176 org-ctags-ask-visit-buffer-or-file 177 org-ctags-visit-buffer-or-file 178 org-ctags-fail-silently)) 179 180 181 (defvar org-ctags-tag-list nil 182 "List of all tags in the active TAGS file. 183 Created as a local variable in each buffer.") 184 185 (defcustom org-ctags-new-topic-template 186 "* <<%t>>\n\n\n\n\n\n" 187 "Text to insert when creating a new org file via opening a hyperlink. 188 The following patterns are replaced in the string: 189 `%t' - replaced with the capitalized title of the hyperlink" 190 :version "24.1" 191 :type 'string) 192 193 194 (add-hook 'org-mode-hook 195 (lambda () 196 (when (and org-ctags-enabled-p 197 (buffer-file-name)) 198 ;; Make sure this file's directory is added to default 199 ;; directories in which to search for tags. 200 (let ((tags-filename 201 (expand-file-name 202 (concat (file-name-directory (buffer-file-name)) 203 "/TAGS")))) 204 (when (file-exists-p tags-filename) 205 (visit-tags-table tags-filename)))))) 206 207 208 (advice-add 'visit-tags-table :after #'org--ctags-load-tag-list) 209 (defun org--ctags-load-tag-list (&rest _) 210 (when (and org-ctags-enabled-p tags-file-name) 211 (setq-local org-ctags-tag-list 212 (org-ctags-all-tags-in-current-tags-table)))) 213 214 215 (defun org-ctags-enable () 216 (put 'org-mode 'find-tag-default-function 'org-ctags-find-tag-at-point) 217 (setq org-ctags-enabled-p t) 218 (dolist (fn org-ctags-open-link-functions) 219 (add-hook 'org-open-link-functions fn t))) 220 221 222 ;;; General utility functions. =============================================== 223 ;; These work outside org-ctags mode. 224 225 (defun org-ctags-get-filename-for-tag (tag) 226 "TAG is a string. Search the active TAGS file for a matching tag. 227 If the tag is found, return a list containing the filename, line number, and 228 buffer position where the tag is found." 229 (interactive "sTag: ") 230 (unless tags-file-name 231 (call-interactively #'visit-tags-table)) 232 (save-excursion 233 (visit-tags-table-buffer 'same) 234 (when tags-file-name 235 (with-current-buffer (get-file-buffer tags-file-name) 236 (goto-char (point-min)) 237 (cond 238 ((re-search-forward (format "^.*\^?%s\^A\\([0-9]+\\),\\([0-9]+\\)$" 239 (regexp-quote tag)) nil t) 240 (let ((line (string-to-number (match-string 1))) 241 (pos (string-to-number (match-string 2)))) 242 (cond 243 ((re-search-backward "\n\\(.*\\),[0-9]+\n") 244 (list (match-string 1) line pos)) 245 (t ; can't find a file name preceding the matched 246 ; tag?? 247 (error "Malformed TAGS file: %s" (buffer-name)))))) 248 (t ; tag not found 249 nil)))))) 250 251 252 (defun org-ctags-all-tags-in-current-tags-table () 253 "Read all tags defined in the active TAGS file, into a list of strings. 254 Return the list." 255 (interactive) 256 (let ((taglist nil)) 257 (unless tags-file-name 258 (call-interactively #'visit-tags-table)) 259 (save-excursion 260 (visit-tags-table-buffer 'same) 261 (with-current-buffer (get-file-buffer tags-file-name) 262 (goto-char (point-min)) 263 (while (re-search-forward "^.*\^?\\(.*\\)\^A\\([0-9]+\\),\\([0-9]+\\)$" 264 nil t) 265 (push (substring-no-properties (match-string 1)) taglist))) 266 taglist))) 267 268 269 (defun org-ctags-string-search-and-replace (search replace string) 270 "Replace all instances of SEARCH with REPLACE in STRING." 271 (replace-regexp-in-string (regexp-quote search) replace string t t)) 272 273 274 ;;; Internal functions ======================================================= 275 276 277 (defun org-ctags-open-file (name &optional title) 278 "Visit or create a file called `NAME.org', and insert a new topic. 279 The new topic will be titled NAME (or TITLE if supplied)." 280 (interactive "sFile name: ") 281 (condition-case v 282 (progn 283 (org-open-file name t) 284 (message "Opened file OK") 285 (goto-char (point-max)) 286 (insert (org-ctags-string-search-and-replace 287 "%t" (capitalize (or title name)) 288 org-ctags-new-topic-template)) 289 (message "Inserted new file text OK") 290 (org-mode-restart)) 291 (error (error "Error %S in org-ctags-open-file" v)))) 292 293 294 ;;;; Misc interoperability with etags system ================================= 295 296 297 (advice-add 'xref-find-definitions :before 298 #'org--ctags-set-org-mark-before-finding-tag) 299 (defun org--ctags-set-org-mark-before-finding-tag (&rest _) 300 "Before trying to find a tag, save our current position on org mark ring." 301 (save-excursion 302 (when (and (derived-mode-p 'org-mode) org-ctags-enabled-p) 303 (org-mark-ring-push)))) 304 305 306 307 (defun org-ctags-find-tag-at-point () 308 "Determine default tag to search for, based on text at point. 309 If there is no plausible default, return nil." 310 (let (from to bound) 311 (when (or (ignore-errors 312 ;; Look for hyperlink around `point'. 313 (save-excursion 314 (search-backward "[[") (setq from (+ 2 (point)))) 315 (save-excursion 316 (goto-char from) 317 (search-forward "]") (setq to (- (point) 1))) 318 (and (> to from) (>= (point) from) (<= (point) to))) 319 (progn 320 ;; Look at text around `point'. 321 (save-excursion 322 (skip-syntax-backward "w_") (setq from (point))) 323 (save-excursion 324 (skip-syntax-forward "w_") (setq to (point))) 325 (> to from)) 326 ;; Look between `line-beginning-position' and `point'. 327 (save-excursion 328 (and (setq bound (line-beginning-position)) 329 (skip-syntax-backward "^w_" bound) 330 (> (setq to (point)) bound) 331 (skip-syntax-backward "w_") 332 (setq from (point)))) 333 ;; Look between `point' and `line-end-position'. 334 (save-excursion 335 (and (setq bound (line-end-position)) 336 (skip-syntax-forward "^w_" bound) 337 (< (setq from (point)) bound) 338 (skip-syntax-forward "w_") 339 (setq to (point))))) 340 (buffer-substring-no-properties from to)))) 341 342 343 ;;; Functions for use with 'org-open-link-functions' hook ================= 344 345 346 (defun org-ctags-find-tag (name) 347 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 348 Look for a tag called `NAME' in the current TAGS table. If it is found, 349 visit the file and location where the tag is found." 350 (interactive "sTag: ") 351 (let ((old-buf (current-buffer)) 352 (old-pnt (point-marker)) 353 (old-mark (copy-marker (mark-marker)))) 354 (condition-case nil 355 (progn (xref-find-definitions name) 356 t) 357 (error 358 ;; only restore old location if find-tag raises error 359 (set-buffer old-buf) 360 (goto-char old-pnt) 361 (set-marker (mark-marker) old-mark) 362 nil)))) 363 364 365 (defun org-ctags-visit-buffer-or-file (name &optional create) 366 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 367 Visit buffer named `NAME.org'. If there is no such buffer, visit the file 368 with the same name if it exists. If the file does not exist, then behavior 369 depends on the value of CREATE. 370 371 If CREATE is nil (default), then return nil. Do not create a new file. 372 If CREATE is t, create the new file and visit it. 373 If CREATE is the symbol `ask', then ask the user if they wish to create 374 the new file." 375 (interactive) 376 (let ((filename (concat (substitute-in-file-name 377 (expand-file-name name)) 378 ".org"))) 379 (cond 380 ((get-buffer (concat name ".org")) 381 ;; Buffer is already open 382 (pop-to-buffer-same-window (get-buffer (concat name ".org")))) 383 ((file-exists-p filename) 384 ;; File exists but is not open --> open it 385 (message "Opening existing org file `%S'..." 386 filename) 387 (org-open-file filename t)) 388 ((or (eql create t) 389 (and (eql create 'ask) 390 (y-or-n-p (format-message 391 "File `%s.org' not found; create?" name)))) 392 (org-ctags-open-file filename name)) 393 (t ;; File does not exist, and we don't want to create it. 394 nil)))) 395 396 397 (defun org-ctags-ask-visit-buffer-or-file (name) 398 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 399 Wrapper for org-ctags-visit-buffer-or-file, which ensures the user is 400 asked before creating a new file." 401 (org-ctags-visit-buffer-or-file name 'ask)) 402 403 404 (defun org-ctags-append-topic (name &optional narrowp) 405 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 406 Append a new toplevel heading to the end of the current buffer. The 407 heading contains NAME surrounded by <<angular brackets>>, thus making 408 the heading a destination for the tag `NAME'." 409 (interactive "sTopic: ") 410 (widen) 411 (goto-char (point-max)) 412 (newline 2) 413 (message "Adding topic in buffer %s" (buffer-name)) 414 (insert (org-ctags-string-search-and-replace 415 "%t" (capitalize name) org-ctags-new-topic-template)) 416 (backward-char 4) 417 (end-of-line) 418 (forward-line 2) 419 (when narrowp 420 ;;(org-tree-to-indirect-buffer 1) ;; opens new frame 421 (org-narrow-to-subtree)) 422 t) 423 424 425 (defun org-ctags-ask-append-topic (name &optional narrowp) 426 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 427 Wrapper for org-ctags-append-topic, which first asks the user if they want 428 to append a new topic." 429 (if (y-or-n-p (format-message 430 "Topic `%s' not found; append to end of buffer?" name)) 431 (org-ctags-append-topic name narrowp) 432 nil)) 433 434 435 (defun org-ctags-rebuild-tags-file-then-find-tag (name) 436 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 437 Like ORG-CTAGS-FIND-TAG, but calls the external ctags program first, 438 to rebuild (update) the TAGS file." 439 (unless tags-file-name 440 (call-interactively #'visit-tags-table)) 441 (when (buffer-file-name) 442 (org-ctags-create-tags)) 443 (org-ctags-find-tag name)) 444 445 446 (defun org-ctags-ask-rebuild-tags-file-then-find-tag (name) 447 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 448 Wrapper for org-ctags-rebuild-tags-file-then-find-tag." 449 (if (and (buffer-file-name) 450 (y-or-n-p 451 (format-message 452 "Tag `%s' not found. Rebuild table `%s/TAGS' and look again?" 453 name 454 (file-name-directory (buffer-file-name))))) 455 (org-ctags-rebuild-tags-file-then-find-tag name) 456 nil)) 457 458 459 (defun org-ctags-fail-silently (_name) 460 "This function is intended to be used in ORG-OPEN-LINK-FUNCTIONS. 461 Put as the last function in the list if you want to prevent Org's 462 default behavior of free text search." 463 t) 464 465 466 ;;; User-visible functions =================================================== 467 468 469 (defun org-ctags-create-tags (&optional directory-name) 470 "(Re)create tags file in the directory of the active buffer. 471 The file will contain tag definitions for all the files in the 472 directory and its subdirectories which are recognized by ctags. 473 This will include files ending in `.org' as well as most other 474 source files (.C, .H, .EL, .LISP, etc). All the resulting tags 475 end up in one file, called TAGS, located in the directory. This 476 function may take several seconds to finish if the directory or 477 its subdirectories contain large numbers of taggable files." 478 (interactive) 479 (cl-assert (buffer-file-name)) 480 (let ((dir-name (or directory-name 481 (file-name-directory (buffer-file-name)))) 482 (exitcode nil)) 483 (save-excursion 484 (setq exitcode 485 (shell-command 486 (format (concat "%s --langdef=orgmode --langmap=orgmode:.org " 487 "--regex-orgmode=\"%s\" -f \"%s\" -e -R \"%s\"") 488 org-ctags-path-to-ctags 489 org-ctags-tag-regexp 490 (expand-file-name (concat dir-name "/TAGS")) 491 (expand-file-name (concat dir-name "/*"))))) 492 (cond 493 ((eql 0 exitcode) 494 (setq-local org-ctags-tag-list 495 (org-ctags-all-tags-in-current-tags-table))) 496 (t 497 ;; This seems to behave differently on Linux, so just ignore 498 ;; error codes for now 499 ;;(error "Calling ctags executable resulted in error code: %s" 500 ;; exitcode) 501 nil))))) 502 503 504 (defvar org-ctags-find-tag-history nil 505 "History of tags visited by org-ctags-find-tag-interactive.") 506 507 (defun org-ctags-find-tag-interactive () 508 "Prompt for the name of a tag, with autocompletion, then visit the named tag. 509 Uses `ido-mode' if available. 510 If the user enters a string that does not match an existing tag, create 511 a new topic." 512 (interactive) 513 (let* ((tag (ido-completing-read "Topic: " org-ctags-tag-list 514 nil 'confirm nil 'org-ctags-find-tag-history))) 515 (when tag 516 (cond 517 ((member tag org-ctags-tag-list) 518 ;; Existing tag 519 (push tag org-ctags-find-tag-history) 520 (xref-find-definitions tag)) 521 (t 522 ;; New tag 523 (run-hook-with-args-until-success 524 'org-open-link-functions tag)))))) 525 526 527 (org-ctags-enable) 528 529 (provide 'org-ctags) 530 531 ;;; org-ctags.el ends here