From db62d51b65a3dfa487ff56b921224593f0ca14b0 Mon Sep 17 00:00:00 2001 From: THEON-1 Date: Sun, 4 Jan 2026 17:46:21 +0100 Subject: [PATCH] clang + julia lsp --- lua/lsp/arduino.lua | 1 - lua/lsp/clangd.lua | 104 +++++++++++++++++++++++++++++++++ lua/lsp/init.lua | 3 +- lua/lsp/julia.lua | 136 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 lua/lsp/clangd.lua create mode 100644 lua/lsp/julia.lua diff --git a/lua/lsp/arduino.lua b/lua/lsp/arduino.lua index ce112f0..610a18e 100644 --- a/lua/lsp/arduino.lua +++ b/lua/lsp/arduino.lua @@ -6,7 +6,6 @@ local config = { }, filetypes = { "arduino", - "cpp", }, root_dir = function (bufnr, on_dir) local fname = vim.api.nvim_buf_get_name(bufnr) diff --git a/lua/lsp/clangd.lua b/lua/lsp/clangd.lua new file mode 100644 index 0000000..4b7302c --- /dev/null +++ b/lua/lsp/clangd.lua @@ -0,0 +1,104 @@ +---@brief +--- +--- https://clangd.llvm.org/installation.html +--- +--- - **NOTE:** Clang >= 11 is recommended! See [#23](https://github.com/neovim/nvim-lspconfig/issues/23). +--- - If `compile_commands.json` lives in a build directory, you should +--- symlink it to the root of your source tree. +--- ``` +--- ln -s /path/to/myproject/build/compile_commands.json /path/to/myproject/ +--- ``` +--- - clangd relies on a [JSON compilation database](https://clang.llvm.org/docs/JSONCompilationDatabase.html) +--- specified as compile_commands.json, see https://clangd.llvm.org/installation#compile_commandsjson + +-- https://clangd.llvm.org/extensions.html#switch-between-sourceheader +local function switch_source_header(bufnr, client) + local method_name = 'textDocument/switchSourceHeader' + ---@diagnostic disable-next-line:param-type-mismatch + if not client or not client:supports_method(method_name) then + return vim.notify(('method %s is not supported by any servers active on the current buffer'):format(method_name)) + end + local params = vim.lsp.util.make_text_document_params(bufnr) + ---@diagnostic disable-next-line:param-type-mismatch + client:request(method_name, params, function(err, result) + if err then + error(tostring(err)) + end + if not result then + vim.notify('corresponding file cannot be determined') + return + end + vim.cmd.edit(vim.uri_to_fname(result)) + end, bufnr) +end + +local function symbol_info(bufnr, client) + local method_name = 'textDocument/symbolInfo' + ---@diagnostic disable-next-line:param-type-mismatch + if not client or not client:supports_method(method_name) then + return vim.notify('Clangd client not found', vim.log.levels.ERROR) + end + local win = vim.api.nvim_get_current_win() + local params = vim.lsp.util.make_position_params(win, client.offset_encoding) + ---@diagnostic disable-next-line:param-type-mismatch + client:request(method_name, params, function(err, res) + if err or #res == 0 then + -- Clangd always returns an error, there is no reason to parse it + return + end + local container = string.format('container: %s', res[1].containerName) ---@type string + local name = string.format('name: %s', res[1].name) ---@type string + vim.lsp.util.open_floating_preview({ name, container }, '', { + height = 2, + width = math.max(string.len(name), string.len(container)), + focusable = false, + focus = false, + title = 'Symbol Info', + }) + end, bufnr) +end + +---@class ClangdInitializeResult: lsp.InitializeResult +---@field offsetEncoding? string + +---@type vim.lsp.Config +local config = { + cmd = { 'clangd' }, + filetypes = { 'c', 'cpp', 'objc', 'objcpp', 'cuda' }, + root_markers = { + '.clangd', + '.clang-tidy', + '.clang-format', + 'compile_commands.json', + 'compile_flags.txt', + 'configure.ac', -- AutoTools + '.git', + }, + capabilities = { + textDocument = { + completion = { + editsNearCursor = true, + }, + }, + offsetEncoding = { 'utf-8', 'utf-16' }, + }, + ---@param init_result ClangdInitializeResult + on_init = function(client, init_result) + if init_result.offsetEncoding then + client.offset_encoding = init_result.offsetEncoding + end + end, + on_attach = function(client, bufnr) + vim.api.nvim_buf_create_user_command(bufnr, 'LspClangdSwitchSourceHeader', function() + switch_source_header(bufnr, client) + end, { desc = 'Switch between source/header' }) + + vim.api.nvim_buf_create_user_command(bufnr, 'LspClangdShowSymbolInfo', function() + symbol_info(bufnr, client) + end, { desc = 'Show symbol info' }) + end, +} + +vim.lsp.config['clangd'] = config +vim.lsp.enable('clangd') + diff --git a/lua/lsp/init.lua b/lua/lsp/init.lua index ff45efd..24c066d 100644 --- a/lua/lsp/init.lua +++ b/lua/lsp/init.lua @@ -1,6 +1,7 @@ # https://github.com/neovim/nvim-lspconfig/blob/master/lsp require("lsp.arduino") +require("lsp.clangd") +require("lsp.julia") require("lsp.latex") require("lsp.lua") require("lsp.python") - diff --git a/lua/lsp/julia.lua b/lua/lsp/julia.lua new file mode 100644 index 0000000..7c7306c --- /dev/null +++ b/lua/lsp/julia.lua @@ -0,0 +1,136 @@ +---@brief +--- +--- https://github.com/julia-vscode/julia-vscode +--- +--- LanguageServer.jl, SymbolServer.jl and StaticLint.jl can be installed with `julia` and `Pkg`: +--- ```sh +--- julia --project=~/.julia/environments/nvim-lspconfig -e 'using Pkg; Pkg.add("LanguageServer"); Pkg.add("SymbolServer"); Pkg.add("StaticLint")' +--- ``` +--- where `~/.julia/environments/nvim-lspconfig` is the location where +--- the default configuration expects LanguageServer.jl, SymbolServer.jl and StaticLint.jl to be installed. +--- +--- To update an existing install, use the following command: +--- ```sh +--- julia --project=~/.julia/environments/nvim-lspconfig -e 'using Pkg; Pkg.update()' +--- ``` +--- +--- Note: In order to have LanguageServer.jl pick up installed packages or dependencies in a +--- Julia project, you must make sure that the project is instantiated: +--- ```sh +--- julia --project=/path/to/my/project -e 'using Pkg; Pkg.instantiate()' +--- ``` +--- +--- Note: The julia programming language searches for global environments within the `environments/` +--- folder of `$JULIA_DEPOT_PATH` entries. By default this simply `~/.julia/environments` + +local root_files = { 'Project.toml', 'JuliaProject.toml' } + +local function activate_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 + ---@diagnostic disable-next-line: param-type-mismatch + 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 + +local cmd = { + 'julia', + '--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, SymbolServer, StaticLint + 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) + ]], +} + +---@type vim.lsp.Config +local config = { + cmd = cmd, + filetypes = { 'julia' }, + root_markers = root_files, + on_attach = function(_, bufnr) + vim.api.nvim_buf_create_user_command(bufnr, 'LspJuliaActivateEnv', activate_env, { + desc = 'Activate a Julia environment', + nargs = '?', + complete = 'file', + }) + end, +} + +vim.lsp.config['julials'] = config +vim.lsp.enable('julials') +