AllanChain / blog

Blogging in GitHub issues. Building with Astro.
https://allanchain.github.io/blog/
MIT License
13 stars 0 forks source link

Working with and formatting Julia in Neovim #193

Open AllanChain opened 2 years ago

AllanChain commented 2 years ago

View Post on Blog

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

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

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...
}

And don't forget to run

:PackerCompile
:PackerInstall

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
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
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,
    }
}

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,
}

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

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

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",
    },
  },
}