-
Notifications
You must be signed in to change notification settings - Fork 26
/
citre-readtags.el
1185 lines (1036 loc) · 46.4 KB
/
citre-readtags.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
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; citre-readtags.el --- A readtags abstraction layer -*- lexical-binding: t -*-
;; Copyright (C) 2020 Hao Wang
;; Author: Hao Wang <[email protected]>
;; Maintainer: Hao Wang <[email protected]>
;; Created: 04 May 2020
;; 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:
;; A readtags abstraction layer.
;;; 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-common-tag)
(require 'citre-common-util)
(require 'citre-readtags-tables)
(require 'cl-lib)
(require 'subr-x)
;;;; User options
(defcustom citre-readtags-program nil
"The name or path of the readtags program.
Set this if readtags is not in your PATH, or its name is not
\"readtags\".
Citre requires the readtags program provided by Universal Ctags."
:type '(choice (string :tag "Path or name of the readtags program")
(const :tag "readtags" nil))
:group 'citre)
;;;; Internals: Basic Helpers
(defun citre-readtags--string-non-empty-p (string)
"Test if STRING is an non-empty string."
(and (stringp string)
(not (string-empty-p string))))
(defun citre-readtags--strip-text-property-in-list (object)
"Recursively traverse OBJECT and strip properties in strings."
(pcase object
((and (pred stringp) val) (substring-no-properties val))
((and (pred consp) val)
(cons (citre-readtags--strip-text-property-in-list (car val))
(citre-readtags--strip-text-property-in-list (cdr val))))
(val val)))
(defmacro citre-readtags--error-on-arg (arg test)
"Test ARG using TEST, and throw an error if it fails.
When calling the APIs, some arguments are likely to be calculated
based on information fetched from the environment, and they may
have problems like being empty, or not having the right type for
being nil. This should be used to test them."
`(unless (ignore-errors (funcall ,test ,arg))
(error "%s fails on %s. It is a %s: %S"
(quote ,test) (upcase (symbol-name (quote ,arg)))
(type-of ,arg) ,arg)))
;;;; Internals: Additional information handling
(defvar citre-readtags--tags-file-info-alist nil
"Alist for storing additional info about tags files.
Since tags files can offer ambiguous info, we use this variable
to store additional info to ascertain them.
This alist looks like:
(alist of:
tags file -> hash table of additional info:
(info field -> value))
Info fields and their corresponding values are:
- `time': The last update time of the file info, which is, the
hash table. It's in the style of (current-time).
- `remotep': Whether the tags file is a remote file.
- `dir': The full current working directory when generating the
tags file. It's a remote dir when tags file is a remote file
name.
- `os': When the local part of `dir' is unix-style path (begins
with a slash), this is `unix', or it's `nt'. We have such a
field since `system-type' can't tell us about the remote
machine, and the behavior of many file functions depends on the
*local* system type.
- `kind-table': A hash table for getting full-length kinds from
single-letter kinds, like
`citre-readtags--kind-name-single-to-full-table', or nil if the
TAG_KIND_DESCRIPTION pseudo tags are not presented.")
(defvar citre-readtags--tags-file-cwd-guess-table
(make-hash-table :test #'equal)
"A hash table for guessed cwd for tags files.
This is used in `citre-readtags--get-dir'. See its docstring for
details. This is intended for upper components to set since it's
easier for them to infer the cwd.
Its keys are canonical paths of tags files, values are their
cwds (absolute path, can be remote or local when the tags file is
a remote file).")
(defvar citre-readtags--dont-prompt-for-cwd nil
"In `citre-readtags--get-dir', don't ask the user for the cwd.
When non-nil, it uses the directory of the tags file as the cwd
instead. This is for running unit tests only, as tags files in
them don't have TAG_PROC_CWD ptag.")
(defun citre-readtags--get-dir-os (ptag-cwd tagsfile)
"Get the `dir' and `os' info of TAGSFILE.
PTAG-CWD is the value of TAG_PROC_CWD pseudo tag, and is used
when non-nil. If it's nil, we have fallbacks:
- Get the guessed cwd from
`citre-readtags--tags-file-cwd-guess-table'. This table is
intended fo upper components to set because they better
understanding of the project structure.
- Prompt the user to choose a dir.
This returns a cons pair like (dir . os)."
(let* ((dir (or ptag-cwd
(gethash tagsfile citre-readtags--tags-file-cwd-guess-table)
(if citre-readtags--dont-prompt-for-cwd
(file-name-directory tagsfile)
(read-directory-name
(format "Root dir of tags file %s: " tagsfile)))))
(dir (expand-file-name dir))
(dir-local (file-local-name dir)))
;; Ctags on windows generates disk symbol in capital letter, while if DIR
;; is given by Emacs, it may be a small letter. We don't use `system-type'
;; to detect since we may work on a remote Unix machine on Windows.
(unless (eq (aref dir-local 0) ?/)
(setq dir-local (citre-upcase-first-letter dir-local)))
(cons
;; If tagsfile is a remote file, we may have to prefix dir by the remote
;; identifier (e.g., if dir comes from the TAG_PROC_CWD ptag).
(if-let* ((remote-id (file-remote-p tagsfile)))
(concat remote-id dir-local)
dir-local)
(pcase (aref dir-local 0)
(?/ 'unix)
(_ 'nt)))))
(defun citre-readtags--get-kind-table (kind-descs)
"Get the `kind-table' info.
KIND-DESCS is the values of TAG_KIND_DESCRIPTION pseudo tags."
(when kind-descs
(let ((prefix-len (length "!_TAG_KIND_DESCRIPTION!"))
(table (make-hash-table :test #'equal)))
(dolist (kind-desc kind-descs)
(let* ((lang (substring (car kind-desc) prefix-len))
(kind-pair (split-string (nth 1 kind-desc) ","))
(kind (car kind-pair))
(kind-full (nth 1 kind-pair)))
(unless (gethash lang table)
(puthash lang (make-hash-table :test #'equal) table))
(puthash kind kind-full (gethash lang table))))
table)))
(defun citre-readtags--fetch-tags-file-info (tagsfile)
"Write the additional info of TAGSFILE to `citre-readtags--tags-file-info-alist'.
TAGSFILE is the canonical path of the tags file. The info is
returned."
(let* ((recent-mod (file-attribute-modification-time
(file-attributes tagsfile)))
(info (make-hash-table :test #'eq))
(ptag-cwd (nth 1 (car (citre-readtags-get-pseudo-tags
"TAG_PROC_CWD" tagsfile))))
(kind-descs (citre-readtags-get-pseudo-tags
"TAG_KIND_DESCRIPTION" tagsfile 'prefix)))
;; time
(puthash 'time recent-mod info)
;; remotep
(puthash 'remotep (file-remote-p tagsfile) info)
;; dir
(pcase-let ((`(,dir . ,os) (citre-readtags--get-dir-os ptag-cwd tagsfile)))
(puthash 'dir dir info)
(puthash 'os os info))
;; kind-table
(puthash 'kind-table
(citre-readtags--get-kind-table kind-descs)
info)
info))
;;;; Internals: Tags file filtering & parsing
;;;;; Get lines
(defun citre-readtags--get-lines
(tagsfile &optional name match case-fold filter sorter action)
"Get lines in TAGSFILE using readtags.
See `citre-readtags-get-tags' to know about NAME, MATCH, CASE-FOLD,
FILTER, and SORTER. ACTION can be nil, to get regular tags, or
any valid actions in readtags, e.g., \"-D\", to get pseudo tags."
(let* ((match (or match 'exact))
(extras (concat
"-Ene"
(pcase match
('exact "")
('prefix "p")
(_ (error "Unexpected value of MATCH")))
(if case-fold "i" "")))
(tagsfile (substring-no-properties tagsfile))
(name (when name (substring-no-properties name)))
(filter (citre-readtags--strip-text-property-in-list filter))
(sorter (citre-readtags--strip-text-property-in-list sorter))
inhibit-message
cmd)
;; Program name
(push (or citre-readtags-program "readtags") cmd)
;; Read from this tags file
(push "-t" cmd)
(push (file-local-name tagsfile) cmd)
;; Filter expression
(when filter (push "-Q" cmd) (push (format "%S" filter) cmd))
(when sorter (push "-S" cmd) (push (format "%S" sorter) cmd))
;; Extra arguments
(push extras cmd)
;; Action
(if action (push action cmd)
(if (or (null name) (string-empty-p name))
(push "-l" cmd)
(push "-" cmd)
(push name cmd)))
(citre-get-output-lines (nreverse cmd))))
;;;;; Parse tagline
(defun citre-readtags--read-field-value (value)
"Translate escaped sequences in VALUE.
See tags(5) manpage to know about the escaped sequences. VALUE
should be a field value in a tags file."
(if-let* ((backslash-idx
(citre-string-match-all-escaping-backslash value)))
(let ((last 0)
(i nil)
(parts nil))
(while (setq i (pop backslash-idx))
(push (substring value last i) parts)
(let ((char (aref value (1+ i))))
(pcase char
(?x (progn
(setq last (+ 4 i))
(push (char-to-string (string-to-number
(substring value (+ 2 i) (+ 4 i))
16))
parts)))
(_ (progn
(setq last (+ 2 i))
(push (pcase char
(?t "\t") (?r "\r") (?n "\n") (?\\ "\\")
(?a "\a") (?b "\b") (?v "\v") (?f "\f")
(_ (error "Invalid escape sequence")))
parts))))))
(push (substring value last) parts)
(apply #'concat (nreverse parts)))
value))
(defun citre-readtags--forward-pattern (line pos)
"Jump over the pattern field.
LINE is a tagline, POS is the start position of the pattern field
in it. This returns its end position."
;; If pattern begins with a number, it will be like one of
;;
;; - 20;"
;; - 20;/pattern/;"
;; - 20;?pattern?;"
(when (<= ?0 (aref line pos) ?9)
;; We jump after the first semicolon.
(setq pos (1+ (string-match ";" line pos))))
(pcase (aref line pos)
(?\" (1+ pos))
(c (let ((;; We want a regexp that takes us to near the end of the pattern,
;; which means the end of the match should be the closing
;; delimiter, so there's still a ;" between it and the end of the
;; pattern.
pat-end-regexp
(pcase c
;; Search for an unescaped slash. It must be the ending
;; delimiter.
(?/ "[^\\\\]\\(\\\\\\\\\\)*/")
;; The same.
(?? "[^\\\\]\\(\\\\\\\\\\)*\\?")
(_ (error "Invalid pattern field")))))
(setq pos
;; Search from after the opening delimiter.
(progn (string-match pat-end-regexp line (1+ pos))
(match-end 0)))
(+ 2 pos)))))
(defun citre-readtags--lexer-forward-field-name (line length lexer)
"Move the lexer forward the following field name.
LINE is a tagline. LENGTH is its length. LEXER is a vector like
[POS N], where POS is the beginning position of a field, and it's
the Nth field in the line (N counts from 0).
This sets POS to the beginning of the field value, and returns
the field name as a symbol. When there's no more field to parse,
this returns nil, and the caller should stop parsing."
(let ((pos (aref lexer 0))
(n (aref lexer 1)))
(pcase n
(0 'name)
(1 'input)
(2 'pattern)
(_ (when (< pos length)
(let ((sep (string-match ":" line pos)))
(cond
;; The kind field may not begin with "kind:". It's always the
;; 4th field (N=3), but the 4th field is not always the kind
;; field.
((and (eq n 3)
(or (null sep)
(when-let* ((tab (string-match "\t" line pos)))
(> sep tab))))
'kind)
(sep
(let ((field-name (intern (substring line pos sep))))
(pcase field-name
((or 'class 'struct)
'scope)
(_
(setf (aref lexer 0) (1+ sep))
field-name))))
(t (error "Invalid LINE")))))))))
(defun citre-readtags--lexer-forward-field-value
(line length lexer &optional parse-value)
"Move the lexer forward the following field value.
LINE is a tagline. LENGTH is its length. LEXER is a vector like
[POS N], where POS is the beginning position of a field value,
and it's the Nth field in the line (N counts from 0).
This sets POS to the beginning of the next field, and add 1 to N.
If PARSE-VALUE is non-nil, returns the field value."
(let ((pos (aref lexer 0))
(nfield (aref lexer 1))
tab value)
(pcase nfield
(2 (setq tab (citre-readtags--forward-pattern line pos))
(when parse-value
(setq value (substring line pos tab))))
(_ (setq tab (or (string-match "\t" line pos)
length))
(when parse-value
(setq value
(citre-readtags--read-field-value
(substring line pos tab))))))
(setf (aref lexer 0) (1+ tab))
(setf (aref lexer 1) (1+ nfield))
value))
(defun citre-readtags--parse-line (line &optional tagsfile-info
require optional exclude
require-ext optional-ext ext-dep
parse-all-fields)
"Parse a tagline LINE.
This returns a hash table called \"tag\" by Citre. Its keys are
the fields, values are their values. It can be utilized by
`citre-get-tag-field'.
Optional arguments can be used to specify the fields wanted in
the returned tag. REQUIRE, OPTIONAL, EXCLUDE and PARSE-ALL-FIELDS
are similar to `citre-readtags-get-tags', but extension fields can't
appear in them. Use these for extension fields:
- REQUIRE-EXT: A list containing extension fields that must be
presented. If any of these fields can't be get, an error will
occur.
- OPTIONAL-EXT: A list containing fields that's optional. For
any field in it, if it can be get, it will be recorded; if
can't, it's ignored, and no error will occur.
The normal fields they depend on should appear in either REQUIRE,
OPTIONAL or EXT-DEP to make sure they are captured.
TAGSFILE-INFO is needed to offer additional information for these
extension fields. It is the additional info of the tags file
containing LINE. Such TAGSFILE-INFO should be get using
`citre-readtags-tags-file-info'.
The arguments must satisfy certain conditions, which the caller
should take care of. `citre-readtags--parse-line' doesn't check them
for the sake of performance. Other than those mentioned above,
we still have:
- All lists specifying needed fields should not contain
duplicated elements.
- REQUIRE, OPTIONAL and EXCLUDE shouldn't intersect with each
other.
- EXT-DEP shouldn't intersect with REQUIRE or OPTIONAL.
- OPTIONAL and EXCLUDE should not be used together."
(let* ((tag (citre-make-tag))
(parse-all-fields (or exclude parse-all-fields))
(require-num (length require))
(require-counter 0)
(optional-num (length optional))
(optional-counter 0)
(ext-dep-num (length ext-dep))
(ext-dep-counter 0)
(lexer (vector 0 0))
(len (length line))
field
(write (lambda (field)
(citre-set-tag-field
field
(citre-readtags--lexer-forward-field-value line len lexer t)
tag))))
(cl-block nil
(while (setq field (citre-readtags--lexer-forward-field-name
line len lexer))
(cond
((memq field require)
(funcall write field)
(cl-incf require-counter))
((memq field optional)
(funcall write field)
(cl-incf optional-counter))
(t (or
(when (memq field ext-dep)
(funcall write field)
(cl-incf ext-dep-counter)
t)
(when (and parse-all-fields (null (memq field exclude)))
(funcall write field)
t)
(citre-readtags--lexer-forward-field-value line len lexer nil))))
(when (and (null parse-all-fields)
(eq require-counter require-num)
(eq optional-counter optional-num)
(eq ext-dep-counter ext-dep-num))
(cl-return))))
(when (< require-counter require-num)
(error "Fields not found in tags file: %s"
(string-join
(mapcar #'symbol-name
(cl-set-difference require
(hash-table-keys tag)))
", ")))
(dolist (field require-ext)
(citre-readtags--write-ext-field tag field tagsfile-info))
(dolist (field optional-ext)
(ignore-errors
(citre-readtags--write-ext-field tag field tagsfile-info)))
(if parse-all-fields
;; Excluded field may be written because it's in `ext-dep'.
(dolist (field exclude)
(remhash field tag))
(dolist (field ext-dep)
(remhash field tag)))
tag))
;;;;; Extension fields
(defvar citre-readtags--ext-fields-dependency-alist
'((ext-abspath . (input))
(ext-kind-full . (kind language input)))
"Alist of extension fields and their dependencies.
Its keys are extension fields offered by Citre, values are lists
of (normal) fields the the extension field depends on.")
(defvar citre-readtags--ext-fields-method-table
#s(hash-table
test eq
data
(ext-abspath
citre-readtags--get-ext-abspath
ext-kind-full
citre-readtags--get-ext-kind-full))
"Hash table of extension fields and the methods to get them.
Its keys are extension fields offered by Citre, and values are
functions that return the value of the extension field. The
arguments of the functions are:
- TAG: A hash table containing the fields that the extension
field depends on.
- TAGSFILE-INFO: The additional info of the tags file. See
`citre-readtags--tags-file-info' to know how to make use of it.
If the extension field can't be calculated, the functions should
signal an error, rather than return nil.
The needed TAG and TAGSFILE-INFO are specified by
`citre-readtags--ext-fields-dependency-alist'.
`citre-readtags--write-ext-field' takes care to pass the needed
arguments to the functions.
If the function only needs TAG, consider make it an extra
extension field (see `citre-readtags-extra-ext-fields-table').")
(defun citre-readtags--write-ext-field
(tag field tagsfile-info)
"Write the value of extension field FIELD to TAG.
TAG should contain the fields that FIELD depends on.
TAGSFILE-INFO is the additional info that FIELD depends on."
(if-let* ((method (gethash field citre-readtags--ext-fields-method-table)))
(citre-set-tag-field field (funcall method tag tagsfile-info) tag)
(error "Invalid FIELD")))
;;;;;; ext-abspath
(defun citre-readtags--get-ext-abspath (tag tagsfile-info)
"Return the full path of the input file.
This needs the `input' field to be presented in TAG, and if its
value is a relative path, `dir' info in TAGSFILE-INFO is used.
This returns a remote path when the tagsfile is remote."
(let* ((input (or (citre-get-tag-field 'input tag)
(error "\"input\" field not found in TAG")))
(remotep (gethash 'remotep tagsfile-info))
(os (gethash 'os tagsfile-info))
(input-absolute-p (pcase os
('unix (eq (aref input 0) ?/))
;; tags file uses capital letter on Windows.
('nt (and (<= ?A (aref input 0) ?Z)
(eq (aref input 1) ?:))))))
(if input-absolute-p
(if remotep
(concat (file-remote-p (gethash 'dir tagsfile-info)) input)
input)
(expand-file-name input (gethash 'dir tagsfile-info)))))
;;;;;; ext-kind-full
(defun citre-readtags--get-ext-kind-full (tag tagsfile-info)
"Return full-length kind name.
This needs the `kind' field to be presented in TAG. If the tags
file uses full-length kind name (told by TAGSFILE-INFO), it's
returned directly. If not, then:
- The language is guessed first.
- The single-letter kind is converted to full-length, based on
the TAG_KIND_DESCRIPTION pseudo tags, or
`citre-readtags--kind-name-single-to-full-table' if it's not
presented.
If this fails, the single-letter kind is returned directly."
(if-let* ((kind (citre-get-tag-field 'kind tag))
;; Check if the kind is single letter.
(single-letter-p (eq (length kind) 1))
(lang (citre-get-tag-field 'extra-lang tag))
(table (or (gethash 'kind-table tagsfile-info)
citre-readtags--kind-name-single-to-full-table))
(table (gethash lang table))
(kind-full (gethash kind table)))
kind-full
kind))
;;;;; Get tags from tags files
(cl-defun citre-readtags--get-tags
(tagsfile &optional name match case-fold
&key filter sorter
require optional exclude parse-all-fields)
"Get tags in TAGSFILE.
This is like `citre-readtags-get-tags', which actually calls this
function internally. The difference is this is a interface
that's closer to actual readtags command line calls. The
differences are:
- NAME: If this is a non-empty string, use the NAME action.
Otherwise use the -l action.
- MATCH: Can only be nil, `exact' or `prefix', which translates
to arguments controlling the NAME action.
- CASE-FOLD: Only controls the NAME action.
Notice when calling `citre-readtags-get-tags' with NAME being
`substr' or `regexp', it generates a filter expression to do
that, and is merged with FILTER by a logical `and'.
For SORTER, REQUIRE, OPTIONAL, EXCLUDE, and PARSE-ALL-FIELDS, see
`citre-readtags-get-tags'."
(when (and optional exclude)
(error "OPTIONAL and EXCLUDE can't be used together"))
(when (cl-intersection require exclude)
(error "REQUIRE and EXCLUDE can't intersect"))
(when (cl-intersection optional exclude)
(error "OPTIONAL and EXCLUDE can't intersect"))
(let* ((optional (cl-set-difference optional require))
(find-field-depends
(lambda (field)
(alist-get field citre-readtags--ext-fields-dependency-alist)))
(ext-fields (mapcar #'car
citre-readtags--ext-fields-dependency-alist))
(require-ext (cl-intersection require ext-fields))
(optional-ext (cl-intersection optional ext-fields))
(ext-dep (cl-delete-duplicates
(apply #'append
(append
(mapcar find-field-depends require-ext)
(mapcar find-field-depends optional-ext)))))
(ext-dep (cl-set-difference ext-dep require))
(ext-dep (cl-set-difference ext-dep optional))
(require (cl-delete-duplicates
(cl-set-difference require ext-fields)))
(optional (cl-delete-duplicates
(cl-set-difference optional ext-fields)))
(exclude (cl-delete-duplicates exclude))
(info (when (or require-ext optional-ext)
(citre-readtags-tags-file-info tagsfile))))
(when (cl-intersection exclude ext-fields)
(error "EXCLUDE shouldn't contain extension fields"))
(mapcar (lambda (line)
(citre-readtags--parse-line
line info
require optional exclude
require-ext optional-ext ext-dep
parse-all-fields))
(citre-readtags--get-lines
tagsfile name match case-fold
filter sorter nil))))
;;;; APIs
;;;;; Tags file info
(defun citre-readtags-tags-file-info (tagsfile)
"Return the additional info of tags file TAGSFILE.
TAGSFILE is the absolute path of the tags file. The return
value is a valid value in `citre-readtags--tags-file-info-alist'.
This function caches the info, and uses the cache when possible."
(citre-readtags--error-on-arg tagsfile #'stringp)
(unless (and (file-exists-p tagsfile)
(not (file-directory-p tagsfile)))
(error "%s doesn't exist" tagsfile))
(let ((tagsfile (file-truename tagsfile))
(recent-mod (file-attribute-modification-time
(file-attributes tagsfile)))
(info (alist-get tagsfile
citre-readtags--tags-file-info-alist
nil nil #'equal)))
(if (and info (equal (gethash 'time info) recent-mod))
info
(let ((info (citre-readtags--fetch-tags-file-info tagsfile)))
;; Seems `setf' in Emacs 26 doesn't return the last VAL.
(setf (alist-get tagsfile
citre-readtags--tags-file-info-alist
nil nil #'equal)
info)
info))))
;;;;; Build filter expressions
;;;;;; Internals
(defun citre-readtags--filter-regexp-builder (str1 str2 case-fold)
"Build filter expression that matches STR1 by STR2.
STR1 can be a string or a symbol representing a field. STR2 must
be a string. When CASE-FOLD is non-nil, do case-insensitive
matching."
(unless (stringp str2)
(error "STR2 must be a string"))
`((string->regexp ,str2
:case-fold
,(pcase case-fold
('nil 'false)
(_ 'true)))
,str1))
(defun citre-readtags--csv-contain-regexp-builder (str)
"Build a regexp that matches a CSV string that contains STR.
STR can also be a list of strings, then the regexp matches a CSV
string that contains any element in STR.
This is for use in readtags filter."
(let ((string-or-list-of-string-p
(lambda (str)
(or (stringp str)
(and str (null (cl-position nil (mapcar #'stringp str))))))))
(citre-readtags--error-on-arg str string-or-list-of-string-p))
(when (stringp str)
(setq str (list str)))
(concat "(^|,) ?("
(mapconcat #'citre-readtags-regexp-quote str "|")
")(,|$)"))
(defun citre-readtags--filter-case-fold-string-builder (str case-fold)
"Convert STR by CASE-FOLD.
STR can be a string or a symbol representing a field. When
case-fold is non-nil, its downcased version is returned.
Otherwise it's directly returned."
(if case-fold
(if (symbolp str)
`(downcase ,str)
(downcase str))
str))
;;;;;; APIs
(defun citre-readtags-regexp-quote (str)
"Return a regexp that matches STR in readtags filter expressions.
Readtags uses POSIX extended regular expressions (ERE), which is
different from regexp in Emacs."
;; (rx (or "(" ")" "[" "]" "{" "}" "." "*" "+" "^" "$" "|" "?" "\\"))
(replace-regexp-in-string "[]$(-+.?[\\{|}^]" "\\\\\\&" str))
(defun citre-readtags-filter (str1 str2 match
&optional case-fold invert keep-missing)
"Return a filter expression that matches STR1 and STR2.
Both STRs can be a string, or a symbol of the field name. STR2
can also be a list of strings if MATCH is `csv-contain', see
below. MATCH could be:
- `eq': See if STR1 equals STR2.
- `prefix': See if STR1 is prefixed by STR2.
- `suffix': See if STR1 is suffixed by STR2.
- `substr': See if STR1 contains STR2.
- `regexp': See if STR1 can be matched by STR2, which is a
regexp. \"/\" in strings doesn't need to be escaped. STR2
must be a string.
- `csv-contain': See if STR1 contains STR2 as a member, where
STR1 is a comma-separated list. STR2 can be a string, or a
list of strings, then it sees if STR1 contains any element in
STR2.
The order of STR1 and STR2 may feel a bit weird for Elisp users.
That's because the convention of readtags is use STR1 as the
\"target string\", and use STR2 as the prefix/suffix/regexp...
If CASE-FOLD is non-nil, do case-insensitive matching. If INVERT
is non-nil, flip the filter so it only keep lines that doesn't
match. If KEEP-MISSING is non-nil, also keep lines where the
fields pointed by STR1 or STR2 (if one/both of them are symbols)
are missing, otherwise only keep lines that have those fields."
(let* (syms
(str-process
(lambda (str)
(if (symbolp str)
(let ((sym (intern (concat "$" (symbol-name str)))))
(push sym syms)
sym)
str)))
(str1 (funcall str-process str1))
(str2 (funcall str-process str2))
filter final-filter)
(setq filter
(if (memq match '(regexp csv-contain))
(let ((str2 (if (eq match 'regexp)
str2
(citre-readtags--csv-contain-regexp-builder
str2))))
(citre-readtags--filter-regexp-builder str1 str2 case-fold))
(let ((symbol-or-string-p (lambda (str)
(or (symbolp str) (stringp str)))))
(citre-readtags--error-on-arg str1 symbol-or-string-p)
(citre-readtags--error-on-arg str2 symbol-or-string-p))
(let* ((str1 (citre-readtags--filter-case-fold-string-builder
str1 case-fold))
(str2 (citre-readtags--filter-case-fold-string-builder
str2 case-fold)))
`(,(intern (concat (symbol-name match) "?")) ,str1 ,str2))))
(when invert
(setq filter `(not ,filter)))
;; The value of a missing field is #f, and applying string operators on it
;; produces an error. So we have to make sure it's not #f beforehand.
(push (if keep-missing 'or 'and) final-filter)
(dolist (sym syms)
(push (if keep-missing `(not ,sym) sym) final-filter))
(push filter final-filter)
(nreverse final-filter)))
(defun citre-readtags-filter-field-exist (field &optional invert)
"Return a filter expression that requires FIELD to exist.
FIELD is a symbol of the field name. When INVERT is non-nil,
require FIELDS to be missing."
(let ((field (intern (concat "$" (symbol-name field)))))
(if invert
`(not ,field)
field)))
(defun citre-readtags-filter-lang (lang)
"Return a filter expression that requires the language to be LANG.
If the `language' field exists, this uses that field, otherwise
filter based on the extension of the `input' field, which may not
be accurate.
Run \"ctags --list-languages\" to see valid values of LANG. Be
careful about the capitalization!"
(let* ((ext (gethash lang citre-readtags--lang-extension-table)))
`(or ,(citre-readtags-filter 'language lang 'eq)
,(if ext
(citre-readtags-filter
'input
(concat "\\.("
(string-join (mapcar #'citre-readtags-regexp-quote ext)
"|")
")$")
'regexp)
'true))))
(defun citre-readtags-filter-kind (kind &optional ignore-missing)
"Return a filter expression that matches the kind field by KIND.
KIND should be a full-length kind. The generated filter works on
tags file using single-letter `kind' field, but it will match
more tags than it should, because a single-letter kind can
corresponds to multiple full-length kinds.
When TAGSFILE is non-nil, it detects if the tags file uses
single-letter kind, and generate simpler (and presumably faster)
filter based on that. When IGNORE-MISSING is non-nil, also keep
tags that don't have `kind' field."
(let ((kinds (gethash kind citre-readtags--kind-name-full-to-single-table)))
(push kind kinds)
(citre-readtags-filter
'kind
(concat "^("
(string-join (mapcar #'citre-readtags-regexp-quote kinds) "|")
")$")
'regexp nil nil ignore-missing)))
(defun citre-readtags-filter-input (filename tagsfile)
"Return a filter expression that matches the input field by FILENAME.
TAGSFILE is the absolute path of the tags file. FILENAME should
be absolute. The generated filter can work no matter the tag
uses relative or absolute path."
(let* ((tagsfile (expand-file-name tagsfile))
;; We use this to match the input field in the tags file, so we need
;; the local path.
(local-name (file-local-name (expand-file-name filename)))
(local-name-nondir (file-name-nondirectory local-name))
;; local-name and the path in a tag may look different, but actually
;; point to the same location, through symlink. If the tag records
;; the symlink path, and FILENAME is the truepath, we can't solve it.
;; But if the opposite is true, we can convert FILENAME to its
;; truepath and match by it.
;; TODO: Test this. For now I don't know if git + symlink has any
;; problem on Windows.
(truename (file-local-name (file-truename filename)))
;; Don't bother with truename if it's the same as local-name.
(truename (unless (equal local-name truename) truename))
(truename-nondir (when truename (file-name-nondirectory truename)))
(truename-nondir (unless (equal truename-nondir local-name-nondir)
truename))
;; When there are "./" or "../" in the input field, given the
;; restrictions of readtags filter expressions, it's hard to match it
;; with the "true" filename. When this happens, we match against the
;; non directory part.
(implied-relative-path-regex
(lambda (file-non-dir-part)
(concat "(^|/)..?/"
(citre-readtags-regexp-quote file-non-dir-part)
"$")))
(filter (list 'or))
(info (citre-readtags-tags-file-info tagsfile))
(cwd (file-local-name (gethash 'dir info)))
(os (gethash 'os info)))
;; Ctags on windows generates directory symbol in capital letter, while
;; `buffer-file-name' returns it in small letter. We don't use
;; `system-type' to detect since we may work on a remote Unix machine on
;; Windows. We don't need the same treatment for cwd as it uses capital
;; disk symbols on Windows, see `citre-readtags--get-dir'.
(when (eq os 'nt)
(setq local-name (citre-upcase-first-letter local-name))
(when truename
(setq truename (citre-upcase-first-letter truename))))
(dolist (f (list local-name truename))
(when f
(push (citre-readtags-filter 'input f 'eq) filter)
(when (and (string-prefix-p cwd f))
;; We don't use `file-relative-name' due to the same reason. Its
;; behavior depends on the platform.
(push (citre-readtags-filter 'input (substring f (length cwd)) 'eq)
filter))))
(dolist (f (list local-name-nondir truename-nondir))
(when f
(push (citre-readtags-filter 'input
(funcall implied-relative-path-regex f)
'regexp)
filter)))
(nreverse filter)))
;;;;; Build sorter expressions
;;;;;; Internals
(defun citre-readtags--readtags-expr-replace-$-by-& (expr)
"Replace $-entry by &-entry in EXPR.
EXPR is a filter expression."
(if (consp expr)
(cons (citre-readtags--readtags-expr-replace-$-by-& (car expr))
(citre-readtags--readtags-expr-replace-$-by-& (cdr expr)))
(if (and (symbolp expr) (eq (aref (symbol-name expr) 0) ?$))
(intern (concat "&" (substring (symbol-name expr) 1)))
expr)))
(defun citre-readtags--simple-sorter (elt)
"Build a sorter based on ELT.
ELT is an element of the FIELDS arg in `citre-readtags-sorter', and
is one of the \"OPERATOR\" or \"field\" variant."
(let* ((variant (car elt))
(field (nth 1 elt))
(entry (lambda (prefix)
(intern (concat prefix (symbol-name field)))))
($-entry (funcall entry "$"))
(&-entry (funcall entry "&"))
(opd (lambda (entry)
(pcase variant
('field entry)
(_ `(,(car elt) ,entry)))))
($-opd (funcall opd $-entry))
(&-opd (funcall opd &-entry)))
`(if (and ,$-entry ,&-entry)
,(pcase (nth 2 elt)
('+ `(<> ,$-opd ,&-opd))
('- `(<> ,&-opd ,$-opd))
(_ (error "Invalid element: %s" elt)))
;; For tags without the specified field, the order is uncertain.
0)))
(defun citre-readtags--filter-sorter (elt)
"Build a sorter based on ELT.
ELT is an element of the FIELDS arg in `citre-readtags-sorter', and
is the \"filter\" variant."
(let* (($-filter (nth 1 elt))
(&-filter (citre-readtags--readtags-expr-replace-$-by-& $-filter))
(vals (pcase (nth 2 elt)
('+ '(-1 1))
('- '(1 -1))
(_ (error "Invalid element: %s" elt)))))
`(<> (if ,$-filter ,@vals)
(if ,&-filter ,@vals))))
;;;;;; The API
(defun citre-readtags-sorter (&rest args)
"Return a sorter expression based on ARGS.
The return value can be used as the :sorter argument in
`citre-readtags-get-tags'.
Each element of FIELDS can be:
- A symbol. For example, `input' means sort based on the input
field, in ascending order.
- A list
(field SYMBOL +/-)
For example, `(field line +)' means sorting based on the line
field, in ascending order, and `(field line -)' means in
descending order.
- A list
(OPERATOR SYMBOL +/-)
For example, `(length name +)' means sorting based on the
lengths of the tag names, in ascending order, and `(length name
-)' means in the descending order.
- A list
(expr SORTER-EXPR)
For example, `(expr (if (and $name &name) (<> $name &name) 0))'
means sorting based on the `name' field. SORTER-EXPR can be
any valid sorter expression, but it should be built with care:
always check if the fields are avaliable (like in the example)
to prevent runtime error of readtags.
- A list
(filter FILTER-EXPR +/-)
For example, `(filter (eq? $kind \"file\") +)' means puting
tags with \"file\" kind above others, and `(filter (eq? $kind
\"file\") -)' means putting them below others.
In readtags, if you sort directly based on a field that's missing
in some lines, it will throw an error. Here, all above variants
except the \"filter\" one are processed so that this won't
happen, and the order of tags involving missing fields is
uncertain. For \"filter\" variant, it's recommended to build the