nvf/modules/plugins/languages/julia.nix
2025-08-01 12:00:43 +02:00

195 lines
7 KiB
Nix

{
lib,
pkgs,
config,
...
}: let
inherit (builtins) attrNames;
inherit (lib.options) mkEnableOption mkOption;
inherit (lib.types) enum;
inherit (lib.modules) mkIf mkMerge;
inherit (lib.meta) getExe;
inherit (lib.nvim.types) mkGrammarOption singleOrListOf;
inherit (lib.generators) mkLuaInline;
inherit (lib.nvim.attrsets) mapListToAttrs;
inherit (lib.nvim.dag) entryBefore;
defaultServers = ["julials"];
servers = {
julials = {
enable = true;
cmd =
mkLuaInline
/*
lua
*/
''
{
'${getExe (pkgs.julia.withPackages ["LanguageServer"])}',
'--startup-file=no',
'--history-file=no',
'-e',
[[
# Load LanguageServer.jl: attempt to load from ~/.julia/environments/nvim-lspconfig
# with the regular load path as a fallback
ls_install_path = joinpath(
get(DEPOT_PATH, 1, joinpath(homedir(), ".julia")),
"environments", "nvim-lspconfig"
)
pushfirst!(LOAD_PATH, ls_install_path)
using LanguageServer
popfirst!(LOAD_PATH)
depot_path = get(ENV, "JULIA_DEPOT_PATH", "")
project_path = let
dirname(something(
## 1. Finds an explicitly set project (JULIA_PROJECT)
Base.load_path_expand((
p = get(ENV, "JULIA_PROJECT", nothing);
p === nothing ? nothing : isempty(p) ? nothing : p
)),
## 2. Look for a Project.toml file in the current working directory,
## or parent directories, with $HOME as an upper boundary
Base.current_project(),
## 3. First entry in the load path
get(Base.load_path(), 1, nothing),
## 4. Fallback to default global environment,
## this is more or less unreachable
Base.load_path_expand("@v#.#"),
))
end
@info "Running language server" VERSION pwd() project_path depot_path
server = LanguageServer.LanguageServerInstance(stdin, stdout, project_path, depot_path)
server.runlinter = true
run(server)
]],
}
'';
filetypes = ["julia"];
root_markers = ["Project.toml" "JuliaProject.toml"];
on_attach =
mkLuaInline
/*
lua
*/
''
function(_, bufnr)
vim.api.nvim_buf_create_user_command(bufnr, 'LspJuliaActivateEnv', activate_julia_env, {
desc = 'Activate a Julia environment',
nargs = '?',
complete = 'file',
})
end
'';
};
};
cfg = config.vim.languages.julia;
in {
options = {
vim.languages.julia = {
enable = mkEnableOption "Julia language support";
treesitter = {
enable = mkEnableOption "Julia treesitter" // {default = config.vim.languages.enableTreesitter;};
package = mkGrammarOption pkgs "julia";
};
lsp = {
enable = mkEnableOption "Julia LSP support" // {default = config.vim.lsp.enable;};
servers = mkOption {
type = singleOrListOf (enum (attrNames servers));
default = defaultServers;
description = ''
Julia LSP Server to Use
::: {.note}
The entirety of Julia is bundled with nvf, if you enable this
option, since there is no way to provide only the LSP server.
If you want to avoid that, you have to change
[vim.lsp.servers.julials.cmd](#opt-vim.lsp.servers._name_.cmd) to use
the Julia binary in {env}`PATH`, and add the `LanguageServer`
package to Julia in your devshells.
Check the source file of this option for the full `cmd`.
:::
'';
};
};
};
};
config = mkIf cfg.enable (mkMerge [
(mkIf cfg.treesitter.enable {
vim.treesitter.enable = true;
vim.treesitter.grammars = [cfg.treesitter.package];
})
(mkIf cfg.lsp.enable {
vim.luaConfigRC.julia-util =
entryBefore ["lsp-servers"]
/*
lua
*/
''
local function activate_julia_env(path)
assert(vim.fn.has 'nvim-0.10' == 1, 'requires Nvim 0.10 or newer')
local bufnr = vim.api.nvim_get_current_buf()
local julials_clients = vim.lsp.get_clients { bufnr = bufnr, name = 'julials' }
assert(
#julials_clients > 0,
'method julia/activateenvironment is not supported by any servers active on the current buffer'
)
local function _activate_env(environment)
if environment then
for _, julials_client in ipairs(julials_clients) do
julials_client:notify('julia/activateenvironment', { envPath = environment })
end
vim.notify('Julia environment activated: \n`' .. environment .. '`', vim.log.levels.INFO)
end
end
if path then
path = vim.fs.normalize(vim.fn.fnamemodify(vim.fn.expand(path), ':p'))
local found_env = false
for _, project_file in ipairs(root_files) do
local file = vim.uv.fs_stat(vim.fs.joinpath(path, project_file))
if file and file.type then
found_env = true
break
end
end
if not found_env then
vim.notify('Path is not a julia environment: \n`' .. path .. '`', vim.log.levels.WARN)
return
end
_activate_env(path)
else
local depot_paths = vim.env.JULIA_DEPOT_PATH
and vim.split(vim.env.JULIA_DEPOT_PATH, vim.fn.has 'win32' == 1 and ';' or ':')
or { vim.fn.expand '~/.julia' }
local environments = {}
vim.list_extend(environments, vim.fs.find(root_files, { type = 'file', upward = true, limit = math.huge }))
for _, depot_path in ipairs(depot_paths) do
local depot_env = vim.fs.joinpath(vim.fs.normalize(depot_path), 'environments')
vim.list_extend(
environments,
vim.fs.find(function(name, env_path)
return vim.tbl_contains(root_files, name) and string.sub(env_path, #depot_env + 1):match '^/[^/]*$'
end, { path = depot_env, type = 'file', limit = math.huge })
)
end
environments = vim.tbl_map(vim.fs.dirname, environments)
vim.ui.select(environments, { prompt = 'Select a Julia environment' }, _activate_env)
end
end
'';
vim.lsp.servers =
mapListToAttrs (n: {
name = n;
value = servers.${n};
})
cfg.lsp.servers;
})
]);
}