Citre: 先进的 Ctags 前端

logo

项目地址:https://github.com/universal-ctags/citre

Citre 也许是目前最先进的 Ctags 的编辑器前端。

基本功能

Citre 提供了自动补全(通过 completion-at-point)、xref 和 imenu 支持。

(自动补全,左右分别是 company 和 selectrum 的 UI)

Citre 提供了 citre-jump 命令,通过 completing-read 界面让你浏览和跳转到一个符号的定义:

Citre 通过 readtags 程序(Universal Ctags 自带)来使用 tags 文件。得益于 readtags 的二分搜索,Citre 即使在 Linux 内核这样的大项目中也拥有不错的速度。

citre-peek

citre-peek 是最强力的代码阅读工具。

在传统的代码阅读工具中,我们为了追踪一个调用链,经常需要打开一大堆 buffer,在它们之间来回切换以理解代码,还要时常清理掉无用的 buffer,效率十分低下。

citre-peek 会在当前 buffer 中用一个小窗口(称为 peek window)来显示符号的定义:

当然,仅仅是这样就太无聊了。请注意上图中,peek window 的最底下显示了代码阅读的历史。它是哪来的呢?事实上,你可以 peek 一个 peek window 中的符号,这称为 “peek-through”,这样就会形成一个链状的阅读历史。

不仅如此,你可以浏览这个链条,并且在某一个节点上 peek 另一个符号。这样就会形成分支,产生一个树状的历史。citre-peek 允许你浏览和编辑这棵树。

最重要的是,这一切都不需要你离开当前 buffer。当你将 peek window 收起来,就好像什么都没有发生过一样,非常清净。

听起来很复杂?事实上,citre-peek 是相当好用的,请看文档。跟着操作一遍,我相信你会喜欢上这个工具 :wink:

小故事:Universal Ctags 的主要开发者 Masatake YAMATO 也是 Citre 的贡献者(也是目前除我之外的唯一贡献者)。他在 RedHat 工作,日常阅读内核源码,因此对「好用的代码阅读工具」有迫切的需要。是他一直提供各种灵感与反馈,使我将 citre-peek 发展为目前的样子。

使用

请阅读 README。我也写了详细的用户手册与开发者手册,它们的链接都包含在 README 里面了。

重要更新

Q & A

大家在回复中提了很多很好的问题,我把它们汇总放在这里。

  • Ctags 与 LSP 之类智能工具的优缺点?回答
  • 怎样一起使用 Citre 和 LSP?wiki
  • 怎样扩展 Ctags,使 tags 文件记录我想让它记录的符号?问题回答
  • Ctags 支持 TAGS 格式(即 etags 的输出格式),Emacs 对 TAGS 格式有很好的原生/插件支持。Citre 相比它们有何长处?问题回答
  • 关于自动更新 tags 文件,问题回答
  • Ctags 与 gtags(GNU Global)各自的优缺点?回答
  • Ctags 支持多少语言?回答
  • Citre 支持多少语言?回答
  • Citre 的终极形态,问题回答
30赞

哇!感谢!紫薯🍮

看起来很不错的样子

1赞

能像这个图一样看calltree嘛?

这个图是 calltree.pl 吧 :wink:

目前 Citre 没有这个功能,而且要实现它并不是「免费的」。我的意思是 ctags 目前不能 tag 函数引用,因此仅利用 tags 文件画出这么一个图是不可能的。

如果我们自己来实现的话,以下是一种方式:

  • 对一个函数,利用 tags 文件找到它的定义。
  • tags 文件记录了定义所在的文件,以及开始和结束的行号。接下来我们自己写一个匹配「函数调用」的正则(calltree.pl 应该也是这么做的),把这个文件的这一段里调用的函数都找出来。
  • 对被调用的函数,回到第一步做相同的事情,直至达到所需的深度。

另一个方向的树(也就是一层层画出「被哪些函数调用」)怎么画,我还没有想法,但是对大工程来说想必会相当慢。

我目前不太想做这个东西,因为:

  1. 如前所述,这个功能不是免费的,我们需要对每个语言写一个正则。我希望 Citre 的工具尽量对所有 Ctags 支持的语言都可以用。
  2. 我不是很确定这样的图在实际的代码阅读中有多大用处。

如果您非常需要这个功能,我建议您利用 citre-utils.elcitre-core.el 中的 API 来做一个工具自己用,或者您可以尝试说服我实现它 :wink:

主要是我自己阅读代码时发现,一个函数/类的调用栈很长的情况下,按顺序阅读查定义的方式比较慢。对关键函数/类查引用的方式会更快一些。

比如 rgw 对象存储中 lifecycle 的实现会调用大量公共基础实现(put/get/delete)。按查定义的方式整个路径会很长且不容易定位问题。通过查引用能直接定位到较深的调用栈中。常规的查引用一般都只能查一层,这时候一个问题就是 每个引用都要开一个 buffer, 和 citre-peek 解决的问题十分类似。

后来使用 calltree.pl 能直接对完整的调用栈做一个总览,深度也可以自己控制。对查引用这个场景来说十分方便。

查引用的另一个问题是很多时候结果并不完整。比如 ccls 对很多模板引用都查不到。按我最近碰到的例子

Deleting::react 方法实际在 boost 的状态机中被调用,但是 ccls 没识别出来。使用 calltree.pl 能比较完整的显示所有路径。

感觉如果 ctags 支持查引用的话实现起来应该会更方便一些。

谢谢你的解释。我对「查引用」的用处一直不是很清楚,现在我知道一个使用场景了 :wink:

您说的这个使用方式不仅是要求「列出从 lifecycle 开始的 calltree」,而且需要「被调用函数的名字里含有 put/get/delete」,倘若对查引用的场景缺乏认识,这样的需求并不容易想到,可见 calltree.pl 的命令行界面设计是深思熟虑的。

如果我可以多了解一些查引用的使用场景,或许我可以想明白一个好的查引用工具应该是什么样的。

Ctags 可以用正则或者 optscript 实现用户自定义 tag 规则,所以如果我们想要 reference tags 的话,是可以有的。

另请参考 https://github.com/universal-ctags/ctags/issues/651

citre-peek 的思路也很赞 :+1: ,或许我可以尝试结合 reference tags 和 citre-peek 做出一个类似 calltree.pl 的界面出来。

1赞

Go for it :wink:

如果您需要帮助,请 email 我。我的 email 在 Citre 的代码里。

有种商业软件客服的聊天感觉 :rofl: :rofl: :rofl: :rofl: :rofl:

6赞

Citre 为您提供优质的支持与服务 :wink:

2赞

看起来很不错的样子,性能不高或者不方便用lsp的场景很适合

有时候在多语言的工程中,希望从一个语言直接跳到另一个语言的定义(包括从文档跳到代码中的定义),这个事也只有 Ctags 可以做到。

我在这份文档中比较了 Ctags 与现在的智能工具的优缺点,Ctags 其实是有一些不能忽视的优点的。

5赞

TAGS确实可以作为lsp的另一个选择,我正在考虑要不要集成到 Centaur Emacs

我想确认下您说的 TAGS 是指 Emacs 自带的那个 etags 生成的,还是 Ctags 生成的那个 tags 文件?

另外看到 Centaur Emacs 有 paypal 的捐赠链接,想问下您有提现过吗 :rofl: 如果有的话,是怎么操作的呢?

我用的新编译的universal-ctags,但是遇到这个错误:

Error in menu-bar-update-hook (imenu-update-menubar): (error "readtags exits 1
ctags: Unknown option: -t")

我也编译了一下最新的 uctags,但不能复现。我怀疑你用的 readtags 程序可能不是 uctags 自带的。

试一下 $ readtags -h 看看有没有这么个 option:

-t TAGFILE | --tag-file TAGFILE
    Use specified tag file (default: "tags").

Edit: 我刚想到一种可能性,你是不是把 citre-readtags-program 改成了 ctags 的路径?

感谢大神,ctags我经常用,也知道 Ctags 可以用正则或者 optscript 实现用户自定义 tag 规则,但实际这么做时又搞不定,想请教一个这方面的问题,比如我写了以下代码:

return array(
  "test_func"=>function(string $str="test_func"){
    return $str;
  }
);

这是一段php代码,返回一个数组其中定义了一个函数,将其保存为org.php文件,然后调用这个函数时我是这么写的:

$org=require("./org.php");
echo $org["test_func"]("hello");

这种写法在php里应该比较少见,但在其它语言有近似用法的不少(比如nodejs的require等 )

我用universal-ctags生成的tags文件并不识别这种写法,不能在函数调用处$org["test_func"]()跳转到函数定义,想通过自定义tag规则来支持这种写法,但正则水平不行,没搞定,只好来请教擅长ctags的高手们

对,确实是这个原因,我设置为nil就正常了。

$ cat options.ctags
--langdef=PHPext{base=PHP}
--kinddef-PHPext=a,arrayfunc,function defined in arrays
--regex-PHPext=/"(.*)"=>function/\1/a/

$ cat test.php
return array(
  "test_func"=>function(string $str="test_func"){
    return $str;
  }
);

$ ctags --options=./options.ctags --fields='*' -f - test.php
test_func       test.php        /^  "test_func"=>function(string $str="test_func"){$/;" kind:arrayfunc  line:2  language:PHPext      roles:def       extras:subparser

您可以把 options.ctags 存在 ~/.ctags.d/ 或运行 ctags 时目录的 ./.ctags.d/ 文件夹下,ctags 运行时就会自动加载它。

解析:

  • --langdef=PHPext{base=PHP}

    定义一个叫 PHPext 的新语言(取 PHP extended 之意),作为 PHP 的 subparser 使用。

    扩展一个 parser 时,定义一种新语言是被鼓励的,这样就不会和语言本身的 kind 冲突。如果运行一下 $ ctags --list-kinds=php,可以看到已经有一个简写为 a 的 kind,表示 aliases

  • --kinddef-PHPext=a,arrayfunc,function defined in arrays

    为 PHPext 定义一个简写为 a,全名为 arrayfunc 的 kind,含义是 function defined in arrays

  • --regex-PHPext=/"(.*)"=>function/\1/a/

    这一行的语法是 --regex-<LANG>=<PATTERN>/<NAME>/[<KIND>/]LONGFLAGS。我们一段一段看:

    • "(.*)"=>function

      用于匹配的模式。加括号的地方是这个函数的名字。

    • \1

      把加括号的模式匹配到的部分拿出来作为 tag 的名字。

    • a

      规定这个 tag 的 kind 为刚刚定义的 a

请参阅 Extending ctags with Regex parser (optlib)

如果还有问题,欢迎随时讨论,这样我可以多点例子来证明 ctags is hackable :wink:

2赞