diff --git a/lib/ruby_lsp/requests/code_action_resolve.rb b/lib/ruby_lsp/requests/code_action_resolve.rb index e6bd7b38d..5e8537675 100644 --- a/lib/ruby_lsp/requests/code_action_resolve.rb +++ b/lib/ruby_lsp/requests/code_action_resolve.rb @@ -42,6 +42,10 @@ def perform refactor_method when CodeActions::TOGGLE_BLOCK_STYLE_TITLE switch_block_style + when CodeActions::CREATE_ATTRIBUTE_READER, + CodeActions::CREATE_ATTRIBUTE_WRITER, + CodeActions::CREATE_ATTRIBUTE_ACCESSOR + create_attribute_accessor else Error::UnknownCodeAction end @@ -329,6 +333,93 @@ def switch_block_body(body, indentation) indentation ? body_content.gsub(";", "\n") : "#{body_content.gsub("\n", ";")} " end + + sig { returns(T.any(Interface::CodeAction, Error)) } + def create_attribute_accessor + source_range = @code_action.dig(:data, :range) + return Error::EmptySelection if source_range[:start] == source_range[:end] + + node = @document.locate_first_within_range( + @code_action.dig(:data, :range), + node_types: [ + Prism::InstanceVariableAndWriteNode, + Prism::InstanceVariableOperatorWriteNode, + Prism::InstanceVariableOrWriteNode, + Prism::InstanceVariableReadNode, + Prism::InstanceVariableTargetNode, + Prism::InstanceVariableWriteNode, + ], + ) + return Error::EmptySelection if node.nil? + + node = T.cast( + node, + T.any( + Prism::InstanceVariableAndWriteNode, + Prism::InstanceVariableOperatorWriteNode, + Prism::InstanceVariableOrWriteNode, + Prism::InstanceVariableReadNode, + Prism::InstanceVariableTargetNode, + Prism::InstanceVariableWriteNode, + ), + ) + + scanner = @document.create_scanner + start_index = scanner.find_char_position( + line: node.location.start_line, + character: node.location.start_character_column, + ) + node_context = RubyDocument.locate( + @document.parse_result.value, + start_index, + node_types: [ + Prism::ClassNode, + Prism::ModuleNode, + Prism::SingletonClassNode, + ], + code_units_cache: @document.code_units_cache, + ) + closest_node = node_context.node + return Error::InvalidTargetRange if closest_node.nil? + + attribute_name = node.name[1..] + indentation = " " * (closest_node.location.start_column + 2) + attribute_accessor_source = T.must( + ( + case @code_action[:title] + when CodeActions::CREATE_ATTRIBUTE_READER + "#{indentation}attr_reader :#{attribute_name}\n\n" + when CodeActions::CREATE_ATTRIBUTE_WRITER + "#{indentation}attr_writer :#{attribute_name}\n\n" + when CodeActions::CREATE_ATTRIBUTE_ACCESSOR + "#{indentation}attr_accessor :#{attribute_name}\n\n" + end + ), + ) + + target_start_line = closest_node.location.start_line + target_range = { + start: { line: target_start_line, character: 0 }, + end: { line: target_start_line, character: 0 }, + } + + Interface::CodeAction.new( + title: @code_action[:title], + edit: Interface::WorkspaceEdit.new( + document_changes: [ + Interface::TextDocumentEdit.new( + text_document: Interface::OptionalVersionedTextDocumentIdentifier.new( + uri: @code_action.dig(:data, :uri), + version: nil, + ), + edits: [ + create_text_edit(target_range, attribute_accessor_source), + ], + ), + ], + ), + ) + end end end end diff --git a/lib/ruby_lsp/requests/code_actions.rb b/lib/ruby_lsp/requests/code_actions.rb index 483db9394..490b73f20 100644 --- a/lib/ruby_lsp/requests/code_actions.rb +++ b/lib/ruby_lsp/requests/code_actions.rb @@ -12,6 +12,9 @@ class CodeActions < Request EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable" EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method" TOGGLE_BLOCK_STYLE_TITLE = "Refactor: Toggle block style" + CREATE_ATTRIBUTE_READER = "Create Attribute Reader" + CREATE_ATTRIBUTE_WRITER = "Create Attribute Writer" + CREATE_ATTRIBUTE_ACCESSOR = "Create Attribute Accessor" class << self extend T::Sig @@ -65,6 +68,21 @@ def perform kind: Constant::CodeActionKind::REFACTOR_REWRITE, data: { range: @range, uri: @uri.to_s }, ) + code_actions << Interface::CodeAction.new( + title: CREATE_ATTRIBUTE_READER, + kind: Constant::CodeActionKind::EMPTY, + data: { range: @range, uri: @uri.to_s }, + ) + code_actions << Interface::CodeAction.new( + title: CREATE_ATTRIBUTE_WRITER, + kind: Constant::CodeActionKind::EMPTY, + data: { range: @range, uri: @uri.to_s }, + ) + code_actions << Interface::CodeAction.new( + title: CREATE_ATTRIBUTE_ACCESSOR, + kind: Constant::CodeActionKind::EMPTY, + data: { range: @range, uri: @uri.to_s }, + ) end code_actions diff --git a/test/expectations/code_action_resolve/create_attribute_accessor.json b/test/expectations/code_action_resolve/create_attribute_accessor.json new file mode 100644 index 000000000..5848dd3d8 --- /dev/null +++ b/test/expectations/code_action_resolve/create_attribute_accessor.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "", + "title": "Create Attribute Accessor", + "data": { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 16 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Create Attribute Accessor", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": " attr_accessor :foo\n\n" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/create_attribute_reader.exp.json b/test/expectations/code_action_resolve/create_attribute_reader.exp.json new file mode 100644 index 000000000..7a19c7f24 --- /dev/null +++ b/test/expectations/code_action_resolve/create_attribute_reader.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "", + "title": "Create Attribute Reader", + "data": { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 16 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Create Attribute Reader", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": " attr_reader :foo\n\n" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_action_resolve/create_attributer_writer.exp.json b/test/expectations/code_action_resolve/create_attributer_writer.exp.json new file mode 100644 index 000000000..cc6f78f05 --- /dev/null +++ b/test/expectations/code_action_resolve/create_attributer_writer.exp.json @@ -0,0 +1,47 @@ +{ + "params": { + "kind": "", + "title": "Create Attribute Writer", + "data": { + "range": { + "start": { + "line": 3, + "character": 0 + }, + "end": { + "line": 4, + "character": 0 + } + }, + "uri": "file:///fake" + } + }, + "result": { + "title": "Create Attribute Writer", + "edit": { + "documentChanges": [ + { + "textDocument": { + "uri": "file:///fake", + "version": null + }, + "edits": [ + { + "range": { + "start": { + "line": 2, + "character": 0 + }, + "end": { + "line": 2, + "character": 0 + } + }, + "newText": " attr_accessor :foo\n\n" + } + ] + } + ] + } + } +} diff --git a/test/expectations/code_actions/aref_field.exp.json b/test/expectations/code_actions/aref_field.exp.json index 22fbe7a4b..38f36d873 100644 --- a/test/expectations/code_actions/aref_field.exp.json +++ b/test/expectations/code_actions/aref_field.exp.json @@ -69,6 +69,57 @@ }, "uri": "file:///fake" } + }, + { + "title": "Create Attribute Reader", + "kind": "", + "data": { + "range": { + "start": { + "line": 2, + "character": 9 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "uri": "file:///fake" + } + }, + { + "title": "Create Attribute Writer", + "kind": "", + "data": { + "range": { + "start": { + "line": 2, + "character": 9 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "uri": "file:///fake" + } + }, + { + "title": "Create Attribute Accessor", + "kind": "", + "data": { + "range": { + "start": { + "line": 2, + "character": 9 + }, + "end": { + "line": 2, + "character": 13 + } + }, + "uri": "file:///fake" + } } ] } diff --git a/test/fixtures/create_attribute_accessor.rb b/test/fixtures/create_attribute_accessor.rb new file mode 100644 index 000000000..8859cec27 --- /dev/null +++ b/test/fixtures/create_attribute_accessor.rb @@ -0,0 +1,5 @@ +module Foo + def bar + @foo = "foo" + end +end diff --git a/test/fixtures/create_attribute_reader.rb b/test/fixtures/create_attribute_reader.rb new file mode 100644 index 000000000..5833bef11 --- /dev/null +++ b/test/fixtures/create_attribute_reader.rb @@ -0,0 +1,5 @@ +class Foo + def bar + @foo = "foo" + end +end diff --git a/test/fixtures/create_attribute_writer.rb b/test/fixtures/create_attribute_writer.rb new file mode 100644 index 000000000..3798ad029 --- /dev/null +++ b/test/fixtures/create_attribute_writer.rb @@ -0,0 +1,7 @@ +class Foo + class << self + def bar + @foo = "foo" + end + end +end diff --git a/test/requests/code_actions_expectations_test.rb b/test/requests/code_actions_expectations_test.rb index f0c6be9c6..48e3c504f 100644 --- a/test/requests/code_actions_expectations_test.rb +++ b/test/requests/code_actions_expectations_test.rb @@ -49,7 +49,10 @@ def map_actions(expectation) refactors = expectation .select { |action| action["kind"].start_with?("refactor") } .map { |action| code_action_for_refactor(action) } - result = [*quickfixes, *refactors] + empty_kind = expectation + .select { |action| action["kind"] == "" } + .map { |action| code_action_for_refactor(action) } + result = [*quickfixes, *refactors, *empty_kind] JSON.parse(result.to_json) end @@ -83,4 +86,15 @@ def code_action_for_refactor(refactor) }, ) end + + def code_action_for_empty_kind(expectations) + LanguageServer::Protocol::Interface::CodeAction.new( + title: expectations["title"], + kind: expectations["kind"], + data: { + range: expectations.dig("data", "range"), + uri: expectations.dig("data", "uri"), + }, + ) + end end