# typed: strict # frozen_string_literal: true module RubyLsp module Listeners class FoldingRanges extend T::Sig include Requests::Support::Common sig do params( response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::FoldingRange], comments: T::Array[Prism::Comment], dispatcher: Prism::Dispatcher, ).void end def initialize(response_builder, comments, dispatcher) @response_builder = response_builder @requires = T.let([], T::Array[Prism::CallNode]) @comments = comments dispatcher.register( self, :on_if_node_enter, :on_in_node_enter, :on_rescue_node_enter, :on_when_node_enter, :on_interpolated_string_node_enter, :on_array_node_enter, :on_block_node_enter, :on_case_node_enter, :on_case_match_node_enter, :on_class_node_enter, :on_module_node_enter, :on_for_node_enter, :on_hash_node_enter, :on_singleton_class_node_enter, :on_unless_node_enter, :on_until_node_enter, :on_while_node_enter, :on_else_node_enter, :on_ensure_node_enter, :on_begin_node_enter, :on_def_node_enter, :on_call_node_enter, :on_lambda_node_enter, ) end sig { void } def finalize_response! push_comment_ranges emit_requires_range end sig { params(node: Prism::IfNode).void } def on_if_node_enter(node) add_statements_range(node) end sig { params(node: Prism::InNode).void } def on_in_node_enter(node) add_statements_range(node) end sig { params(node: Prism::RescueNode).void } def on_rescue_node_enter(node) add_statements_range(node) end sig { params(node: Prism::WhenNode).void } def on_when_node_enter(node) add_statements_range(node) end sig { params(node: Prism::InterpolatedStringNode).void } def on_interpolated_string_node_enter(node) opening_loc = node.opening_loc || node.location closing_loc = node.closing_loc || node.parts.last&.location || node.location add_lines_range(opening_loc.start_line, closing_loc.start_line - 1) end sig { params(node: Prism::ArrayNode).void } def on_array_node_enter(node) add_simple_range(node) end sig { params(node: Prism::BlockNode).void } def on_block_node_enter(node) add_simple_range(node) end sig { params(node: Prism::CaseNode).void } def on_case_node_enter(node) add_simple_range(node) end sig { params(node: Prism::CaseMatchNode).void } def on_case_match_node_enter(node) add_simple_range(node) end sig { params(node: Prism::ClassNode).void } def on_class_node_enter(node) add_simple_range(node) end sig { params(node: Prism::ModuleNode).void } def on_module_node_enter(node) add_simple_range(node) end sig { params(node: Prism::ForNode).void } def on_for_node_enter(node) add_simple_range(node) end sig { params(node: Prism::HashNode).void } def on_hash_node_enter(node) add_simple_range(node) end sig { params(node: Prism::SingletonClassNode).void } def on_singleton_class_node_enter(node) add_simple_range(node) end sig { params(node: Prism::UnlessNode).void } def on_unless_node_enter(node) add_simple_range(node) end sig { params(node: Prism::UntilNode).void } def on_until_node_enter(node) add_simple_range(node) end sig { params(node: Prism::WhileNode).void } def on_while_node_enter(node) add_simple_range(node) end sig { params(node: Prism::ElseNode).void } def on_else_node_enter(node) add_simple_range(node) end sig { params(node: Prism::EnsureNode).void } def on_ensure_node_enter(node) add_simple_range(node) end sig { params(node: Prism::BeginNode).void } def on_begin_node_enter(node) add_simple_range(node) end sig { params(node: Prism::DefNode).void } def on_def_node_enter(node) params = node.parameters parameter_loc = params&.location location = node.location if params && parameter_loc.end_line > location.start_line # Multiline parameters add_lines_range(location.start_line, parameter_loc.end_line) add_lines_range(parameter_loc.end_line + 1, location.end_line - 1) else add_lines_range(location.start_line, location.end_line - 1) end end sig { params(node: Prism::CallNode).void } def on_call_node_enter(node) # If we find a require, don't visit the child nodes (prevent `super`), so that we can keep accumulating into # the `@requires` array and then push the range whenever we find a node that isn't a CallNode if require?(node) @requires << node return end location = node.location add_lines_range(location.start_line, location.end_line - 1) end sig { params(node: Prism::LambdaNode).void } def on_lambda_node_enter(node) add_simple_range(node) end private sig { void } def push_comment_ranges # Group comments that are on consecutive lines and then push ranges for each group that has at least 2 comments @comments.chunk_while do |this, other| this.location.end_line + 1 == other.location.start_line end.each do |chunk| next if chunk.length == 1 @response_builder << Interface::FoldingRange.new( start_line: T.must(chunk.first).location.start_line - 1, end_line: T.must(chunk.last).location.end_line - 1, kind: "comment", ) end end sig { void } def emit_requires_range if @requires.length > 1 @response_builder << Interface::FoldingRange.new( start_line: T.must(@requires.first).location.start_line - 1, end_line: T.must(@requires.last).location.end_line - 1, kind: "imports", ) end @requires.clear end sig { params(node: Prism::CallNode).returns(T::Boolean) } def require?(node) message = node.message return false unless message == "require" || message == "require_relative" receiver = node.receiver return false unless receiver.nil? || receiver.slice == "Kernel" arguments = node.arguments&.arguments return false unless arguments arguments.length == 1 && arguments.first.is_a?(Prism::StringNode) end sig { params(node: T.any(Prism::IfNode, Prism::InNode, Prism::RescueNode, Prism::WhenNode)).void } def add_statements_range(node) statements = node.statements return unless statements body = statements.body return if body.empty? add_lines_range(node.location.start_line, T.must(body.last).location.end_line) end sig { params(node: Prism::Node).void } def add_simple_range(node) location = node.location add_lines_range(location.start_line, location.end_line - 1) end sig { params(start_line: Integer, end_line: Integer).void } def add_lines_range(start_line, end_line) emit_requires_range return if start_line >= end_line @response_builder << Interface::FoldingRange.new( start_line: start_line - 1, end_line: end_line - 1, kind: "region", ) end end end end