Fork me on GitHub

在Emacs里面,查找symbol并跳转到其定义上是通过etags来完成的,和Vim的相类似。但是在用了一段时间之后,发觉etags的跳转在对python的支持有时候很不智能,经常会跳转到import语句而不是def语句,这个让我颇为恼火,当时就下决心要抽个时间看看Emacs里面的实现是怎么回事,有没有什么改进的余地。

首先简单的介绍一下etags的用法。一般要用etags,就要经过下面几步:

  1. 在源文件的根目录下,执行后面的语句:find . -name '*.c' -exec etags -a {} \;

    这个会生成一个TAGS文件, 是Emacs用来查找tags的默认名字。

  2. 打开emacs,在想要看定义的symbol(变脸或函数)上面按M-.(英文里面的句号), 或者直接M-x find-tag来查找。然后会提示TAGS的目录,输入就是了。

  3. 一般来说,到了这一步,Emacs就会跳转到对应的symbol定义处了。

在讲解之前,先说清楚一个概念,就是tag。tag就是在etags里面识别出来的一个作为标识symbol。

那么上面的1主要处理的是tag的生成,而2是从生成的tag里面查找。所以我的问题主要是在2里面,也就是Emacs是怎么查找tag的。(不过在了解了机制之后,发现对于python来说,原来我的问题是落在1里面的,这个是后话)

在Emacs里面,所有和find-tag函数相关的东西都定义在etags.el里面,这个也是提供和etags对接的一个Emacs库。

在调用find-tag的时候,就会执行下面的语句:

1
2
3
4
5
6
7
8
(defun find-tag (tagname &optional next-p regexp-p)
  (interactive (find-tag-interactive "Find tag: "))
  (let* ((buf (find-tag-noselect tagname next-p regexp-p)) ;****
         (pos (with-current-buffer buf (point))))
    (condition-case nil
        (switch-to-buffer buf)
      (error (pop-to-buffer buf)))
    (goto-char pos)))

实际上调用的第3行的find-tag-noselect。那么就看看find-tag-noselect干了些什么。

下面是find-tag-noselect的定义:

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
(defun find-tag-noselect (tagname &optional next-p regexp-p)
  (interactive (find-tag-interactive "Find tag: "))

  (setq find-tag-history (cons tagname find-tag-history))
  ;; Save the current buffer's value of `find-tag-hook' before
  ;; selecting the tags table buffer.  For the same reason, save value
  ;; of `tags-file-name' in case it has a buffer-local value.
  (let ((local-find-tag-hook find-tag-hook))
    (if (eq '- next-p)
        ;; Pop back to a previous location.
        (if (ring-empty-p tags-location-ring)
            (error "No previous tag locations")
          (let ((marker (ring-remove tags-location-ring )))
            (prog1
                ;; Move to the saved location.
                (set-buffer (or (marker-buffer marker)
                                (error "The marked buffer has been deleted")))
              (goto-char (marker-position marker))
              ;; Kill that marker so it doesn't slow down editing.
              (set-marker marker nil nil)
              ;; Run the user's hook.  Do we really want to do this for pop?
              (run-hooks 'local-find-tag-hook))))
      ;; Record whence we came.
      (ring-insert find-tag-marker-ring (point-marker))
      (if (and next-p last-tag)
          ;; Find the same table we last used.
          (visit-tags-table-buffer 'same)
        ;; Pick a table to use.
        (visit-tags-table-buffer)
        ;; Record TAGNAME for a future call with NEXT-P non-nil.
        (setq last-tag tagname))
      ;; Record the location so we can pop back to it later.
      (let ((marker (make-marker)))
        (with-current-buffer
            ;; find-tag-in-order does the real work.
            (find-tag-in-order
             (if (and next-p last-tag) last-tag tagname)
             (if regexp-p
                 find-tag-regexp-search-function
               find-tag-search-function)
             (if regexp-p
                 find-tag-regexp-tag-order
               find-tag-tag-order)
             (if regexp-p
                 find-tag-regexp-next-line-after-failure-p
               find-tag-next-line-after-failure-p)
             (if regexp-p "matching" "containing")
             (or (not next-p) (not last-tag)))
          (set-marker marker (point))
          (run-hooks 'local-find-tag-hook)
          (ring-insert tags-location-ring marker)
          (current-buffer))))))

首先看到find-tag-noselectnext-p为负数的情况下是会跳回到之前的tag,而不是跳转到当前tag的位置。
而在接下来的判断中,find-tag-noselect首先把tag-table打开,然后记录下当前的tag,以便在之后跳回到这个tag。

最重要的就是调用了find-tag-in-order。从名字可以看出,这个函数是从tag-table中逐个逐个的找tag。实际上,find-tag-in-order是首先利用一个general的search函数粗略的匹配tag,然后再用order参数(一个函数列表)里面的函数按照不同的标准来进行进一步的匹配。

下面是find-tag-in-order的定义:

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
(defun find-tag-in-order (pattern
                          search-forward-func
                          order
                          next-line-after-failure-p
                          matching
                          first-search)
  ;; State is saved so that the loop can be continued.
  (let (file                            ;name of file containing tag
        tag-info                        ;where to find the tag in FILE
        (first-table t)
        (tag-order order)
        (match-marker (make-marker))
        goto-func
        (case-fold-search (if (memq tags-case-fold-search '(nil t))
                              tags-case-fold-search
                            case-fold-search))
        )
    (save-excursion

      (if first-search
          (setq tag-lines-already-matched nil)
        (visit-tags-table-buffer 'same))

      ;; Get a qualified match.
      (catch 'qualified-match-found

        ;; Iterate over the list of tags tables.
        (while (or first-table
                   (visit-tags-table-buffer t))

          (and first-search first-table
               ;; Start at beginning of tags file.
               (goto-char (point-min)))

          (setq first-table nil)

          ;; Iterate over the list of ordering predicates.
          (while order
            (while (funcall search-forward-func pattern nil t)
              ;; Naive match found.  Qualify the match.
              (and (funcall (car order) pattern)
                   ;; Make sure it is not a previous qualified match.
                   (not (member (set-marker match-marker (save-excursion
                                                           (beginning-of-line)
                                                           (point)))
                                tag-lines-already-matched))
                   (throw 'qualified-match-found nil))
              (if next-line-after-failure-p
                  (forward-line 1)))
            ;; Try the next flavor of match.
            (setq order (cdr order))
            (goto-char (point-min)))
          (setq order tag-order))
        ;; We throw out on match, so only get here if there were no matches.
        ;; Clear out the markers we use to avoid duplicate matches so they
        ;; don't slow down editting and are immediately available for GC.
        (while tag-lines-already-matched
          (set-marker (car tag-lines-already-matched) nil nil)
          (setq tag-lines-already-matched (cdr tag-lines-already-matched)))
        (set-marker match-marker nil nil)
        (error "No %stags %s %s" (if first-search "" "more ")
               matching pattern))

      ;; Found a tag; extract location info.
      (beginning-of-line)
      (setq tag-lines-already-matched (cons match-marker
                                            tag-lines-already-matched))
      ;; Expand the filename, using the tags table buffer's default-directory.
      ;; We should be able to search for file-name backwards in file-of-tag:
      ;; the beginning-of-line is ok except when positioned on a "file-name" tag.
      (setq file (expand-file-name
                  (if (memq (car order) '(tag-exact-file-name-match-p
                                          tag-file-name-match-p
                                          tag-partial-file-name-match-p))
                      (save-excursion (forward-line 1)
                                      (file-of-tag))
                    (file-of-tag)))
            tag-info (funcall snarf-tag-function))

      ;; Get the local value in the tags table buffer before switching buffers.
      (setq goto-func goto-tag-location-function)
      (tag-find-file-of-tag-noselect file)
      (widen)
      (push-mark)
      (funcall goto-func tag-info)

      ;; Return the buffer where the tag was found.
      (current-buffer))))

其中最为重要的就是while的一部分。首先order不为空,然后用search-forward-func(默认是search-forward)来查找这个tag的pattern,如果找到了,就再用(car order)来进行仔细的匹配。所以order里面的函数就是匹配的关键,就看里面有些什么匹配函数了。order是在find-tag-noselect传进来的。

order在tag是正则表达式的时候是find-tag-regexp-tag-order,而在tag是普通的字符串的时候就是find-tag-tag-order。这里我着重看了一下find-tag-tag-order,它默认是下面的列表:

1
2
3
4
5
6
7
8
(tag-exact-file-name-match-p
 tag-file-name-match-p
 tag-exact-match-p
 tag-implicit-name-match-p
 tag-symbol-match-p
 tag-word-match-p
 tag-partial-file-name-match-p
 tag-any-match-p)

所以你可以看到,他是先按tag是不是完全匹配文件名,然后再去匹配看看是不是匹配tag,如果还是找不到的话,就去部分的匹配。而一般来说只有在一个tag在精准匹配里面找不到的时候,才可能去部分的匹配。也还有一种可能,那就生成tag的时候本身就生成错了,导致一些不是tag的地方也变成了tag。而在Python中,etags生成的tag就真的不全是我们想要的!

用etags对python进行tag的生成的时候,会把import语句也当成是tag的一种,从而生成在TAGS文件里面,所以用emacs的find-tag跳转的时候就会发现,当我想找一个tag的定义的时候,他却经常的跳到了import的地方,就是这个原因。

这里也大概的说一下etags生成的tag-table格式(我还没有看过etags的源码,是通过看TAGS文件总结出来的)。

x7F
<filename>,<No1>
<Matched tag line>x7F[<tag>x01]<Line num>,<No2>
.....

每个tag文件都用x7F来分隔每个scan的文件。

后记:

通过查看了解etags.el的实现,我大致明白了Emacs里面一个library的构成和编写方式。也通过它明白了Emacs里面调试的一些小技巧,通过edebug能够比较好的了解一个elisp程序的运行状态。通过这个库,我明白其实Emacs里面的灵活性主要是通过elisp实现的,而其实它上面的插件都不是太成熟,但是由于elisp具有较好的可读性,而且用emacs的人都比较经得起折腾(或者说hack源码或者自己动手的能力较强),所以导致了emacs的用户都说Emacs是神器。事实上,我现在用的也还不是太熟,算是在进阶阶段,感觉Emacs有些功能还是不错的,至少在拓展性上我觉得比Vim要强,不过打字速度还真没有vim来的快,看个人用的怎么样了。

理解Emacs Lisp Library的编写,就需要看看requireprovide这两个函数,还有一些defgroupdefcustom


知识共享许可协议
作品airekans创作,采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。


blog comments powered by Disqus

Published

18 May 2012

Tags