Working with and formatting Julia in Neovim

2022-11-16
Header: julia-nvim

It requires some effort to make Neovim work pleasantly with Julia, especially the formatting part. But it's possible with null-ls and precompiling.

Julia is a language designed for scientific computing. Since it's not a general-purpose language, it's lesser-known in the community of programmers, and thus the support is not as rich as Python. The good news is that the community of Julia is mature enough, the editor support in VS Code is great, and it requires just a little effort to have a pleasant editing experience in Neovim.

Basic setup #

I'm using Neovim with NvChad. NvChad comes with simple plugin management based on packer.nvim, LSP installation and configuring stuff with mason.nvim and nvim-lspconfig, Syntax highlighting with nvim-treesitter.

To enable syntax highlighting for Julia, just run

:TSInstall julia
vim

To install Julia LSP, first, open the mason window and find julia-lsp, and install it. Then you need to configure the LSP as described in NvChad's documentation

-- ~/.config/nvim/lua/custom/plugins/lspconfig.lua
local on_attach = require("plugins.configs.lspconfig").on_attach
local capabilities = require("plugins.configs.lspconfig").capabilities

local lspconfig = require "lspconfig"
local servers = { "html", "cssls", "clangd", "julials" }
--                 a bunch of other LSPs...  ^^^^^^^^^

for _, lsp in ipairs(servers) do
  lspconfig[lsp].setup {
    on_attach = on_attach,
    capabilities = capabilities,
  }
end
lua

Another cool feature of Julia is that you can use Unicode symbols for variable names and some operations. Typing \alpha<Tab> in Julia REPL will give you the symbol α. If used properly, it can make the math-heavy code more readable and elegant. BeautifulAlgorithms.jl has a bunch of examples. And enabling this feature in Neovim is as easy as installing the julia-vim plugin.

-- ~/.config/nvim/lua/custom/plugins/init.lua
return {
  ["JuliaEditorSupport/julia-vim"] = {},
  -- Others...
}
lua

And don't forget to run

:PackerCompile
:PackerInstall
vim

The remaining problem: formatting #

NvChad used to come with null-ls.nvim installed, but removed it afterward. It's easy to install null-ls.nvim.

[!NOTE] null-ls is not maintained. Please use its fork none-ls instead.

Although JuliaFormatter.jl provides formatting for Julia, null-ls doesn't have built-in support for that. Therefore, I must write the integration myself.

In Julia REPL: (] means entering Pkg mode)

]add JuliaFormatter
julia
cd ~/.config/nvim/lua/custom
mkdir plugins/null-ls
mv plugins/null-ls.lua plugins/null-ls/init.lua
vim plugins/null-ls/julia.lua
bash
local h = require("null-ls.helpers")
local methods = require("null-ls.methods")

return {
    method = methods.internal.FORMATTING,
    name = "JuliaFormatter",
    meta = {
        url = "https://github.com/domluna/JuliaFormatter.jl",
        description = "An opinionated code formatter for Julia.",
    },
    filetypes = { "julia" },
    generator = h.formatter_factory {
        command = "juliafmt",
        args = {
            "-e",
            "using JuliaFormatter; println(format_text(String(read(stdin))))",
        },
        to_stdin = true,
        timeout = 30000,
    }
}
lua

And add julia-formatter to null-ls sources:

local ok, julia = pcall(require, "custom.plugins.null-ls.julia")
if not ok then
  return
end


-- ...
local sources = {
  -- ...
  julia,
}

null_ls.setup {
  sources = sources,
}
lua

Then you just open a Julia file with Neovim, wait for the LSP ready, and hit <leader> f m to format the code.

However, this is not the final solution. JuliaFormatter can be very slow to format files because the JIT compilation is quite slow. The formatting usually takes 10 to 20 seconds. One solution is provided at JuliaFormatter.jl#633 (issue-comment). We just need to precompile the library. Also, JuliaFormatter doesn't pick up project configuration if passed via stdin, so I have to change the juliafmt file whenever I want to change some options, and the changes are applied "globally". That's quite inconvenient, so we'd better use a temp file.

My solution is to put the following content to ~/.local/bin/juliafmt:

#!/bin/bash

OUTPUT_SYSIMAGE=~/.local/lib/juliafmt.so
FORMAT_CMD="using JuliaFormatter; format_file(\"$1\")" 

if [ "$1" == "--compile" ]; then
  echo "using JuliaFormatter; format_file(\"/tmp/juliafmt.jl\")" > /tmp/juliafmt.jl
  julia -e 'using Pkg
  Pkg.activate(temp=true)
  Pkg.add(["JuliaFormatter", "PackageCompiler"])
  using PackageCompiler
  create_sysimage(
    ["JuliaFormatter"],
    sysimage_path="'$OUTPUT_SYSIMAGE'",
    precompile_execution_file="/tmp/juliafmt.jl"
  )'
else
  if [ -f "$OUTPUT_SYSIMAGE" ]; then
    julia -J $OUTPUT_SYSIMAGE  -e "$FORMAT_CMD"
  else
    julia -e "$FORMAT_CMD"
  fi
fi
bash

And run

chmod u+x ~/.local/bin/juliafmt
# If you don't run compile, the script will fall back to old JIT-every-time behavior
juliafmt --compile
shell

If everything works, now you have a juliafmt executable to replace julia -e 'blah blah'. Now we can just change the plugins/null-ls/julia.lua to

local h = require "null-ls.helpers"
local methods = require "null-ls.methods"

return {
  method = methods.internal.FORMATTING,
  name = "JuliaFormatter",
  meta = {
    url = "https://github.com/domluna/JuliaFormatter.jl",
    description = "An opinionated code formatter for Julia.",
  },
  filetypes = { "julia" },
  generator = h.formatter_factory {
    command = "juliafmt",
    to_temp_file = true,
    from_temp_file = true,
    args = {
      "$FILENAME",
    },
  },
}
lua
Leave your comments and reactions on GitHub