Skip to content
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
35 changes: 33 additions & 2 deletions lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,48 @@ def autocorrect_action

#: -> Array[Interface::TextEdit]
def offense_replacements
source_lines = @document.cache_fetch("rubocop_source_lines") do
lines = @document.source.lines
# String#lines omits the empty logical line at the end of a newline-terminated document.
lines << "" if lines.empty? || @document.source.end_with?("\n")
lines
end #: Array[String]

@offense.corrector.as_replacements.map do |range, replacement|
Interface::TextEdit.new(
range: Interface::Range.new(
start: Interface::Position.new(line: range.line - 1, character: range.column),
end: Interface::Position.new(line: range.last_line - 1, character: range.last_column),
start: position(range.line, range.column, source_lines.fetch(range.line - 1)),
end: position(range.last_line, range.last_column, source_lines.fetch(range.last_line - 1)),
),
new_text: replacement,
)
end
end

#: (Integer line, Integer column, String source_line) -> Interface::Position
def position(line, column, source_line)
Interface::Position.new(
line: line - 1,
character: position_column(column, source_line),
)
end

#: (Integer column, String source_line) -> Integer
def position_column(column, source_line)
# RuboCop columns count codepoints, while LSP columns use the negotiated encoding.
prefix = source_line.slice(0, column) || ""
case @document.encoding
when Encoding::UTF_8
prefix.bytesize
when Encoding::UTF_16LE
prefix.codepoints.sum do |codepoint|
codepoint > RubyLsp::Document::Scanner::SURROGATE_PAIR_START ? 2 : 1
end
else
prefix.length
end
end

#: -> Interface::CodeAction
def disable_line_action
Interface::CodeAction.new(
Expand Down
105 changes: 98 additions & 7 deletions test/requests/code_actions_formatting_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,50 @@ def test_disable_line__emoji
assert_disable_line("emoji", "Lint/UselessAssignment")
end

def test_autocorrect_string_literals_with_multibyte_characters_utf8
assert_string_literal_autocorrected("utf-8")
end

def test_autocorrect_string_literals_with_multibyte_characters_utf16
assert_string_literal_autocorrected("utf-16")
end

def test_autocorrect_string_literals_with_multibyte_characters_utf32
assert_string_literal_autocorrected("utf-32")
end

def test_autocorrect_trailing_empty_lines_utf8
assert_trailing_empty_lines_autocorrected("utf-8")
end

def test_autocorrect_trailing_empty_lines_utf16
assert_trailing_empty_lines_autocorrected("utf-16")
end

def test_autocorrect_trailing_empty_lines_utf32
assert_trailing_empty_lines_autocorrected("utf-32")
end

def test_autocorrect_empty_document
document = RubyLsp::RubyDocument.new(
source: "",
version: 1,
uri: URI::Generic.from_path(path: __FILE__),
global_state: RubyLsp::GlobalState.new,
)
range = stub(line: 1, column: 0, last_line: 1, last_column: 0)
offense = stub(corrector: stub(as_replacements: [[range, "# comment"]]))
diagnostic = RubyLsp::Requests::Support::RuboCopDiagnostic.new(document, offense, document.uri)

edit = diagnostic.send(:offense_replacements).first
assert_equal(0, edit.range.start.line)
assert_equal(0, edit.range.start.character)
assert_equal(0, edit.range.end.character)

range.stubs(:last_line).returns(2)
assert_raises(IndexError) { diagnostic.send(:offense_replacements) }
end

def test_disable_line__lambda_indentation
assert_disable_line("lambda_indentation", "Layout/IndentationConsistency")
end
Expand Down Expand Up @@ -50,6 +94,45 @@ def foo; end

private

def assert_trailing_empty_lines_autocorrected(position_encoding)
["\n", "\r\n"].each do |newline|
source = "value = \"こんにちは 😀\"#{newline}#{newline}"
expected = "value = \"こんにちは 😀\"\n"
assert_corrects_to_expected(
"Layout/TrailingEmptyLines",
"Autocorrect Layout/TrailingEmptyLines",
source,
expected,
position_encoding: position_encoding,
)
end

assert_corrects_to_expected(
"Layout/TrailingEmptyLines",
"Autocorrect Layout/TrailingEmptyLines",
"\n\n",
"\n",
position_encoding: position_encoding,
)
end

def assert_string_literal_autocorrected(position_encoding)
source = <<~RUBY
value = 'こんにちは 😀'
RUBY
expected = <<~RUBY
value = "こんにちは 😀"
RUBY

assert_corrects_to_expected(
"Style/StringLiterals",
"Autocorrect Style/StringLiterals",
source,
expected,
position_encoding: position_encoding,
)
end

def assert_disable_line(fixture, cop_name)
assert_fixtures_match(
"rubocop_#{fixture}",
Expand Down Expand Up @@ -84,9 +167,13 @@ def load_expectation(name)
[source, expected]
end

#: (String diagnostic_code, String code_action_title, String source, String expected) -> untyped
def assert_corrects_to_expected(diagnostic_code, code_action_title, source, expected)
document, _diagnostic, result = setup_code_action_context(source, diagnostic_code)
#: (String diagnostic_code, String code_action_title, String source, String expected, ?position_encoding: String?) -> untyped
def assert_corrects_to_expected(diagnostic_code, code_action_title, source, expected, position_encoding: nil)
document, _diagnostic, result = setup_code_action_context(
source,
diagnostic_code,
position_encoding: position_encoding,
)

selected_action = find_code_action_by_title(result, code_action_title) #: as !nil

Expand All @@ -105,12 +192,16 @@ def assert_corrects_to_expected(diagnostic_code, code_action_title, source, expe
assert_equal(document.source, expected)
end

#: (String source, String diagnostic_code) -> [RubyLsp::RubyDocument, LanguageServer::Protocol::Interface::Diagnostic, Array[LanguageServer::Protocol::Interface::CodeAction]?]
def setup_code_action_context(source, diagnostic_code)
#: (String source, String diagnostic_code, ?position_encoding: String?) -> [RubyLsp::RubyDocument, LanguageServer::Protocol::Interface::Diagnostic, Array[LanguageServer::Protocol::Interface::CodeAction]?]
def setup_code_action_context(source, diagnostic_code, position_encoding: nil)
global_state = RubyLsp::GlobalState.new
global_state.apply_options({
options = {
initializationOptions: { linters: ["rubocop_internal"] },
})
}
if position_encoding
options[:capabilities] = { general: { positionEncodings: [position_encoding] } }
end
global_state.apply_options(options)
global_state.register_formatter(
"rubocop_internal",
RubyLsp::Requests::Support::RuboCopFormatter.new,
Expand Down
Loading