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