diff --git a/README.md b/README.md index 073b260..318c7f8 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,27 @@ ### KNOWN ISSUES * when using airblade/vim-rooter: Changes Vim working directory to project root.( https://github.com/airblade/vim-rooter ) - doesn't work well with nerdtree because it unsets `autochdir` and because of that, I can't open NerdTree in the VCS root on vim start. -### TODO -I started with vim but now, I'm using neovim... the config has bloated quite a bit and its compatibility with vim may have been broken. -At some point, I might have to split the config for vim and neovim exclusive. - -neovim has tree-sittersupport, native lua support, more exclusive plugins, better out-of-the-box config, several other optimisations (e.g. [better file change detection](https://github.com/neovim/neovim/issues/1380) (you can use [this workaround](https://github.com/GLaDOS-418/vim/blob/ea23b01022f56358030163471ed2f484ad9d4407/vimrc#L430) ) ), -it has an inbuilt library 'Checkhealth' to see if everything's installed properly or not. It has more robust async support (RPC API), native lsp support and a better dap support, embedded terminal support, floating windows etc. -a few of which eventually found its ways to vim but, apparently nvim does them better. - -You can embed nvim into other editors (e.g. [vscode-neovim](https://github.com/vscode-neovim/vscode-neovim), [firenvim](https://github.com/glacambre/firenvim) ), -no more half-baked vim emulations and more work is being done on this and more. +### ~~TODO~~ Migration Notes + +~~I started with vim but now, I'm using neovim... the config has bloated quite a bit and its compatibility with vim may have been broken. At some point, I might have to split the config for vim and neovim exclusive.~~ + +This part is mostly done now. I already ended up splitting plugin stuff for vim and nvim, while keeping the common settings shared. +So this is not really a TODO anymore, more like the reasons why nvim support got added here in the first place. + +As for reasons (excuses) for migration away from vim towards nvim: + +* nvim has tree-sitter support through [`nvim-treesitter`](https://github.com/nvim-treesitter/nvim-treesitter). +* ~~native lua support~~. Nvim has built-in [Lua support](https://neovim.io/doc/user/lua.txt.html). Latest Vim also has [a Lua interface](https://vimhelp.org/if_lua.txt.html) when built with `+lua` (REF: [regular Vim has lua?! | r/neovim](https://www.reddit.com/r/neovim/comments/11gkxgj/regular_vim_has_lua/)) +* nvim has [`:checkhealth`](https://neovim.io/doc/user/pi_health.html), which is useful when plugins or external tools stop working. +* ~~more robust async support~~. Nvim has a proper [API/RPC surface](https://neovim.io/doc/user/api/). Latest Vim also has [jobs, channels, and `lsp` / `dap` channel modes](https://vimhelp.org/channel.txt.html). +* nvim has a native [LSP client](https://neovim.io/doc/user/lsp.html) in core. +* dap support is better for me in practice. Vim now has [DAP protocol plumbing in channels](https://vimhelp.org/channel.txt.html), but that is not the same as a builtin debug client. See the [DAP spec](https://microsoft.github.io/debug-adapter-protocol/). +* ~~embedded terminal support~~. Nvim has a [terminal](https://neovim.io/doc/user/terminal.html). Latest Vim also has [terminal windows](https://vimhelp.org/terminal.txt.html). +* ~~floating windows / popup kind of UI~~. Latest Vim also has [popup windows](https://vimhelp.org/popup.txt.html). +* file change detection was another reason i kept leaning nvim way back then; see [this old nvim issue](https://github.com/neovim/neovim/issues/1380). In Vim I had to use [workarounds like this](https://github.com/GLaDOS-418/vim/blob/ea23b01022f56358030163471ed2f484ad9d4407/vimrc#L430). +* nvim can be embedded into other editors through things like [vscode-neovim](https://github.com/vscode-neovim/vscode-neovim) and [firenvim](https://github.com/glacambre/firenvim). +* Lua plugins and user config fit nvim more naturally because [Lua is part of the runtime model](https://neovim.io/doc/user/lua.txt.html). +* Biggest of all, vim could have only vim plugins, nvim can have both vim and nvim plugins. (although i haven't tried with the optional lua support in vim) + +A few of these did eventually find its ways to vim too. but for this config, nvim still seems better imo. +but, given i spent too much time configuring vim, i'm still keeping the vim config around (sunk cost maybe?). \ No newline at end of file diff --git a/nvim/lua/lsp_cfg.lua b/nvim/lua/lsp_cfg.lua index de19a57..cf4c257 100644 --- a/nvim/lua/lsp_cfg.lua +++ b/nvim/lua/lsp_cfg.lua @@ -2,9 +2,83 @@ --- LSP CONFIG ----------------------------------------- -local zero = require("lsp-zero") +-- Native LSP completion capabilities. +-- Why this exists: +-- - nvim-cmp advertises extra completion features to language servers. +-- - without this, LSP completion still works, but it is less capable. +-- ref: +-- - https://github.com/hrsh7th/cmp-nvim-lsp +local capabilities = require("cmp_nvim_lsp").default_capabilities() + +-- Apply shared defaults to every server, then override per-server below. +-- This keeps the common path small now that lsp-zero is gone. +vim.lsp.config("*", { + capabilities = capabilities, +}) + +-- Shared formatting helper for both the LSP attach map and the global alias. +-- Why conform instead of raw vim.lsp.buf.format(): +-- - some filetypes already use formatter chaining/fallback rules here. +-- - this keeps and lf aligned. +local format_with_conform = function() + require("conform").format({ + timeout_ms = 1000, + async = false, + lsp_fallback = true, + }) +end + +-- Stop all LSP clients attached to one buffer. +-- - built-in `:LspStop` is not always exposed in this setup/runtime. +-- - this gives me one stable command I can remember. +-- - use `client:stop()` here; `vim.lsp.stop_client()` is deprecated. +local stop_lsp_for_buffer = function(bufnr) + local clients = vim.lsp.get_clients({ bufnr = bufnr }) + + if vim.tbl_isempty(clients) then + vim.notify("No LSP clients attached to this buffer.", vim.log.levels.INFO) + return false + end + + local client_names = {} + for _, client in ipairs(clients) do + table.insert(client_names, client.name) + client:stop() + end + + vim.notify("Stopped LSP for this buffer: " .. table.concat(client_names, ", "), vim.log.levels.INFO) + return true +end + +-- Restart all LSP clients for one buffer by stopping them, then re-editing. +-- Why `:edit`: +-- - LSP servers are enabled from the normal lspconfig/Mason path. +-- - reopening the buffer is enough to trigger attach again. +local restart_lsp_for_buffer = function(bufnr) + if stop_lsp_for_buffer(bufnr) then + vim.cmd("edit") + end +end + +----------------------------------------- +--- LSP COMMAND HELPERS +----------------------------------------- -zero.on_attach(function(_, bufnr) +-- To add new commands: +-- - keep these buffer-scoped unless there is a real need for a global action +-- - put the real logic in a Lua helper above, then expose a tiny command here +vim.api.nvim_create_user_command("LspBufStop", function() + stop_lsp_for_buffer(0) +end, { desc = "Stop LSP clients attached to the current buffer" }) + +vim.api.nvim_create_user_command("LspBufRestart", function() + restart_lsp_for_buffer(0) +end, { desc = "Restart LSP clients attached to the current buffer" }) + +-- Buffer-local LSP maps. +-- Called from the native `LspAttach` event so mappings only exist when a +-- language server is actually attached to the buffer. +local attach_lsp_keymaps = function(bufnr) local opts = { buffer = bufnr, remap = false } vim.keymap.set("n", "K", "lua vim.lsp.buf.hover()", opts) @@ -21,15 +95,11 @@ zero.on_attach(function(_, bufnr) vim.keymap.set("i", "", "lua vim.lsp.buf.signature_help()", opts) vim.keymap.set("n", "ca", "lua vim.lsp.buf.code_action()", opts) --- vim.keymap.set({ 'n', 'x' }, '', 'lua vim.lsp.buf.format({async = true})', opts) - vim.keymap.set({ "n", "x" }, "", function() - require("conform").format({ - timeout_ms = 1000, - async = false, - lsp_fallback = true, - }) - end, opts) + vim.keymap.set({ "n", "x" }, "", format_with_conform, opts) -- Inlay hints + -- Why disabled by default: + -- - they are useful on demand, but too noisy as an always-on default. vim.lsp.inlay_hint.enable(false, { bufnr = bufnr }) -- disabled by default vim.keymap.set("n", "lh", function() @@ -65,19 +135,38 @@ zero.on_attach(function(_, bufnr) ---------------------------------------------------- vim.keymap.set("n", "k", "ClangdSwitchSourceHeader", opts) vim.keymap.set("n", "th", "ClangdTypeHierarchy", opts) -end) +end -- required later during formatter/linter auto setup local lsp_augroup = vim.api.nvim_create_augroup("Lsp", { clear = true }) +-- Native attach hook replaces the old lsp-zero on_attach callback. +-- How to add something new: +-- - add new buffer-local LSP mappings inside `attach_lsp_keymaps()` +-- - add new attach-time behavior in this autocmd callback if it truly depends +-- on an active client +vim.api.nvim_create_autocmd("LspAttach", { + group = lsp_augroup, + callback = function(args) + attach_lsp_keymaps(args.buf) + end, +}) + ----------------------------------------- --- INSTALL LSP SERVERS ----------------------------------------- +-- Install and auto-enable LSP servers through Mason. -- find names for servers at: -- https://mason-registry.dev/registry/list -- https://github.com/williamboman/mason-lspconfig.nvim#available-lsp-servers +-- How to add a new server: +-- 1. add its lspconfig name to `ensure_installed` +-- 2. if it needs custom behavior, add a `vim.lsp.config("", {...})` +-- block in the setup section below +-- 3. if it should stay installed but NOT auto-start, add it to +-- `automatic_enable.exclude` -- NOTE: don't add earthlyls. It hangs on certain Earthfile. --- To check what LSP server is active on current file run --- :lua print(vim.inspect(vim.lsp.get_active_clients())) @@ -97,9 +186,12 @@ require("mason-lspconfig").setup({ "bashls", -- bash "pylsp", -- python }, - handlers = { - zero.default_setup, - jdtls = zero.noop, + automatic_enable = { + -- Java usually wants a dedicated jdtls flow, so keep Mason installs but + -- do not auto-enable it from the generic path. + exclude = { + "jdtls", + }, }, }) @@ -108,50 +200,59 @@ require("mason-lspconfig").setup({ ----------------------------------------- -- https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md - -vim.lsp.config('clangd', { +-- How to add a custom server config: +-- - use `vim.lsp.config("", {...})` +-- - only add a block when the default Mason/native config is not enough +-- - keep shared defaults in `vim.lsp.config("*", ...)` above + +-- clangd: extra flags for background indexing, clang-tidy, and richer header +-- completion behavior. +vim.lsp.config("clangd", { single_file_support = true, capabilities = capabilities, - cmd = { "clangd", "--background-index", "--clang-tidy" , "--completion-style=detailed", "--header-insertion=iwyu"}, + cmd = { "clangd", "--background-index", "--clang-tidy", "--completion-style=detailed", "--header-insertion=iwyu" }, }) -- NOTE: disabled because mason installed stylua requires a GLIBC version that's ABI-incompatible with my system. -- vim.lsp.enable('stylua') -vim.lsp.config('lua_ls', { - on_init = function(client) - if client.workspace_folders then - local path = client.workspace_folders[1].name - if - path ~= vim.fn.stdpath('config') - and (vim.uv.fs_stat(path .. '/.luarc.json') or vim.uv.fs_stat(path .. '/.luarc.jsonc')) - then - return - end - end - - client.config.settings.Lua = vim.tbl_deep_extend('force', client.config.settings.Lua, { - runtime = { - -- Tell the language server which version of Lua you're using (LuaJIT since it's Neovim) - version = 'LuaJIT', - -- Tell the language server how to find Lua modules same way as Neovim (see `:h lua-module-load`) - path = { - 'lua/?.lua', - 'lua/?/init.lua', - }, - }, - -- Make the server aware of Neovim runtime files - workspace = { - checkThirdParty = false, - library = { - vim.env.VIMRUNTIME - } - } - }) - end, - settings = { - Lua = {} - } +-- lua_ls: teach the server about Neovim's runtime and avoid stomping over +-- project-local `.luarc.json` / `.luarc.jsonc` files when they already exist. +vim.lsp.config("lua_ls", { + capabilities = capabilities, + on_init = function(client) + if client.workspace_folders then + local path = client.workspace_folders[1].name + if + path ~= vim.fn.stdpath("config") + and (vim.uv.fs_stat(path .. "/.luarc.json") or vim.uv.fs_stat(path .. "/.luarc.jsonc")) + then + return + end + end + + client.config.settings.Lua = vim.tbl_deep_extend("force", client.config.settings.Lua, { + runtime = { + -- Tell the language server which version of Lua you're using (LuaJIT since it's Neovim) + version = "LuaJIT", + -- Tell the language server how to find Lua modules same way as Neovim (see `:h lua-module-load`) + path = { + "lua/?.lua", + "lua/?/init.lua", + }, + }, + -- Make the server aware of Neovim runtime files + workspace = { + checkThirdParty = false, + library = { + vim.env.VIMRUNTIME, + }, + }, + }) + end, + settings = { + Lua = {}, + }, }) -- require("sonarlint").setup({ @@ -172,13 +273,15 @@ vim.lsp.config('lua_ls', { -- }, -- }) --- default setup of servers --- do I need this after mason-lspconfig/handlers ? --- lsp_zero.setup_servers({'rust_analyzer', 'gopls'}) - ----------------------------------------- --- INSTALL FORMATTERS & LINTERS ----------------------------------------- + +-- Non-LSP tools managed through Mason. +-- How to add something new: +-- - add the Mason package name to `ensure_installed` +-- - if it formats code, wire it into conform below +-- - if it lints code, wire it into nvim-lint below require("mason-tool-installer").setup({ ensure_installed = { -- "stylua", @@ -191,7 +294,7 @@ require("mason-tool-installer").setup({ "gotests", "golangci-lint", - "clang-format", + --"clang-format", " there's some issue during mason's installation and it was getting stuck. "cpplint", -- "cmake-format", -- not sure about the name @@ -283,11 +386,20 @@ vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, { ----------------------------------------- --- DIAGNOSTICS ----------------------------------------- -zero.set_sign_icons({ - error = "✘", - warn = "▲", - hint = "H", - info = "»", + +-- Diagnostic signs. +-- Why configure this explicitly: +-- - native vim.diagnostic replaced lsp-zero's sign helper +-- - keeping the icons here preserves the old visual language +vim.diagnostic.config({ + signs = { + text = { + [vim.diagnostic.severity.ERROR] = "✘", + [vim.diagnostic.severity.WARN] = "▲", + [vim.diagnostic.severity.HINT] = "H", + [vim.diagnostic.severity.INFO] = "»", + }, + }, }) ----------------------------------------- @@ -299,8 +411,12 @@ zero.set_sign_icons({ --- ************************ local cmp = require("cmp") -local cmp_action = require("lsp-zero").cmp_action() --- local cmp_format = require("lsp-zero").cmp_format({ +local luasnip = require("luasnip") + +-- Completion popup formatting. +-- What this does: +-- - keeps symbol/icon, label, and source aligned in readable columns +-- - trims noisy signature text from the right-hand menu local cmp_format = { fields = { "kind", "abbr", "menu" }, format = function(entry, item) @@ -316,7 +432,6 @@ local cmp_format = { path = "[PATH]", luasnip = "[SNIP]", calc = "[CALC]", - nvim_lsp_signature_help = "[SIG]", look = "[LOOK]", }, })(entry, item) @@ -416,6 +531,9 @@ end, { desc = 'Switch Copilot model' }) --- COMPLETION SOURCES --- ************************ +-- Skip some heavier completion sources for very large files. +-- Why: +-- - completion latency matters more than extra sources once files get big local bufIsBig = function(bufnr) local max_filesize = 100 * 1024 -- 100 KB local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(bufnr)) @@ -428,6 +546,9 @@ end -- default sources for all buffers -- https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources +-- How to add a new source: +-- - add it to this table +-- - give it a menu label in `cmp_format` above if you want it to show cleanly local default_cmp_sources = cmp.config.sources({ { name = "nvim_lsp" }, @@ -436,7 +557,6 @@ local default_cmp_sources = cmp.config.sources({ { name = "emoji" }, -- hrsh7th/cmp-emoji { name = "path" }, -- hrsh7th/cmp-path { name = "luasnip" }, -- saadparwaiz1/cmp_luasnip - { name = "nvim_lsp_signature_help" }, -- hrsh7th/cmp-nvim-lsp-signature-help { name = "calc" }, -- hrsh7th/cmp-calc { name = "git" }, -- petertriho/cmp-git { name = "codecompanion" }, @@ -480,6 +600,9 @@ require("luasnip.loaders.from_vscode").lazy_load() require("luasnip.loaders.from_vscode").lazy_load({ paths = { vim.fn.stdpath("config") .. "/snips" } }) -- friendly-snippets - extend snippet groups +-- How to add a new snippet subgroup: +-- - register it in `nvim/snips/package.json` +-- - then extend the base filetype here, like cpp -> {"cppdoc", "gtest"} require("luasnip").filetype_extend("c", { "cdoc" }) -- gtest stays a snippet group layered onto cpp; it is not a separate editor filetype. require("luasnip").filetype_extend("cpp", { "cppdoc", "gtest" }) @@ -491,8 +614,28 @@ require("luasnip").filetype_extend("cpp", { "cppdoc", "gtest" }) cmp.setup({ mapping = { [""] = cmp.mapping.complete(), - [""] = cmp_action.luasnip_supertab(), - [""] = cmp_action.luasnip_shift_supertab(), + -- Super-tab behavior: + -- 1. move in cmp menu when it is open + -- 2. otherwise expand or jump through snippets + -- 3. otherwise fall back to literal + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_locally_jumpable() then + luasnip.expand_or_jump() + else + fallback() + end + end, { "i", "s" }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.locally_jumpable(-1) then + luasnip.jump(-1) + else + fallback() + end + end, { "i", "s" }), [""] = cmp.mapping.confirm({ behavior = cmp.ConfirmBehavior.Replace, select = false, -- do not insert first item on list on @@ -505,7 +648,7 @@ cmp.setup({ snippet = { -- REQUIRED - you must specify a snippet engine expand = function(args) - require("luasnip").lsp_expand(args.body) -- For `luasnip` users. + luasnip.lsp_expand(args.body) -- For `luasnip` users. end, }, diff --git a/vim/plugins/nvim.vim b/vim/plugins/nvim.vim index ecbd857..34ab7c5 100644 --- a/vim/plugins/nvim.vim +++ b/vim/plugins/nvim.vim @@ -83,7 +83,6 @@ Plug 'obsidian-nvim/obsidian.nvim' " access obsidian from nvim " LSP SUPPORT {{{1 "------------------------------------------------------------ -Plug 'VonHeikemen/lsp-zero.nvim', {'branch': 'v3.x'} " lsp config Plug 'williamboman/mason.nvim', {'do': ':MasonUpdate' } " install LSP servers Plug 'williamboman/mason-lspconfig.nvim' " bridge between lspconfig and mason Plug 'WhoIsSethDaniel/mason-tool-installer.nvim' " install third party tools @@ -110,7 +109,6 @@ Plug 'hrsh7th/cmp-nvim-lua' " nvim-cmp source for neovim Lua API Plug 'hrsh7th/cmp-emoji' " nvim-cmp source for emojis Plug 'hrsh7th/cmp-path' " nvim-cmp source for filesystem paths Plug 'hrsh7th/cmp-calc' " evaluate mathematical expressions -Plug 'hrsh7th/cmp-nvim-lsp-signature-help' " displaying function signatures with the current parameter emphasized Plug 'petertriho/cmp-git' " git source for nvim-cmp Plug 'octaltree/cmp-look' " source for linux 'look' tool Plug 'saadparwaiz1/cmp_luasnip' @@ -182,7 +180,7 @@ Plug 'HampusHauffman/block.nvim' Plug 'numToStr/Comment.nvim' Plug 'stevearc/aerial.nvim' " alternative to majutsushi/tagbar Plug 'Bekaboo/dropbar.nvim' " breadcrumbs -" Plug 'SmiteshP/nvim-navic' " TODO: check dropbar vs nvim-navic + lsp-zero +" Plug 'SmiteshP/nvim-navic' " TODO: check dropbar vs nvim-navic Plug 'kevinhwang91/nvim-ufo' " set up code folding Plug 'nicolas-martin/region-folding.nvim' " region folding diff --git a/vim/plugins/nvim_settings.vim b/vim/plugins/nvim_settings.vim index a889cda..45abf99 100644 --- a/vim/plugins/nvim_settings.vim +++ b/vim/plugins/nvim_settings.vim @@ -73,8 +73,10 @@ nnoremap td TodoTelescope " - ~/.vim/plugged/nvim-treesitter/README.md nnoremap n lua require('neogen_cfg').run() -" lsp-zero {{{2 -nnoremap lf LspZeroFormat +" lsp {{{2 +" keep the formatting alias, but point it directly at conform now that +" native vim.lsp owns the attach/config flow instead of lsp-zero. +nnoremap lf lua require('conform').format({ timeout_ms = 1000, async = false, lsp_fallback = true }) " snippets {{{2 let g:snipMate = { 'snippet_version' : 1 }