├── .gitignore ├── Readme.md └── gtags-mode.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Readme 2 | ====== 3 | 4 | This package provides GNU Global integration with xref, project, 5 | completion-at-point (capf) and imenu in emacs. 6 | 7 | There are some other packages with their own approach and set of more 8 | complete/complex features, maps and functionalities; like ggtags, 9 | gtags.el, gxref, agtags and some others. They are listed with a brief 10 | description in: [related sites](https://www.gnu.org/software/global/links.html). 11 | 12 | This package let all the work to the EMACS tools available for such 13 | functionalities and avoids external dependencies. Unlike other 14 | packages; this module does not create extra special maps, bindings or 15 | menus, but just adds support to the mentioned features to use 16 | gtags/global as a backend when possible. We do special emphasis on 17 | minimalism, simplicity, efficiency and tramp support. 18 | 19 | This package may be extended in the future with new features and to 20 | support other tools, but only if they are required and included in 21 | an emacs distribution and don't need external dependencies. 22 | 23 | Usage 24 | ----- 25 | 26 | Just load and enable the mode: `gtags-mode` or call it in a hook as 27 | usual. The mode is a global-minor-mode. 28 | 29 | With `use-packages` 30 | 31 | ```elisp 32 | (use-package gtags-mode 33 | :hook ((emacs-startup . gtags-mode))) 34 | ``` 35 | or simply 36 | 37 | ```elisp 38 | (unless (fboundp 'gtags-mode) 39 | (autoload #'gtags-mode "gtags-mode" nil t)) 40 | (add-hook 'emacs-startup-hook #'gtags-mode) 41 | ``` 42 | 43 | There are only 3 extra commands that the user may need to know: 44 | 45 | - `gtags-mode` : To enable the global minor mode. 46 | - `gtags-mode-create` : To create a gtags database in case it doesn't exist for the current project. 47 | - `gtags-mode-update` : To manually update an existent database; specially useful if the project has been modified outside emacs. 48 | 49 | Configuration 50 | ------------- 51 | 52 | This packages tries to do its best to not require user configuration. 53 | The package provides a minimal set of configuration options in case 54 | the global/gtags executable are not in the usual locations, so the 55 | user can set them. `gtags-mode-global-executable` 56 | `gtags-mode-gtags-executable` are buffer local configuration options 57 | to set the path or names in case the user needs it or the user setup 58 | is do special that the command `executable-find` fails searching. 59 | 60 | TRAMP users can use 61 | [*connection-local-variable*](https://www.gnu.org/software/emacs/manual/html_node/elisp/Connection-Local-Variables.html) 62 | to set these values per individual hosts or users if needed. 63 | 64 | `gtags-mode-features` is the list of enabled features in 65 | gtags-mode. By default all the features: `(project xref completion 66 | imenu hooks)` are enabled. The user only needs to remove one of these 67 | entries and restart `gtags-mode` to disable it. 68 | TODO: Improve the custom type for this variable in order to restrict 69 | possible values. 70 | 71 | The custom variable `gtags-mode-lighter` can be used to change the 72 | default mode-line message to use when the mode is enabled. 73 | 74 | The verbosity of messages printed can be controlled with 75 | `gtags-mode-verbose-level` higher verbose level implies more 76 | messages and 0 prints no messages at all (not recommended!!). The most 77 | verbose messages are not printed in the echo area, but only in the 78 | `\*Messages\*` buffer. 79 | 80 | It is possible to pass extra arguments to update commands using the 81 | variable `gtags-mode-update-args`. Usually these can be used to use 82 | different `global` backends. For example, to use 83 | [universal-ctags](https://ctags.io/) instead of the default backend, 84 | the Arch Linux configuration is: 85 | 86 | ```lisp 87 | (setq gtags-mode-update-args "--gtagsconf=/usr/share/gtags/gtags.conf --gtagslabel=universal-ctags") 88 | 89 | ``` 90 | 91 | This variable can be set in the 92 | [*.dir-locals*](https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html) 93 | or as a 94 | [*connection-local-variable*](https://www.gnu.org/software/emacs/manual/html_node/elisp/Connection-Local-Variables.html) 95 | 96 | -------------------------------------------------------------------------------- /gtags-mode.el: -------------------------------------------------------------------------------- 1 | ;;; gtags-mode.el --- GNU Global integration with xref, project and imenu. -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2022-2025 Free Software Foundation, Inc. 4 | 5 | ;; Author: Jimmy Aguilar Mena 6 | ;; URL: https://github.com/Ergus/gtags-mode 7 | ;; Keywords: xref, project, imenu, gtags, global 8 | ;; Version: 1.9.4 9 | ;; Package-Requires: ((emacs "28")) 10 | 11 | ;; This program is free software: you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; GNU Global integration with xref, project, completion-at-point 27 | ;; (capf) and imenu. 28 | 29 | ;; There are many other packages with their own approach and set of 30 | ;; more complete/complex features, maps and functionalities; like 31 | ;; ggtags, gtags.el, gxref, agtags and some others referenced in: 32 | ;; https://www.gnu.org/software/global/links.html 33 | 34 | ;; This package let all the work to the EMACS tools available for such 35 | ;; functions and avoids external dependencies. Unlike the other 36 | ;; packages; this module does not create extra special maps, bindings 37 | ;; or menus, but just adds support to the mentioned features to use 38 | ;; gtags/global as a backend when possible. We do special emphasis on 39 | ;; minimalism, simplicity, efficiency and tramp support. 40 | 41 | ;; This package may be extended in the future with new features and to 42 | ;; support other tools, but only if they are required and included in 43 | ;; an EMACS distribution and don't need external dependencies. 44 | 45 | ;;; Code: 46 | 47 | (require 'xref) 48 | (require 'files-x) 49 | (eval-when-compile (require 'subr-x)) 50 | 51 | (defgroup gtags-mode nil 52 | "GNU Global group for xref." 53 | :group 'xref) 54 | 55 | (defcustom gtags-mode-global-executable "global" 56 | "GNU Global executable." 57 | :type 'string 58 | :local t) 59 | 60 | (defcustom gtags-mode-gtags-executable "gtags" 61 | "GNU Gtags executable." 62 | :type 'string 63 | :local t) 64 | 65 | (defcustom gtags-mode-update-args "" 66 | "Extra arguments to use when updating the database. 67 | For example to use universal-ctags you can set this to: 68 | \"--gtagsconf=/usr/share/gtags/gtags.conf --gtagslabel=universal-ctags\"" 69 | :type 'string 70 | :local t) 71 | 72 | (defcustom gtags-mode-lighter " Gtags" 73 | "The text displayed in the mode line." 74 | :type 'string 75 | :risky t) 76 | 77 | (defcustom gtags-mode-features '(project xref completion imenu hooks) 78 | "The list of features enabled in gtags-mode. 79 | This variable must be set before enabling gtags-mode" 80 | :type '(set (const project) 81 | (const xref) 82 | (const completion) 83 | (const imenu) 84 | (const hooks)) 85 | :risky t) 86 | 87 | (defcustom gtags-mode-verbose-level 2 88 | "The text displayed in the mode line." 89 | :type 'natnum 90 | :risky t) 91 | 92 | (defvar gtags-mode--alist nil 93 | "Full list of Global roots. 94 | The address is absolute for remote hosts.") 95 | (put 'gtags-mode--alist 'risky-local-variable t) 96 | 97 | (defvar-local gtags-mode--global (executable-find gtags-mode-global-executable)) 98 | (defvar-local gtags-mode--gtags (executable-find gtags-mode-gtags-executable)) 99 | (defvar-local gtags-mode--plist nil 100 | "Project Global root for this buffer.") 101 | 102 | (defconst gtags-mode--output-format-regex 103 | "^\\(.+?\\) \\([^ ]+\\) \\([[:digit:]]+\\) \\(.*\\)" 104 | "Regex to filter the output with `gtags-mode--output-format-options'.") 105 | 106 | (defconst gtags-mode--output-format-options 107 | '("--result=cscope" "--path-style=relative" "--color=never") 108 | "Command line options to use with `gtags-mode--output-format-regex'.") 109 | 110 | (defsubst gtags-mode--message (level format-string &rest args) 111 | "Print log messages when the `gtags-mode-verbose' is greater than LEVEL. 112 | Message with lower verbose level are more important. Messages with level 113 | lower than 2 are also printed in the echo area as they are considered 114 | warnings or errors." 115 | (when (>= gtags-mode-verbose-level level) 116 | (let ((inhibit-message (> level 1))) 117 | (apply #'message (concat "gtags-mode: " format-string) args)))) 118 | 119 | ;; Connection functions 120 | (defun gtags-mode--set-connection-locals () 121 | "Set connection local variables when possible and needed." 122 | (when-let* ((remote (file-remote-p default-directory)) 123 | ((not (and (local-variable-p 'gtags-mode--global) 124 | (local-variable-p 'gtags-mode--gtags)))) 125 | (criteria (connection-local-criteria-for-default-directory)) 126 | (symvars (intern (format "gtags-mode--%s-vars" remote))) 127 | (enable-connection-local-variables t)) 128 | (unless (alist-get symvars connection-local-profile-alist) 129 | (with-connection-local-variables ;; because *-executable can be set as connection local 130 | (let ((global (if (local-variable-p 'gtags-mode-global-executable) 131 | gtags-mode-global-executable 132 | (file-name-nondirectory gtags-mode-global-executable))) 133 | (gtags (if (local-variable-p 'gtags-mode-gtags-executable) 134 | gtags-mode-gtags-executable 135 | (file-name-nondirectory gtags-mode-gtags-executable)))) 136 | (connection-local-set-profile-variables 137 | symvars 138 | `((gtags-mode--global . ,(executable-find global t)) 139 | (gtags-mode--gtags . ,(executable-find gtags t)))) 140 | (connection-local-set-profiles criteria symvars)))) 141 | (hack-connection-local-variables-apply criteria))) 142 | 143 | (defvar gtags-mode--pr-async nil) 144 | 145 | ;; Async functions 146 | (defun gtags-mode--exec-async-sentinel (process event) 147 | "Sentinel to run when PROCESS emits EVENT. 148 | This is the sentinel set in `gtags-mode--exec-async'." 149 | (if (and (eq (process-status process) 'exit) ;; On success 150 | (eql (process-exit-status process) 0)) 151 | (let ((temp-buffer (process-buffer process)) 152 | (parent-buffer (process-get process :parent-buffer)) 153 | (extra-sentinel (process-get process :extra-sentinel))) 154 | (when (buffer-name temp-buffer) ;; kill temp buffer 155 | (while (accept-process-output process)) 156 | (kill-buffer temp-buffer)) 157 | (when (buffer-live-p parent-buffer) ;; work on parent buffer 158 | (with-current-buffer parent-buffer 159 | (when (functionp extra-sentinel) ;; run extra sentinel 160 | (funcall extra-sentinel)) 161 | (when gtags-mode--plist ;; clear cache 162 | (setq gtags-mode--plist (plist-put gtags-mode--plist :cache nil)))))) 163 | (with-current-buffer (process-buffer process) ;; In failure print error 164 | (while (accept-process-output process)) 165 | (gtags-mode--message 1 "Global async error output:\n%s" 166 | (string-trim 167 | (buffer-substring-no-properties (point-min) (point-max)))))) 168 | (setq gtags-mode--pr-async nil) 169 | (gtags-mode--message 2 "Async %s: result: %s elapsed: %.06f" 170 | (process-get process :command) 171 | (string-trim event) 172 | (float-time (time-subtract (current-time) 173 | (process-get process :start-time))))) ;; Always notify 174 | 175 | (defun gtags-mode--exec-async (cmd &rest args) 176 | "Run CMD with ARGS on TARGET asynchronously. 177 | Start an asynchronous process and sets 178 | `gtags-mode--exec-async-sentinel' as the process sentinel. 179 | Returns the process object." 180 | (if-let* ((cmd (buffer-local-value cmd (current-buffer))) 181 | (command (append `(,cmd) (string-split gtags-mode-update-args) args)) 182 | (start-time (current-time)) 183 | ((if (not gtags-mode--pr-async) 184 | t 185 | (gtags-mode--message 1 "Cannot run command: There is a gtags or global process already running.") 186 | nil)) 187 | (pr (make-process :name (format "%s-async" cmd) 188 | :buffer (generate-new-buffer " *temp*" t) 189 | :command command 190 | :sentinel #'gtags-mode--exec-async-sentinel 191 | :file-handler t))) 192 | (progn 193 | (gtags-mode--message 2 "Starting async process: %s" command) 194 | ;; In future not needed with `remote-commands'. 195 | (set-process-plist pr (list :parent-buffer (current-buffer) 196 | :command command 197 | :start-time start-time)) 198 | (setq gtags-mode--pr-async pr)) 199 | (gtags-mode--message 1 "Can't start async %s subprocess" cmd) 200 | nil)) 201 | 202 | (defun gtags-mode--exec-sync (&rest args) 203 | "Run global with ARGS on TARGET synchronously. 204 | On success return a list of strings or nil if any error occurred." 205 | (if-let* ((cmd gtags-mode--global)) ;; Required for with-temp-buffer 206 | (with-temp-buffer 207 | (let* ((start-time (current-time)) 208 | (status (apply #'process-file cmd nil (current-buffer) nil args)) 209 | (output (string-trim 210 | (buffer-substring-no-properties (point-min) (point-max))))) 211 | (if (eq status 0) 212 | (string-lines output t) 213 | (gtags-mode--message 1 "Global sync error output:\n%s" output) 214 | (gtags-mode--message 1 "Sync %s %s: exited abnormally with code: %s elapsed: %.06f" 215 | cmd args status 216 | (float-time (time-subtract (current-time) start-time))) 217 | nil))) 218 | (gtags-mode--message 1 "Can't start sync %s subprocess" cmd) 219 | nil)) 220 | 221 | (defsubst gtags-mode--get-root (dir) 222 | "Get the top dbpath given DIR. 223 | Includes the remote prefix concatenation when needed." 224 | ;; The first check is an Heuristic to create new plists only when 225 | ;; visiting real files This optimizes when there is not tags file to 226 | ;; avoid calling the external process repeatedly i.e in magit 227 | ;; buffers that are regenerated every time and forgets the local 228 | ;; variables 229 | (when-let* (((and gtags-mode--global buffer-file-name)) 230 | ;; Then suppress any warning when searching the root 231 | ;; because `global' returns an error value when there is 232 | ;; not dbpath found. 233 | (gtags-mode-verbose-level 0) 234 | (default-directory dir) 235 | ;; The check id independent of the concat because when 236 | ;; empty (no root) on remote systems, the concat will 237 | ;; returns always a non nil value. 238 | (root (car (gtags-mode--exec-sync "--print-dbpath")))) 239 | (concat (file-remote-p dir) ;; add remote prefix if remote 240 | (file-name-as-directory root)))) ;; add a / at the end if missing 241 | 242 | ;; Utilities functions (a bit less low level) ======================== 243 | (defun gtags-mode--get-plist (dir) 244 | "Return the plist for DIR from `gtags-mode--alist'." 245 | (seq-find (lambda (plist) 246 | (string-prefix-p (plist-get plist :gtagsroot) dir)) 247 | gtags-mode--alist)) 248 | 249 | (defun gtags-mode--create-plist (dir) 250 | "Return dbpath for DIR or nil if none." 251 | (when-let* ((root (gtags-mode--get-root dir))) 252 | (gtags-mode--message 2 "Gtags file in %s applies to default-directory: %s" root dir) 253 | (or (gtags-mode--get-plist root) ;; already exist 254 | (car (push `(:gtagsroot ,root :cache nil) gtags-mode--alist))))) 255 | 256 | (defun gtags-mode--set-local-plist (dir) 257 | "Set and return the buffer local value of `gtags-mode--plist'." 258 | (let ((default-directory (file-truename dir))) 259 | (gtags-mode--set-connection-locals) 260 | (setq-local gtags-mode--plist 261 | (or (gtags-mode--get-plist default-directory) 262 | (gtags-mode--create-plist default-directory))) 263 | (when (and gtags-mode--plist buffer-file-name) 264 | (setq-local gtags-mode--plist 265 | (plist-put gtags-mode--plist 266 | :true-file-name (file-truename buffer-file-name)))))) 267 | 268 | (defun gtags-mode--local-plist (&optional dir) 269 | "Set and return the buffer local value of `gtags-mode--plist'." 270 | (if (local-variable-p 'gtags-mode--plist) 271 | gtags-mode--plist 272 | (gtags-mode--set-local-plist (or dir default-directory)))) 273 | 274 | (defun gtags-mode--list-completions (prefix) 275 | "Get the list of completions for PREFIX. 276 | When PREFIX is nil or empty; return the entire list of 277 | completions usually from the cache when possible." 278 | (cond ;; TODO: use with-memoization in the future it will be on emacs 29.1 279 | ((not (gtags-mode--local-plist default-directory)) 280 | (error "Calling `gtags-mode--list-completions' with no gtags-mode--plist")) 281 | ((and (stringp prefix) 282 | (not (string-match-p "\\`[ \t\n\r-]*\\'" prefix)) ;; not match empty or only - 283 | (gtags-mode--exec-sync "--directory" 284 | (file-local-name 285 | (plist-get (gtags-mode--local-plist default-directory) :gtagsroot)) 286 | (if completion-ignore-case "--ignore-case" "--match-case") 287 | "--through" "--completion" 288 | (substring-no-properties prefix)))) 289 | ((plist-get gtags-mode--plist :cache)) 290 | (t (setq gtags-mode--plist 291 | (plist-put gtags-mode--plist 292 | :cache (gtags-mode--exec-sync 293 | "--directory" 294 | (file-local-name 295 | (plist-get (gtags-mode--local-plist default-directory) :gtagsroot)) 296 | "--through" "--completion"))) 297 | (plist-get gtags-mode--plist :cache)))) 298 | 299 | (defun gtags-mode--filter-find-symbol (args symbol creator) 300 | "Run `gtags-mode--exec-sync' with ARGS on SYMBOL and filter output with CREATOR. 301 | Returns the results as a list of CREATORS outputs similar to 302 | `mapcar'. Creator should be a function with 4 input arguments: 303 | name, code, file, line." 304 | (if-let* ((root (plist-get (gtags-mode--local-plist default-directory) :gtagsroot))) 305 | (delete nil (mapcar 306 | (lambda (line) 307 | (when (string-match gtags-mode--output-format-regex line) 308 | (funcall creator 309 | (match-string-no-properties 2 line) ;; name 310 | (match-string-no-properties 4 line) ;; code 311 | (concat root (match-string-no-properties 1 line)) ;; file 312 | (string-to-number (match-string-no-properties 3 line))))) ;; line 313 | (apply #'gtags-mode--exec-sync 314 | (append gtags-mode--output-format-options args 315 | `("--directory" ,(file-local-name root) ,symbol))))) 316 | (error "Calling gtags-mode--filter-find-symbol without GTAGSROOT") 317 | nil)) 318 | 319 | (defun gtags-mode--update-buffers-plist () 320 | "Actions to perform after creating a database. 321 | This iterates over the buffers and tries to reset 322 | `gtags-mode--plist' when it is nil." 323 | (dolist (buff (buffer-list)) 324 | (gtags-mode--message 2 "Updating all buffers") 325 | (with-current-buffer buff 326 | (when buffer-file-name 327 | (if (buffer-local-value 'gtags-mode--plist buff) 328 | (setq-local gtags-mode--list-cache-plist nil) ;; cleanup the cache 329 | (gtags-mode--set-connection-locals) 330 | (kill-local-variable 'gtags-mode--plist) ;; kill the local to reset it 331 | (gtags-mode--local-plist default-directory)))))) 332 | 333 | ;; Interactive commands ============================================== 334 | 335 | ;;;###autoload 336 | (defun gtags-mode-create (root-dir) 337 | "Create a GLOBAL GTAGS file in ROOT-DIR asynchronously." 338 | (interactive "DCreate GLOBAL files in directory: ") 339 | (when-let* ((default-directory root-dir) 340 | (pr (gtags-mode--exec-async 'gtags-mode--gtags))) 341 | (process-put pr :extra-sentinel #'gtags-mode--update-buffers-plist))) 342 | 343 | (defun gtags-mode-update () 344 | "Update GLOBAL project database." 345 | (interactive) 346 | (when-let* (((gtags-mode--local-plist default-directory)) 347 | (default-directory (plist-get gtags-mode--plist :gtagsroot)) 348 | (pr (gtags-mode--exec-async 'gtags-mode--global "--update"))) 349 | (process-put pr :extra-sentinel #'gtags-mode--update-buffers-plist))) 350 | 351 | ;; Hooks ============================================================= 352 | (defun gtags-mode--after-save-hook () 353 | "After save hook to update GLOBAL database with changed data. 354 | This function re-checks the local value for gtags-mode--plist or tries 355 | to set it. This is needed when saving new created files because they 356 | won't have `buffer-file-name' but will just acquire one." 357 | (when (and buffer-file-name 358 | (or gtags-mode--plist 359 | (gtags-mode--set-local-plist default-directory))) 360 | (when-let* ((default-directory (plist-get gtags-mode--plist :gtagsroot)) 361 | (true-file-name (plist-get gtags-mode--plist :true-file-name))) 362 | (gtags-mode--exec-async 363 | 'gtags-mode--global "--single-update" (file-relative-name true-file-name))))) 364 | 365 | ;; xref integration ================================================== 366 | (defun gtags-mode--xref-find-symbol (args symbol) 367 | "Run GNU Global to create xref input list with ARGS on SYMBOL. 368 | Return as a list of xref location objects." 369 | (gtags-mode--filter-find-symbol 370 | args symbol 371 | (lambda (_name code file line) 372 | (xref-make code (xref-make-file-location file line 0))))) 373 | 374 | (cl-defmethod xref-backend-identifier-completion-table ((_backend (head :gtagsroot))) 375 | "List all symbols." 376 | (gtags-mode--list-completions nil)) 377 | 378 | (cl-defmethod xref-backend-definitions ((_backend (head :gtagsroot)) symbol) 379 | "List all definitions for SYMBOL." 380 | (gtags-mode--xref-find-symbol '("--definition") symbol)) 381 | 382 | (cl-defmethod xref-backend-references ((_backend (head :gtagsroot)) symbol) 383 | "List all referenced for SYMBOL." 384 | (gtags-mode--xref-find-symbol '("--reference" "--symbol") symbol)) 385 | 386 | (cl-defmethod xref-backend-apropos ((_backend (head :gtagsroot)) symbol) 387 | "List grepped list of candidates SYMBOL." 388 | (gtags-mode--xref-find-symbol '("--grep") symbol)) 389 | 390 | ;; imenu integration ================================================= 391 | (defun gtags-mode--imenu-goto-function (_name line) 392 | "Function to goto with imenu when LINE info." 393 | (funcall-interactively #'goto-line line)) 394 | 395 | (defun gtags-mode--imenu-advice () 396 | "Make imenu use Global." 397 | (when (and buffer-file-name (gtags-mode--local-plist default-directory)) 398 | (gtags-mode--filter-find-symbol 399 | '("--file") (file-name-nondirectory buffer-file-name) 400 | (lambda (name _code _file line) 401 | (list name line #'gtags-mode--imenu-goto-function))))) 402 | 403 | ;; project integration =============================================== 404 | 405 | (cl-defmethod project-root ((project (head :gtagsroot))) 406 | "Root for PROJECT." 407 | (plist-get project :gtagsroot)) 408 | 409 | (cl-defmethod project-files ((project (head :gtagsroot)) &optional dirs) 410 | "List files inside all the PROJECT or in DIRS if specified." 411 | (let* ((default-directory (project-root project)) 412 | (dirs (or (delete-dups dirs) `(,default-directory))) 413 | (results (if project-files-relative-names 414 | (gtags-mode--exec-sync 415 | "--path-style=relative" "--path" 416 | (string-remove-prefix default-directory (car dirs))) 417 | (mapcan 418 | (lambda (dir) 419 | (when (string-prefix-p default-directory dir) 420 | (mapcar (lambda (file) 421 | (expand-file-name file default-directory)) 422 | (gtags-mode--exec-sync 423 | "--path-style=through" "--path" 424 | (string-remove-prefix default-directory dir))))) 425 | dirs)))) 426 | (if (length> dirs 1) (delete-dups results) results))) 427 | 428 | (cl-defmethod project-buffers ((project (head :gtagsroot))) 429 | "Return the list of all live buffers that belong to PROJECT." 430 | (delq nil 431 | (mapcar (lambda (buff) 432 | (cond 433 | ((eq (buffer-local-value 'gtags-mode--plist buff) project) buff) 434 | ((local-variable-p 'gtags-mode--plist buff) nil) 435 | (t (with-current-buffer buff 436 | (when (eq (gtags-mode--local-plist default-directory) project) 437 | (current-buffer)))))) 438 | (buffer-list)))) 439 | 440 | ;; Completion-at-point-function (capf) =============================== 441 | (defun gtags-mode-completion-function () 442 | "Generate completion list." 443 | (if (gtags-mode--local-plist default-directory) 444 | (when-let* ((bounds (bounds-of-thing-at-point 'symbol))) 445 | (list (car bounds) (point) 446 | (completion-table-dynamic #'gtags-mode--list-completions) 447 | :exclusive 'no)))) 448 | 449 | (defmacro gtags-mode--with-feature (feature &rest body) 450 | (declare (indent 1) (debug t)) 451 | `(when (memq ,feature gtags-mode-features) 452 | ,@body)) 453 | 454 | ;;;###autoload 455 | (define-minor-mode gtags-mode 456 | "Use GNU Global as backend for project, xref, capf and imenu. 457 | When the buffer is not in a global-project, then all these tools 458 | rely on their original or user configured default behavior." 459 | :global t 460 | :lighter gtags-mode-lighter 461 | (cond 462 | (gtags-mode 463 | (gtags-mode--with-feature 'project 464 | (add-hook 'project-find-functions #'gtags-mode--local-plist)) 465 | (gtags-mode--with-feature 'xref 466 | (add-hook 'xref-backend-functions #'gtags-mode--local-plist)) 467 | (gtags-mode--with-feature 'completion 468 | (add-hook 'completion-at-point-functions #'gtags-mode-completion-function)) 469 | (gtags-mode--with-feature 'hooks 470 | (add-hook 'after-save-hook #'gtags-mode--after-save-hook)) 471 | (gtags-mode--with-feature 'imenu 472 | (advice-add 'imenu-create-index-function :before-until #'gtags-mode--imenu-advice))) 473 | (t 474 | (remove-hook 'project-find-functions #'gtags-mode--local-plist) 475 | (remove-hook 'xref-backend-functions #'gtags-mode--local-plist) 476 | (remove-hook 'completion-at-point-functions #'gtags-mode-completion-function) 477 | (remove-hook 'after-save-hook #'gtags-mode--after-save-hook) 478 | (advice-remove 'imenu-create-index-function #'gtags-mode--imenu-advice)))) 479 | 480 | (provide 'gtags-mode) 481 | ;;; gtags-mode.el ends here 482 | --------------------------------------------------------------------------------