dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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