Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OCaml support with ocamlformat #154

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ helpfiles in the `doc/` directory. The helpfiles are also available via
* Java (google-java-format or clang-format)
* JavaScript (clang-format or [prettier](https://prettier.io))
* JSON (js-beautify)
* [OCaml](https://ocaml.org) ([ocamlformat](https://github.com/ocaml-ppx/ocamlformat))
* Proto (clang-format)
* Python (Autopep8, Black, or YAPF)
* Rust ([rustfmt](https://github.com/rust-lang/rustfmt))
Expand Down
1 change: 1 addition & 0 deletions autoload/codefmt.vim
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
" * fish: fish_indent
" * gn: gn
" * go: gofmt
" * ocaml: ocamlformat
" * python: autopep8, black, yapf


Expand Down
86 changes: 86 additions & 0 deletions autoload/codefmt/ocamlformat.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
" Copyright 2020 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')


""
" @private
" Formatter: ocamlformat
function! codefmt#ocamlformat#GetFormatter() abort
let l:formatter = {
\ 'name': 'ocamlformat',
\ 'setup_instructions': 'Install ocamlformat' .
\ '(https://github.com/ocaml-ppx/ocamlformat)'}

function l:formatter.IsAvailable() abort
return executable(s:plugin.Flag('ocamlformat_executable'))
endfunction

function l:formatter.AppliesToBuffer() abort
return &filetype is# 'ocaml'
endfunction

""
" Reformat the current buffer with ocamlformat or the binary named in
" @flag(ocamlformat_executable), only targeting the range between {startline} and
" {endline}.
function l:formatter.FormatRange(startline, endline) abort
let l:cmd = [ s:plugin.Flag('ocamlformat_executable'), '-' ]
let l:fname = expand('%:p')
if !empty(l:fname)
let l:cmd += ['--name', l:fname]
let l:fname_pattern = '"' . escape(l:fname, '\') . '"'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider defining this closer to the usage. Currently it looks really fishy doing all this escaping and concatenation without any hints what it's used for until much further down.

else
" assume it's an OCaml implementation file (.ml) if no file name is
" provided
let l:cmd += ['--impl']
let l:fname_pattern = '\<standard input\>'
end
try
" NOTE: ocamlformat does not support range formatting.
" See https://github.com/ocaml-ppx/ocamlformat/pull/1188
call codefmt#formatterhelpers#AttemptFakeRangeFormatting(
\ a:startline, a:endline, l:cmd)
catch /ERROR(ShellError):/
" Parse all the errors and stick them in the quickfix list.
let l:errors = []
let l:matchidx = 1
while 1
let l:tokens = matchlist(v:exception,
\ '\vFile ' . l:fname_pattern . ', line (\d+), characters (\d+)-\d+:\n(.*)\n', 0, l:matchidx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the double indirection matching the output using (1) a pattern built from (2) a dynamic variable, could you just do a looser match for anything that looks like "filename or " and skip any that don't match? This seems like it could be brittle if e.g. paths may be absolute/relative, or have any trivial extra characters like "./".

if empty(l:tokens)
break
endif
call add(l:errors, {
\ 'filename': @%,
\ 'lnum': l:tokens[1] + a:startline - 1,
\ 'col': l:tokens[2],
\ 'text': l:tokens[3]})
let l:matchidx = l:matchidx + 1
endwhile

if empty(l:errors)
" Couldn't parse ocamlformat error format; display it all.
call maktaba#error#Shout('Error formatting file: %s', v:exception)
else
call setqflist(l:errors, 'r')
cc 1
endif
endtry
endfunction

return l:formatter
endfunction
13 changes: 9 additions & 4 deletions doc/codefmt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ Default: 'dartfmt' `
The path to the js-beautify executable.
Default: 'js-beautify' `

*codefmt:black_executable*
The path to the black executable.
Default: 'black' `

*codefmt:yapf_executable*
The path to the yapf executable.
Default: 'yapf' `

*codefmt:black_executable*
The path to the black executable.
Default: 'black' `

*codefmt:gn_executable*
The path to the gn executable.
Default: 'gn' `
Expand Down Expand Up @@ -122,6 +122,10 @@ Default: 'zprint' `
The path to the fish_indent executable.
Default: 'fish_indent' `

*codefmt:ocamlformat_executable*
The path to the ocamlformat executable.
Default: 'ocamlformat' `

*codefmt:plugin[autocmds]*
Configures whether plugin/autocmds.vim should be loaded.
Default: 1 `
Expand Down Expand Up @@ -168,6 +172,7 @@ The current list of defaults by filetype is:
* fish: fish_indent
* gn: gn
* go: gofmt
* ocaml: ocamlformat
* python: autopep8, black, yapf

==============================================================================
Expand Down
4 changes: 4 additions & 0 deletions instant/flags.vim
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,7 @@ call s:plugin.Flag('zprint_executable', 'zprint')
""
" The path to the fish_indent executable.
call s:plugin.Flag('fish_indent_executable', 'fish_indent')

""
" The path to the ocamlformat executable.
call s:plugin.Flag('ocamlformat_executable', 'ocamlformat')
1 change: 1 addition & 0 deletions plugin/register.vim
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ call s:registry.AddExtension(codefmt#googlejava#GetFormatter())
call s:registry.AddExtension(codefmt#shfmt#GetFormatter())
call s:registry.AddExtension(codefmt#zprint#GetFormatter())
call s:registry.AddExtension(codefmt#fish_indent#GetFormatter())
call s:registry.AddExtension(codefmt#ocamlformat#GetFormatter())
100 changes: 100 additions & 0 deletions vroom/ocamlformat.vroom
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
The built-in ocamlformat formatter knows how to format ocaml 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 ocamlformat formatter expects the ocamlformat executable to be installed on your system.

@clear
% f()
:FormatCode ocamlformat
! ocamlformat .*
$ ;;
$ f ()
;;
f ()
@end

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

@clear
:Glaive codefmt ocamlformat_executable='myocamlformat'
:FormatCode ocamlformat
! myocamlformat .*
$ ;;
$ f ()
:Glaive codefmt ocamlformat_executable='ocamlformat'


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

@clear
% let x=5 in<CR>
| let inc x=x+1 in<CR>
| print_endline (string_of_int (inc x))

:FormatCode ocamlformat
! ocamlformat .*
$ ;;
$ let x = 5 in
$ let inc x = x + 1 in
$ print_endline (string_of_int (inc x))
;;
let x = 5 in
let inc x = x + 1 in
print_endline (string_of_int (inc x))
@end

The ocaml filetype will use the ocamlformat formatter by default. The path to the
file being edited is passed to ocamlformat so that it can adjust to whether the
file is an implementation or an interface file.

@clear
% f()

:silent file /foo/bar/test.ml
:set filetype=ocaml
:FormatCode
! ocamlformat - --name /foo/bar/test.ml .*
$ ;;
$ f ()
;;
f ()
@end

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

@clear
% print_string "Hello";;<CR>
|let x=1 in let y=2 in<CR>
|y+2

:2,3FormatLines ocamlformat
! ocamlformat .*
$ ;;
$ let x = 1 in
$ let y = 2 in
$ y + 2
print_string "Hello";;
;;
let x = 1 in
let y = 2 in
y + 2
@end

NOTE: the ocamlformat formatter does not natively support range formatting, so there
are certain limitations like not being able to format misaligned braces.