class RubyLsp::Requests::CodeActionResolve

textDocument/codeAction response. We can use it for scenarios that require more computation such as refactoring.
request is used to to resolve the edit field for a given code action, if it is not already provided in the
The [code action resolve](microsoft.github.io/language-server-protocol/specification#codeAction_resolve)

def create_attribute_accessor

: -> (Interface::CodeAction)
def create_attribute_accessor
  source_range = @code_action.dig(:data, :range)
  node = if source_range[:start] != source_range[:end]
    @document.locate_first_within_range(
      @code_action.dig(:data, :range),
      node_types: CodeActions::INSTANCE_VARIABLE_NODES,
    )
  end
  if node.nil?
    node_context = @document.locate_node(
      source_range[:start],
      node_types: CodeActions::INSTANCE_VARIABLE_NODES,
    )
    node = node_context.node
    unless CodeActions::INSTANCE_VARIABLE_NODES.include?(node.class)
      raise EmptySelectionError, "Invalid selection for refactor"
    end
  end
  node = node #: as Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode
  node_context = @document.locate_node(
    {
      line: node.location.start_line,
      character: node.location.start_character_column,
    },
    node_types: [
      Prism::ClassNode,
      Prism::ModuleNode,
      Prism::SingletonClassNode,
    ],
  )
  closest_node = node_context.node
  if closest_node.nil?
    raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
  end
  attribute_name = node.name[1..]
  indentation = " " * (closest_node.location.start_column + 2)
  attribute_accessor_source = 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 #: as !nil
  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

def create_text_edit(range, new_text)

: (Hash[Symbol, untyped] range, String new_text) -> Interface::TextEdit
def create_text_edit(range, new_text)
  Interface::TextEdit.new(
    range: Interface::Range.new(
      start: Interface::Position.new(line: range.dig(:start, :line), character: range.dig(:start, :character)),
      end: Interface::Position.new(line: range.dig(:end, :line), character: range.dig(:end, :character)),
    ),
    new_text: new_text,
  )
end

def initialize(document, global_state, code_action)

: (RubyDocument document, GlobalState global_state, Hash[Symbol, untyped] code_action) -> void
def initialize(document, global_state, code_action)
  super()
  @document = document
  @global_state = global_state
  @code_action = code_action
end

def perform

: -> (Interface::CodeAction)
@override
def perform
  raise EmptySelectionError, "Invalid selection for refactor" if @document.source.empty?
  case @code_action[:title]
  when CodeActions::EXTRACT_TO_VARIABLE_TITLE
    refactor_variable
  when CodeActions::EXTRACT_TO_METHOD_TITLE
    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
    raise UnknownCodeActionError, "Unknown code action: #{@code_action[:title]}"
  end
end

def recursively_switch_nested_block_styles(node, indentation)

: (Prism::BlockNode node, String? indentation) -> 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

def refactor_method

: -> (Interface::CodeAction)
def refactor_method
  source_range = @code_action.dig(:data, :range)
  raise EmptySelectionError, "Invalid selection for refactor" if source_range[:start] == source_range[:end]
  start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
  extracted_source = @document.source[start_index...end_index] #: as !nil
  # Find the closest method declaration node, so that we place the refactor in a valid position
  node_context = RubyDocument.locate(
    @document.ast,
    start_index,
    node_types: [Prism::DefNode],
    code_units_cache: @document.code_units_cache,
  )
  closest_node = node_context.node
  unless closest_node
    raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
  end
  target_range = if closest_node.is_a?(Prism::DefNode)
    end_keyword_loc = closest_node.end_keyword_loc
    unless end_keyword_loc
      raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
    end
    end_line = end_keyword_loc.end_line - 1
    character = end_keyword_loc.end_column
    indentation = " " * end_keyword_loc.start_column
    new_method_source = <<~RUBY.chomp
      #{indentation}def #{NEW_METHOD_NAME}
      #{indentation}  #{extracted_source}
      #{indentation}end
    RUBY
    {
      start: { line: end_line, character: character },
      end: { line: end_line, character: character },
    }
  else
    new_method_source = <<~RUBY
      #{indentation}def #{NEW_METHOD_NAME}
      #{indentation}  #{extracted_source.gsub("\n", "\n  ")}
      #{indentation}end
    RUBY
    line = [0, source_range.dig(:start, :line) - 1].max
    {
      start: { line: line, character: source_range.dig(:start, :character) },
      end: { line: line, character: source_range.dig(:start, :character) },
    }
  end
  Interface::CodeAction.new(
    title: CodeActions::EXTRACT_TO_METHOD_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, new_method_source),
            create_text_edit(source_range, NEW_METHOD_NAME),
          ],
        ),
      ],
    ),
  )
end

def refactor_variable

: -> (Interface::CodeAction)
def refactor_variable
  source_range = @code_action.dig(:data, :range)
  raise EmptySelectionError, "Invalid selection for refactor" if source_range[:start] == source_range[:end]
  start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
  extracted_source = @document.source[start_index...end_index] #: as !nil
  # Find the closest statements node, so that we place the refactor in a valid position
  node_context = RubyDocument
    .locate(@document.ast,
      start_index,
      node_types: [
        Prism::StatementsNode,
        Prism::BlockNode,
      ],
      code_units_cache: @document.code_units_cache)
  closest_statements = node_context.node
  parent_statements = node_context.parent
  if closest_statements.nil? || closest_statements.child_nodes.compact.empty?
    raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
  end
  # Find the node with the end line closest to the requested position, so that we can place the refactor
  # immediately after that closest node
  closest_node = closest_statements.child_nodes.compact.min_by do |node|
    distance = source_range.dig(:start, :line) - (node.location.end_line - 1)
    distance <= 0 ? Float::INFINITY : distance
  end #: as !nil
  if closest_node.is_a?(Prism::MissingNode)
    raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
  end
  closest_node_loc = closest_node.location
  # If the parent expression is a single line block, then we have to extract it inside of the one-line block
  if parent_statements.is_a?(Prism::BlockNode) &&
      parent_statements.location.start_line == parent_statements.location.end_line
    variable_source = " #{NEW_VARIABLE_NAME} = #{extracted_source};"
    character = source_range.dig(:start, :character) - 1
    target_range = {
      start: { line: closest_node_loc.end_line - 1, character: character },
      end: { line: closest_node_loc.end_line - 1, character: character },
    }
  else
    # If the closest node covers the requested location, then we're extracting a statement nested inside of it. In
    # that case, we want to place the extraction at the start of the closest node (one line above). Otherwise, we
    # want to place the extract right below the closest node
    if closest_node_loc.start_line - 1 <= source_range.dig(
      :start,
      :line,
    ) && closest_node_loc.end_line - 1 >= source_range.dig(:end, :line)
      indentation_line_number = closest_node_loc.start_line - 1
      target_line = indentation_line_number
    else
      target_line = closest_node_loc.end_line
      indentation_line_number = closest_node_loc.end_line - 1
    end
    lines = @document.source.lines
    indentation_line = lines[indentation_line_number]
    unless indentation_line
      raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
    end
    indentation = indentation_line[/\A */] #: as !nil
      .size
    target_range = {
      start: { line: target_line, character: indentation },
      end: { line: target_line, character: indentation },
    }
    line = lines[target_line]
    unless line
      raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
    end
    variable_source = if line.strip.empty?
      "\n#{" " * indentation}#{NEW_VARIABLE_NAME} = #{extracted_source}"
    else
      "#{NEW_VARIABLE_NAME} = #{extracted_source}\n#{" " * indentation}"
    end
  end
  Interface::CodeAction.new(
    title: CodeActions::EXTRACT_TO_VARIABLE_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(source_range, NEW_VARIABLE_NAME),
            create_text_edit(target_range, variable_source),
          ],
        ),
      ],
    ),
  )
end

def switch_block_body(body, indentation)

: (Prism::Node body, String? indentation) -> 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

def switch_block_style

: -> (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]
  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
  node = target.block
  unless node.is_a?(Prism::BlockNode)
    raise InvalidTargetRangeError, "Couldn't find an appropriate location to place extracted refactor"
  end
  indentation = " " * target.location.start_column unless node.opening_loc.slice == "do"
  Interface::CodeAction.new(
    title: CodeActions::TOGGLE_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: recursively_switch_nested_block_styles(node, indentation),
            ),
          ],
        ),
      ],
    ),
  )
end