diff --git a/lib/ruby_lsp/requests/code_action_resolve.rb b/lib/ruby_lsp/requests/code_action_resolve.rb index 962c3f2a58..c4e384a511 100644 --- a/lib/ruby_lsp/requests/code_action_resolve.rb +++ b/lib/ruby_lsp/requests/code_action_resolve.rb @@ -23,6 +23,8 @@ module Requests # class CodeActionResolve < Request extend T::Sig + include Support::Common + NEW_VARIABLE_NAME = "new_variable" NEW_METHOD_NAME = "new_method" @@ -50,11 +52,70 @@ def perform refactor_variable when CodeActions::EXTRACT_TO_METHOD_TITLE refactor_method + when CodeActions::SWITCH_BLOCK_STYLE_TITLE + switch_block_style else Error::UnknownCodeAction end end + private + + sig { returns(T.any(Interface::CodeAction, Error)) } + def switch_block_style + return Error::EmptySelection if @document.source.empty? + + source_range = @code_action.dig(:data, :range) + return Error::EmptySelection if source_range[:start] == source_range[:end] + + target = @document.locate_first_within_range( + @code_action.dig(:data, :range), + node_types: [Prism::CallNode], + ) + + return Error::InvalidTargetRange unless target.is_a?(Prism::CallNode) + + node = target.block + return Error::InvalidTargetRange unless node.is_a?(Prism::BlockNode) + + parameters = node.parameters + body = node.body + + # If the block is using `do...end` style, we change it to a single line brace block. Newlines are turned into + # semi colons, so that the result is valid Ruby code and still a one liner. If the block is using brace style, + # we do the opposite and turn it into a `do...end` block, making all semi colons into newlines. + new_source = if node.opening_loc.slice == "do" + source = +"{ " + source << "#{parameters.slice} " if parameters + source << "#{body.slice.gsub("\n", ";")} " if body + source << "}" + else + indentation = " " * target.location.start_column + source = +"do" + source << " #{parameters.slice}" if parameters + source << "\n#{indentation} " + source << body.slice.gsub(";", "\n") if body + source << "\n#{indentation}end" + end + + Interface::CodeAction.new( + title: CodeActions::SWITCH_BLOCK_STYLE_TITLE, + edit: Interface::WorkspaceEdit.new( + document_changes: [ + Interface::TextDocumentEdit.new( + text_document: Interface::OptionalVersionedTextDocumentIdentifier.new( + uri: @code_action.dig(:data, :uri), + version: nil, + ), + edits: [ + Interface::TextEdit.new(range: range_from_location(node.location), new_text: new_source), + ], + ), + ], + ), + ) + end + sig { returns(T.any(Interface::CodeAction, Error)) } def refactor_variable return Error::EmptySelection if @document.source.empty? @@ -206,8 +267,6 @@ def refactor_method ) end - private - sig { params(range: T::Hash[Symbol, T.untyped], new_text: String).returns(Interface::TextEdit) } def create_text_edit(range, new_text) Interface::TextEdit.new( diff --git a/lib/ruby_lsp/requests/code_actions.rb b/lib/ruby_lsp/requests/code_actions.rb index d710501fd9..f60aec7cf9 100644 --- a/lib/ruby_lsp/requests/code_actions.rb +++ b/lib/ruby_lsp/requests/code_actions.rb @@ -21,6 +21,7 @@ class CodeActions < Request EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable" EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method" + SWITCH_BLOCK_STYLE_TITLE = "Refactor: Switch block style" class << self extend T::Sig @@ -69,6 +70,11 @@ def perform kind: Constant::CodeActionKind::REFACTOR_EXTRACT, data: { range: @range, uri: @uri.to_s }, ) + code_actions << Interface::CodeAction.new( + title: SWITCH_BLOCK_STYLE_TITLE, + kind: Constant::CodeActionKind::REFACTOR_REWRITE, + data: { range: @range, uri: @uri.to_s }, + ) end code_actions diff --git a/test/expectations/code_action_resolve/aref_call_aref_assign.exp.json b/test/expectations/code_action_resolve/aref_call_aref_assign.exp.json new file mode 100644 index 0000000000..d9c6689965 --- /dev/null +++ b/test/expectations/code_action_resolve/aref_call_aref_assign.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Switch block style", + "data": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 58 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Switch block style", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 0, + "character": 26 + }, + "end": { + "line": 0, + "character": 58 + } + }, + "newText": "do |a|\n a[\"field\"] == \"expected\"\nend" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/nested_block_calls.exp.json b/test/expectations/code_action_resolve/nested_block_calls.exp.json new file mode 100644 index 0000000000..4c0cad78a3 --- /dev/null +++ b/test/expectations/code_action_resolve/nested_block_calls.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Switch block style", + "data": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 3, + "character": 3 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Switch block style", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 0, + "character": 29 + }, + "end": { + "line": 3, + "character": 3 + } + }, + "newText": "{ |a| nested_call(fourth_call).each do |b|; end }" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_actions/aref_field.exp.json b/test/expectations/code_actions/aref_field.exp.json index 77e89b68f3..4e7478c73e 100644 --- a/test/expectations/code_actions/aref_field.exp.json +++ b/test/expectations/code_actions/aref_field.exp.json @@ -52,6 +52,23 @@ }, "uri": "file:///fake" } + }, + { + "title": "Refactor: Switch block style", + "kind": "refactor.rewrite", + "data": { + "range": { + "start": { + "line": 2, + "character": 9 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "uri": "file:///fake" + } } ] } diff --git a/test/fixtures/nested_block_calls.rb b/test/fixtures/nested_block_calls.rb new file mode 100644 index 0000000000..43de2c3b02 --- /dev/null +++ b/test/fixtures/nested_block_calls.rb @@ -0,0 +1,4 @@ +method_call(other_call).each do |a| + nested_call(fourth_call).each do |b| + end +end diff --git a/test/requests/code_actions_expectations_test.rb b/test/requests/code_actions_expectations_test.rb index 4cf9376b39..f0c6be9c60 100644 --- a/test/requests/code_actions_expectations_test.rb +++ b/test/requests/code_actions_expectations_test.rb @@ -76,7 +76,7 @@ def code_action_for_diagnostic(diagnostic) def code_action_for_refactor(refactor) LanguageServer::Protocol::Interface::CodeAction.new( title: refactor["title"], - kind: LanguageServer::Protocol::Constant::CodeActionKind::REFACTOR_EXTRACT, + kind: refactor["kind"], data: { range: refactor.dig("data", "range"), uri: refactor.dig("data", "uri"),