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