emacs + lsp mode 找不到类型定义

问题用到的代码: rfm: fork from https://github.com/padgettr/rfm - Gitee.com

如下图右边,可以在/usr/include 下面找到 _GdkDragContext 的类型定义

但是,如果光标停留在emacs里 GdkDragContext 类型上再 lsp find definition, 我会跳转到如下图:

也就是GdkDragContext 的定义,我如何才能进一步跳转到 _GdkDragContext定义呢?

之所以会问这个问题,是因为我在调试程序过程中遇到在 gdb里显示 GdkDragContext 类型的变量为 incomplete type : issue when adding Link option besides copy and move in drag and drop · Issue #2 · padgettr/rfm · GitHub

我不确定这是个emacs 问题,也许是Makefile相关的问题,也许是别的啥问题,但我不知道这个问题还能到哪里去问。因为表现上(在emacs里跳转到定义)和这个貌似也密切相关,所以就在这里问了,谢谢!

还有,上个图片对应gdktypes.h在emacs里提示有好多错误,但我实际上make这个项目是没有错的,我通常都会先 make clean, 然后再 bear make

  1. 看你的截图我感觉有点找不到北。
  2. 环境&版本信息没交代。
  3. emacs -Q 试一下。
  4. Flycheck 可有可无。

环境 gentoo linux, About emacs 里显示的版本号是 29.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.34。

gentoo 上面安装emacs 时的USE flag如下: Installed versions: 29.0.9999(29-vcs)*l(01:45:40 PM 09/24/2022)(acl alsa cairo dbus gif gmp gpm gtk gui inotify jpeg lcms png ssl svg systemd threads tiff xft xpm zlib -X -Xaw3d -aqua -athena -dynamic-loading -games -gfile -gsettings -gzip-el -harfbuzz -imagemagick -jit -json -kerberos -libxml2 -livecd -m17n-lib -mailutils -motif -selinux -sound -source -sqlite -toolkit-scroll-bars -webp -wide-int -xwidgets)

emacs -Q 是啥?我执行这个命令后emacs打开,直接进了scratch buffer 看不到版本和环境信息。

我感觉是项目配置没搞好 (e.g. compile_commands.json)

我按照 GTK+-3.24.34 ,下载 gtk+3的包,解压, configure, bear make 后,可以看到 compile_comands.json 文件。 emacs 进去后,lsp-find-reference 貌似可以工作,但是还是没法进入 _GdkDragContext, grep 倒是可以找到 _GdkDragContext 所在的文件。

你用的什么lsp client?ccls or clangd?makefile或者compile_commands.json对不对,有没有检查过? 这问题提得大家找不着北

我用 clangd, 这个上图白底蓝字的状态栏里有显示。

至于lsp-client, 不知道自己理解得对不对: 我在 .emacs 启动文件里面添加的是 lsp-mode, 从 emacs package manager 里看到的是 lsp-mode-20210521.446 这个包。

我不知道还有别的啥lsp-client。 不管是否相同,首先按 https://www.linuxfromscratch.org/blfs/view/svn/x/gtk3.html 下载和安装gtk+3包,./configure之后 bear make 生成 compile_commands.json, 您能否从 GdkDragContext 定义通过 lsp-find-definition 跳转到 _GdkDragContext 定义?

如果您是可以的,那么可能是我这边配置或版本啥的问题。

至于检查 Makefile 以及compile_commands.json, 我水平不够。但我想gtk+3是如此基础及常用的一个包。

至于我这边lsp client 的配置过程,我自己的记录是 https://blog.csdn.net/weixin_42417818/article/details/113076926

gdktypes.h 里面有 typedef struct _GdkDragContext GdkDragContext; 这一行,注释说是 forward declaration of commonly used types

而_GdkDragContext 是定义在gdkdndprivate.h 文件里的

但我并没 在 gdktypes.h 文件里找到直接或间接 #include gdkdndprivate.h

是不是说这个forward declaration, 只要某个引用 GdkDndContext 类型的文件(假如叫A),直接或间接 include 了 gdktypes.h, 同时A也直接或间接 include 了 gdkdndprivate.h 文件 就可以了? 也就是说gdktypes.h 和 gdkdndprivate.h 文件只有通过文件A才发生关系,脱离A的上下文就没有关系? 另外,有没有啥方便的方法,找到一个具体的文件A?

不管如何,在 gdktypes.h 文件里 _GdkDndContext上无法通过lsp-find-definition 跳转到 gdkdndprivate.h 里 _GdkDndContext 定义上总是有些让人困惑的。我观察到的现象到底是我这里的特例,还是本该如此,抑或只是clangd的某种局限?

更新:有可能是clangd的问题,我在 Emacs lsp-mode with clangd cannot find definition from forward declaration - clangd - LLVM Discussion Forums 里发了下这个问题

见鬼,上面在llvm论坛发的帖子访问不到了。

我如果在emacs 里面手工打开gdkdndprivate.h这个文件,lsp-find-definition 就可以了。重新启动emacs 又没用了。

感觉 Getting started with clangd — Extra Clang Tools 10 documentation 里面说的background index 和这个有关。

我在compile_commands.json相同目录下是可以看到 .cache/clangd/index 目录的,下面好多.idx 文件,难道都没起作用?

Gtk 就是故意把 private data 设成 incomplete type,只对用户暴露一个指针。GObject 模拟 private data 的典型套路即是如此

LSP 找不到定义是预期行为。因为 typedef struct _X X 上下都没有 struct _X 的定义,而且这完全是作者故意的。如果你真的想要跳转到 struct _X ,就要依赖于别的工具,LSP 不行(不改源代码的话)。最简单的,可以用 dumb-jump。

(讲真,写 C,LSP 不如 tags……)

1 个赞

LSP 不行(不改源代码的话)

你是说emacs lsp-mode 代码不行还是 clangd 不行?clangd 的文档说是有 background index 和 手工索引(clangd-indexer),但我在机器上没能 locate 到 clangd-indexer, 应该有这么一个执行文件吗?

再说如果lsp不行,如何解释我打开 gdkdndprivate.h文件后lsp-find-definition就能跳转,而且关掉这个文件后(只要不重启emacs),还能跳转并自动打开文件。看上去像是有某种基于打开的文件建立的内存索引。如果只是内存索引,如何解释 .cache/clangd/index 目录下的那么多.idx文件?

(讲真,写 C,LSP 不如 tags……)

我目前还没把自己定位成c programmer, 更倾向与定位成开源桌面supporter,最多也就是小修小改,fix bug。我之前在windows平台上也是一开始先做支持很长时间再开始集中精力写代码的。后来因为开源闭源啦,保密协议之类的观点转到了gnu/linux。目前对我来说,读代码定位问题更重要,而我发现,要定位一个问题,有时候要牵涉到c,c++,python, shell,java,rust, … 。lsp 和 tags 我都不熟,lsp比较新,听上去貌似有道理,所以一开始就选lsp.

看gtk代码是因为发现 gtk_drag_context_get_actions 在wayland 和 xorg 下表现不一样(我在问题里给了链接)

我说 LSP 不行,意思其实是:只依赖于 context 的 precise goto def 不行。任何这个原理的工具都不行。LSP 只是一个接口,肯定是无所谓能不能的,但是如果你不要 Language Server 提供的 “Server” 功能的话又干嘛用 LSP 呢?(换句话说,在我们平时使用 “LSP” 这个词的意义上,把 tags 做成一个 LSP server 没什么意义,虽然 technically 它就是 LSP server。)我就这么用这个词了。clangd 当然也可以让你跳转,但是并不是 precise 的,在 LSP ~= “context-aware” 的意义上,我觉得不太 “LSP”。你可以不同意我对这个词的用法。 :^)

不妨自己试试下面这么小的项目,在 typedef struct _Priv Priv 上也跳不过去。

// fuckpriv.h
struct _Priv {
     int a;
};
// fuck-impl.c
#include "fuck.h"
#include "fuckpriv.h"

static Priv global = {
     .a = 100
};

Priv *get_priv()
{
     return &global;
}

int read_priv(Priv *priv)
{
     return priv->a;
}
// fuck.c
#include <stdio.h>
#include "fuck.h"

int main()
{
     Priv *p = get_priv();
     int x = read_priv(p);
     printf("%d\n", x);
}
// fuck.h
typedef struct _Priv Priv;

Priv *get_priv();
int read_priv(Priv *priv);

更难的是,你完全可以把 fuck.[ch] 和不同的 fuck-impl1 fuck-impl2 fuck-impl3 link 起来… 那 lsp server 要提供精确的跳转就得分析 Makefile 了——目前还没人做这件事。实际上上面的代码稍作修改,就可以让 clangd 跳转到错误的定义上。

打开新文件后,lsp client 向 clangd 发送 textDocument/didOpen 通知,clangd 此时开始做 AST build 和 diagnostics 再把 diagnostics 通过 textDocument/publishDiagnostics 发送回 lsp client,这时候就能看到小红线了——显然得让 lsp server 知道有这个文件、分析过,才会给出 diagnostics。然而在 fuck.c 和 fuck.h 里时,如何让 lsp server 知道 fuckpriv.h 存在呢?明显上下文里是没有的。

clangd 在加入新文件之后,会为它建立 index。context-aware go-to def 如果没找到精确结果,会做文本匹配(参看 SymbolIndex 类的接口)。这解释了为什么打开 .h 后就能“找到”定义了,虽然确切地说,是“猜到”。

BackgroundIndex

听起来更适合 tags 了(

可以跳啊,为什么说跳不过去

❯ clangd --version
Apple clangd version 14.0.0 (clang-1400.0.29.202)
Features: mac+xpc
Platform: x86_64-apple-darwin22.1.0

你的版本是?

对了,你得重启一下 emacs。

替代的做法是:关掉 fuckpriv.h,然后重启 lsp server。

试了试clangd,还真跳不过去,我用ccls是可以的

是可以。看来 ccls 和 clangd 的 index 工作方式不太一样。

edit: ccls 看起来会在启动时把所有东西都 index 一遍. ccls README:

It starts indexing the whole project (including subprojects if exist) parallelly when you open the first file, while the main thread can serve requests before the indexing is complete.

看了下代码好像又和 clangd 是一样的,不太清楚哪里有区别了……

又试了试,fuck.h上面include fuckpriv.h文件clangd就可以找到了,我猜这里编译参数加上–include=fuckpriv.h应该也有效

手动 symver 的也会找不到定义(

还没来的及实验你提供的小例子,先感谢下,并谢谢对于 textDocument/didOpen的解释。

LSP 只是一个接口,肯定是无所谓能不能的,但是如果你不要 Language Server 提供的 “Server” 功能的话又干嘛用 LSP 呢?(换句话说,在我们平时使用 “LSP” 这个词的意义上,把 tags 做成一个 LSP server 没什么意义,虽然 technically 它就是 LSP server。)

lsp 强调编辑器和编译器间的接口统一; tags 作为一个名词,我猜近似与编译输出的symbol和其他信息,也就是强调索引数据。但吸引我一开始选lsp-mode的一个宣传说的是,在建立这个索引的过程,lsp server 用的就是你的编译器,而传统的tags 建立这个索引时用的 代码可能和你的编译过程执行的代码有小的出入。当然,这个我之是看文字解释,自己还没能力去核实代码。

你完全可以把 fuck.[ch] 和不同的 fuck-impl1 fuck-impl2 fuck-impl3 link 起来… 那 lsp server 要提供精确的跳转就得分析 Makefile 了——目前还没人做这件事

我的理解是compile_commands.json 里面会包含 fuch.[ch] 到底和哪个impl link起来,不管你用的是make还是cmake还是别的啥。 我用 bear make 生成 compile_commands.json. bear 的解释说是用的比较"hacky"的办法,替换了底层的库,以便截获项目编译时编译器的命令及参数。