this repo has no description
1+++ 2title = "Dumb Elixir VIsual (and iMproved) editor" 3description = "How I have configured Vim for working with Elixir and Erlang projects" 4date = 2019-04-13T21:40:05+02:00 5 6[taxonomies] 7tags = [ 8 "beam", 9 "vim" 10] 11+++ 12 13> Earlier published on [Medium](https://medium.com/@hauleth/dumb-elixir-visual-and-improved-editor-53c23a0800e4) 14 15![My example session in NeoVim with QuickFix displaying Credo warnings](/img/vim-session.png) 16 17I am quite orthodox Vim user and I like to know everything that is happening in 18my editor configuration. By no means I am "minimal" Vim user, I currently have 1948 plugins installed, I just very carefully pick my plugins (few of them I have 20written myself) to use only plugins that I understand and I can fix on my own in 21case of problems. This results with configuration I understand and control in 22all means. 23 24## What this article is not 25 26This isn't "introduction into Vim" article neither "how to configure Vim for 27Elixir development". Here I will try to describe how I use Vim for Elixir and 28Erlang development, you can find some nice ideas and tips that may help you, but 29by any means I do not mean that this is configuration you should or should not 30use. You should use whatever you like (even if I do not like what you use) as 31long as you understand what you use and why you use that. 32 33> *Any sufficiently complicated set of Vim plugins contains an ad hoc, 34> informally-specified, bug-ridden, slow implementation of half of Vim's 35> features.* 36> -- robertmeta's tenth rule. 37 38Now we can start. 39 40## Basics 41 42I am using NeoVim 0.3.4, but almost everything I will describe there should work 43in Vim 8.1+ as well. If you will encounter any problems, then please let me 44know. 45 46Vim doesn't (as 13.04.2019) support Elixir out of the box, so what we 47need is to install [`vim-elixir`][] plugin which will provide niceties like syntax 48colouring and indentation. Even if syntax colouring isn't your thing then I 49would still recommend installing it as it provides other things that it provides 50few other niceties that I will describe later. 51 52But how to install it? In truly minimal setup you can just create 53`pack/elixir/start` directory within your `~/.vim` folder 54(`$XDG_CONFIG_DIR/nvim` in case of NeoVim) and clone given repository there, 55however I am using [`vim-packager`][] which is based on [`minpac`][] and is 56truly minimal package manager (it is important distinction from [`vim-plug`][] 57or others that also manages plugin loading, these plugins only manage fetching) 58which even do not need to be loaded during "normal" runtime, only when you are 59updating plugins. 60 61## Project navigation 62 63A lot of people, when come from other editors, often install NERDTree to have 64"project drawer" functionality within Vim, because this is what they are used 65to. Unfortunately "[split windows and the project drawer go together like oil 66and vinegar][oil-and-vinegar]" and it can result in painful experience or, which 67is even worse, avoiding built in functionalities, because these do not mix well. 68Vim comes with built in NetRW plugin for working with remote files and directory 69tries. However for me this plugin is bloated as well and I would love to get rid 70of it (unfortunately it is not currently possible as few functionalities relies 71on it, namely dictionaries) so I replaced everything with [`dirvish`][]. 72Additionally I often use fuzzy finder, which in my case is [`vim-picker`][] with 73[`fzy`][] which for me is much faster and more precise than popular FZF. 74 75These tools are great when we are navigating in tree that is new to us or do not 76have explicit structure. When we are working on Elixir projects then we know 77before hand that there will be some commonly shared structure, like 78`lib/<name>.ex` will contain source code, `test/<name>_test.exs` will contain 79test files, etc. What is more we know that `<name>` part will be shared between 80file and its unit tests. This is very powerful assumption, as this allow us to 81use [`vim-projectionist`][] with ease. This plugin provide 3 main 82functionalities (for me): 83 84- Jumping to the files basing on their path, so for example I can use `:Elib 85 foo` to automatically jump to file `lib/foo.ex`. It doesn't seems like much, 86 but it also provides fuzzy finding, and allows me to define more specific 87 matches, like `:Econtroller foo` will open 88 `lib/app_web/controllers/foo_controller.ex` (not exactly that as I use 89 different project layout, but that is topic on another article). 90- File templates, so when I start editing file (doesn't matter how I opened it, 91 so this do not require to use above `:Elib` command), so when I start editing 92 test file it automatically add scaffold which I can configure per project. 93- Alternate files which in short are "related" files. For example when I edit 94 file `lib/foo/bar/baz.ex` and I run `:A` it will create (if not exist) and 95 jump to the file `test/foo/bar/baz_test.exs` which will be already scaffolded 96 by the earlier functionality. Recently it even became possible to have 97 multiple alternates. 98 99Whole plugin is configured by `.projections.json` file, but it would be 100infeasible to add this file to each project you work for. Fortunately there is 101solution for that, we can define "heuristics" that will try to match for given 102project structure and provide such features "globally". My configuration for 103that looks like this: 104 105```vim 106let g:projectionist_heuristics['mix.exs'] = { 107 \ 'apps/*/mix.exs': { 'type': 'app' }, 108 \ 'lib/*.ex': { 109 \ 'type': 'lib', 110 \ 'alternate': 'test/{}_test.exs', 111 \ 'template': ['defmodule {camelcase|capitalize|dot} do', 'end'], 112 \ }, 113 \ 'test/*_test.exs': { 114 \ 'type': 'test', 115 \ 'alternate': 'lib/{}.ex', 116 \ 'template': [ 117 \ 'defmodule {camelcase|capitalize|dot}Test do', 118 \ ' use ExUnit.Case', 119 \ '', 120 \ ' alias {camelcase|capitalize|dot}, as: Subject', 121 \ '', 122 \ ' doctest Subject', 123 \ 'end' 124 \ ], 125 \ }, 126 \ 'mix.exs': { 'type': 'mix' }, 127 \ 'config/*.exs': { 'type': 'config' }, 128 \ '*.ex': { 129 \ 'makery': { 130 \ 'lint': { 'compiler': 'credo' }, 131 \ 'test': { 'compiler': 'exunit' }, 132 \ 'build': { 'compiler': 'mix' } 133 \ } 134 \ }, 135 \ '*.exs': { 136 \ 'makery': { 137 \ 'lint': { 'compiler': 'credo' }, 138 \ 'test': { 'compiler': 'exunit' }, 139 \ 'build': { 'compiler': 'mix' } 140 \ } 141 \ } 142 \ } 143 144let g:projectionist_heuristics['rebar.config'] = { 145 \ '*.erl': { 146 \ 'template': ['-module({basename}).', '', '-export([]).', ''], 147 \ }, 148 \ 'src/*.app.src': { 'type': 'app' }, 149 \ 'src/*.erl': { 150 \ 'type': 'src', 151 \ 'alternate': 'test/{}_SUITE.erl', 152 \ }, 153 \ 'test/*_SUITE.erl': { 154 \ 'type': 'test', 155 \ 'alternate': 'src/{}.erl', 156 \ }, 157 \ 'rebar.config': { 'type': 'rebar' } 158 \ } 159``` 160 161### This will provide: 162 163#### For Elixir: 164 165- `lib` that will contain project source files which will be already filled with 166 module named `Foo.BarBaz` for file named `lib/foo/bar_baz.ex` (jump by `:Elib 167 foo/bar_baz`) 168- `test` for test files which will be instantiated with module named 169 `Foo.BarBazTest` for file `test/foo/bar_baz_test.exs` that will already use 170 `ExUnit.Case` (you can jump via `:Etest foo/bar_baz`), will define 171 `alias Foo.BarBaz, as: Subject`, and will run doctests for that module 172- `config` for configuration files 173- `mix` for `mix.exs` 174 175It will also define test files as default alternates for each source file (and 176vice versa, because alternate files do not need to be symmetric), so if you 177run `:A` in file `lib/foo/bar_baz.ex` it will automatically jump to the 178`test/foo/bar_baz_test.exs`. 179 180#### For Erlang: 181 182- `src` for source files 183- `app` for `*.app.src` files 184- `test` for common test suites 185- `rebar` for `rebar.config` file 186 187The relation between source files and test files is exactly the same as in 188Elixir projects. 189 190One thing can bring your attention, why the hell I define helpers for `mix.exs` 191and `rebar.config` as you can simply use `:e <file>`. The answer is simple, `:e` 192will work for files in Vim working directory while `:E` will work form the 193projectionist root, aka directory where is `.projections.json` file defined (or 194in case of heuristics, from the directory that matched files). This mean that 195when I edit files in umbrella application I can use `:Emix` (or `:Erebar`) to 196edit current sub-project config file and `:e mix.exs` to edit global one. 197 198## Completion and language server 199 200For completion and code formatting I use [`vim-lsp`][]. I have tried most of the 201language server clients out there, but I always come back to this one for a few 202reasons: 203 204- It is implemented only in VimL which mean that I am not forced to installing 205 any compatibility layers or fighting with different runtimes. 206- It is simple enough that I can easily dig into it, and fix problems that I 207 have encountered. 208- It doesn't override any built in Vim functionality and instead provide set of 209 commands that you can then bind to whatever mappings you want. 210- It do not force me to use autocompletion, which I do not use at all. At the 211 same time it provides seamless integration with built-in Vim functionality of 212 omnicompletion and user completion by providing `lsp#complete` function. 213 214This approach of not providing default mappings is really nice for power users, 215as this allow us to define everything on our own. For example some of plugins 216use <kbd>&lt;C-]&gt;</kbd> for jumping to definition, which I often use (it is 217jump to tag definition) and shadowing it would be problematic for me. So in the 218end I have created my own set of mappings, that have additional feature of being 219present only if there is any server that supports them: 220 221```vim 222func! s:setup_ls(...) abort 223 let l:servers = lsp#get_whitelisted_servers() 224 225 for l:server in l:servers 226 let l:cap = lsp#get_server_capabilities(l:server) 227 228 if has_key(l:cap, 'completionProvider') 229 setlocal omnifunc=lsp#complete 230 endif 231 232 if has_key(l:cap, 'hoverProvider') 233 setlocal keywordprg=:LspHover 234 endif 235 236 if has_key(l:cap, 'definitionProvider') 237 nmap <silent> <buffer> gd <plug>(lsp-definition) 238 endif 239 240 if has_key(l:cap, 'referencesProvider') 241 nmap <silent> <buffer> gr <plug>(lsp-references) 242 endif 243 endfor 244endfunc 245 246augroup LSC 247 autocmd! 248 autocmd User lsp_setup call lsp#register_server({ 249 \ 'name': 'ElixirLS', 250 \ 'cmd': {_->['elixir-ls']}, 251 \ 'whitelist': ['elixir', 'eelixir'] 252 \}) 253 autocmd User lsp_setup call lsp#register_server({ 254 \ 'name': 'RLS', 255 \ 'cmd': {_->['rls']}, 256 \ 'whitelist': ['rust'] 257 \}) 258 autocmd User lsp_setup call lsp#register_server({ 259 \ 'name': 'solargraph', 260 \ 'cmd': {server_info->['solargraph', 'stdio']}, 261 \ 'initialization_options': {"diagnostics": "true"}, 262 \ 'whitelist': ['ruby'], 263 \ }) 264 autocmd User lsp_setup call lsp#register_server({ 265 \ 'name': 'dot', 266 \ 'cmd': {server_info->['dot-language-server', '--stdio']}, 267 \ 'whitelist': ['dot'], 268 \ }) 269 270 autocmd User lsp_server_init call <SID>setup_ls() 271 autocmd BufEnter * call <SID>setup_ls() 272augroup END 273``` 274 275## Running tasks and linting 276 277A lot of people "cannot live" without lint-as-you-type feature, but I think, 278that not using such functionality makes me a less sloppy and better programmer. 279It makes me to think when I write and do not rely on some magical friend that 280will always watch over my shoulder. However when the problem happens in my code 281I would like to know where and quickly jump to the place where error occurred. 282Additionally I would like to run tasks in the background without interruption to 283my current work. All of it became possible with introduction of asynchronous 284tasks in NeoVim and Vim 8. So I have created plugin [`asyncdo.vim`][] that 285allows me to easily implement `:Make` command that works exactly like built 286in [`:make`][], but do not halt my normal work. Together with [`vim-makery`][] 287(which nicely integrates with `vim-projectionsit`) and built in functionality of 288[`:compiler`][], which is supported by `vim-elixir`, it allows me to easily run all 289sorts of commands very easily. If you look into projectionist heuristics above 290you will see that there is `"makery"` key defined for `*.ex` and `*.exs` files. 291That allows me to run `:Mlint %` to run Credo on current file and the results 292will be present within QuickFix window which together with my [`qfx.vim`][] will 293mark lines with errors using signs. In the same manner I can run `:Mtest` to run 294tests for whole project and have failed tasks visible in QuickFix window. 295 296## Other utilities 297 298There is bunch of other plugins that are quite helpful when it comes to working 299on Elixir projects and do not interfere with Vim features, ex.: 300 301- [`vim-dadbod`][] which allows you to run SQL queries from within Vim, and [I 302 have written integration with Ecto][ecto-dadbod] which is provided with 303 `vim-elixir` by default. So if you are working on Elixir application that has 304 `MyApp.Repo` Ecto repository then you can run `:DB MyApp.Repo` and Vim will 305 open your DB client within separate terminal that will be connected to your DB 306- [`vim-endwise`][] that will automatically add end to your do blocks 307- [`direnv.vim`][] simplify management of environment variables in per-directory 308 manner 309- [`vim-editorconfig`][] ([sgur][]'s one, not official one) - pure VimL support 310 for [EditorConfig][] files 311 312## Summary 313 314I hope that you find some nice ideas within this article that will help in 315improving your own Vim configuration without adding much clutter. 316 317No, I will not publish my own `vimrc` in fear that some of you will copy it as 318is (also not that this is particularly troublesome for anyone who is aware of 319Google to find it). Instead I highly suggest You to dig into your own 320configuration and for each line ask yourself: 321 322 323- Do I know what this line **does**? 324- Do I really **need** this line? 325 326And if answer for any of these questions is **no** then remove such line. In the 327end you either learn what for it was, or that you never needed it. 328 329[`dirvish`]: https://github.com/justinmk/vim-dirvish 330[oil-and-vinegar]: http://vimcasts.org/blog/2013/01/oil-and-vinegar-split-windows-and-project-drawer/ 331[`vim-elixir`]: https://github.com/elixir-editors/vim-elixir 332[`vim-packager`]: https://github.com/kristijanhusak/vim-packager 333[`minpac`]: https://github.com/k-takata/minpac 334[`vim-plug`]: https://github.com/junegunn/vim-plug 335[`vim-picker`]: https://github.com/srstevenson/vim-picker 336[`fzy`]: https://github.com/jhawthorn/fzy 337[`vim-projectionist`]: https://github.com/tpope/vim-projectionist 338[`vim-lsp`]: https://github.com/prabirshrestha/vim-lsp 339[`asyncdo.vim`]: https://github.com/hauleth/asyncdo.vim 340[`vim-makery`]: https://github.com/igemnace/vim-makery 341[`qfx.vim`]: https://gitlab.com/hauleth/qfx.vim 342[`vim-dadbod`]: https://github.com/tpope/vim-dadbod 343[`vim-endwise`]: https://github.com/tpope/vim-endwise 344[`direnv.vim`]: https://github.com/direnv/direnv.vim 345[`vim-editorconfig`]: https://github.com/sgur/vim-editorconfig 346[sgur]: https://github.com/sgur 347[ecto-dadbod]: https://github.com/elixir-editors/vim-elixir/pull/481 348[EditorConfig]: https://editorconfig.org 349[`:make`]: https://vimhelp.org/quickfix.txt.html#%3Amake 350[`:compiler`]: https://vimhelp.org/quickfix.txt.html#%3Acompiler