Navigating Julia source code files using Xref in Emacs
In recent versions of Emacs, two packages (maybe more) have been incorporated to help with writing code. These are Eglot (aka Emacs Polyglot) and tree-sitter (Emacs specific description). I've not used either of these so will not try to explain what they can do.
Xref
I'm old skool and use a system that has been part of Emacs for at least a decade1, xref. The xref code describes itself as
[providing] a somewhat generic infrastructure for cross referencing commands, in particular "find-definition".
This capability is pretty much all that I need an editor to give me when editing source code. If you want more, the packages mentioned above may be more suitable.
Xref allows you to find definitions of variables, types, and functions. For instance, consider the code in this screenshot:
Point is indicated by the solid light blue block cursor on the first line and is on the Julia data type Layout
. The numbers on the left are line numbers, displayed by Emacs using display-line-numbers-mode
with display-line-numbers-type
set to 'visual
which shows the current line number with other lines relative to it.
Invoking xref-find-definitions
pops up a buffer which looks like
This buffer shows to possible definitions for Layout
, one as a struct
in a file called QTHEN.jl
and the other as a module
in a file called Layout.jl
. Hitting RET
on any of the specific lines of code in this buffer will navigate to the actual line in the source file:
At this point, xref-go-back
will go back to where xref-find-definitions
was invoked. The history of jumps is recorded and both xref-go-back
and xref-go-forward
will allow you to traverse this history.2 This feature is key to the use of Xref: being able to jump back and forward in references means you do not lose your place in whatever code you are writing. See this recent post by Chris Maiorana.
Tags
For xref to work, you have to define tags for all of the source code you want considered by xref. For a description of tags, check out the Xref part of the Emacs manual.
Emacs comes with the etags
program which extracts information from source files to create a tags table, usually in a file called TAGS
. Unfortunately, etags
does not have specifications for the Julia language. Luckily, there is an extended version of etags
, called exuberant-ctags
(at least on Debian) which can be made to understand Julia code by creating a language file using the julia-ctags
package3.
Most documentation assumes that you have a single tags table for all your code. This can be useful, in that you can find anything in your code base with it, but sometimes you would rather have a more discriminating search. For instance, when looking for the definition of a symbol in a Julia package, I would like Emacs to first look for such a symbol in the current package and only look in other packages if the symbol is not present locally. Luckily, Emacs has a variable, tags-table-list
, which specifies the set of directories in which to look for tags files, with the search considering the directories in turn until the desired symbol is found.
I use the following shell script to periodically (via cron
) update all my tags tables. I have a tags table for each Julia package, all of which are siblings of each other in a [...]/research/julia
directory, a tags table the combines all the symbols from all of my Julia packages, and then a tags table that also includes the symbols defined in the base Julia system.
#!/bin/sh -f # # generate tags, especially for Julia projects. For each project, we # generate a project specific TAGS file. We also generate one for all # projects together. These different tags files will be used by Emacs # by defining the `tags-table-list` variable. ctags="${HOME}/synced/emacs/julia-ctags/ctags" myjuliapackages="${HOME}/synced/research/julia" # # generate full set of tags: echo "Generating full set of tags in ${myjuliapackages}" ctags-exuberant -R -e --options=${ctags} --totals=yes -f ${myjuliapackages}/TAGS ${myjuliapackages} # now for each project for file in $(ls -1 ${myjuliapackages}) do project="${myjuliapackages}/${file}" echo "Checking project ${project}" if [ -d ${project} ] then echo "Processing project ${project}:" ctags-exuberant -R -e --options=${ctags} --totals=yes -f ${project}/TAGS ${project} fi done # generate set of tags for the full Julia system echo "Processing full Julia base package:" juliabase="${HOME}/git/julia/base" ctags-exuberant -R -e --options=${ctags} --totals=yes -f ${juliabase}/TAGS ${juliabase}
The default value of tags-table-list
for me is
("~/synced/research/julia/TAGS" "~/git/julia/base/TAGS")
which includes first the tags file for all of my Julia packages combined and then the tags for the Julia system itself. For any project where I would like the search to consider symbols in that package initially before looking in all the other packages, I define a local variable:
# Local Variables: # tags-table-list: ("../TAGS" "../../TAGS" "~/git/julia/base/TAGS") # End:
noting that the tags file for a specific package is in the parent directory for the actual source code due to Julia coding conventions.
Blog navigation
Previous: Calling python code from Julia for objective function in optimization
Index
You can find me on Mastodon should you wish to comment on this entry or the whole blog.
References
Footnotes:
probably longer but the oldest entry in the git revision history for xref.el
is from December 2014
In general, I avoid specifying the key bindings for commands as my own Emacs configuration is highly customised. Most of the commands I mention do have default bindings and you can use the where-is
command (bound by default to C-h w
) to find where any command is bound to.