Skip to content
111 changes: 111 additions & 0 deletions autoload/codefmt/mixformat.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
" Copyright 2022 Google Inc. All rights reserved.
"
" Licensed under the Apache License, Version 2.0 (the "License");
" you may not use this file except in compliance with the License.
" You may obtain a copy of the License at
"
" http://www.apache.org/licenses/LICENSE-2.0
"
" Unless required by applicable law or agreed to in writing, software
" distributed under the License is distributed on an "AS IS" BASIS,
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
" See the License for the specific language governing permissions and
" limitations under the License.

let s:plugin = maktaba#plugin#Get('codefmt')
let s:cmdAvailable = {}

""
" @private
" Formatter: mixformat
function! codefmt#mixformat#GetFormatter() abort
let l:formatter = {
\ 'name': 'mixformat',
\ 'setup_instructions': 'mix is usually installed with Elixir ' .
\ '(https://elixir-lang.org/install.html). ' .
\ "If mix is not in your path, configure it in .vimrc:\n" .
\ 'Glaive codefmt mix_executable=/path/to/mix' }

function l:formatter.IsAvailable() abort
let l:cmd = codefmt#formatterhelpers#ResolveFlagToArray('mix_executable')
if codefmt#ShouldPerformIsAvailableChecks() && !executable(l:cmd[0])
return 0
endif
return 1
endfunction

function l:formatter.AppliesToBuffer() abort
return &filetype is# 'elixir' || &filetype is# 'eelixir'
\ || &filetype is# 'heex'
endfunction

""
" Reformat the current buffer using mix format, only targeting {ranges}.
function l:formatter.FormatRange(startline, endline) abort
let l:filename = expand('%:p')
if empty(l:filename)
let l:dir = getcwd()
" Default filename per https://hexdocs.pm/mix/Mix.Tasks.Format.html
let l:filename = 'stdin.exs'
else
let l:dir = s:findMixDir(l:filename)
endif
" mix format docs: https://hexdocs.pm/mix/main/Mix.Tasks.Format.html
let l:cmd = codefmt#formatterhelpers#ResolveFlagToArray('mix_executable')
" Specify stdin as the file
let l:cmd = l:cmd + ['format', '--stdin-filename=' . l:filename, '-']
let l:syscall = maktaba#syscall#Create(l:cmd).WithCwd(l:dir)
try
" mix format doesn't have a line-range option, but does a reasonable job
" (except for leading indent) when given a full valid expression
call codefmt#formatterhelpers#AttemptFakeRangeFormatting(
\ a:startline, a:endline, l:syscall)
catch /ERROR(ShellError):/
" Parse all the errors and stick them in the quickfix list.
let l:errors = []
for l:line in split(v:exception, "\n")
" Example output:
" ** (SyntaxError) foo.exs:57:28: unexpected reserved word: end
" (blank line)
" HINT: it looks like the "end" on line 56 does not have a matching "do" defined before it
" (blank line), (stack trace with 4-space indent)
" TODO gather additional details between error message and stack trace
let l:tokens = matchlist(l:line,
\ printf('\v^\*\* (\(\k+\)) [^:]+:(\d+):(\d+):\s*(.*)'))
if !empty(l:tokens)
call add(l:errors, {
\ 'filename': @%,
\ 'lnum': l:tokens[2] + a:startline - 1,
\ 'col': l:tokens[3],
\ 'text': printf('%s %s', l:tokens[1], l:tokens[4])})
endif
endfor
if empty(l:errors)
" Couldn't parse mix error format; display it all.
call maktaba#error#Shout('Error formatting range: %s', v:exception)
else
call setqflist(l:errors, 'r')
cc 1
endif
endtry
endfunction

return l:formatter
endfunction

" Finds the directory to run mix from. Looks for a mix.exs file first; if that
" is not found looks for a .formatter.exs file, falling back to the parent of
" filepath.
function! s:findMixDir(filepath) abort
let l:path = empty(a:filepath) ? getcwd() : fnamemodify(a:filepath, ':h')
let l:root = findfile('mix.exs', l:path . ';')
if empty(l:root)
let l:root = findfile('.formatter.exs', l:path . ';')
endif
if empty(l:root)
let l:root = l:path
else
let l:root = fnamemodify(l:root, ':h')
endif
return l:root
endfunction
5 changes: 5 additions & 0 deletions doc/codefmt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The current list of defaults by filetype is:
* clojure: cljstyle, zprint
* dart: dartfmt
* fish: fish_indent
* elixir: mixformat
* gn: gn
* go: gofmt
* haskell: ormolu
Expand Down Expand Up @@ -87,6 +88,10 @@ Default: 'dartfmt' `
The path to the js-beautify executable.
Default: 'js-beautify' `

*codefmt:mix_executable*
The path to the mix executable for Elixir.
Default: 'mix' `

*codefmt:yapf_executable*
The path to the yapf executable.
Default: 'yapf' `
Expand Down
4 changes: 4 additions & 0 deletions instant/flags.vim
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ call s:plugin.Flag('dartfmt_executable', 'dartfmt')
" The path to the js-beautify executable.
call s:plugin.Flag('js_beautify_executable', 'js-beautify')

""
" The path to the mix executable for Elixir.
call s:plugin.Flag('mix_executable', 'mix')

""
" The path to the yapf executable.
call s:plugin.Flag('yapf_executable', 'yapf')
Expand Down
2 changes: 2 additions & 0 deletions plugin/register.vim
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
" * clojure: cljstyle, zprint
" * dart: dartfmt
" * fish: fish_indent
" * elixir: mixformat
" * gn: gn
" * go: gofmt
" * haskell: ormolu
Expand Down Expand Up @@ -62,6 +63,7 @@ call s:registry.AddExtension(codefmt#clangformat#GetFormatter())
call s:registry.AddExtension(codefmt#cljstyle#GetFormatter())
call s:registry.AddExtension(codefmt#zprint#GetFormatter())
call s:registry.AddExtension(codefmt#dartfmt#GetFormatter())
call s:registry.AddExtension(codefmt#mixformat#GetFormatter())
call s:registry.AddExtension(codefmt#fish_indent#GetFormatter())
call s:registry.AddExtension(codefmt#gn#GetFormatter())
call s:registry.AddExtension(codefmt#gofmt#GetFormatter())
Expand Down
103 changes: 103 additions & 0 deletions vroom/mixformat.vroom
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

The built-in mixformat formatter knows how to format Elixir code.
If you aren't familiar with basic codefmt usage yet, see main.vroom first.

We'll set up codefmt and configure the vroom environment, then jump into some
examples.

:source $VROOMDIR/setupvroom.vim

:let g:repeat_calls = []
:function FakeRepeat(...)<CR>
| call add(g:repeat_calls, a:000)<CR>
:endfunction
:call maktaba#test#Override('repeat#set', 'FakeRepeat')

:call codefmt#SetWhetherToPerformIsAvailableChecksForTesting(0)


The mixformat formatter expects the mix executable to be installed on your
system.

% IO.puts("Hello world")
:FormatCode mixformat
! cd .* && mix format .* - 2>.*
$ IO.puts("Hello world")

The name or path of the mixformat executable can be configured via the
mix_executable flag if the default of "mix" doesn't work.

:Glaive codefmt mix_executable='someothermix'
:FormatCode mixformat
! cd .* && someothermix format .* - 2>.*
$ IO.puts("Hello world")
:Glaive codefmt mix_executable='mix'


You can format any buffer with mixformat specifying the formatter explicitly.

@clear
% def foo() do<CR>
|IO.puts("Hello"); IO.puts("World");<CR>
|end

:FormatCode mixformat
! cd .* && mix format .* - 2>.*
$ def foo() do
$ IO.puts("Hello")
$ IO.puts("World")
$ end
def foo() do
IO.puts("Hello")
IO.puts("World")
end
@end

The elixir, eelixer, and heex filetypes will use the mixformat formatter
by default.

@clear
% IO.puts("Hello world")

:set filetype=elixir
:FormatCode
! cd .* && mix format .* - 2>.*
$ IO.puts("Hello world")

:set filetype=eelixir
:FormatCode
! cd .* && mix format .* - 2>.*
$ IO.puts("Hello world")

:set filetype=heex
:FormatCode
! cd .* && mix format .* - 2>.*
$ IO.puts("Hello world")

:set filetype=

It can format specific line ranges of code using :FormatLines.

@clear
% defmodule Foo do<CR>
|def bar(list) do<CR>
|[head | tail] = list; IO.puts(head)<CR>
|end<CR>
|end

:2,4FormatLines mixformat
! cd .* && mix format .* - 2>.*
$ def bar(list) do
$ [head | tail] = list
$ IO.puts(head)
$ end
defmodule Foo do
def bar(list) do
[head | tail] = list
IO.puts(head)
end
end
@end

NOTE: the mix formatter does not natively support range formatting, so there
are certain limitations like misaligning indentation levels.