diff --git a/lib/ruby_lsp/requests/code_action_resolve.rb b/lib/ruby_lsp/requests/code_action_resolve.rb index a0800f868b..e4d7e6a68c 100644 --- a/lib/ruby_lsp/requests/code_action_resolve.rb +++ b/lib/ruby_lsp/requests/code_action_resolve.rb @@ -51,20 +51,42 @@ def perform #: -> (Interface::CodeAction) def switch_block_style source_range = @code_action.dig(:data, :range) - raise EmptySelectionError, "Invalid selection for refactor" if source_range[:start] == source_range[:end] + if source_range[:start] == source_range[:end] + block_context = @document.locate_node( + source_range[:start], + node_types: [Prism::BlockNode], + ) + node = block_context.node + unless node.is_a?(Prism::BlockNode) + raise InvalidTargetRangeError, "Cursor is not inside a block" + end - target = @document.locate_first_within_range( - @code_action.dig(:data, :range), - node_types: [Prism::CallNode], - ) + # Find the call node at the block node's start position. + # This should be the call node whose block the cursor is inside of. + call_context = RubyDocument.locate( + @document.ast, + node.location.cached_start_code_units_offset(@document.code_units_cache), + node_types: [Prism::CallNode], + code_units_cache: @document.code_units_cache, + ) + target = call_context.node + unless target.is_a?(Prism::CallNode) && target.block == node + raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor" + end + else + target = @document.locate_first_within_range( + @code_action.dig(:data, :range), + node_types: [Prism::CallNode], + ) - unless target.is_a?(Prism::CallNode) - raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor" - end + unless target.is_a?(Prism::CallNode) + raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor" + end - node = target.block - unless node.is_a?(Prism::BlockNode) - raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor" + node = target.block + unless node.is_a?(Prism::BlockNode) + raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor" + end end indentation = " " * target.location.start_column unless node.opening_loc.slice == "do" diff --git a/lib/ruby_lsp/requests/code_actions.rb b/lib/ruby_lsp/requests/code_actions.rb index dbc6d91ef0..5861f4cea8 100644 --- a/lib/ruby_lsp/requests/code_actions.rb +++ b/lib/ruby_lsp/requests/code_actions.rb @@ -63,12 +63,8 @@ def perform kind: Constant::CodeActionKind::REFACTOR_EXTRACT, data: { range: @range, uri: @uri.to_s }, ) - code_actions << Interface::CodeAction.new( - title: TOGGLE_BLOCK_STYLE_TITLE, - kind: Constant::CodeActionKind::REFACTOR_REWRITE, - data: { range: @range, uri: @uri.to_s }, - ) end + code_actions.concat(toggle_block_style_action) code_actions.concat(attribute_actions) code_actions @@ -113,6 +109,25 @@ def attribute_actions ), ] end + + #: -> Array[Interface::CodeAction] + def toggle_block_style_action + if @range[:start] == @range[:end] + block_context = @document.locate_node( + @range[:start], + node_types: [Prism::BlockNode], + ) + return [] unless block_context.node.is_a?(Prism::BlockNode) + end + + [ + Interface::CodeAction.new( + title: TOGGLE_BLOCK_STYLE_TITLE, + kind: Constant::CodeActionKind::REFACTOR_REWRITE, + data: { range: @range, uri: @uri.to_s }, + ), + ] + end end end end diff --git a/test/expectations/code_action_resolve/aref_call_aref_assign_without_selection.exp.json b/test/expectations/code_action_resolve/aref_call_aref_assign_without_selection.exp.json new file mode 100644 index 0000000000..6234613d9e --- /dev/null +++ b/test/expectations/code_action_resolve/aref_call_aref_assign_without_selection.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Toggle block style", + "data": { + "range": { + "start": { + "line": 0, + "character": 29 + }, + "end": { + "line": 0, + "character": 29 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Toggle 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_without_selection.exp.json b/test/expectations/code_action_resolve/nested_block_calls_without_selection.exp.json new file mode 100644 index 0000000000..fbcc0cde88 --- /dev/null +++ b/test/expectations/code_action_resolve/nested_block_calls_without_selection.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Toggle block style", + "data": { + "range": { + "start": { + "line": 1, + "character": 30 + }, + "end": { + "line": 1, + "character": 30 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Toggle 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 { |b| } }" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/nested_block_calls_without_selection_inner.exp.json b/test/expectations/code_action_resolve/nested_block_calls_without_selection_inner.exp.json new file mode 100644 index 0000000000..3253164f62 --- /dev/null +++ b/test/expectations/code_action_resolve/nested_block_calls_without_selection_inner.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Toggle block style", + "data": { + "range": { + "start": { + "line": 1, + "character": 37 + }, + "end": { + "line": 1, + "character": 37 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Toggle block style", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 1, + "character": 32 + }, + "end": { + "line": 2, + "character": 5 + } + }, + "newText": "{ |b| }" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection.exp.json b/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection.exp.json new file mode 100644 index 0000000000..962db12217 --- /dev/null +++ b/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Toggle block style", + "data": { + "range": { + "start": { + "line": 0, + "character": 31 + }, + "end": { + "line": 0, + "character": 31 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Toggle block style", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 0, + "character": 29 + }, + "end": { + "line": 0, + "character": 74 + } + }, + "newText": "do |a|\n nested_call(fourth_call).each do |b|\n \n end\nend" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection_inner.exp.json b/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection_inner.exp.json new file mode 100644 index 0000000000..c2ef44dc85 --- /dev/null +++ b/test/expectations/code_action_resolve/nested_oneline_blocks_without_selection_inner.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "refactor.rewrite", + "title": "Refactor: Toggle block style", + "data": { + "range": { + "start": { + "line": 0, + "character": 68 + }, + "end": { + "line": 0, + "character": 68 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Refactor: Toggle block style", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 0, + "character": 65 + }, + "end": { + "line": 0, + "character": 72 + } + }, + "newText": "do |b|\n \n end" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_actions/toggle_block_outside_block.exp.json b/test/expectations/code_actions/toggle_block_outside_block.exp.json new file mode 100644 index 0000000000..f2dd748e64 --- /dev/null +++ b/test/expectations/code_actions/toggle_block_outside_block.exp.json @@ -0,0 +1,22 @@ +{ + "params": { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "context": { + "diagnostics": [] + } + }, + "result": [] +} diff --git a/test/expectations/code_actions/toggle_block_without_selection.exp.json b/test/expectations/code_actions/toggle_block_without_selection.exp.json new file mode 100644 index 0000000000..5160ff328c --- /dev/null +++ b/test/expectations/code_actions/toggle_block_without_selection.exp.json @@ -0,0 +1,40 @@ +{ + "params": { + "range": { + "start": { + "line": 0, + "character": 22 + }, + "end": { + "line": 0, + "character": 22 + } + }, + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "context": { + "diagnostics": [] + } + }, + "result": [ + { + "title": "Refactor: Toggle block style", + "kind": "refactor.rewrite", + "data": { + "range": { + "start": { + "line": 0, + "character": 22 + }, + "end": { + "line": 0, + "character": 22 + } + }, + "uri": "file:///fake" + } + } + ] +} diff --git a/test/fixtures/aref_call_aref_assign_without_selection.rb b/test/fixtures/aref_call_aref_assign_without_selection.rb new file mode 100644 index 0000000000..5d8c504ba4 --- /dev/null +++ b/test/fixtures/aref_call_aref_assign_without_selection.rb @@ -0,0 +1 @@ +object["attributes"].find { |a| a["field"] == "expected" }["value"] = "changed" diff --git a/test/fixtures/nested_block_calls_without_selection.rb b/test/fixtures/nested_block_calls_without_selection.rb new file mode 100644 index 0000000000..43de2c3b02 --- /dev/null +++ b/test/fixtures/nested_block_calls_without_selection.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/fixtures/nested_block_calls_without_selection_inner.rb b/test/fixtures/nested_block_calls_without_selection_inner.rb new file mode 100644 index 0000000000..43de2c3b02 --- /dev/null +++ b/test/fixtures/nested_block_calls_without_selection_inner.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/fixtures/nested_oneline_blocks_without_selection.rb b/test/fixtures/nested_oneline_blocks_without_selection.rb new file mode 100644 index 0000000000..62caf97a7a --- /dev/null +++ b/test/fixtures/nested_oneline_blocks_without_selection.rb @@ -0,0 +1 @@ +method_call(other_call).each { |a| nested_call(fourth_call).each { |b| } } diff --git a/test/fixtures/nested_oneline_blocks_without_selection_inner.rb b/test/fixtures/nested_oneline_blocks_without_selection_inner.rb new file mode 100644 index 0000000000..62caf97a7a --- /dev/null +++ b/test/fixtures/nested_oneline_blocks_without_selection_inner.rb @@ -0,0 +1 @@ +method_call(other_call).each { |a| nested_call(fourth_call).each { |b| } } diff --git a/test/fixtures/toggle_block_outside_block.rb b/test/fixtures/toggle_block_outside_block.rb new file mode 100644 index 0000000000..1cc5655739 --- /dev/null +++ b/test/fixtures/toggle_block_outside_block.rb @@ -0,0 +1 @@ +some_call { block_body } diff --git a/test/fixtures/toggle_block_without_selection.rb b/test/fixtures/toggle_block_without_selection.rb new file mode 100644 index 0000000000..1cc5655739 --- /dev/null +++ b/test/fixtures/toggle_block_without_selection.rb @@ -0,0 +1 @@ +some_call { block_body }