Skip to content

Commit

Permalink
Decouple request and listener logic (#1247)
Browse files Browse the repository at this point in the history
* Remove redundant run_expectations template

* Decouple CodeLens' request and listener logic

* Decouple Hover's request and listener logic

* Decouple Definition's request and listener logic

* Decouple DocumentSymbol's request and listener logic

* Decouple Completion's request and listener logic

* Decouple SemanticHighlight's request and listener logic

* Decouple FoldingRange's request and listener logic

* Decouple InlayHints' request and listener logic

* Decouple SignatureHelp's request and listener logic

* Decouple DocumentHighlight's request and listener logic

* Decouple DocumentLink's request and listener logic

* Decouple Listener from Request
  • Loading branch information
st0012 authored Jan 11, 2024
1 parent 47fb992 commit 31aa8ef
Show file tree
Hide file tree
Showing 35 changed files with 2,931 additions and 2,674 deletions.
2 changes: 1 addition & 1 deletion lib/ruby_lsp/check_docs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def run_task
# documented
features = ObjectSpace.each_object(Class).select do |k|
klass = T.unsafe(k)
klass < Requests::Request && klass != Listener && klass != ExtensibleListener
klass < Requests::Request
end

missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
Expand Down
224 changes: 49 additions & 175 deletions lib/ruby_lsp/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,30 @@ def run(request)
nil
end
when "textDocument/documentHighlight"
document_highlight(uri, request.dig(:params, :position))
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)
request = Requests::DocumentHighlight.new(document, request.dig(:params, :position), dispatcher)
dispatcher.dispatch(document.tree)
request.response
when "textDocument/onTypeFormatting"
on_type_formatting(uri, request.dig(:params, :position), request.dig(:params, :ch))
when "textDocument/hover"
hover(uri, request.dig(:params, :position))
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)
Requests::Hover.new(
document,
@index,
request.dig(:params, :position),
dispatcher,
document.typechecker_enabled?,
).response
when "textDocument/inlayHint"
inlay_hint(uri, request.dig(:params, :range))
hints_configurations = T.must(@store.features_configuration.dig(:inlayHint))
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)
request = Requests::InlayHints.new(document, request.dig(:params, :range), hints_configurations, dispatcher)
dispatcher.visit(document.tree)
request.response
when "textDocument/codeAction"
code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
when "codeAction/resolve"
Expand All @@ -169,11 +186,36 @@ def run(request)
nil
end
when "textDocument/completion"
completion(uri, request.dig(:params, :position))
when "textDocument/definition"
definition(uri, request.dig(:params, :position))
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)
Requests::Completion.new(
document,
@index,
request.dig(:params, :position),
document.typechecker_enabled?,
dispatcher,
).response
when "textDocument/signatureHelp"
signature_help(uri, request.dig(:params, :position), request.dig(:params, :context))
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)

Requests::SignatureHelp.new(
document,
@index,
request.dig(:params, :position),
request.dig(:params, :context),
dispatcher,
).response
when "textDocument/definition"
dispatcher = Prism::Dispatcher.new
document = @store.get(uri)
Requests::Definition.new(
document,
@index,
request.dig(:params, :position),
dispatcher,
document.typechecker_enabled?,
).response
when "workspace/didChangeWatchedFiles"
did_change_watched_files(request.dig(:params, :changes))
when "workspace/symbol"
Expand Down Expand Up @@ -243,38 +285,6 @@ def perform_initial_indexing
end
end

sig do
params(
uri: URI::Generic,
position: T::Hash[Symbol, T.untyped],
context: T.nilable(T::Hash[Symbol, T.untyped]),
).returns(T.any(T.nilable(Interface::SignatureHelp), T::Hash[Symbol, T.untyped]))
end
def signature_help(uri, position, context)
current_signature = context && context[:activeSignatureHelp]
document = @store.get(uri)
target, parent, nesting = document.locate_node(
{ line: position[:line], character: position[:character] - 2 },
node_types: [Prism::CallNode],
)

# If we're typing a nested method call (e.g.: `foo(bar)`), then we may end up locating `bar` as the target method
# call incorrectly. To correct that, we check if there's an active signature with the same name as the parent node
# and then replace the target
if current_signature && parent.is_a?(Prism::CallNode)
active_signature = current_signature[:activeSignature] || 0

if current_signature.dig(:signatures, active_signature, :label)&.start_with?(parent.message)
target = parent
end
end

dispatcher = Prism::Dispatcher.new
listener = Requests::SignatureHelp.new(nesting, @index, dispatcher)
dispatcher.dispatch_once(target)
listener.response
end

sig { params(query: T.nilable(String)).returns(T::Array[Interface::WorkspaceSymbol]) }
def workspace_symbol(query)
Requests::WorkspaceSymbol.new(query, @index).response
Expand All @@ -285,56 +295,6 @@ def show_syntax_tree(uri, range)
{ ast: Requests::ShowSyntaxTree.new(@store.get(uri), range).response }
end

sig do
params(
uri: URI::Generic,
position: T::Hash[Symbol, T.untyped],
).returns(T.nilable(T.any(T::Array[Interface::Location], Interface::Location)))
end
def definition(uri, position)
document = @store.get(uri)
target, parent, nesting = document.locate_node(
position,
node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
)

target = parent if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)

dispatcher = Prism::Dispatcher.new
base_listener = Requests::Definition.new(uri, nesting, @index, dispatcher, document.typechecker_enabled?)
dispatcher.dispatch_once(target)
base_listener.response
end

sig do
params(
uri: URI::Generic,
position: T::Hash[Symbol, T.untyped],
).returns(T.nilable(Interface::Hover))
end
def hover(uri, position)
document = @store.get(uri)
target, parent, nesting = document.locate_node(
position,
node_types: Requests::Hover::ALLOWED_TARGETS,
)

if (Requests::Hover::ALLOWED_TARGETS.include?(parent.class) &&
!Requests::Hover::ALLOWED_TARGETS.include?(target.class)) ||
(parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
target = parent
end

# Instantiate all listeners
dispatcher = Prism::Dispatcher.new
hover = Requests::Hover.new(uri, @index, nesting, dispatcher, document.typechecker_enabled?)

# Emit events for all listeners
dispatcher.dispatch_once(target)

hover.response
end

sig do
params(uri: URI::Generic, content_changes: T::Array[T::Hash[Symbol, T.untyped]], version: Integer).returns(Object)
end
Expand Down Expand Up @@ -404,41 +364,6 @@ def on_type_formatting(uri, position, character)
Requests::OnTypeFormatting.new(@store.get(uri), position, character).response
end

sig do
params(
uri: URI::Generic,
position: T::Hash[Symbol, T.untyped],
).returns(T.nilable(T::Array[Interface::DocumentHighlight]))
end
def document_highlight(uri, position)
document = @store.get(uri)

target, parent = document.locate_node(position)
dispatcher = Prism::Dispatcher.new
listener = Requests::DocumentHighlight.new(target, parent, dispatcher)
dispatcher.visit(document.tree)
listener.response
end

sig do
params(
uri: URI::Generic,
range: T::Hash[Symbol, T.untyped],
).returns(T.nilable(T::Array[Interface::InlayHint]))
end
def inlay_hint(uri, range)
document = @store.get(uri)

start_line = range.dig(:start, :line)
end_line = range.dig(:end, :line)

dispatcher = Prism::Dispatcher.new
hints_configurations = T.must(@store.features_configuration.dig(:inlayHint))
listener = Requests::InlayHints.new(start_line..end_line, hints_configurations, dispatcher)
dispatcher.visit(document.tree)
listener.response
end

sig do
params(
uri: URI::Generic,
Expand Down Expand Up @@ -509,57 +434,6 @@ def semantic_tokens_range(uri, range)
Requests::Support::SemanticTokenEncoder.new.encode(listener.response)
end

sig do
params(
uri: URI::Generic,
position: T::Hash[Symbol, T.untyped],
).returns(T.nilable(T::Array[Interface::CompletionItem]))
end
def completion(uri, position)
document = @store.get(uri)

# 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(position) - 1
matched, parent, nesting = document.locate(
document.tree,
char_position,
node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
)
return unless matched && parent

target = case matched
when Prism::CallNode
message = matched.message

if message == "require"
args = matched.arguments&.arguments
return if args.nil? || args.is_a?(Prism::ForwardingArgumentsNode)

argument = args.first
return unless argument.is_a?(Prism::StringNode)
return unless (argument.location.start_offset..argument.location.end_offset).cover?(char_position)

argument
else
matched
end
when Prism::ConstantReadNode, Prism::ConstantPathNode
if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
parent
else
matched
end
end

return unless target

dispatcher = Prism::Dispatcher.new
listener = Requests::Completion.new(@index, nesting, dispatcher, document.typechecker_enabled?)
dispatcher.dispatch_once(target)
listener.response
end

sig { params(id: String, title: String, percentage: Integer).void }
def begin_progress(id, title, percentage: 0)
return unless @store.supports_progress
Expand Down
54 changes: 2 additions & 52 deletions lib/ruby_lsp/listener.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module RubyLsp
# Listener is an abstract class to be used by requests for listening to events emitted when visiting an AST using the
# Prism::Dispatcher.
class Listener < Requests::Request
class Listener
extend T::Sig
extend T::Helpers
extend T::Generic
Expand All @@ -20,7 +20,7 @@ def initialize(dispatcher)
@dispatcher = dispatcher
end

sig { override.returns(ResponseType) }
sig { returns(ResponseType) }
def response
_response
end
Expand All @@ -30,54 +30,4 @@ def response
sig { abstract.returns(ResponseType) }
def _response; end
end

# ExtensibleListener is an abstract class to be used by requests that accept addons.
class ExtensibleListener < Listener
extend T::Sig
extend T::Generic

ResponseType = type_member

abstract!

# When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
# ivars have been initialized. This is because the constructor of ExtensibleListener calls
# `initialize_external_listener` which may depend on the subclass's ivars.
sig { params(dispatcher: Prism::Dispatcher).void }
def initialize(dispatcher)
super
@response_merged = T.let(false, T::Boolean)
@external_listeners = T.let(
Addon.addons.filter_map do |ext|
initialize_external_listener(ext)
end,
T::Array[RubyLsp::Listener[ResponseType]],
)
end

# Merge responses from all external listeners into the base listener's response. We do this to return a single
# response to the editor including the results of all addons
sig { void }
def merge_external_listeners_responses!
@external_listeners.each { |l| merge_response!(l) }
end

sig { override.returns(ResponseType) }
def response
merge_external_listeners_responses! unless @response_merged
super
end

sig do
abstract.params(addon: RubyLsp::Addon).returns(T.nilable(RubyLsp::Listener[ResponseType]))
end
def initialize_external_listener(addon); end

# Does nothing by default. Requests that accept addons should override this method to define how to merge responses
# coming from external listeners
sig { abstract.params(other: Listener[T.untyped]).returns(T.self_type) }
def merge_response!(other)
end
end
private_constant(:ExtensibleListener)
end
Loading

0 comments on commit 31aa8ef

Please sign in to comment.