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 ruby formatting support using Rubocop. #220

Merged
merged 6 commits into from
Jun 1, 2023
Merged
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 @@ -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)
Expand Down
23 changes: 19 additions & 4 deletions autoload/codefmt/formatterhelpers.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Fun fact: You can also do let l:ignoreerrors = get(a:, 1, 0) for the same effect, slightly more cryptic but a bit more literal once you're used to the syntax.

Either way works fine here though, and I'm focused on getting this PR merged quickly after all the delays. =)

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
Expand Down
64 changes: 64 additions & 0 deletions autoload/codefmt/rubocop.vim
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion doc/codefmt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions instant/flags.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions plugin/register.vim
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
" * nix: nixpkgs-fmt
" * ocaml: ocamlformat
" * python: autopep8, black, yapf
" * ruby: rubocop
" * rust: rustfmt
" * sh: shfmt
" * swift: swift-format
Expand Down Expand Up @@ -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())
Expand Down
60 changes: 60 additions & 0 deletions vroom/rubocop.vroom
Original file line number Diff line number Diff line change
@@ -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(...)<CR>
| call add(g:repeat_calls, a:000)<CR>
: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 <CR> <CR> 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 <CR> <CR> end
:FormatCode rubocop
! rubocop .*
$ =========
$ def SomeClass
$ end
$ 1 (status)
def SomeClass
end