-
Notifications
You must be signed in to change notification settings - Fork 27
/
citre-global.el
439 lines (376 loc) · 15.7 KB
/
citre-global.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
;;; citre-global.el --- Finding references using GNU Global in Citre -*- lexical-binding:t -*-
;; Copyright (C) 2021 Hao Wang
;; Author: Hao Wang <amaikinono@gmail.com>
;; Maintainer: Hao Wang <amaikinono@gmail.com>
;; Created: 24 Sep 2021
;; Keywords: convenience, tools
;; Homepage: https://github.com/universal-ctags/citre
;; Version: 0.4.1
;; Package-Requires: ((emacs "26.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; GNU Global backend.
;;; Code:
;; To see the outline of this file, run M-x outline-minor-mode and
;; then press C-c @ C-t. To also show the top-level functions and
;; variable declarations in each section, run M-x occur with the
;; following query: ^;;;;* \|^(
;;;; Libraries
(require 'citre-backend-interface)
;; We only use `citre-get-symbol' in this module.
(require 'citre-tags)
(require 'citre-ui-jump)
(require 'citre-ui-peek)
;;;; User Options
(defcustom citre-gtags-program nil
"The name or path of the gtags program.
Set this if gtags is not in your PATH, or its name is not
\"gtags\"."
:type '(choice (string :tag "Path or name of the gtags program")
(const :tag "gtags" nil))
:group 'citre)
(defcustom citre-global-program nil
"The name or path of the GNU Global program.
Set this if global is not in your PATH, or its name is not
\"global\"."
:type '(choice (string :tag "Path or name of the global program")
(const :tag "global" nil))
:group 'citre)
(defcustom citre-gtags-args '("--compact" "--objdir")
"Arguments for running gtags.
On Windows, the \"--objdir\" argument may cause \"Objdir not
found\" error. If this happens, you need to customize this
option to not use \"--objdir\", and gtags will always create
database in the project directory."
:type '(repeat string)
:group 'citre)
(defcustom citre-global-completion-case-sensitive t
"Case sensitivity of auto-completion using global backend."
:type 'boolean
:group 'citre)
;;;; Global program interface
;;;;; Internals
(defun citre-global--get-output-lines (args)
"Get output from global program.
ARGS is the arguments passed to the program."
(let ((prog (or citre-global-program "global")))
(when (citre-executable-find prog)
(citre-get-output-lines (append (list prog) args)))))
(defun citre-global--get-lines (name &optional mode case-fold start-file)
"Find tags related to NAME using global and return the outputed lines.
If MODE is
- `completion', find tags that are completions to NAME;
- `definition', find tags that are definitions of NAME;
- `reference', find tags that are references to NAME.
The output is in grep format. When CASE-FOLD is non-nil, do
case-insensitive matching. When START-FILE is non-nil, sort the
result by nearness (see the help message of global) start from
START-FILE."
(let* ((name (when name (substring-no-properties name)))
cmd)
(pcase mode
('completion (push "--completion" cmd))
('definition (push "--definition" cmd))
('reference (push "--reference" cmd)
(push "--symbol" cmd))
(_ (error "Invalid MODE")))
(when case-fold (push "--ignore-case" cmd))
;; Global doesn't know how to expand "~", so we need to expand START-FILE.
(when start-file (let* ((filename (expand-file-name start-file))
(file (if (file-remote-p start-file)
(file-local-name filename)
filename)))
(push (concat "--nearness=" file) cmd)))
(setq cmd (append (nreverse cmd)
(list "--color=never"
"--encode-path= :"
"--result=grep"
"--literal"
"--" name)))
(citre-global--get-output-lines cmd)))
(defun citre-global--get-tag-lines-in-file (&optional file)
"Find definitions in FILE using global and return the outputed lines.
The output is in cxref format. FILE can be absolute or relative
to `default-directory', and is the current file if it's nil."
(let ((file (or file (file-relative-name buffer-file-name))))
(citre-global--get-output-lines (list "--file" file))))
(defun citre-global--parse-path (path)
"Translate escaped sequences in PATH.
The path should come from the output of global, with the
\"--encode-path\" option."
(let ((last 0)
(i nil)
(parts nil))
(while (setq i (string-match "%" path last))
(push (substring path last i) parts)
(push (char-to-string (string-to-number
(substring path (1+ i) (+ 3 i))
16))
parts)
(setq last (+ 3 i)))
(push (substring path last) parts)
(apply #'concat (nreverse parts))))
(defun citre-global--parse-grep-line (line rootdir &optional name reference)
"Parse a LINE in the output of global in grep format.
ROOTDIR is the working directory when running the global command.
The return value is a tag contains `ext-abspath' and `line'
fields.
If NAME is given, is used as the `name' field.
If REFERENCE is non-nil, \"reference\" is used as the `extras' field."
(if (string-match (rx line-start
(group-n 1 (+ (not (any ":"))))
":"
(group-n 2 (+ num))
":")
line)
(let ((path (match-string 1 line))
(linum (match-string 2 line))
(tag nil))
;; We don't record the pattern field since it's generate in real time,
;; so it can't be used to deal with file updates.
(setq path (expand-file-name (citre-global--parse-path path) rootdir))
(setq tag
(citre-make-tag 'name (when name (substring-no-properties name))
'ext-abspath path
'line linum))
(when reference (citre-set-tag-field 'extras "reference" tag))
tag)
(error "Invalid LINE")))
(defun citre-global--parse-cxref-line (line)
"Parse a LINE in the output of global in cxref format.
The return value is a tag containing `name' and `line' fields.
If the line cannot be parsed, return nil."
(let ((pat (rx line-start
(group (+? not-newline))
(+ " ")
(group (+ digit)))))
(when (string-match pat line)
(citre-make-tag 'name (match-string 1 line)
'line (match-string 2 line)))))
;;;;; API
(defvar-local citre--global-dbpath nil
"Buffer-local cache for global database path.
See `citre-global-dbpath' to know how this is used.")
(defun citre-global-dbpath (&optional dir)
"Get global database path.
This is the directory containing the GTAGS file. When DIR is
non-nil, find database of that directory, otherwise find the
database of current directory.
When the global program is not found on the machine, return nil
as global program is needed to get the database path."
(pcase citre--global-dbpath
('none nil)
((and val (pred stringp) (pred citre-dir-exists-p)) val)
(_ (let ((default-directory (or dir default-directory)))
(condition-case nil
(setq citre--global-dbpath
(car (citre-global--get-output-lines
'("--print-dbpath"))))
(error (setq citre--global-dbpath 'none)
nil))))))
(defun citre-global-clear-dbpath-cache ()
"Clear the cache of buffer -> global database path.
Use this when a new database is created."
(dolist (b (buffer-list))
(with-current-buffer b
(kill-local-variable 'citre--global-dbpath))))
(defun citre-global-get-tags (&optional name mode case-fold start-file)
"Get tags using global.
When NAME is non-nil, get tags for NAME, otherwise get
tags for the symbol under point.
See `citre-global--get-lines' for valid value of MODE.
When CASE-FOLD is non-nil, do case-insensitive matching.
By default, the result is sort by nearness (see the `--nearness'
option in global) start from the current file or directory.
START-FILE can be nil to keep this behavior, be a string to
specify the start file, or be a symbol (like `alpha') to use the
default alphabetical sort.
Global program is run under current `default-directory'."
(when (citre-global-dbpath)
(let ((name (or name (citre-tags-get-symbol)))
(start-file
(pcase start-file
('nil (with-selected-window (or (minibuffer-selected-window)
(selected-window))
(or (buffer-file-name) default-directory)))
((pred stringp) start-file)
((pred symbolp) nil))))
;; `completion' mode needs special treatment as global just prints the
;; symbols in this mode, rather than printing in grep format as in other
;; modes.
(if (eq mode 'completion)
(mapcar (lambda (name)
(citre-make-tag 'name name))
(citre-global--get-lines
name mode case-fold start-file))
(mapcar (lambda (line)
(citre-global--parse-grep-line line default-directory name
(eq mode 'reference)))
(citre-global--get-lines
name mode case-fold start-file))))))
(defun citre-global-get-tags-in-file (&optional file)
"Find definitions in FILE using global and return the tags.
The tags have `name' and `line' fields. Use the current file if
FILE is nil."
(when (citre-global-dbpath)
(cl-delete nil
(mapcar #'citre-global--parse-cxref-line
(citre-global--get-tag-lines-in-file file))
:test #'eq)))
;;;; Tags file generating & updating
;;;;; Commands
;;;###autoload
(defun citre-global-create-database ()
"Create gtags database."
(interactive)
(let* ((project (funcall citre-project-root-function))
(default-directory
(read-directory-name
"I want to tag this dir using gtags: "
project)))
(make-process
:name "gtags"
:buffer (get-buffer-create "*citre-gtags*")
:command (append (list (or citre-gtags-program "gtags"))
citre-gtags-args)
:connection-type 'pipe
:stderr nil
:sentinel
(lambda (proc _msg)
(unwind-protect
(pcase (process-status proc)
('exit
(pcase (process-exit-status proc)
(0 (message "Finished tagging"))
(s (user-error "Gtags exits %s. See *citre-gtags* buffer"
s))))
(s (user-error "Abnormal status of gtags: %s. \
See *citre-gtags* buffer" s)))
(citre-global-clear-dbpath-cache)))
:file-handler t)
(message "Tagging...")))
;;;###autoload
(defun citre-global-update-database ()
"Update the gtags database in use.
If no database is found, prompt the user to create one."
(interactive)
(let ((prog (or citre-global-program "global")))
(make-process
:name "global"
:buffer (get-buffer-create "*citre-global-update*")
:command (list (or citre-global-program "global")
"--update")
:connection-type 'pipe
:stderr nil
:sentinel
(lambda (proc _msg)
(pcase (process-status proc)
('exit
(pcase (process-exit-status proc)
(0 (message "Finished updating"))
(_ (if (citre-executable-find prog t)
(when (y-or-n-p "Can't find database. Create one? ")
(citre-global-create-database))
(user-error "Can't find global program")))))
(s (user-error "Abnormal status of global: %s. \
See *citre-global-update* buffer" s))))
:file-handler t)
(message "Updating...")))
;;;###autoload
(defun citre-global-update-file (&optional f)
"Update the gtags database of a single file F.
If no database is found, prompt the user to create one."
(interactive)
(let* ((prog (or citre-global-program "global"))
(filename (expand-file-name (or f (buffer-file-name))))
(file (if (file-remote-p filename)
(file-local-name filename)
filename)))
(make-process
:name "global"
:buffer (get-buffer-create "*citre-global-update-file*")
:command (list prog "--single-update" file)
:connection-type 'pipe
:stderr nil
:sentinel
(lambda (proc _msg)
(pcase (process-status proc)
('exit
(pcase (process-exit-status proc)
(0 (message "Finished updating file \"%s\"" file))
(_ (if (citre-executable-find prog t)
(when (y-or-n-p "Can't find database. Create one? ")
(citre-global-create-database))
(user-error "Can't find global program")))))
(s (user-error "Abnormal status of global: %s. \
See *citre-global-update-file* buffer" s))))
:file-handler t)
(message "Updating file \"%s\"..." file)))
;;;; Get identifiers
(defun citre-global-get-identifiers ()
"Get a list of identifiers in current project."
(when (citre-global-dbpath)
(citre-global--get-output-lines
'("--completion"))))
;;;; Completion backend
;; TODO: Do we need to cache the result like tags backend?
(defun citre-global-get-completions ()
"Get tags of completions of symbol at point."
(when-let* ((symbol (citre-tags-get-symbol))
(bounds (citre-get-property 'bounds symbol))
(tags (citre-global-get-tags
symbol 'completion
(not citre-global-completion-case-sensitive)
'alpha)))
(list (car bounds) (cdr bounds)
;; Sort by length
(sort tags (lambda (a b)
(< (length (citre-get-tag-field 'name a))
(length (citre-get-tag-field 'name b))))))))
;;;; Find definitions backend
(defun citre-global-get-definitions ()
"Get tags of definitions to symbol at point."
(citre-global-get-tags nil 'definition))
(defun citre-global-get-definitions-of-id (id)
"Get tags of definitions of ID."
(citre-global-get-tags id 'definition))
;;;; Find references backend
(defun citre-global-get-references ()
"Get tags of references to symbol at point."
(citre-global-get-tags nil 'reference))
(defun citre-global-get-references-of-id (id)
"Get tags of references of ID."
(citre-global-get-tags id 'reference))
;;;; Backend definition
(defvar citre-global-backend
(citre-backend-create
:usable-probe #'citre-global-dbpath
:symbol-at-point-fn #'citre-tags-symbol-at-point
:completions-fn #'citre-global-get-completions
:id-list-fn #'citre-global-get-identifiers
:defs-fn #'citre-global-get-definitions
:defs-of-id-fn #'citre-global-get-definitions-of-id
:refs-fn #'citre-global-get-references
:refs-of-id-fn #'citre-global-get-references-of-id
:tags-in-buffer-fn #'citre-global-get-tags-in-file)
"Global backend.")
(citre-register-backend 'global citre-global-backend)
(provide 'citre-global)
;; Local Variables:
;; indent-tabs-mode: nil
;; outline-regexp: ";;;;* "
;; fill-column: 79
;; emacs-lisp-docstring-fill-column: 65
;; sentence-end-double-space: t
;; End:
;;; citre-global.el ends here