From d193290b8029a3a118d2ba01740717288af3d328 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Tue, 13 Aug 2024 10:49:15 -0400 Subject: [PATCH 1/2] Make Document generic --- lib/ruby_lsp/document.rb | 13 +-- lib/ruby_lsp/erb_document.rb | 5 +- lib/ruby_lsp/requests/code_actions.rb | 2 +- lib/ruby_lsp/requests/completion.rb | 2 +- lib/ruby_lsp/requests/definition.rb | 2 +- lib/ruby_lsp/requests/diagnostics.rb | 2 +- lib/ruby_lsp/requests/document_highlight.rb | 2 +- lib/ruby_lsp/requests/formatting.rb | 2 +- lib/ruby_lsp/requests/hover.rb | 2 +- lib/ruby_lsp/requests/inlay_hints.rb | 2 +- lib/ruby_lsp/requests/on_type_formatting.rb | 2 +- .../requests/prepare_type_hierarchy.rb | 2 +- lib/ruby_lsp/requests/selection_ranges.rb | 3 +- lib/ruby_lsp/requests/show_syntax_tree.rb | 2 +- lib/ruby_lsp/requests/signature_help.rb | 2 +- lib/ruby_lsp/requests/support/formatter.rb | 4 +- .../requests/support/rubocop_diagnostic.rb | 2 +- .../requests/support/rubocop_formatter.rb | 4 +- .../requests/support/syntax_tree_formatter.rb | 4 +- lib/ruby_lsp/ruby_document.rb | 7 +- lib/ruby_lsp/server.rb | 90 +++++++++++++++++-- lib/ruby_lsp/store.rb | 6 +- 22 files changed, 127 insertions(+), 35 deletions(-) diff --git a/lib/ruby_lsp/document.rb b/lib/ruby_lsp/document.rb index 96d1ab16a..2adb145f4 100644 --- a/lib/ruby_lsp/document.rb +++ b/lib/ruby_lsp/document.rb @@ -12,10 +12,13 @@ class LanguageId < T::Enum extend T::Sig extend T::Helpers + extend T::Generic + + ParseResultType = type_member abstract! - sig { returns(Prism::ParseResult) } + sig { returns(ParseResultType) } attr_reader :parse_result sig { returns(String) } @@ -38,10 +41,10 @@ def initialize(source:, version:, uri:, encoding: Encoding::UTF_8) @version = T.let(version, Integer) @uri = T.let(uri, URI::Generic) @needs_parsing = T.let(true, T::Boolean) - @parse_result = T.let(parse, Prism::ParseResult) + @parse_result = T.let(parse, ParseResultType) end - sig { params(other: Document).returns(T::Boolean) } + sig { params(other: Document[T.untyped]).returns(T::Boolean) } def ==(other) self.class == other.class && uri == other.uri && @source == other.source end @@ -54,7 +57,7 @@ def language_id; end type_parameters(:T) .params( request_name: String, - block: T.proc.params(document: Document).returns(T.type_parameter(:T)), + block: T.proc.params(document: Document[ParseResultType]).returns(T.type_parameter(:T)), ).returns(T.type_parameter(:T)) end def cache_fetch(request_name, &block) @@ -93,7 +96,7 @@ def push_edits(edits, version:) @cache.clear end - sig { abstract.returns(Prism::ParseResult) } + sig { abstract.returns(ParseResultType) } def parse; end sig { abstract.returns(T::Boolean) } diff --git a/lib/ruby_lsp/erb_document.rb b/lib/ruby_lsp/erb_document.rb index 7efc158fb..38c25024a 100644 --- a/lib/ruby_lsp/erb_document.rb +++ b/lib/ruby_lsp/erb_document.rb @@ -4,8 +4,11 @@ module RubyLsp class ERBDocument < Document extend T::Sig + extend T::Generic - sig { override.returns(Prism::ParseResult) } + ParseResultType = type_member { { fixed: Prism::ParseResult } } + + sig { override.returns(ParseResultType) } def parse return @parse_result unless @needs_parsing diff --git a/lib/ruby_lsp/requests/code_actions.rb b/lib/ruby_lsp/requests/code_actions.rb index 4da1bd832..a9db23fa1 100644 --- a/lib/ruby_lsp/requests/code_actions.rb +++ b/lib/ruby_lsp/requests/code_actions.rb @@ -37,7 +37,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), range: T::Hash[Symbol, T.untyped], context: T::Hash[Symbol, T.untyped], ).void diff --git a/lib/ruby_lsp/requests/completion.rb b/lib/ruby_lsp/requests/completion.rb index 9e52e2185..83e25d587 100644 --- a/lib/ruby_lsp/requests/completion.rb +++ b/lib/ruby_lsp/requests/completion.rb @@ -46,7 +46,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), global_state: GlobalState, params: T::Hash[Symbol, T.untyped], sorbet_level: RubyDocument::SorbetLevel, diff --git a/lib/ruby_lsp/requests/definition.rb b/lib/ruby_lsp/requests/definition.rb index 8a0941fa1..99772065a 100644 --- a/lib/ruby_lsp/requests/definition.rb +++ b/lib/ruby_lsp/requests/definition.rb @@ -38,7 +38,7 @@ class Definition < Request sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), global_state: GlobalState, position: T::Hash[Symbol, T.untyped], dispatcher: Prism::Dispatcher, diff --git a/lib/ruby_lsp/requests/diagnostics.rb b/lib/ruby_lsp/requests/diagnostics.rb index 088f1157a..6bfa95a7d 100644 --- a/lib/ruby_lsp/requests/diagnostics.rb +++ b/lib/ruby_lsp/requests/diagnostics.rb @@ -32,7 +32,7 @@ def provider end end - sig { params(global_state: GlobalState, document: Document).void } + sig { params(global_state: GlobalState, document: RubyDocument).void } def initialize(global_state, document) super() @active_linters = T.let(global_state.active_linters, T::Array[Support::Formatter]) diff --git a/lib/ruby_lsp/requests/document_highlight.rb b/lib/ruby_lsp/requests/document_highlight.rb index c8efafe68..76ffb174d 100644 --- a/lib/ruby_lsp/requests/document_highlight.rb +++ b/lib/ruby_lsp/requests/document_highlight.rb @@ -29,7 +29,7 @@ class DocumentHighlight < Request sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), position: T::Hash[Symbol, T.untyped], dispatcher: Prism::Dispatcher, ).void diff --git a/lib/ruby_lsp/requests/formatting.rb b/lib/ruby_lsp/requests/formatting.rb index 7dd9b8141..f27a0831c 100644 --- a/lib/ruby_lsp/requests/formatting.rb +++ b/lib/ruby_lsp/requests/formatting.rb @@ -40,7 +40,7 @@ def provider end end - sig { params(global_state: GlobalState, document: Document).void } + sig { params(global_state: GlobalState, document: RubyDocument).void } def initialize(global_state, document) super() @document = document diff --git a/lib/ruby_lsp/requests/hover.rb b/lib/ruby_lsp/requests/hover.rb index 56a4c72a2..c260f44f4 100644 --- a/lib/ruby_lsp/requests/hover.rb +++ b/lib/ruby_lsp/requests/hover.rb @@ -32,7 +32,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), global_state: GlobalState, position: T::Hash[Symbol, T.untyped], dispatcher: Prism::Dispatcher, diff --git a/lib/ruby_lsp/requests/inlay_hints.rb b/lib/ruby_lsp/requests/inlay_hints.rb index 05d737de3..1485a388f 100644 --- a/lib/ruby_lsp/requests/inlay_hints.rb +++ b/lib/ruby_lsp/requests/inlay_hints.rb @@ -52,7 +52,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), range: T::Hash[Symbol, T.untyped], hints_configuration: RequestConfig, dispatcher: Prism::Dispatcher, diff --git a/lib/ruby_lsp/requests/on_type_formatting.rb b/lib/ruby_lsp/requests/on_type_formatting.rb index 30e8a2ce4..60fcaec3f 100644 --- a/lib/ruby_lsp/requests/on_type_formatting.rb +++ b/lib/ruby_lsp/requests/on_type_formatting.rb @@ -42,7 +42,7 @@ def provider sig do params( - document: Document, + document: RubyDocument, position: T::Hash[Symbol, T.untyped], trigger_character: String, client_name: String, diff --git a/lib/ruby_lsp/requests/prepare_type_hierarchy.rb b/lib/ruby_lsp/requests/prepare_type_hierarchy.rb index e82d684ec..ae9c6a8f3 100644 --- a/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +++ b/lib/ruby_lsp/requests/prepare_type_hierarchy.rb @@ -35,7 +35,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), index: RubyIndexer::Index, position: T::Hash[Symbol, T.untyped], ).void diff --git a/lib/ruby_lsp/requests/selection_ranges.rb b/lib/ruby_lsp/requests/selection_ranges.rb index 5bd9d8292..288c72bac 100644 --- a/lib/ruby_lsp/requests/selection_ranges.rb +++ b/lib/ruby_lsp/requests/selection_ranges.rb @@ -23,7 +23,8 @@ module Requests class SelectionRanges < Request extend T::Sig include Support::Common - sig { params(document: Document).void } + + sig { params(document: T.any(RubyDocument, ERBDocument)).void } def initialize(document) super() @document = document diff --git a/lib/ruby_lsp/requests/show_syntax_tree.rb b/lib/ruby_lsp/requests/show_syntax_tree.rb index f3ca6523f..1a723c40a 100644 --- a/lib/ruby_lsp/requests/show_syntax_tree.rb +++ b/lib/ruby_lsp/requests/show_syntax_tree.rb @@ -20,7 +20,7 @@ module Requests class ShowSyntaxTree < Request extend T::Sig - sig { params(document: Document, range: T.nilable(T::Hash[Symbol, T.untyped])).void } + sig { params(document: RubyDocument, range: T.nilable(T::Hash[Symbol, T.untyped])).void } def initialize(document, range) super() @document = document diff --git a/lib/ruby_lsp/requests/signature_help.rb b/lib/ruby_lsp/requests/signature_help.rb index d6865770a..ebe07f5ae 100644 --- a/lib/ruby_lsp/requests/signature_help.rb +++ b/lib/ruby_lsp/requests/signature_help.rb @@ -41,7 +41,7 @@ def provider sig do params( - document: Document, + document: T.any(RubyDocument, ERBDocument), global_state: GlobalState, position: T::Hash[Symbol, T.untyped], context: T.nilable(T::Hash[Symbol, T.untyped]), diff --git a/lib/ruby_lsp/requests/support/formatter.rb b/lib/ruby_lsp/requests/support/formatter.rb index 01299bfb4..d7f1d47ab 100644 --- a/lib/ruby_lsp/requests/support/formatter.rb +++ b/lib/ruby_lsp/requests/support/formatter.rb @@ -10,13 +10,13 @@ module Formatter interface! - sig { abstract.params(uri: URI::Generic, document: Document).returns(T.nilable(String)) } + sig { abstract.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) } def run_formatting(uri, document); end sig do abstract.params( uri: URI::Generic, - document: Document, + document: RubyDocument, ).returns(T.nilable(T::Array[Interface::Diagnostic])) end def run_diagnostic(uri, document); end diff --git a/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb b/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb index 3a9c7026e..d543c117b 100644 --- a/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +++ b/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb @@ -31,7 +31,7 @@ class RuboCopDiagnostic # TODO: avoid passing document once we have alternative ways to get at # encoding and file source - sig { params(document: Document, offense: RuboCop::Cop::Offense, uri: URI::Generic).void } + sig { params(document: RubyDocument, offense: RuboCop::Cop::Offense, uri: URI::Generic).void } def initialize(document, offense, uri) @document = document @offense = offense diff --git a/lib/ruby_lsp/requests/support/rubocop_formatter.rb b/lib/ruby_lsp/requests/support/rubocop_formatter.rb index 62454c2f0..5ab4ed174 100644 --- a/lib/ruby_lsp/requests/support/rubocop_formatter.rb +++ b/lib/ruby_lsp/requests/support/rubocop_formatter.rb @@ -17,7 +17,7 @@ def initialize @format_runner = T.let(RuboCopRunner.new("-a"), RuboCopRunner) end - sig { override.params(uri: URI::Generic, document: Document).returns(T.nilable(String)) } + sig { override.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) } def run_formatting(uri, document) filename = T.must(uri.to_standardized_path || uri.opaque) @@ -29,7 +29,7 @@ def run_formatting(uri, document) sig do override.params( uri: URI::Generic, - document: Document, + document: RubyDocument, ).returns(T.nilable(T::Array[Interface::Diagnostic])) end def run_diagnostic(uri, document) diff --git a/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb b/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb index ff8ba70f8..c0b8a3b84 100644 --- a/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +++ b/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb @@ -29,7 +29,7 @@ def initialize ) end - sig { override.params(uri: URI::Generic, document: Document).returns(T.nilable(String)) } + sig { override.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) } def run_formatting(uri, document) path = uri.to_standardized_path return if path && @options.ignore_files.any? { |pattern| File.fnmatch?("*/#{pattern}", path) } @@ -40,7 +40,7 @@ def run_formatting(uri, document) sig do override.params( uri: URI::Generic, - document: Document, + document: RubyDocument, ).returns(T.nilable(T::Array[Interface::Diagnostic])) end def run_diagnostic(uri, document) diff --git a/lib/ruby_lsp/ruby_document.rb b/lib/ruby_lsp/ruby_document.rb index d8ab64f31..048eb2bff 100644 --- a/lib/ruby_lsp/ruby_document.rb +++ b/lib/ruby_lsp/ruby_document.rb @@ -3,6 +3,11 @@ module RubyLsp class RubyDocument < Document + extend T::Sig + extend T::Generic + + ParseResultType = type_member { { fixed: Prism::ParseResult } } + class SorbetLevel < T::Enum enums do None = new("none") @@ -13,7 +18,7 @@ class SorbetLevel < T::Enum end end - sig { override.returns(Prism::ParseResult) } + sig { override.returns(ParseResultType) } def parse return @parse_result unless @needs_parsing diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index b5adf4856..a7f21acbd 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -341,7 +341,12 @@ def text_document_did_change(message) def text_document_selection_range(message) uri = message.dig(:params, :textDocument, :uri) ranges = @store.cache_fetch(uri, "textDocument/selectionRange") do |document| - Requests::SelectionRanges.new(document).perform + case document + when RubyDocument, ERBDocument + Requests::SelectionRanges.new(document).perform + else + [] + end end # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange), @@ -363,6 +368,11 @@ def run_combined_requests(message) uri = URI(message.dig(:params, :textDocument, :uri)) document = @store.get(uri) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + # If the response has already been cached by another request, return it cached_response = document.cache_get(message[:method]) if cached_response @@ -407,6 +417,12 @@ def text_document_semantic_tokens_range(message) range = params[:range] uri = params.dig(:textDocument, :uri) document = @store.get(uri) + + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + start_line = range.dig(:start, :line) end_line = range.dig(:end, :line) @@ -436,6 +452,10 @@ def text_document_formatting(message) end document = @store.get(uri) + unless document.is_a?(RubyDocument) + send_empty_response(message[:id]) + return + end response = Requests::Formatting.new(@global_state, document).perform send_message(Result.new(id: message[:id], response: response)) @@ -452,6 +472,12 @@ def text_document_document_highlight(message) params = message[:params] dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + request = Requests::DocumentHighlight.new(document, params[:position], dispatcher) dispatcher.dispatch(document.parse_result.value) send_message(Result.new(id: message[:id], response: request.perform)) @@ -462,6 +488,11 @@ def text_document_on_type_formatting(message) params = message[:params] document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -481,6 +512,11 @@ def text_document_hover(message) dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -495,7 +531,7 @@ def text_document_hover(message) ) end - sig { params(document: Document).returns(RubyDocument::SorbetLevel) } + sig { params(document: Document[T.untyped]).returns(RubyDocument::SorbetLevel) } def sorbet_level(document) return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker return RubyDocument::SorbetLevel::Ignore unless document.is_a?(RubyDocument) @@ -509,6 +545,12 @@ def text_document_inlay_hint(message) hints_configurations = T.must(@store.features_configuration.dig(:inlayHint)) dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + request = Requests::InlayHints.new(document, params[:range], hints_configurations, dispatcher) dispatcher.visit(document.parse_result.value) send_message(Result.new(id: message[:id], response: request.perform)) @@ -519,6 +561,11 @@ def text_document_code_action(message) params = message[:params] document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -581,7 +628,10 @@ def text_document_diagnostic(message) document = @store.get(uri) response = document.cache_fetch("textDocument/diagnostic") do |document| - Requests::Diagnostics.new(@global_state, document).perform + case document + when RubyDocument + Requests::Diagnostics.new(@global_state, document).perform + end end send_message( @@ -604,6 +654,11 @@ def text_document_completion(message) dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -632,6 +687,11 @@ def text_document_signature_help(message) dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -653,6 +713,11 @@ def text_document_definition(message) dispatcher = Prism::Dispatcher.new document = @store.get(params.dig(:textDocument, :uri)) + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + send_message( Result.new( id: message[:id], @@ -710,9 +775,16 @@ def workspace_symbol(message) sig { params(message: T::Hash[Symbol, T.untyped]).void } def text_document_show_syntax_tree(message) params = message[:params] + document = @store.get(params.dig(:textDocument, :uri)) + + unless document.is_a?(RubyDocument) + send_empty_response(message[:id]) + return + end + response = { ast: Requests::ShowSyntaxTree.new( - @store.get(params.dig(:textDocument, :uri)), + document, params[:range], ).perform, } @@ -722,11 +794,19 @@ def text_document_show_syntax_tree(message) sig { params(message: T::Hash[Symbol, T.untyped]).void } def text_document_prepare_type_hierarchy(message) params = message[:params] + document = @store.get(params.dig(:textDocument, :uri)) + + unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument) + send_empty_response(message[:id]) + return + end + response = Requests::PrepareTypeHierarchy.new( - @store.get(params.dig(:textDocument, :uri)), + document, @global_state.index, params[:position], ).perform + send_message(Result.new(id: message[:id], response: response)) end diff --git a/lib/ruby_lsp/store.rb b/lib/ruby_lsp/store.rb index 8aebcdcc8..0e1a5f2d5 100644 --- a/lib/ruby_lsp/store.rb +++ b/lib/ruby_lsp/store.rb @@ -18,7 +18,7 @@ class NonExistingDocumentError < StandardError; end sig { void } def initialize - @state = T.let({}, T::Hash[String, Document]) + @state = T.let({}, T::Hash[String, Document[T.untyped]]) @supports_progress = T.let(true, T::Boolean) @features_configuration = T.let( { @@ -33,7 +33,7 @@ def initialize @client_name = T.let("Unknown", String) end - sig { params(uri: URI::Generic).returns(Document) } + sig { params(uri: URI::Generic).returns(Document[T.untyped]) } def get(uri) document = @state[uri.to_s] return document unless document.nil? @@ -100,7 +100,7 @@ def delete(uri) .params( uri: URI::Generic, request_name: String, - block: T.proc.params(document: Document).returns(T.type_parameter(:T)), + block: T.proc.params(document: Document[T.untyped]).returns(T.type_parameter(:T)), ).returns(T.type_parameter(:T)) end def cache_fetch(uri, request_name, &block) From d879724430b04232d8a4560d3a49a2ffce0eb268 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Tue, 13 Aug 2024 11:04:12 -0400 Subject: [PATCH 2/2] Move locate to RubyDocument and ERBDocument --- lib/ruby_lsp/document.rb | 109 ------------------ lib/ruby_lsp/erb_document.rb | 10 ++ lib/ruby_lsp/requests/code_action_resolve.rb | 4 +- lib/ruby_lsp/requests/completion.rb | 2 +- lib/ruby_lsp/ruby_document.rb | 113 +++++++++++++++++++ 5 files changed, 126 insertions(+), 112 deletions(-) diff --git a/lib/ruby_lsp/document.rb b/lib/ruby_lsp/document.rb index 2adb145f4..840bb9c6c 100644 --- a/lib/ruby_lsp/document.rb +++ b/lib/ruby_lsp/document.rb @@ -107,115 +107,6 @@ def create_scanner Scanner.new(@source, @encoding) end - sig do - params( - position: T::Hash[Symbol, T.untyped], - node_types: T::Array[T.class_of(Prism::Node)], - ).returns(NodeContext) - end - def locate_node(position, node_types: []) - locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types) - end - - sig do - params( - node: Prism::Node, - char_position: Integer, - node_types: T::Array[T.class_of(Prism::Node)], - ).returns(NodeContext) - end - def locate(node, char_position, node_types: []) - queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)]) - closest = node - parent = T.let(nil, T.nilable(Prism::Node)) - nesting_nodes = T.let( - [], - T::Array[T.any( - Prism::ClassNode, - Prism::ModuleNode, - Prism::SingletonClassNode, - Prism::DefNode, - Prism::BlockNode, - Prism::LambdaNode, - Prism::ProgramNode, - )], - ) - - nesting_nodes << node if node.is_a?(Prism::ProgramNode) - call_node = T.let(nil, T.nilable(Prism::CallNode)) - - until queue.empty? - candidate = queue.shift - - # Skip nil child nodes - next if candidate.nil? - - # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the - # same order as the visiting mechanism, which means searching the child nodes before moving on to the next - # sibling - T.unsafe(queue).unshift(*candidate.child_nodes) - - # Skip if the current node doesn't cover the desired position - loc = candidate.location - next unless (loc.start_offset...loc.end_offset).cover?(char_position) - - # If the node's start character is already past the position, then we should've found the closest node - # already - break if char_position < loc.start_offset - - # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and - # need to pop the stack - previous_level = nesting_nodes.last - nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset - - # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the - # target when it is a constant - case candidate - when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode, - Prism::LambdaNode - nesting_nodes << candidate - end - - if candidate.is_a?(Prism::CallNode) - arg_loc = candidate.arguments&.location - blk_loc = candidate.block&.location - if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) || - (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position)) - call_node = candidate - end - end - - # If there are node types to filter by, and the current node is not one of those types, then skip it - next if node_types.any? && node_types.none? { |type| candidate.class == type } - - # If the current node is narrower than or equal to the previous closest node, then it is more precise - closest_loc = closest.location - if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset - parent = closest - closest = candidate - end - end - - # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That - # is, when targeting Bar in the following example: - # - # ```ruby - # class Foo::Bar; end - # ``` - # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even - # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior - if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode) - last_level = nesting_nodes.last - - if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) && - last_level.constant_path == closest - nesting_nodes.pop - end - end - - NodeContext.new(closest, parent, nesting_nodes, call_node) - end - class Scanner extend T::Sig diff --git a/lib/ruby_lsp/erb_document.rb b/lib/ruby_lsp/erb_document.rb index 38c25024a..f0f5ab6b0 100644 --- a/lib/ruby_lsp/erb_document.rb +++ b/lib/ruby_lsp/erb_document.rb @@ -28,6 +28,16 @@ def language_id LanguageId::ERB end + sig do + params( + position: T::Hash[Symbol, T.untyped], + node_types: T::Array[T.class_of(Prism::Node)], + ).returns(NodeContext) + end + def locate_node(position, node_types: []) + RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types) + end + class ERBScanner extend T::Sig diff --git a/lib/ruby_lsp/requests/code_action_resolve.rb b/lib/ruby_lsp/requests/code_action_resolve.rb index dea14042a..2ff6af3d6 100644 --- a/lib/ruby_lsp/requests/code_action_resolve.rb +++ b/lib/ruby_lsp/requests/code_action_resolve.rb @@ -112,7 +112,7 @@ def refactor_variable extracted_source = T.must(@document.source[start_index...end_index]) # Find the closest statements node, so that we place the refactor in a valid position - node_context = @document + node_context = RubyDocument .locate(@document.parse_result.value, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode]) closest_statements = node_context.node @@ -206,7 +206,7 @@ def refactor_method extracted_source = T.must(@document.source[start_index...end_index]) # Find the closest method declaration node, so that we place the refactor in a valid position - node_context = @document.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode]) + node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode]) closest_def = T.cast(node_context.node, Prism::DefNode) return Error::InvalidTargetRange if closest_def.nil? diff --git a/lib/ruby_lsp/requests/completion.rb b/lib/ruby_lsp/requests/completion.rb index 83e25d587..693fc914f 100644 --- a/lib/ruby_lsp/requests/completion.rb +++ b/lib/ruby_lsp/requests/completion.rb @@ -60,7 +60,7 @@ def initialize(document, global_state, params, sorbet_level, dispatcher) # Completion always receives the position immediately after the character that was just typed. Here we adjust it # back by 1, so that we find the right node char_position = document.create_scanner.find_char_position(params[:position]) - 1 - node_context = document.locate( + node_context = RubyDocument.locate( document.parse_result.value, char_position, node_types: [ diff --git a/lib/ruby_lsp/ruby_document.rb b/lib/ruby_lsp/ruby_document.rb index 048eb2bff..eab94d9bd 100644 --- a/lib/ruby_lsp/ruby_document.rb +++ b/lib/ruby_lsp/ruby_document.rb @@ -18,6 +18,109 @@ class SorbetLevel < T::Enum end end + class << self + extend T::Sig + + sig do + params( + node: Prism::Node, + char_position: Integer, + node_types: T::Array[T.class_of(Prism::Node)], + ).returns(NodeContext) + end + def locate(node, char_position, node_types: []) + queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)]) + closest = node + parent = T.let(nil, T.nilable(Prism::Node)) + nesting_nodes = T.let( + [], + T::Array[T.any( + Prism::ClassNode, + Prism::ModuleNode, + Prism::SingletonClassNode, + Prism::DefNode, + Prism::BlockNode, + Prism::LambdaNode, + Prism::ProgramNode, + )], + ) + + nesting_nodes << node if node.is_a?(Prism::ProgramNode) + call_node = T.let(nil, T.nilable(Prism::CallNode)) + + until queue.empty? + candidate = queue.shift + + # Skip nil child nodes + next if candidate.nil? + + # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the + # same order as the visiting mechanism, which means searching the child nodes before moving on to the next + # sibling + T.unsafe(queue).unshift(*candidate.child_nodes) + + # Skip if the current node doesn't cover the desired position + loc = candidate.location + next unless (loc.start_offset...loc.end_offset).cover?(char_position) + + # If the node's start character is already past the position, then we should've found the closest node + # already + break if char_position < loc.start_offset + + # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level + # and need to pop the stack + previous_level = nesting_nodes.last + nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset + + # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of + # the target when it is a constant + case candidate + when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode, + Prism::LambdaNode + nesting_nodes << candidate + end + + if candidate.is_a?(Prism::CallNode) + arg_loc = candidate.arguments&.location + blk_loc = candidate.block&.location + if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) || + (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position)) + call_node = candidate + end + end + + # If there are node types to filter by, and the current node is not one of those types, then skip it + next if node_types.any? && node_types.none? { |type| candidate.class == type } + + # If the current node is narrower than or equal to the previous closest node, then it is more precise + closest_loc = closest.location + if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset + parent = closest + closest = candidate + end + end + + # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. + # That is, when targeting Bar in the following example: + # + # ```ruby + # class Foo::Bar; end + # ``` + # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, + # even though the class/module node does indeed enclose the target, because it would lead to incorrect behavior + if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode) + last_level = nesting_nodes.last + + if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) && + last_level.constant_path == closest + nesting_nodes.pop + end + end + + NodeContext.new(closest, parent, nesting_nodes, call_node) + end + end + sig { override.returns(ParseResultType) } def parse return @parse_result unless @needs_parsing @@ -89,5 +192,15 @@ def locate_first_within_range(range, node_types: []) end end end + + sig do + params( + position: T::Hash[Symbol, T.untyped], + node_types: T::Array[T.class_of(Prism::Node)], + ).returns(NodeContext) + end + def locate_node(position, node_types: []) + RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types) + end end end