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
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><C-]></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