Skip to content

Commit 3494825

Browse files
authored
Merge pull request #201 from flwyd/master
Add mixformat to handle Elixir
2 parents cf9683d + e2a0bbf commit 3494825

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
lines changed

autoload/codefmt/mixformat.vim

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
" Copyright 2022 Google Inc. All rights reserved.
2+
"
3+
" Licensed under the Apache License, Version 2.0 (the "License");
4+
" you may not use this file except in compliance with the License.
5+
" You may obtain a copy of the License at
6+
"
7+
" http://www.apache.org/licenses/LICENSE-2.0
8+
"
9+
" Unless required by applicable law or agreed to in writing, software
10+
" distributed under the License is distributed on an "AS IS" BASIS,
11+
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
" See the License for the specific language governing permissions and
13+
" limitations under the License.
14+
15+
let s:plugin = maktaba#plugin#Get('codefmt')
16+
let s:cmdAvailable = {}
17+
18+
""
19+
" @private
20+
" Formatter: mixformat
21+
function! codefmt#mixformat#GetFormatter() abort
22+
let l:formatter = {
23+
\ 'name': 'mixformat',
24+
\ 'setup_instructions': 'mix is usually installed with Elixir ' .
25+
\ '(https://elixir-lang.org/install.html). ' .
26+
\ "If mix is not in your path, configure it in .vimrc:\n" .
27+
\ 'Glaive codefmt mix_executable=/path/to/mix' }
28+
29+
function l:formatter.IsAvailable() abort
30+
let l:cmd = codefmt#formatterhelpers#ResolveFlagToArray('mix_executable')
31+
if codefmt#ShouldPerformIsAvailableChecks() && !executable(l:cmd[0])
32+
return 0
33+
endif
34+
return 1
35+
endfunction
36+
37+
function l:formatter.AppliesToBuffer() abort
38+
return &filetype is# 'elixir' || &filetype is# 'eelixir'
39+
\ || &filetype is# 'heex'
40+
endfunction
41+
42+
""
43+
" Reformat the current buffer using mix format, only targeting {ranges}.
44+
function l:formatter.FormatRange(startline, endline) abort
45+
let l:filename = expand('%:p')
46+
if empty(l:filename)
47+
let l:dir = getcwd()
48+
" Default filename per https://hexdocs.pm/mix/Mix.Tasks.Format.html
49+
let l:filename = 'stdin.exs'
50+
else
51+
let l:dir = s:findMixDir(l:filename)
52+
endif
53+
" mix format docs: https://hexdocs.pm/mix/main/Mix.Tasks.Format.html
54+
let l:cmd = codefmt#formatterhelpers#ResolveFlagToArray('mix_executable')
55+
" Specify stdin as the file
56+
let l:cmd = l:cmd + ['format', '--stdin-filename=' . l:filename, '-']
57+
let l:syscall = maktaba#syscall#Create(l:cmd).WithCwd(l:dir)
58+
try
59+
" mix format doesn't have a line-range option, but does a reasonable job
60+
" (except for leading indent) when given a full valid expression
61+
call codefmt#formatterhelpers#AttemptFakeRangeFormatting(
62+
\ a:startline, a:endline, l:syscall)
63+
catch /ERROR(ShellError):/
64+
" Parse all the errors and stick them in the quickfix list.
65+
let l:errors = []
66+
for l:line in split(v:exception, "\n")
67+
" Example output:
68+
" ** (SyntaxError) foo.exs:57:28: unexpected reserved word: end
69+
" (blank line)
70+
" HINT: it looks like the "end" on line 56 does not have a matching "do" defined before it
71+
" (blank line), (stack trace with 4-space indent)
72+
" TODO gather additional details between error message and stack trace
73+
let l:tokens = matchlist(l:line,
74+
\ printf('\v^\*\* (\(\k+\)) [^:]+:(\d+):(\d+):\s*(.*)'))
75+
if !empty(l:tokens)
76+
call add(l:errors, {
77+
\ 'filename': @%,
78+
\ 'lnum': l:tokens[2] + a:startline - 1,
79+
\ 'col': l:tokens[3],
80+
\ 'text': printf('%s %s', l:tokens[1], l:tokens[4])})
81+
endif
82+
endfor
83+
if empty(l:errors)
84+
" Couldn't parse mix error format; display it all.
85+
call maktaba#error#Shout('Error formatting range: %s', v:exception)
86+
else
87+
call setqflist(l:errors, 'r')
88+
cc 1
89+
endif
90+
endtry
91+
endfunction
92+
93+
return l:formatter
94+
endfunction
95+
96+
" Finds the directory to run mix from. Looks for a mix.exs file first; if that
97+
" is not found looks for a .formatter.exs file, falling back to the parent of
98+
" filepath.
99+
function! s:findMixDir(filepath) abort
100+
let l:path = empty(a:filepath) ? getcwd() : fnamemodify(a:filepath, ':h')
101+
let l:root = findfile('mix.exs', l:path . ';')
102+
if empty(l:root)
103+
let l:root = findfile('.formatter.exs', l:path . ';')
104+
endif
105+
if empty(l:root)
106+
let l:root = l:path
107+
else
108+
let l:root = fnamemodify(l:root, ':h')
109+
endif
110+
return l:root
111+
endfunction

doc/codefmt.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The current list of defaults by filetype is:
3737
* clojure: cljstyle, zprint
3838
* dart: dartfmt
3939
* fish: fish_indent
40+
* elixir: mixformat
4041
* gn: gn
4142
* go: gofmt
4243
* haskell: ormolu
@@ -88,6 +89,10 @@ Default: 'dartfmt' `
8889
The path to the js-beautify executable.
8990
Default: 'js-beautify' `
9091

92+
*codefmt:mix_executable*
93+
The path to the mix executable for Elixir.
94+
Default: 'mix' `
95+
9196
*codefmt:yapf_executable*
9297
The path to the yapf executable.
9398
Default: 'yapf' `

instant/flags.vim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ call s:plugin.Flag('dartfmt_executable', 'dartfmt')
8080
" The path to the js-beautify executable.
8181
call s:plugin.Flag('js_beautify_executable', 'js-beautify')
8282

83+
""
84+
" The path to the mix executable for Elixir.
85+
call s:plugin.Flag('mix_executable', 'mix')
86+
8387
""
8488
" The path to the yapf executable.
8589
call s:plugin.Flag('yapf_executable', 'yapf')

plugin/register.vim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
" * clojure: cljstyle, zprint
3232
" * dart: dartfmt
3333
" * fish: fish_indent
34+
" * elixir: mixformat
3435
" * gn: gn
3536
" * go: gofmt
3637
" * haskell: ormolu
@@ -63,6 +64,7 @@ call s:registry.AddExtension(codefmt#clangformat#GetFormatter())
6364
call s:registry.AddExtension(codefmt#cljstyle#GetFormatter())
6465
call s:registry.AddExtension(codefmt#zprint#GetFormatter())
6566
call s:registry.AddExtension(codefmt#dartfmt#GetFormatter())
67+
call s:registry.AddExtension(codefmt#mixformat#GetFormatter())
6668
call s:registry.AddExtension(codefmt#fish_indent#GetFormatter())
6769
call s:registry.AddExtension(codefmt#gn#GetFormatter())
6870
call s:registry.AddExtension(codefmt#gofmt#GetFormatter())

vroom/mixformat.vroom

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
The built-in mixformat formatter knows how to format Elixir code.
3+
If you aren't familiar with basic codefmt usage yet, see main.vroom first.
4+
5+
We'll set up codefmt and configure the vroom environment, then jump into some
6+
examples.
7+
8+
:source $VROOMDIR/setupvroom.vim
9+
10+
:let g:repeat_calls = []
11+
:function FakeRepeat(...)<CR>
12+
| call add(g:repeat_calls, a:000)<CR>
13+
:endfunction
14+
:call maktaba#test#Override('repeat#set', 'FakeRepeat')
15+
16+
:call codefmt#SetWhetherToPerformIsAvailableChecksForTesting(0)
17+
18+
19+
The mixformat formatter expects the mix executable to be installed on your
20+
system.
21+
22+
% IO.puts("Hello world")
23+
:FormatCode mixformat
24+
! cd .* && mix format .* - 2>.*
25+
$ IO.puts("Hello world")
26+
27+
The name or path of the mixformat executable can be configured via the
28+
mix_executable flag if the default of "mix" doesn't work.
29+
30+
:Glaive codefmt mix_executable='someothermix'
31+
:FormatCode mixformat
32+
! cd .* && someothermix format .* - 2>.*
33+
$ IO.puts("Hello world")
34+
:Glaive codefmt mix_executable='mix'
35+
36+
37+
You can format any buffer with mixformat specifying the formatter explicitly.
38+
39+
@clear
40+
% def foo() do<CR>
41+
|IO.puts("Hello"); IO.puts("World");<CR>
42+
|end
43+
44+
:FormatCode mixformat
45+
! cd .* && mix format .* - 2>.*
46+
$ def foo() do
47+
$ IO.puts("Hello")
48+
$ IO.puts("World")
49+
$ end
50+
def foo() do
51+
IO.puts("Hello")
52+
IO.puts("World")
53+
end
54+
@end
55+
56+
The elixir, eelixer, and heex filetypes will use the mixformat formatter
57+
by default.
58+
59+
@clear
60+
% IO.puts("Hello world")
61+
62+
:set filetype=elixir
63+
:FormatCode
64+
! cd .* && mix format .* - 2>.*
65+
$ IO.puts("Hello world")
66+
67+
:set filetype=eelixir
68+
:FormatCode
69+
! cd .* && mix format .* - 2>.*
70+
$ IO.puts("Hello world")
71+
72+
:set filetype=heex
73+
:FormatCode
74+
! cd .* && mix format .* - 2>.*
75+
$ IO.puts("Hello world")
76+
77+
:set filetype=
78+
79+
It can format specific line ranges of code using :FormatLines.
80+
81+
@clear
82+
% defmodule Foo do<CR>
83+
|def bar(list) do<CR>
84+
|[head | tail] = list; IO.puts(head)<CR>
85+
|end<CR>
86+
|end
87+
88+
:2,4FormatLines mixformat
89+
! cd .* && mix format .* - 2>.*
90+
$ def bar(list) do
91+
$ [head | tail] = list
92+
$ IO.puts(head)
93+
$ end
94+
defmodule Foo do
95+
def bar(list) do
96+
[head | tail] = list
97+
IO.puts(head)
98+
end
99+
end
100+
@end
101+
102+
NOTE: the mix formatter does not natively support range formatting, so there
103+
are certain limitations like misaligning indentation levels.

0 commit comments

Comments
 (0)