diff --git a/README.md b/README.md index 237d7f7..6e9a9ed 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ helpfiles in the `doc/` directory. The helpfiles are also available via * OCaml ([ocamlformat](https://github.com/ocaml-ppx/ocamlformat)) * Proto (clang-format) * Python (Autopep8, Black, or YAPF) +* Ruby ([rubocop](https://rubocop.org)) * Rust ([rustfmt](https://github.com/rust-lang/rustfmt)) * TypeScript (clang-format) * Shell (shfmt) diff --git a/autoload/codefmt/formatterhelpers.vim b/autoload/codefmt/formatterhelpers.vim index a1babb8..f2ef96f 100644 --- a/autoload/codefmt/formatterhelpers.vim +++ b/autoload/codefmt/formatterhelpers.vim @@ -75,6 +75,7 @@ endfunction "" " @public +" @usage startline endline cmd \[ignoreerrors] \[skipfirstnlines] " Attempt to format a range of lines from {startline} to {endline} in the " current buffer via a formatter that doesn't natively support range " formatting, which is invoked via {cmd} (a system call represented by either @@ -87,21 +88,35 @@ endfunction " the tool for range formatting and post a URL for that feature request above " code that calls it. " -" @throws ShellError if the {cmd} system call fails +" If [ignoreerrors] is nonzero, the syscall ignores errors. This can be helpful +" for formatters that return nonzero results for reasons unrelated to +" formatting. If [skipfirstnlines] is set to a nonzero number N, the first +" N lines of the formatter output are trimmed. This can be used to trim +" always-present headers. +" +" @throws ShellError if the {cmd} system call fails (and [ignoreerrors] is 0) " @throws WrongType function! codefmt#formatterhelpers#AttemptFakeRangeFormatting( - \ startline, endline, cmd) abort + \ startline, endline, cmd, ...) abort call maktaba#ensure#IsNumber(a:startline) call maktaba#ensure#IsNumber(a:endline) + let l:ignoreerrors = a:0 >= 1 ? a:1 : 0 + let l:skipfirstnlines = a:0 >= 2 ? a:2 : 0 + + call maktaba#ensure#IsNumber(l:ignoreerrors) + call maktaba#ensure#IsNumber(l:skipfirstnlines) + let l:lines = getline(1, line('$')) let l:input = join(l:lines[a:startline - 1 : a:endline - 1], "\n") - let l:result = maktaba#syscall#Create(a:cmd).WithStdin(l:input).Call() + let l:result = + \ maktaba#syscall#Create(a:cmd).WithStdin(l:input).Call(!l:ignoreerrors) let l:formatted = split(l:result.stdout, "\n") " Special case empty slice: neither l:lines[:0] nor l:lines[:-1] is right. let l:before = a:startline > 1 ? l:lines[ : a:startline - 2] : [] - let l:full_formatted = l:before + l:formatted + l:lines[a:endline :] + let l:full_formatted = l:before + l:formatted[l:skipfirstnlines :] + \ + l:lines[a:endline :] call maktaba#buffer#Overwrite(1, line('$'), l:full_formatted) endfunction diff --git a/autoload/codefmt/rubocop.vim b/autoload/codefmt/rubocop.vim new file mode 100644 index 0000000..0687dda --- /dev/null +++ b/autoload/codefmt/rubocop.vim @@ -0,0 +1,64 @@ +" Copyright 2023 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: rubocop +function! codefmt#rubocop#GetFormatter() abort + let l:formatter = { + \ 'name': 'rubocop', + \ 'setup_instructions': 'Install rubocop ' . + \ '(https://rubygems.org/gems/rubocop).'} + + function l:formatter.IsAvailable() abort + return executable(s:plugin.Flag('rubocop_executable')) + endfunction + + function l:formatter.AppliesToBuffer() abort + return &filetype is# 'eruby' || &filetype is# 'ruby' + endfunction + + "" + " Reformat the current buffer with rubocop or the binary named in + " @flag(rubocop_executable), only targeting the range between {startline} and + " {endline}. + " + " @throws ShellError + function l:formatter.FormatRange(startline, endline) abort + " See flag explanations at: + " https://docs.rubocop.org/rubocop/1.51/usage/basic_usage.html + let l:cmd = [s:plugin.Flag('rubocop_executable'), '--stdin', @%, '-a', '--no-color', '-fq', '-o', '/dev/null'] + + " Rubocop exits with an error condition if there are lint errors, even + " after successfully formatting. This is annoying for our purpuoses, + " because we have no way to distinguish lint errors from a 'real' falure. + " Use Call(0) to suppress maktaba's error handling. + let l:ignoreerrors = 1 + " Rubocop is primarily a linter, and by default it outputs lint errors + " first, followed by a dividing line, and then the formatted result. + " '-o /dev/null' in the command line suppresses any lint errors, but the + " divider is always printed. + let l:skipfirstnlines = 1 + + " Rubocop does not support range formatting; see bug: + " https://github.com/Shopify/ruby-lsp/issues/203 + call codefmt#formatterhelpers#AttemptFakeRangeFormatting( + \ a:startline, a:endline, l:cmd, l:ignoreerrors, l:skipfirstnlines) + endfunction + + return l:formatter +endfunction diff --git a/doc/codefmt.txt b/doc/codefmt.txt index db07692..42d3786 100644 --- a/doc/codefmt.txt +++ b/doc/codefmt.txt @@ -383,7 +383,7 @@ codefmt#formatterhelpers#Format({cmd}) *codefmt#formatterhelpers#Format()* Throws ERROR(ShellError) if the {cmd} system call fails codefmt#formatterhelpers#AttemptFakeRangeFormatting({startline}, {endline}, - {cmd}) *codefmt#formatterhelpers#AttemptFakeRangeFormatting()* + {cmd}, {ignoreerrors}, {skipfirstnlines}) *codefmt#formatterhelpers#AttemptFakeRangeFormatting()* Attempt to format a range of lines from {startline} to {endline} in the current buffer via a formatter that doesn't natively support range formatting, which is invoked via {cmd} (a system call represented by either diff --git a/instant/flags.vim b/instant/flags.vim index 3e10fa6..4900c5b 100644 --- a/instant/flags.vim +++ b/instant/flags.vim @@ -174,6 +174,10 @@ call s:plugin.Flag('prettier_options', []) " The path to the swift-format executable. call s:plugin.Flag('swift_format_executable', 'swift-format') +"" +" The path to the Rubocop executable. +call s:plugin.Flag('rubocop_executable', 'rubocop') + "" " @private function s:LookupPrettierExecutable() abort diff --git a/plugin/register.vim b/plugin/register.vim index c1804f5..36f9d6c 100644 --- a/plugin/register.vim +++ b/plugin/register.vim @@ -44,6 +44,7 @@ " * nix: nixpkgs-fmt " * ocaml: ocamlformat " * python: autopep8, black, yapf +" * ruby: rubocop " * rust: rustfmt " * sh: shfmt " * swift: swift-format @@ -80,6 +81,7 @@ call s:registry.AddExtension(codefmt#autopep8#GetFormatter()) call s:registry.AddExtension(codefmt#isort#GetFormatter()) call s:registry.AddExtension(codefmt#black#GetFormatter()) call s:registry.AddExtension(codefmt#yapf#GetFormatter()) +call s:registry.AddExtension(codefmt#rubocop#GetFormatter()) call s:registry.AddExtension(codefmt#rustfmt#GetFormatter()) call s:registry.AddExtension(codefmt#shfmt#GetFormatter()) call s:registry.AddExtension(codefmt#swiftformat#GetFormatter()) diff --git a/vroom/rubocop.vroom b/vroom/rubocop.vroom new file mode 100644 index 0000000..d771f70 --- /dev/null +++ b/vroom/rubocop.vroom @@ -0,0 +1,60 @@ +Rubocop is a linter and formatter for ruby. +If you aren't familiar with basic codefmt usage yet, see main.vroom first. + +First, set up the vroom environment. + + :source $VROOMDIR/setupvroom.vim + + :let g:repeat_calls = [] + :function FakeRepeat(...) + | call add(g:repeat_calls, a:000) + :endfunction + :call maktaba#test#Override('repeat#set', 'FakeRepeat') + + :call codefmt#SetWhetherToPerformIsAvailableChecksForTesting(0) + +By default, the rubocop executable is called. To use this plugin, rubocop +must be installed on your system. (But not for testing; vroom intercepts +system calls.) + :FormatCode rubocop + ! rubocop .* + + + +The name and path of the Rubocop executable can be configured with a flag: + :Glaive codefmt rubocop_executable=some_other_program + :FormatCode rubocop + ! some_other_program .*-fq.* + :Glaive codefmt rubocop_executable=rubocop + + + +Rubocop does basic whitespace management. Though because it's primarily a +linter, it outputs lint errors first, then a separator. Even with the lint +errors disabled (-fq), you still get the separator. + + % def SomeClass end + :FormatCode rubocop + ! rubocop .*-fq.* + $ ========= + $ def SomeClass + $ end + def SomeClass + end + + + +Being a linter, Rubocop cares about style as well as formatting. +When a buffer is stylistically fine, it returns 0, and everything is OK. +But sometimes it will return 1 even though things have gone well; +we should still use the output when that happens. + + % def SomeClass end + :FormatCode rubocop + ! rubocop .* + $ ========= + $ def SomeClass + $ end + $ 1 (status) + def SomeClass + end