Using gtags in neovim

Since LSPs are getting more and more common, the old way of indexing a whole codebase and saving that infomation in a file, then looking it up when needed is falling out of fashion.

Nevertheless, I still depend on it heavily. At work I can not use LSP for most projects, since they are (cross) compiled with a proprietary ARM compiler. LSP needs some info on where header files are and such, and it takes that usually from a compile_commands.json. Which can be generated using a program like Bear. Since there is no makefile, I could only write that compile_commands.json by hand. Which I obviously don’t want.

One of those things that does it the olde way is gtags, from GNU Global. It indexes the codebase and saves everything in a database instead of a textfile. This database can be updated incrementally, which makes subsequent generation much much faster. Also they say the lookup of tags is faster than ctags, but I never noticed.

I use gtags in Emacs all the time, using the excellent ggtags package. But in my recent endeavor getting to use neovim for coding (and mimicing Doom emacs with it as much as possbile) I learned that the gtags situation in the neovim world is a dire situation. There are almost no tutorials available, they contain outdated info etc.

So, here goes.

Installing GNU Global

First we need to get GNU Global. Install via your package manager. As usual this depends on your distro, I use Gentoo, so it is

sudo emerge -av dev-util/global

Tags are easily created, go to the project root directory use that command

gtags

That all, Global will index all subfolders and create GPATH, GRTAGS and GTAGS in that folder. There are ways to do that from vim, but using that in Emacs I found that I can get away with updating the tags very seldom. So doing it form the command line every once in a will is sufficient for me.

Neovim configuration

I use the two plugins gtags.vim and telescope-gtags from ivechan. Since I use Lazy as package manager for vim, they are installed using some simple declarations:

  {
    "ivechan/gtags.vim",
  },
  {
    "ivechan/telescope-gtags",
  },

Thats pretty much all there is to it. In order for Neovim to find the GTAGS file, its working directory must be where that GTAGS file is. (cd to that folder and then start vim in that folder or use :cd in vim)

But since I still have LSP running for some projects (mostly Rust) I have a keybinding conflict. When I am running a file with LSP attached, I want to look the definitions up with LSP, but when I am not running LSP, I wan’t to use gtags.
Emacs has the xref system for this, where you call an xref function to look up a definition and xref has a list of possibilities to resolve that request. It will then try each of the possibilities to get that tag until it has it.
For Neovim I just checked if a LSP server is attached to that buffer and if so (and if that LSP server is not copilot, because that is always running, I call LSP definitions, if not I call gtags definitions.


-- gtags
local xref_goto_definiton = function ()
    local clients = vim.lsp.get_clients( { bufnr = 0 } )
    for _, client in ipairs(clients) do
      print(client.name)
      if client.name ~= "copilot" then
        require('telescope.builtin').lsp_definitions()
        return
      end
    end
    require('telescope-gtags').showDefinition()
end
local xref_goto_references = function ()
    local clients = vim.lsp.get_clients( { bufnr = 0 } )
    for _, client in ipairs(clients) do
      print(client.name)
      if client.name ~= "copilot" then
        require('telescope.builtin').lsp_reference()
        return
      end
    end
    require('telescope-gtags').showReferences()
end
local xref_goto_definition_other_window = function ()
    local clients = vim.lsp.get_clients( { bufnr = 0 } )
    for _, client in ipairs(clients) do
      if client.name ~= "copilot" then
        require('telescope.builtin').lsp_definitions({jump_type="vsplit"})
        return
      end
    end
    require('telescope-gtags').showDefinition() -- there is no vsplit for this yet
end

vim.keymap.set('n', '<leader>cd', xref_goto_definiton, { desc = 'Goto [D]efinition' })
vim.keymap.set('n', '<leader>ch', xref_goto_definition_other_window, { desc = 'Goto [D]efinition other window' })
vim.keymap.set('n', '<leader>cD', xref_goto_references, { desc = 'Goto [R]eferences' })