From c273e3c08b9513c661f987f0a78b35d493187ee2 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Thu, 8 Aug 2024 10:27:23 -0400 Subject: [PATCH] Apply consistent block style to all nested blocks --- lib/ruby_lsp/requests/code_action_resolve.rb | 91 +++++++++++++------ .../nested_block_calls.exp.json | 2 +- .../nested_oneline_blocks.exp.json | 47 ++++++++++ test/fixtures/nested_oneline_blocks.rb | 1 + 4 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 test/expectations/code_action_resolve/nested_oneline_blocks.exp.json create mode 100644 test/fixtures/nested_oneline_blocks.rb diff --git a/lib/ruby_lsp/requests/code_action_resolve.rb b/lib/ruby_lsp/requests/code_action_resolve.rb index c4e384a51..68101d4a5 100644 --- a/lib/ruby_lsp/requests/code_action_resolve.rb +++ b/lib/ruby_lsp/requests/code_action_resolve.rb @@ -47,6 +47,8 @@ def initialize(document, code_action) sig { override.returns(T.any(Interface::CodeAction, Error)) } def perform + return Error::EmptySelection if @document.source.empty? + case @code_action[:title] when CodeActions::EXTRACT_TO_VARIABLE_TITLE refactor_variable @@ -63,8 +65,6 @@ def perform 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] @@ -78,25 +78,7 @@ def switch_block_style 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 + indentation = " " * target.location.start_column unless node.opening_loc.slice == "do" Interface::CodeAction.new( title: CodeActions::SWITCH_BLOCK_STYLE_TITLE, @@ -108,7 +90,10 @@ def switch_block_style version: nil, ), edits: [ - Interface::TextEdit.new(range: range_from_location(node.location), new_text: new_source), + Interface::TextEdit.new( + range: range_from_location(node.location), + new_text: recursively_switch_nested_block_styles(node, indentation), + ), ], ), ], @@ -118,8 +103,6 @@ def switch_block_style sig { returns(T.any(Interface::CodeAction, Error)) } def refactor_variable - return Error::EmptySelection if @document.source.empty? - source_range = @code_action.dig(:data, :range) return Error::EmptySelection if source_range[:start] == source_range[:end] @@ -214,8 +197,6 @@ def refactor_variable sig { returns(T.any(Interface::CodeAction, Error)) } def refactor_method - return Error::EmptySelection if @document.source.empty? - source_range = @code_action.dig(:data, :range) return Error::EmptySelection if source_range[:start] == source_range[:end] @@ -277,6 +258,64 @@ def create_text_edit(range, new_text) new_text: new_text, ) end + + sig { params(node: Prism::BlockNode, indentation: T.nilable(String)).returns(String) } + def recursively_switch_nested_block_styles(node, indentation) + parameters = node.parameters + body = node.body + + # We use the indentation to differentiate between do...end and brace style blocks because only the do...end + # style requires the indentation to build the edit. + # + # 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. + source = +"" + + if indentation + source << "do" + source << " #{parameters.slice}" if parameters + source << "\n#{indentation} " + source << switch_block_body(body, indentation) if body + source << "\n#{indentation}end" + else + source << "{ " + source << "#{parameters.slice} " if parameters + source << switch_block_body(body, nil) if body + source << "}" + end + + source + end + + sig { params(body: Prism::Node, indentation: T.nilable(String)).returns(String) } + def switch_block_body(body, indentation) + # Check if there are any nested blocks inside of the current block + body_loc = body.location + nested_block = @document.locate_first_within_range( + { + start: { line: body_loc.start_line - 1, character: body_loc.start_column }, + end: { line: body_loc.end_line - 1, character: body_loc.end_column }, + }, + node_types: [Prism::BlockNode], + ) + + body_content = body.slice.dup + + # If there are nested blocks, then we change their style too and we have to mutate the string using the + # relative position in respect to the beginning of the body + if nested_block.is_a?(Prism::BlockNode) + location = nested_block.location + correction_start = location.start_offset - body_loc.start_offset + correction_end = location.end_offset - body_loc.start_offset + next_indentation = indentation ? "#{indentation} " : nil + + body_content[correction_start...correction_end] = + recursively_switch_nested_block_styles(nested_block, next_indentation) + end + + indentation ? body_content.gsub(";", "\n") : "#{body_content.gsub("\n", ";")} " + end end end end diff --git a/test/expectations/code_action_resolve/nested_block_calls.exp.json b/test/expectations/code_action_resolve/nested_block_calls.exp.json index 4c0cad78a..ef1a59da8 100644 --- a/test/expectations/code_action_resolve/nested_block_calls.exp.json +++ b/test/expectations/code_action_resolve/nested_block_calls.exp.json @@ -37,7 +37,7 @@ "character": 3 } }, - "newText": "{ |a| nested_call(fourth_call).each do |b|; end }" + "newText": "{ |a| nested_call(fourth_call).each { |b| } }" } ] } diff --git a/test/expectations/code_action_resolve/nested_oneline_blocks.exp.json b/test/expectations/code_action_resolve/nested_oneline_blocks.exp.json new file mode 100644 index 000000000..dfbe92ded --- /dev/null +++ b/test/expectations/code_action_resolve/nested_oneline_blocks.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": 74 + } + }, + "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": 0, + "character": 74 + } + }, + "newText": "do |a|\n nested_call(fourth_call).each do |b|\n \n end\nend" + } + ] + } + ] + } + } +} diff --git a/test/fixtures/nested_oneline_blocks.rb b/test/fixtures/nested_oneline_blocks.rb new file mode 100644 index 000000000..62caf97a7 --- /dev/null +++ b/test/fixtures/nested_oneline_blocks.rb @@ -0,0 +1 @@ +method_call(other_call).each { |a| nested_call(fourth_call).each { |b| } }