diff --git a/home/.config/nvim/init.lua b/home/.config/nvim/init.lua index 4181c48..ebd07d7 100644 --- a/home/.config/nvim/init.lua +++ b/home/.config/nvim/init.lua @@ -30,5 +30,7 @@ require("terminal") -- Initialize the custom window management functionality require("window_management").setup() +require("lsp") + -- See ":help vim.highlight.on_yank()" setup_yank_highlight() diff --git a/home/.config/nvim/lua/language_servers/clangd.lua b/home/.config/nvim/lua/language_servers/clangd.lua index 3a8cb40..45d55ce 100644 --- a/home/.config/nvim/lua/language_servers/clangd.lua +++ b/home/.config/nvim/lua/language_servers/clangd.lua @@ -1,9 +1,9 @@ -local util = require "lspconfig.util" +local utils = require("utils") -- https://clangd.llvm.org/extensions.html#switch-between-sourceheader local function switch_source_header(bufnr) - bufnr = util.validate_bufnr(bufnr) - local clangd_client = util.get_active_client_by_name(bufnr, "clangd") + bufnr = utils.validate_bufnr(bufnr) + local clangd_client = vim.lsp.get_clients({ bufnr = bufnr, name = "clangd" })[1] local params = { uri = vim.uri_from_bufnr(bufnr) } if clangd_client then clangd_client.request("textDocument/switchSourceHeader", params, function(err, result) @@ -21,66 +21,6 @@ local function switch_source_header(bufnr) end end -local function symbol_info() - local bufnr = vim.api.nvim_get_current_buf() - local clangd_client = util.get_active_client_by_name(bufnr, "clangd") - if not clangd_client or not clangd_client.supports_method "textDocument/symbolInfo" then - return vim.notify("Clangd client not found", vim.log.levels.ERROR) - end - local params = vim.lsp.util.make_position_params() - clangd_client.request("textDocument/symbolInfo", params, function(err, res) - if err or #res == 0 then - -- Clangd always returns an error, there is not 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, - border = require("lspconfig.ui.windows").default_options.border or "single", - title = "Symbol Info", - }) - end, bufnr) -end - -local lsp_maps = { - { - "ko", - function() switch_source_header(0) end, - }, - { - "K", - symbol_info, - } -} -local keymaps = { n = {} } -for i, _ in ipairs(lsp_maps) do - local binding, cmd = unpack(lsp_maps[i]) - keymaps.n[binding] = { cmd = cmd } -end -require("utils").add_keymaps(keymaps) - -local root_files = { - ".clangd", - ".clang-tidy", - ".clang-format", - "compile_commands.json", - "compile_flags.txt", - "configure.ac", -- AutoTools -} - -local default_capabilities = { - textDocument = { - completion = { - editsNearCursor = true, - }, - }, - offsetEncoding = { "utf-16" }, -} - return { cmd = { "clangd", @@ -96,26 +36,27 @@ return { "--log=error", -- Log only errors }, filetypes = { "c", "cpp", "objc", "objcpp", "cuda", "proto" }, - root_dir = function(fname) - return util.root_pattern(unpack(root_files))(fname) or util.find_git_ancestor(fname) - end, - single_file_support = true, - capabilities = default_capabilities, - docs = { - description = [[ -https://clangd.llvm.org/installation.html - -- **NOTE:** Clang >= 11 is recommended! See [#23](https://github.com/neovim/nvim-lsp/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 -]], - default_config = { - root_dir = - [[ root_pattern( ".clangd", ".clang-tidy", ".clang-format", "compile_commands.json", "compile_flags.txt", "configure.ac", ".git" ) ]], - capabilities = [[default capabilities, with offsetEncoding utf-8]], - }, + root_markers = { + ".clangd", + ".clang-tidy", + ".clang-format", + "compile_commands.json", + "compile_flags.txt", + "configure.ac", }, + on_attach = function(_, bufnr) + local lsp_maps = { + { + "ko", + function() switch_source_header(0) end, + }, + } + + local keymaps = { n = {} } + for i, _ in ipairs(lsp_maps) do + local binding, cmd = unpack(lsp_maps[i]) + keymaps.n[binding] = { cmd = cmd, opts = { buffer = bufnr } } + end + utils.add_keymaps(keymaps) + end, } diff --git a/home/.config/nvim/lua/language_servers/cmake.lua b/home/.config/nvim/lua/language_servers/cmake.lua new file mode 100644 index 0000000..c140b01 --- /dev/null +++ b/home/.config/nvim/lua/language_servers/cmake.lua @@ -0,0 +1,14 @@ +return { + cmd = { "cmake-language-server" }, + filetypes = { "cmake" }, + root_markers = { + "CMakeLists.txt", + "CMakePresets.json", + "CTestConfig.cmake", + "build", + "cmake", + }, + init_options = { + buildDirectory = "build", + }, +} diff --git a/home/.config/nvim/lua/language_servers/gopls.lua b/home/.config/nvim/lua/language_servers/gopls.lua index f495e2c..d59bc8e 100644 --- a/home/.config/nvim/lua/language_servers/gopls.lua +++ b/home/.config/nvim/lua/language_servers/gopls.lua @@ -1,12 +1,56 @@ +local mod_cache = nil + return { - merge_with_default = true, + cmd = { "gopls" }, + filetypes = { "go", "gomod", "gowork", "gotmpl" }, settings = { gopls = { ["ui.inlayhints.hints"] = { compositeLiteralFields = true, constantValues = true, - parameterNames = true - } + parameterNames = true, + }, + analyses = { + unusedparams = true, + }, + staticcheck = true, + lintTool = "golangci-lint", }, - } + }, + root_dir = function(callback) + local path = vim.fn.expand("%:p") + if not path or path == "" then + callback(nil) + return + end + + -- Asynchronously fetch GOMODCACHE if not already set + if not mod_cache then + vim.system({ "go", "env", "GOMODCACHE" }, { text = true }, function(result) + if result and result.code == 0 and result.stdout then + mod_cache = vim.trim(result.stdout) + else + vim.notify("[gopls] Unable to fetch GOMODCACHE", vim.log.levels.WARN) + mod_cache = nil + end + end) + end + + -- Check if the file is in the module cache + if mod_cache and path:sub(1, #mod_cache) == mod_cache then + local clients = vim.lsp.get_clients({ name = "gopls" }) + if #clients > 0 then + callback(clients[#clients].config.root_dir) + return + end + end + + -- Fallback: Find project root markers + local go_mod_root = vim.fs.find({ "go.work", "go.mod", ".git" }, { upward = true, path = path })[1] + if go_mod_root then + callback(vim.fs.dirname(go_mod_root)) + else + callback(nil) + end + end, } diff --git a/home/.config/nvim/lua/language_servers/lua_ls.lua b/home/.config/nvim/lua/language_servers/lua_ls.lua new file mode 100644 index 0000000..e86b233 --- /dev/null +++ b/home/.config/nvim/lua/language_servers/lua_ls.lua @@ -0,0 +1,39 @@ +return { + cmd = { "lua-language-server" }, + filetypes = { "lua" }, + root_markers = { + ".luarc.json", + ".luarc.jsonc", + ".luacheckrc", + ".stylua.toml", + "stylua.toml", + "selene.toml", + "selene.yml", + ".git" + }, + on_init = function(client) + local path = vim.tbl_get(client, "workspace_folders", 1, "name") + if not path then + return + end + + -- override the lua-language-server settings for Neovim config + client.settings = vim.tbl_deep_extend("force", client.settings, { + Lua = { + runtime = { + version = "LuaJIT" + }, + -- Make the server aware of Neovim runtime files + workspace = { + checkThirdParty = false, + library = { + vim.env.VIMRUNTIME + -- Depending on the usage, you might want to add additional paths here. + -- "${3rd}/luv/library" + -- "${3rd}/busted/library", + } + } + } + }) + end +} diff --git a/home/.config/nvim/lua/lsp.lua b/home/.config/nvim/lua/lsp.lua new file mode 100644 index 0000000..daed9ff --- /dev/null +++ b/home/.config/nvim/lua/lsp.lua @@ -0,0 +1,112 @@ +local utils = require("utils") + +local function chain_on_attach(...) + local funcs = { ... } + return function(client, bufnr) + for _, func in ipairs(funcs) do + func(client, bufnr) + end + end +end + +local function global_on_attach(client, bufnr) + vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) + + if client.server_capabilities.documentFormattingProvider then + vim.api.nvim_buf_create_user_command(bufnr, "Format", vim.lsp.buf.format, { nargs = 0 }) + vim.api.nvim_create_autocmd("BufWritePre", { + buffer = bufnr, + callback = function() + vim.lsp.buf.format() + end, + }) + end + + utils.add_keymaps({ + n = { + ["gd"] = { + cmd = function() + vim.lsp.buf.definition() + end, + opts = { + noremap = true, + silent = true, + buffer = bufnr + } + }, + ["gD"] = { + cmd = function() + vim.lsp.buf.declaration() + end, + opts = { + noremap = true, + silent = true, + buffer = bufnr + } + }, + } + }) +end + +local global_capabilities = require("blink.cmp").get_lsp_capabilities() +global_capabilities.offsetEncoding = { "utf-16" } + +vim.diagnostic.config({ + underline = true, -- Underline diagnostic errors + virtual_text = false, -- Disable inline text messages + signs = true, -- Show icons in the sign column + update_in_insert = true, -- Update diagnostics during insert mode +}) + +vim.lsp.config("*", { + capabilities = global_capabilities, + handlers = { + ["textDocument/publishDiagnostics"] = vim.lsp.diagnostic.on_publish_diagnostics, + }, + root_markers = { ".git" }, +}) + +-- Find all files in lua/language_servers and require them +-- We use them to ensure that the servers are installed and configured +local lua_files_str = vim.fn.globpath(vim.fn.stdpath("config") .. "/lua/language_servers", "*.lua", true) +local has_line_breaks = vim.fn.match(lua_files_str, [[\n]]) > -1 +-- Get an array of all the files in the directory, make sure to account for single file +local lua_files = has_line_breaks and vim.fn.split(lua_files_str, "\n") or { lua_files_str } +-- Remove path and extension and only keep the filename +local server_names = vim.tbl_map(function(file) + return vim.fn.fnamemodify(file, ":t:r") +end, lua_files) + +local errors = {} +utils.foreach(server_names, function(server_name) + local path = "language_servers/" .. server_name + local result, conf = utils.xpcallmsg( + function() return require(path) end, + "Failed to require " .. path, + errors + ) + + if not result or type(conf) ~= "table" or vim.tbl_isempty(conf) or conf.cmd == nil then + error("Invalid configuration for " .. server_name) + return + end + + conf.on_attach = (function() + if conf.on_attach then + return chain_on_attach(global_on_attach, conf.on_attach) + end + + return global_on_attach + end)() + + -- These still throw errors when wrapped by xpcall. + -- Wanted it to just handle incorrect input and let the runtime continue + -- as it would if the require was successful when wrapped. That would be great + -- for WIP LSP configuration, instead we have the ugly if statements above. + vim.lsp.config(server_name, conf) + vim.lsp.enable(server_name) +end) + +if #errors > 0 then + error(table.concat(errors, "\n")) +end diff --git a/home/.config/nvim/lua/plugs/dap.lua b/home/.config/nvim/lua/plugs/dap.lua new file mode 100644 index 0000000..10df314 --- /dev/null +++ b/home/.config/nvim/lua/plugs/dap.lua @@ -0,0 +1,119 @@ +local utils = require("utils") + +local are_stepping_keymaps_active = false +return { + "mfussenegger/nvim-dap", + dependencies = { + "rcarriga/nvim-dap-ui", + { "nvim-neotest/nvim-nio", lazy = true }, + "LiadOz/nvim-dap-repl-highlights", + "theHamsta/nvim-dap-virtual-text", + "Weissle/persistent-breakpoints.nvim", + { + "LarssonMartin1998/nvim-dap-profiles", + opts = {}, + }, + "leoluz/nvim-dap-go", + }, + config = function() + local dap = require("dap") + local dapui = require("dapui") + dapui.setup() + require("persistent-breakpoints").setup { + load_breakpoints_event = { "BufReadPost" } + } + require("dap-go").setup() + + local stepping_keymaps = { + n = { + ["m"] = { + cmd = function() + dap.step_out() + end + }, + ["n"] = { + cmd = function() + dap.step_over() + end + }, + ["i"] = { + cmd = function() + dap.step_into() + end + }, + } + } + + local function enter_debug_mode() + dapui.open() + if not are_stepping_keymaps_active then + utils.add_keymaps(stepping_keymaps) + are_stepping_keymaps_active = true + end + end + + local function exit_debug_mode() + dapui.close() + if are_stepping_keymaps_active then + utils.remove_keymaps(stepping_keymaps) + are_stepping_keymaps_active = false + end + end + + local dap_signs = { + { "DapBreakpoint", { text = "🛑", texthl = "", linehl = "", numhl = "" } }, + { "DapBreakpointRejected", { text = "🔵", texthl = "", linehl = "", numhl = "" } }, + { "DapBreakpointCondition", { text = "🟥", texthl = "", linehl = "", numhl = "" } }, + } + + for _, sign in ipairs(dap_signs) do + vim.fn.sign_define(unpack(sign)) + end + + dap.listeners.after.event_initialized["dapui_config"] = function() + enter_debug_mode() + end + dap.listeners.before.event_terminated["dapui_config"] = function() + exit_debug_mode() + end + dap.listeners.before.event_exited["dapui_config"] = function() + exit_debug_mode() + end + + require("nvim-dap-repl-highlights").setup() + require("nvim-dap-virtual-text").setup() + + local breakpoint_api = require("persistent-breakpoints.api") + utils.add_keymaps({ + n = { + ["dr"] = { + cmd = function() + dap.continue() + end + }, + ["bt"] = { + cmd = function() + breakpoint_api.toggle_breakpoint() + end + }, + ["bc"] = { + cmd = function() + breakpoint_api.set_conditional_breakpoint() + end + }, + ["br"] = { -- breakpoint remove + cmd = function() + breakpoint_api.clear_all_breakpoints() + end + }, + ["ds"] = { + cmd = function() + dap.disconnect({ terminateDebuggee = true }) + dap.close() + exit_debug_mode() + end + }, + } + }) + end, +} diff --git a/home/.config/nvim/lua/plugs/mason.lua b/home/.config/nvim/lua/plugs/mason.lua new file mode 100644 index 0000000..8adf2c2 --- /dev/null +++ b/home/.config/nvim/lua/plugs/mason.lua @@ -0,0 +1,32 @@ +return { + "williamboman/mason.nvim", + dependencies = { "WhoIsSethDaniel/mason-tool-installer.nvim" }, + config = function() + require("mason").setup({}) + require("mason-tool-installer").setup({ + ensure_installed = { + -- LLVM debugger + "codelldb", + + -- C and C++ + "clangd", + "clang-format", + + -- Rust + "rust-analyzer", + + -- Go + "gopls", + "golangci-lint", + "delve", + + -- Lua + "lua-language-server", + + -- CMake + "cmake-language-server", + "cmakelang", + }, + }) + end +} diff --git a/home/.config/nvim/lua/plugs/mason_lsp.lua b/home/.config/nvim/lua/plugs/mason_lsp.lua deleted file mode 100644 index c87b02e..0000000 --- a/home/.config/nvim/lua/plugs/mason_lsp.lua +++ /dev/null @@ -1,254 +0,0 @@ -local utils = require("utils") - -local function get_lsp_conf(default_conf, server_name) - local result, custom_conf = pcall(require, "language_servers/" .. server_name) - if not result or not custom_conf then - return default_conf - elseif custom_conf and custom_conf.merge_with_default then - return vim.tbl_deep_extend("force", default_conf, custom_conf) - end - - return custom_conf -end - -local function setup_lsp(server_names) - local lspconfig = require("lspconfig") - for _, server_name in ipairs(server_names) do - local server = lspconfig[server_name] - if server then - local server_conf = get_lsp_conf(server, server_name) - - local capabilities = server_conf.capabilities or {} - capabilities.offsetEncoding = { "utf-16" } - server_conf.capabilities = require("blink.cmp").get_lsp_capabilities(capabilities) - - server_conf.on_attach = function(client, bufnr) - vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) - - if client.server_capabilities.documentFormattingProvider then - vim.api.nvim_buf_create_user_command(bufnr, "Format", vim.lsp.buf.format, { nargs = 0 }) - vim.api.nvim_create_autocmd("BufWritePre", { - buffer = bufnr, - callback = function() - vim.lsp.buf.format() - end, - }) - end - - utils.add_keymaps({ - n = { - ["gd"] = { - cmd = function() - vim.lsp.buf.definition() - end, - opts = { - noremap = true, - silent = true - } - }, - ["gD"] = { - cmd = function() - vim.lsp.buf.declaration() - end, - opts = { - noremap = true, - silent = true - } - }, - } - }) - end - - server.setup(server_conf) - - -- Run the post_setup function if it exists - if server_conf.post_setup then - server_conf.post_setup() - end - else - error("LSP server not found: " .. server_name) - end - end -end - -local are_stepping_keymaps_active = false -local function setup_dap() - local dap = require("dap") - local dapui = require("dapui") - dapui.setup() - require("persistent-breakpoints").setup { - load_breakpoints_event = { "BufReadPost" } - } - require("dap-go").setup() - - local stepping_keymaps = { - n = { - ["m"] = { - cmd = function() - dap.step_out() - end - }, - ["n"] = { - cmd = function() - dap.step_over() - end - }, - ["i"] = { - cmd = function() - dap.step_into() - end - }, - } - } - - local function enter_debug_mode() - dapui.open() - if not are_stepping_keymaps_active then - utils.add_keymaps(stepping_keymaps) - are_stepping_keymaps_active = true - end - end - - local function exit_debug_mode() - dapui.close() - if are_stepping_keymaps_active then - utils.remove_keymaps(stepping_keymaps) - are_stepping_keymaps_active = false - end - end - - local dap_signs = { - { "DapBreakpoint", { text = "🛑", texthl = "", linehl = "", numhl = "" } }, - { "DapBreakpointRejected", { text = "🔵", texthl = "", linehl = "", numhl = "" } }, - { "DapBreakpointCondition", { text = "🟥", texthl = "", linehl = "", numhl = "" } }, - } - - for _, sign in ipairs(dap_signs) do - vim.fn.sign_define(unpack(sign)) - end - - dap.listeners.after.event_initialized["dapui_config"] = function() - enter_debug_mode() - end - dap.listeners.before.event_terminated["dapui_config"] = function() - exit_debug_mode() - end - dap.listeners.before.event_exited["dapui_config"] = function() - exit_debug_mode() - end - - require("mason-nvim-dap").setup({ - handlers = {} - }) - require("nvim-dap-repl-highlights").setup() - require("nvim-dap-virtual-text").setup() - - local breakpoint_api = require("persistent-breakpoints.api") - utils.add_keymaps({ - n = { - ["dr"] = { - cmd = function() - dap.continue() - end - }, - ["bt"] = { - cmd = function() - breakpoint_api.toggle_breakpoint() - end - }, - ["bc"] = { - cmd = function() - breakpoint_api.set_conditional_breakpoint() - end - }, - ["br"] = { -- breakpoint remove - cmd = function() - breakpoint_api.clear_all_breakpoints() - end - }, - ["ds"] = { - cmd = function() - dap.disconnect({ terminateDebuggee = true }) - dap.close() - exit_debug_mode() - end - }, - } - }) -end - -return { - "williamboman/mason.nvim", - dependencies = { - -- Mason plugins - "WhoIsSethDaniel/mason-tool-installer.nvim", - "RubixDev/mason-update-all", - - -- LSP config - "neovim/nvim-lspconfig", - "saghen/blink.cmp", - "williamboman/mason-lspconfig.nvim", - - -- DAP - "jay-babu/mason-nvim-dap.nvim", - "rcarriga/nvim-dap-ui", - "mfussenegger/nvim-dap", - { "nvim-neotest/nvim-nio", lazy = true }, - "LiadOz/nvim-dap-repl-highlights", - "theHamsta/nvim-dap-virtual-text", - "Weissle/persistent-breakpoints.nvim", - { - "LarssonMartin1998/nvim-dap-profiles", - opts = {}, - }, - "leoluz/nvim-dap-go", - }, - config = function() - -- Find all files in lua/language_servers and require them - -- We use them to ensure that the servers are installed and configured - -- Make sure that the files use the lspconfig naming convention - local lua_files_str = vim.fn.globpath(vim.fn.stdpath("config") .. "/lua/language_servers", "*.lua", true) - local has_line_breaks = vim.fn.match(lua_files_str, [[\n]]) > -1 - -- Get an array of all the files in the directory, make sure to account for single file - local lua_files = has_line_breaks and vim.fn.split(lua_files_str, "\n") or { lua_files_str } - -- Remove path and extension and only keep the filename - local custom_server_confs = vim.tbl_map(function(file) - return vim.fn.fnamemodify(file, ":t:r") - end, lua_files) - - -- Combine the default servers with the custom ones - local server_names = vim.list_extend({ - "bashls", - "cmake", - "lua_ls", - "yamlls", - "zls", - -- "ocamllsp", - "gopls", - }, custom_server_confs) - - -- Create a new table which contains the non LSP Mason installees. - -- IMPORTANT: Make sure to leave rust-analyzer out of this list, as it can cause conflicts with rustaceanvim. - -- Install rust-analyzer using your systems package manager instead. - local mason_installs = vim.list_extend({ - "clang-format", - "codelldb", - "netcoredbg", - "delve", - "golangci-lint", - -- "ocamlearlybird", - -- "ocamlformat", - }, server_names) - - require("mason").setup() - require("mason-lspconfig").setup() - require("mason-tool-installer").setup({ - ensure_installed = mason_installs, - }) - - setup_lsp(server_names) - setup_dap() - - require("mason-update-all").setup() - end, -}