├── LICENSE ├── README.md ├── basic-workflow.md ├── completion.md ├── getting-to-know-nvim.md ├── git.md ├── installing-nvim.md ├── lsp.md ├── macros.md ├── navigation.md ├── oil.md ├── plugins.md ├── scripting.md ├── telescope.md ├── vim-motions.md ├── vim.md └── why-nvim.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 AlphaKeks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # how2nvim 2 | 3 | Hey! 4 | 5 | You probably found this repository by asking a question along the lines of "How do I learn neovim?". 6 | I want to set some expectations. The documents in this repository are my attempt at introducing and 7 | explaining (neo)vim concepts as best as I can, recommending certain practices that I personally 8 | consider "good". There are many possible ways of using (neo)vim and none of them are objectively the 9 | best, but I will try to justify any concrete recommendations I give here. 10 | 11 | You can use this repository to learn (neo)vim from scratch, as reference material to come back to 12 | later, or simply to look for ideas to add to your own setup. Everything is structured with links and 13 | I try to keep every document self-contained, with references to other sources if there are knowledge 14 | requirements. 15 | 16 | If you have any suggestions, ideas, or mistakes to point out, feel free to 17 | [open an issue](https://github.com/AlphaKeks/how2nvim/issues)! 18 | 19 | ## The goals of this repository 20 | 21 | - teach you how to think about and work with vim effectively 22 | - teach you how to interact with vim and integrate it into your workflow 23 | - teach you how to get started if you're completely new 24 | - give you some cool ideas how to script neovim and extend it to fit your needs 25 | 26 | ## What this repository is **not** 27 | 28 | - an attempt to convince you how neovim is the best editor ever and you should leave your favorite 29 | IDE immediately 30 | - a neovim "distribution" that will give you a 1-click installation with 5 million plugins to 31 | recreate VSCode in your terminal as close as possible. If you want that, check out one of the 32 | following: 33 | - [LunarVim](https://www.lunarvim.org/) 34 | - [NvChad](https://nvchad.com/) 35 | - [LazyVim](https://www.lazyvim.org/) 36 | - a "simple starter template". If you want that, check out 37 | [kickstart.nvim](https://github.com/nvim-lua/kickstart.nvim). 38 | 39 | ## Some general advices 40 | 41 | - Read `:help` 42 | - Read `:help :help` 43 | - Read `:help user-manual` 44 | - Read `README.md` files 45 | - Read GitHub Wiki pages of plugins, if they exist 46 | - Ask other people questions, but [don't ask to ask](https://dontasktoask.com) 47 | - When you ask questions, [ask good ones](https://stackoverflow.com/help/how-to-ask) 48 | 49 | # Table of Contents 50 | 51 | - [Why neovim over vim](./why-nvim.md) 52 | - [How to install neovim](./installing-nvim.md) 53 | - [Introduction to vim as an editor](./vim.md) 54 | - [Getting started with vim motions](./vim-motions.md) 55 | - [Getting to know your editor](./getting-to-know-nvim.md) 56 | - [Finding your way around](./navigation.md) 57 | - [A basic development workflow](./basic-workflow.md) 58 | - [LSP](./lsp.md) 59 | - [How to install plugins](./plugins.md) 60 | - [Insert Completion](./completion.md) 61 | - [Telescope - probably the most useful plugin you will ever use](./telescope.md) 62 | - [oil.nvim - filetrees are overrated](./oil.md) 63 | - [Git workflow](./git.md) 64 | - [Macros](./macros.md) 65 | - [Scripting utilities](./scripting.md) 66 | -------------------------------------------------------------------------------- /basic-workflow.md: -------------------------------------------------------------------------------- 1 | # A basic development workflow 2 | 3 | In this section I will talk about builtin tools that vim has to offer to make your workflow actually 4 | **flow**. 5 | 6 | Coming from an IDE with inline error messages, auto-completion and a big green "run" button this 7 | kind of workflow might seem weird to you, but I encourage you to give it a fair try. neovim can have 8 | all those fancy features too (with plugins), but they're opt-in, and you should know both sides. 9 | I personally used neovim for a long time entirely relying on LSP and plugins to even be able to 10 | write code. But now that I've seen both perspectives, I developed my own personal workflow that 11 | includes LSP and semantic completion, but in a way less obvious way, leveraging neovim's builtin 12 | tools as much as makes sense. 13 | 14 | ## The quickfix list 15 | 16 | Unless you're completely new to vim you might have already heard of the **quickfix list**. This list 17 | is a universal place for you to store information that you want to note down and move between 18 | quickly. This could include error messages, search results, or really anything you want. 19 | Traditionally the quickfix list is mostly used with `:make` and `:grep`. So let us have a look at 20 | those commands! 21 | 22 | ### `:make` 23 | 24 | The `:make` command is your entry point to using tools for static analysis. vim does not try to 25 | incorporate all the language-specific functionality any user might need or want. Instead it tries to 26 | give you a framework for composing existing tools that simply work with 27 | [stdin and stdout](https://en.wikipedia.org/wiki/Standard_streams). This means they will take text 28 | or a file as input, and give you back some text on stdout or directly as a file. For example, 29 | [ESLint](https://eslint.org/) is a very popular JavaScript linter that will find problems in your 30 | code. It's a CLI tool which takes text as input, and spits out error messages as output. `:make` 31 | lets you hook up any CLI tool you want by setting the `makeprg` option and will parse its output 32 | according to your current `errorformat` option. While vim or neovim do not provide the tool itself 33 | (i.e. ESLint), they _do_ provide some basic configuration for a lot of tools via the `:compiler` 34 | command. Even though the command is called "compiler", it manages a bunch of tools, including 35 | a large list of linters (including ESLint!). This means, all you need to do is run 36 | `:compiler eslint` and now you can open any JavaScript file you want, run `:make %` and ESLint's 37 | error messages will magically appear in your quickfix list. 38 | 39 | ### Basic usage of the quickfix list 40 | 41 | You can jump between them using the `:cnext` and `:cprevious` commands. `:cclose` will close the 42 | quickfix list if you find it distracting, while `:copen` will open it back up. There is also 43 | `:cwindow` which will only open the quickfix list if there are any errors, and close it otherwise. 44 | For more information see `:help quickfix`. 45 | 46 | ### `:grep` 47 | 48 | The `:grep` command, as the name suggests, will search for text in a given set of files. This 49 | command will use the standard `grep` program found on any Linux system by default, but you can 50 | configure which command it's supposed to use with the `grepprg` option. Similarly to errors, vim 51 | needs to know how to parse the output of your search tool. `grepformat` is what you're looking for 52 | here. I personally use [ripgrep](https://github.com/BurntSushi/ripgrep) as it is by far the fastest 53 | grepping tool on the market right now. If you also want to use it, set the following `grepformat` in 54 | your configuration: `%f:%l:%c:%m` 55 | 56 | Now you can run `:grep some text` and all the occurrences of that text in any files in your current 57 | directory, or any sub-directory, will appear in your quickfix list for you to navigate. 58 | 59 | I also have this command for convenience: 60 | 61 | ```vim 62 | command! -nargs=+ Grep silent grep! | copen | redraw! 63 | ``` 64 | 65 | It defines a user command called `Grep` which takes 1 or more arguments and runs `grep` over the 66 | given arguments. It will then open the quickfix list and redraw the screen. It is basically instant 67 | even for millions of lines of code to search through and a real life saver! 68 | 69 | Here's the Lua version if you really hate vimscript for some reason: 70 | 71 | ```lua 72 | vim.api.nvim_create_user_command("Grep", "silent grep! | copen | redraw!", { nargs = "+" }) 73 | ``` 74 | 75 | As you can see, it's basically the same, just longer. 76 | 77 | ## A general workflow 78 | 79 | Assuming you have chosen your tools by now, how do you properly set this up? 80 | 81 | I will use [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) as examples here, but 82 | the general concepts are applicable to any CLI tool. 83 | 84 | ### ESLint 85 | 86 | Let's start with ESLint. As mentioned previously, ESLint is already a supported `:compiler` in vim, 87 | so all we need to do is put the following code into `after/ftplugin/javascript.lua`: 88 | 89 | ```lua 90 | vim.cmd.compiler("eslint") 91 | ``` 92 | 93 | If you now open a JavaScript file and run `:set makeprg?` you should get the following output: 94 | `makeprg=npx eslint --format compact`. This is suboptimal for 2 reasons: 95 | 96 | 1. It uses `npx` to run eslint. 97 | 2. It uses `eslint` instead of [`eslint_d`](https://github.com/mantoni/eslint_d.js). 98 | 99 | `eslint_d` is a [daemonized](https://en.wikipedia.org/wiki/Daemon_(computing)) version of `eslint`. 100 | It will keep running in the background after you invoke it for the first time and therefore every 101 | subsequent invocation is going to be a lot faster than waiting for node to do a cold start everytime 102 | you invoke `eslint`. The drawback is that you need to manually restart it when you make config 103 | changes, as it won't detect those automatically. `eslint_d restart` should do the trick. 104 | 105 | Because we want to use `eslint_d` instead, we need to adjust `makeprg`: 106 | 107 | ```lua 108 | vim.cmd.compiler("eslint") 109 | vim.bo.makeprg = "eslint_d --format compact" 110 | ``` 111 | 112 | > We still want `:compiler eslint` since it also takes care of `errorformat` for us. 113 | 114 | Now with these setup, you should be able to run `:make %` and get any reported errors into your 115 | quickfix list. Would be convenient if it ran automatically on save, you say? Sure, we can do that. 116 | 117 | Let me introduce you to **autocommands**. They are neovim's event system and basically event 118 | handlers from JavaScript. If you want detailed information about them, read `:help autocmd`. For now 119 | though all you need to know is that there's an event called `BufWritePost` which gets emitted 120 | anytime you write a buffer; _after_ you write it. This is exactly when we want to run ESLint. 121 | 122 | ```lua 123 | vim.cmd.compiler("eslint") 124 | vim.bo.makeprg = "eslint_d --format compact" 125 | 126 | -- This ensures we don't create multiple instances of the same autocommand 127 | -- 128 | -- See `:help autocmd-groups` and `:help nvim_create_augroup` 129 | local group = vim.api.nvim_create_augroup("eslint-on-save", { clear = true }) 130 | 131 | -- This is the current buffer's ID 132 | local buffer = vim.api.nvim_get_current_buf() 133 | 134 | -- Here we create our autocmd (event listener) 135 | -- 136 | -- See `:help autocmd` and `:help nvim_create_autocmd` 137 | vim.api.nvim_create_autocmd("BufWritePre", { 138 | group = group, 139 | buffer = buffer, 140 | callback = function() 141 | -- This is our current working directory, aka our project root. 142 | local cwd = vim.fn.getcwd() 143 | 144 | -- Replace `"src"` with whatever directory you keep your code in. 145 | local src = vim.fs.joinpath(cwd, "src") 146 | 147 | -- Run `eslint_d` on any JavaScript files in our `src` directory 148 | -- (or sub-directories) 149 | vim.cmd.make(src .. "/**/*.js") 150 | end, 151 | }) 152 | ``` 153 | 154 | Now, anytime you save, `eslint_d` will lint your code and your quickfix list will be filled with its 155 | diagnostics. This also has the side effect that you cursor will automatically jump to the first 156 | error; if you don't want that, replace `vim.cmd.make(src .. "/**/*.js")` with 157 | `vim.cmd("make! " .. src .. "/**/*.js")`. The `!` in `make!` will cause it _not_ to jump to the 158 | first error automatically. 159 | 160 | Since we use neovim, we can do even better than this. neovim has a dedicated `vim.diagnostic` API to 161 | display diagnostics as virtual text inside buffers. This means we can take the messages in our 162 | quickfix list and make them appear on the exact lines they are complaining about! And it doesn't 163 | take a lot of code either; `vim.diagnostic` has utility functions for exactly this. 164 | 165 | ```lua 166 | -- This is similar to the autocmd group we saw earlier, but for diagnostics, highlights and other 167 | -- neovim-only things. 168 | -- 169 | -- See `:help namespace` 170 | local namespace = vim.api.nvim_create_namespace("eslint-diagnostics") 171 | 172 | -- The `QuickFixCmdPost` event fires anytime the quickfix list is modified. 173 | -- See `:help QuickFixCmdPost` 174 | vim.api.nvim_create_autocmd("QuickFixCmdPost", { 175 | group = group, 176 | buffer = buffer, 177 | callback = function() 178 | -- Get our quickfix list 179 | local qflist = vim.fn.getqflist() 180 | 181 | -- Transform it into diagnostics 182 | local diagnostics = vim.diagnostic.fromqflist(qflist) 183 | 184 | -- Clear out any old diagnostics 185 | vim.diagnostic.reset(namespace, buffer) 186 | 187 | -- Populate the current buffer's diagnostics 188 | vim.diagnostic.set(namespace, buffer, diagnostics) 189 | end, 190 | }) 191 | ``` 192 | 193 | And with that, we are now running `eslint_d` after every buffer we save, analyzing our entire 194 | project, populating our quickfix list, which triggers diagnostics to appear in our buffer. And what 195 | did it take? Less than 30 lines of Lua without the comments! 196 | 197 | ```lua 198 | vim.cmd.compiler("eslint") 199 | vim.bo.makeprg = "eslint_d --format compact" 200 | 201 | local buffer = vim.api.nvim_get_current_buf() 202 | local augroup = vim.api.nvim_create_augroup("eslint-on-save", { clear = true }) 203 | local namespace = vim.api.nvim_create_namespace("eslint-diagnostics") 204 | 205 | vim.api.nvim_create_autocmd("BufWritePre", { 206 | buffer = buffer, 207 | group = augroup, 208 | callback = function() 209 | local cwd = vim.fn.getcwd() 210 | local src = vim.fs.joinpath(cwd, "src") 211 | 212 | vim.cmd.make(src .. "/**/*.js") 213 | end, 214 | }) 215 | 216 | vim.api.nvim_create_autocmd("QuickFixCmdPost", { 217 | buffer = buffer, 218 | group = group, 219 | callback = function() 220 | local qflist = vim.fn.getqflist() 221 | local diagnostics = vim.diagnostic.fromqflist(qflist) 222 | 223 | vim.diagnostic.reset(namespace, buffer) 224 | vim.diagnostic.set(namespace, buffer, diagnostics) 225 | end, 226 | }) 227 | ``` 228 | 229 | If you are bothered by `:make` being synchronous you can consider using 230 | [vim-dispatch](https://github.com/tpope/vim-dispatch). If you are on neovim 0.10 or higher you can 231 | also use `vim.system()` to asynchronously run shell commands (you can also use `vim.fn.jobstart()` 232 | if you are on an older version). Combined with `vim.json` you could 233 | just call `eslint_d --format json` and parse the results and build diagnostics from it 234 | yourself! 235 | 236 | Okay, now with ESLint out of the way, let's look at Prettier. 237 | 238 | ### Prettier 239 | 240 | Once again, there is a faster alternative to the standard `prettier` called 241 | [`prettierd`](https://github.com/fsouza/prettierd). Same concept as `eslint_d`, same drawback of 242 | having to restart it using `prettierd restart` anytime you change its config. Let's implement it! 243 | We will continue in `after/ftplugin/javascript.lua` and reuse our `augroup` and `buffer` variables 244 | from before. 245 | 246 | ```lua 247 | -- This time we want to run our logic *before* we save. This is because we will swap out the buffer 248 | -- contents with prettier's output right before it actually gets written to disk. 249 | vim.api.nvim_create_autocmd("BufWritePre", { 250 | buffer = buffer, 251 | group = group, 252 | callback = function() 253 | -- Full path to the current file 254 | local filename = vim.fn.expand("%") 255 | local command = { "prettierd", filename } 256 | local opts = { 257 | -- Treat stdout as raw text 258 | text = true, 259 | 260 | -- We will feed prettier our current buffer contents as input via stdin. 261 | stdin = vim.api.nvim_buf_get_lines(buffer, 0, -1, false), 262 | } 263 | 264 | -- You can also use `vim.fn.system()` here if you are on neovim <0.10. 265 | -- See `:help system()` 266 | local result = vim.system(command, opts):wait() 267 | 268 | if result.code ~= 0 then 269 | -- Prettier will return an error if the file has sytnax errors, so we just exit silently. 270 | return 271 | end 272 | 273 | -- Take prettier's output and turn it into an array of lines 274 | local formatted_lines = vim.split(result.stdout, "\n", { trimempty = true }) 275 | 276 | -- Replace the current buffer's contents with those new lines 277 | vim.api.nvim_buf_set_lines(buffer, 0, -1, false, formatted_lines) 278 | end, 279 | }) 280 | ``` 281 | 282 | Now, anytime we save a buffer, we call `prettierd` with our current buffer contents as input, 283 | receive back a formatted version via stdout and replace our buffer with that. Beautiful! 284 | 285 | ### Wrapping up 286 | 287 | Currently all of this only works for JavaScript files, but what if you wanted to share that logic? 288 | Both ESLint and Prettier work with TypeScript as well, and Prettier in particular can format many 289 | other filetypes as well, like HTML for example. So how do we share logic? Lua modules! 290 | 291 | If you read [Getting to know your editor](./getting-to-know-nvim.md) you already know how Lua 292 | modules work. If you don't know how they work, read 293 | [this section](./getting-to-know-nvim.md#lua-modules). 294 | 295 | I personally group all my files under a module called `alphakeks` so none of them clash with plugins 296 | or builtin modules. This means I would create a file called `lua/alphakeks/eslint.lua` with all my 297 | ESLint related functions. 298 | 299 | ```lua 300 | local namespace = vim.api.nvim_create_namespace("eslint-diagnostics") 301 | 302 | local file_patterns = { 303 | ".eslintrc", 304 | ".eslintrc.js", 305 | ".eslintrc.cjs", 306 | ".eslintrc.yaml", 307 | ".eslintrc.yml", 308 | ".eslintrc.json", 309 | "eslint.config.js", 310 | } 311 | 312 | local function invoke(buffer) 313 | buffer = buffer or vim.api.nvim_get_current_buf() 314 | 315 | local config_files = vim.fs.find(file_patterns, { upward = true }) 316 | 317 | -- I don't want to run eslint if the current project does not have a configuration for it. 318 | if #config_files == 0 then 319 | return 320 | end 321 | 322 | vim.cmd.compiler("eslint") 323 | vim.bo[buffer].makeprg = "eslint_d --format compact" 324 | 325 | -- Run eslint on the current file 326 | vim.cmd("make! %") 327 | 328 | local qflist = vim.fn.getqflist() 329 | local diagnostics = vim.diagnostic.fromqflist(qflist) 330 | 331 | vim.diagnostic.reset(namespace, buffer) 332 | vim.diagnostic.set(namespace, buffer, diagnostics) 333 | end 334 | 335 | local function on_save(buffer) 336 | buffer = buffer or vim.api.nvim_get_current_buf() 337 | 338 | return vim.api.nvim_create_autocmd("BufWritePost", { 339 | buffer = buffer, 340 | group = vim.api.nvim_create_augroup("eslint-on-save"), 341 | callback = function() 342 | invoke(buffer) 343 | end, 344 | }) 345 | end 346 | 347 | return { 348 | namespace = namespace, 349 | invoke = invoke, 350 | on_save = on_save, 351 | } 352 | ``` 353 | 354 | Now I can simply call `require("alphakeks.eslint").on_save()` in `after/ftplugin/javascript.lua` as 355 | well as `after/ftplugin/typescript.lua`. 356 | 357 | For Prettier you can do something very similar: 358 | 359 | ```lua 360 | local function invoke(buffer) 361 | buffer = buffer or vim.api.nvim_get_current_buf() 362 | 363 | local filename = vim.fn.expand("%") 364 | local command = { "prettierd", filename } 365 | local opts = { 366 | text = true, 367 | stdin = vim.api.nvim_buf_get_lines(buffer, 0, -1, false), 368 | } 369 | 370 | local result = vim.system(command, opts):wait() 371 | 372 | if result.code ~= 0 then 373 | return 374 | end 375 | 376 | local formatted_lines = vim.split(result.stdout, "\n", { trimempty = true }) 377 | 378 | vim.api.nvim_buf_set_lines(buffer, 0, -1, false, formatted_lines) 379 | end 380 | 381 | local function on_save(buffer) 382 | buffer = buffer or vim.api.nvim_get_current_buf() 383 | 384 | return vim.api.nvim_create_autocmd("BufWritePre", { 385 | buffer = buffer, 386 | group = vim.api.nvim_create_augroup("prettier-on-save"), 387 | callback = function() 388 | invoke(buffer) 389 | end, 390 | }) 391 | end 392 | 393 | return { 394 | invoke = invoke, 395 | on_save = on_save, 396 | } 397 | ``` 398 | 399 | And again, you only need `require("alphakeks.prettier").on_save()` in all your `ftplugin` files. 400 | 401 | ## Conclusion 402 | 403 | I hope you see how much you can do with very little code. Anything I showed you here is applicable 404 | to any CLI tool and once you've done it a few times, scripting neovim will become really easy and 405 | feel very satisfying. Lua is an elegant language, neovim has a very useful API and there are lots of 406 | tools that work over stdin/stdout, so don't be afraid not finding a plugin for your favorite tool; 407 | just integrate it yourself! 408 | -------------------------------------------------------------------------------- /completion.md: -------------------------------------------------------------------------------- 1 | # Insert Completion 2 | 3 | While most people use some sort of auto-completion plugin, vim and neovim actually have pretty 4 | decent completion functionality built-in. If you take a look at `:help ins-completion` you will see 5 | a list of various completion sources, like buffer words, entire lines, file paths, tags, dictionary 6 | words, and more. To be fair, some of these are pretty useless. When writing code you will probably 7 | not find yourself using a thesaurus very frequently. However, there are two completion mechanisms 8 | that stand out here: `userfunc` and `omnifunc`. Both work the exact same way, don't ask me why 9 | there's two of them; for simplicity's sake I'm just going to say "omnifunc" from now on, but I mean 10 | both, technically. 11 | 12 | `:help 'omnifunc'` will link you to another help page, namely `:help complete-functions`, which 13 | explains in detail how to write your own `omnifunc`. Many of these already come with vim and will be 14 | set automatically based on your filetype. neovim also ships with `vim.lsp.omnifunc` which will be 15 | set automatically when a language server attaches to the current buffer. This will give you 16 | completions supplied by any attached language servers. 17 | 18 | While `omnifunc` does allow for quite powerful completion, especially when combined with LSP, the 19 | "framework" around it is not very flexible. Plugins like 20 | [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) have become really popular for two reasons: 21 | 22 | - They provide actual **auto**-completion 23 | - They are much easier to use and extend than `omnifunc` 24 | 25 | I really wish this was different, but hey, here we are. 26 | 27 | ## Setting up [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) 28 | 29 | > If you don't know how to install plugins, read [How to install plugins](./plugins.md). 30 | 31 | The first thing you'll want to do is install [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) as 32 | a plugin. You will also need a **snippet engine**, the most popular of which nowadays is 33 | [LuaSnip](https://github.com/L3MON4D3/LuaSnip). This is necessary for expanding snippets coming from 34 | e.g. your LSP. 35 | 36 | On that note, nvim-cmp requires you to install completion **sources**, which are responsible for 37 | sending completions to nvim-cmp, which can then display them. A non-exhaustive list of sources you 38 | may want to consider: 39 | 40 | - [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp) for LSP completion 41 | - [cmp-buffer](https://github.com/hrsh7th/cmp-buffer) for buffer word completion 42 | - [cmp-path](https://github.com/hrsh7th/cmp-path) for file system path completion 43 | - [cmp_luasnip](https://github.com/saadparwaiz1/cmp_luasnip) for snippet completion (assuming you 44 | use LuaSnip) 45 | 46 | After installing it you need to call its `.setup()` function somewhere in your config: 47 | 48 | ```lua 49 | local cmp = require("cmp") 50 | local luasnip = require("luasnip") 51 | 52 | cmp.setup({ 53 | -- Here you can put keymaps for completion 54 | mapping = cmp.mapping.preset.insert({ 55 | -- Confirm a completion 56 | [""] = cmp.mapping.confirm(), 57 | 58 | -- Cancel the completion 59 | [""] = cmp.mapping.abort(), 60 | 61 | -- Force the completion menu to open 62 | [""] = cmp.mapping.complete(), 63 | 64 | -- Select the next item 65 | [""] = cmp.mapping(function(fallback) 66 | if cmp.visible() then 67 | cmp.select_next_item() 68 | else 69 | fallback() 70 | end 71 | end), 72 | 73 | -- Select the previous item 74 | [""] = cmp.mapping(function(fallback) 75 | if cmp.visible() then 76 | cmp.select_prev_item() 77 | else 78 | fallback() 79 | end 80 | end), 81 | }), 82 | 83 | -- Here you can put your list of completion sources. 84 | -- 85 | -- The order is important. It determines the priority of each source and affects how they will 86 | -- appear in your completion menu. 87 | sources = { 88 | { name = "nvim_lsp" }, 89 | { name = "luasnip" }, 90 | { name = "path" }, 91 | { name = "buffer" }, 92 | }, 93 | 94 | snippet = { 95 | expand = function(args) 96 | luasnip.lsp_expand(args.body) 97 | end, 98 | }, 99 | }) 100 | ``` 101 | 102 | Another important thing to note here is that by default LSP servers will not send certain 103 | completions unless you tell them to. This is because of **capabilities**, as I have talked about in 104 | the [LSP](./lsp.md) section of this repo. [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp) 105 | exposes a function for these extended capabilities, which you should pass to each server's setup to 106 | get snippets and auto-imports. 107 | 108 | ```lua 109 | -- If you use `vim.lsp.start`, this will be the exact same. lspconfig's `.setup()` function takes 110 | -- the same arguments as `vim.lsp.start`. 111 | local lspconfig = require("lspconfig") 112 | local capabilities = require("cmp_nvim_lsp").default_capabilities() 113 | 114 | lspconfig.rust_analyzer.setup({ 115 | capabilities = capabilities, 116 | }) 117 | 118 | lspconfig.tsserver.setup({ 119 | capabilities = capabilities, 120 | }) 121 | 122 | lspconfig.lua_ls.setup({ 123 | capabilities = capabilities, 124 | }) 125 | 126 | -- ... 127 | ``` 128 | 129 | ## Writing a simple `omnifunc` for LSP 130 | 131 | > This section exists mostly for fun. If you just want completion to work, and especially if you 132 | > want **auto**-completion, I highly recommend [nvim-cmp](https://github.com/hrsh7th/nvim-cmp). 133 | 134 | We will start by creating a file to put this function in. Let's say `lua/alphakeks/completion.lua`. 135 | 136 | ```lua 137 | ---@param findstart 0 | 1 138 | ---@param base string 139 | local function omnifunc(findstart, base) 140 | end 141 | 142 | return { omnifunc = omnifunc } 143 | ``` 144 | 145 | This is the outline for it. In another file, probably an `LspAttach` autocmd we can then set this as 146 | our `omnifunc`: 147 | 148 | ```lua 149 | vim.api.nvim_create_autocmd("LspAttach", { 150 | callback = function(event) 151 | vim.bo[event.buf].omnifunc = "v:lua.require('alphakeks.completion').omnifunc" 152 | end, 153 | }) 154 | ``` 155 | 156 | Our function will be called twice each time we press ``. In the first call `findstart` 157 | will be `1`, signalling that we need to find the start column of our completion. 158 | 159 | ```lua 160 | ---@param findstart 0 | 1 161 | ---@param base string 162 | local function omnifunc(findstart, base) 163 | if findstart == 1 then 164 | local window = vim.api.nvim_get_current_win() 165 | local cursor_col = vim.api.nvim_win_get_cursor(window)[2] 166 | local line = vim.api.nvim_get_current_line():sub(1, cursor_col) 167 | local start_col = vim.fn.match(line, "\\k*$") + 1 168 | 169 | return start_col 170 | end 171 | 172 | local words = {} 173 | 174 | return { words = words, refresh = "always" } 175 | end 176 | ``` 177 | 178 | We get our cursor's position and use it to extract the text of the current line up to our cursor. We 179 | then match against `\k*$`, which is vim regex. `\k` is any "keyword" (see `:help 'iskeyword'`), and 180 | we want the last one so we also match against `$` (the end of the text). 181 | 182 | The second time our function is called, `base` will be the text starting at the column returned in 183 | the first call, up to our cursor. Currently we're just returning an empty table for `words`, so we 184 | don't get any errors, but we're actually supposed to return completion items here. 185 | `:help complete-items` has a detailed description of what these are supposed to look like. 186 | 187 | To get our completion items, we need to make an LSP request and do some data transformation. 188 | 189 | ```lua 190 | local function omnifunc(findstart, base) 191 | if findstart == 1 then 192 | -- *snip* 193 | end 194 | 195 | local result = { words = {}, refresh = "always" } 196 | 197 | local buffer = vim.api.nvim_get_current_buf() 198 | local clients = vim.lsp.get_clients({ bufnr = buffer, method = "textDocument/completion" }) 199 | 200 | -- We don't have any LSP clients that can do completion, so we return an empty list. 201 | if vim.tbl_isempty(clients) then 202 | return result 203 | end 204 | 205 | local params = vim.lsp.util.make_position_params() 206 | local lsp_results = vim.lsp.buf_request_sync(0, "textDocument/completion", params) 207 | 208 | if lsp_results == nil then 209 | print("No LSP completions") 210 | return result 211 | end 212 | 213 | -- Each server could have sent a result, so we need to check each one 214 | for _, lsp_result in ipairs(lsp_results) do 215 | if lsp_result.err ~= nil then 216 | print("Error while requesting completions! " .. vim.inspect(lsp_result.err)) 217 | goto continue 218 | end 219 | 220 | -- This server has sent 0 items 221 | if lsp_result.result == nil then 222 | goto continue 223 | end 224 | 225 | -- For each item we need to extract relevant information 226 | for _, completion in ipairs(lsp_result.result.items) do 227 | local item = {} 228 | 229 | if completion.label ~= nil then 230 | item.word = completion.label 231 | end 232 | 233 | if completion.insertText ~= nil then 234 | item.word = completion.insertText 235 | end 236 | 237 | if item.word == nil then 238 | goto continue 239 | end 240 | 241 | -- Filter out any items that don't match what we typed 242 | if not vim.startswith(item.word, base) then 243 | goto continue 244 | end 245 | 246 | item.kind = vim.lsp.protocol.CompletionItemKind[completion.kind] 247 | 248 | table.insert(result.words, item) 249 | end 250 | 251 | ::continue:: 252 | end 253 | 254 | return result 255 | end 256 | ``` 257 | 258 | This is already enough to give you basic semantic completion. You could go ahead and extend this to 259 | include more / different information in the completion menu, or setup autocommands to display 260 | documentation for the current item in a floating window, but those are details you can figure out 261 | yourself. 262 | 263 | If you have a slow language server you will notice that a completion request can block for 264 | a substantial amount of time. To avoid this, we can make our requests asynchronously, and that's 265 | what I want to show here as well. 266 | 267 | If you read `:help ins-completion` carefully, you have noticed that during the first call to our 268 | function we can return `-2` or `-3` as well, signalling that we will either supply completions later 269 | (`-2`), or never (`-3`). We can use this to make async completion requests and exit our function 270 | immediately, letting a callback function call `vim.fn.complete()` later to supply the actual 271 | results. This means that we actually won't need either of our parameters, since we never get called 272 | twice anyway. 273 | 274 | ```lua 275 | local function omnifunc() 276 | local buffer = vim.api.nvim_get_current_buf() 277 | local clients = vim.lsp.get_clients({ bufnr = buffer, method = "textDocument/completion" }) 278 | 279 | if vim.tbl_isempty(clients) then 280 | -- Signal that we can't provide any completions 281 | return -3 282 | end 283 | 284 | local window = vim.api.nvim_get_current_win() 285 | local cursor_col = vim.api.nvim_win_get_cursor(window)[2] 286 | local line = vim.api.nvim_get_current_line():sub(1, cursor_col) 287 | local start_col = vim.fn.match(line, "\\k*$") + 1 288 | local base = line:sub(start_col) 289 | local completions = {} 290 | 291 | -- This will be called once our request is done 292 | local callback = function() 293 | vim.fn.complete(start_col, completions) 294 | end 295 | 296 | for _, client in ipairs(clients) do 297 | local params = vim.lsp.util.make_position_params(window, client.offset_encoding) 298 | 299 | -- Make an async request 300 | client.request("textDocument/completion", params, function(err, result) 301 | if err ~= nil then 302 | print("Error while requesting completions! " .. vim.inspect(err)) 303 | return 304 | end 305 | 306 | if result == nil then 307 | print("No LSP completions.") 308 | return 309 | end 310 | 311 | local items = {} 312 | 313 | for _, completion in ipairs(result) do 314 | local item = {} 315 | 316 | --[[ same logic as earlier to extract information ]] 317 | 318 | table.insert(items, item) 319 | 320 | ::continue:: 321 | end 322 | 323 | callback() 324 | end) 325 | end 326 | 327 | -- Signal that we want to stay in completion mode and supply actual results asynchronously 328 | return -2 329 | end 330 | ``` 331 | 332 | This will be noticably faster and smoother. 333 | 334 | I have [a much more complicated version of this](https://git.sr.ht/~alphakeks/.dotfiles/tree/4e50d8c69596cd72d97246c45aef0799c87b9a09/item/nvim/lua/alphakeks/lsp/completion.lua) 335 | in [my dotfiles](https://git.sr.ht/~alphakeks/.dotfiles/tree/4e50d8c69596cd72d97246c45aef0799c87b9a09/item/nvim), 336 | if you are interested. 337 | 338 | -------------------------------------------------------------------------------- /getting-to-know-nvim.md: -------------------------------------------------------------------------------- 1 | # Getting to know your editor 2 | 3 | In this section I will cover more details about **configuring** and **extending** neovim as an 4 | editor. This will partially apply to vim as well, so I will explicitly mention anything that is 5 | neovim-exclusive. It's also kind of over the place with random topics, as I don't really see an 6 | order here, just pick and choose what you're interested in :) 7 | 8 | As I already mentioned, neovim heavily invests into Lua. This means that you can write your 9 | entire configuration using Lua! (There are some exceptions to this, such as the `autoload/` 10 | directory, but for the most part everything just works with Lua) 11 | 12 | ## Where to put configuration 13 | 14 | neovim adheres to the [XDG base directory standard](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). 15 | This means that your configuration will live in `$XDG_CONFIG_HOME`, while files like logs or plugins 16 | will go into `$XDG_DATA_HOME` and `$XDG_STATE_HOME`. 17 | 18 | On Linux and MacOS these directories will be the following by default: 19 | 20 | - `$XDG_CONFIG_HOME` => `~/.config/nvim` 21 | - `$XDG_DATA_HOME` => `~/.local/share/nvim` 22 | - `$XDG_STATE_HOME` => `~/.local/state/nvim` 23 | 24 | For Windows it will be these: 25 | 26 | - `$XDG_CONFIG_HOME` => `~/AppData/Local/nvim` 27 | - `$XDG_DATA_HOME` => `~/AppData/Local/nvim-data` 28 | - `$XDG_STATE_HOME` => `~/AppData/Local/nvim-data` 29 | 30 | You can also check these directories using 31 | 32 | - `$XDG_CONFIG_HOME` => `:echo stdpath('config')` 33 | - `$XDG_DATA_HOME` => `:echo stdpath('data')` 34 | - `$XDG_STATE_HOME` => `:echo stdpath('state')` 35 | 36 | Once you have found your config directory, create **either** an `init.vim` **or** an `init.lua` file 37 | inside of it. This will be the entry point for your config. I will talk about other relevant 38 | directories later, but any code examples I show you can put into your `init.{vim,lua}` file. It's 39 | also worth mentioning that you can run any vimscript code in command mode (`:`), and run any Lua 40 | code using the `:lua` vimscript command. `:lua=` followed by a Lua expression will print the result 41 | of that expression. 42 | 43 | ## Options 44 | 45 | The most basic way of customizing vim is using **options**. There are a lot of them 46 | (see `:help option-list`), and they are all documented. You can look up the documentation for 47 | a particular option using `:help '