Skip to content

Commit

Permalink
Refactor listener pattern to pass emitter around
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed May 3, 2023
1 parent da926f9 commit ffc9ce5
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 293 deletions.
58 changes: 31 additions & 27 deletions lib/ruby_lsp/event_emitter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,103 +13,107 @@ module RubyLsp
#
# ```ruby
# target_node = document.locate_node(position)
# listener = Requests::Hover.new
# EventEmitter.new(listener).emit_for_target(target_node)
# emitter = EventEmitter.new
# listener = Requests::Hover.new(emitter, @message_queue)
# emitter.emit_for_target(target_node)
# listener.response
# ```
class EventEmitter < SyntaxTree::Visitor
extend T::Sig

sig { params(listeners: Listener[T.untyped]).void }
def initialize(*listeners)
@listeners = listeners
sig { void }
def initialize
@listeners = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[Symbol, T::Array[Listener[T.untyped]]])
super()
end

sig { params(listener: Listener[T.untyped], events: Symbol).void }
def register(listener, *events)
events.each { |event| T.must(@listeners[event]) << listener }
end

# Emit events for a specific node. This is similar to the regular `visit` method, but avoids going deeper into the
# tree for performance
sig { params(node: T.nilable(SyntaxTree::Node)).void }
def emit_for_target(node)
case node
when SyntaxTree::Command
@listeners.each { |l| T.unsafe(l).on_command(node) if l.registered_for_event?(:on_command) }
@listeners[:on_command]&.each { |l| T.unsafe(l).on_command(node) }
when SyntaxTree::CallNode
@listeners.each { |l| T.unsafe(l).on_call(node) if l.registered_for_event?(:on_call) }
@listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
when SyntaxTree::TStringContent
@listeners.each do |l|
T.unsafe(l).on_tstring_content(node) if l.registered_for_event?(:on_tstring_content)
end
@listeners[:on_tstring_content]&.each { |l| T.unsafe(l).on_tstring_content(node) }
when SyntaxTree::ConstPathRef
@listeners.each { |l| T.unsafe(l).on_const_path_ref(node) if l.registered_for_event?(:on_const_path_ref) }
@listeners[:on_const_path_ref]&.each { |l| T.unsafe(l).on_const_path_ref(node) }
when SyntaxTree::Const
@listeners.each { |l| T.unsafe(l).on_const(node) if l.registered_for_event?(:on_const) }
@listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
end
end

# Visit dispatchers are below. Notice that for nodes that create a new scope (e.g.: classes, modules, method defs)
# we need both an `on_*` and `after_*` event. This is because some requests must know when we exit the scope
sig { override.params(node: SyntaxTree::ClassDeclaration).void }
def visit_class(node)
@listeners.each { |l| T.unsafe(l).on_class(node) if l.registered_for_event?(:on_class) }
@listeners[:on_class]&.each { |l| T.unsafe(l).on_class(node) }
super
@listeners.each { |l| T.unsafe(l).after_class(node) if l.registered_for_event?(:after_class) }
@listeners[:after_class]&.each { |l| T.unsafe(l).after_class(node) }
end

sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
def visit_module(node)
@listeners.each { |l| T.unsafe(l).on_module(node) if l.registered_for_event?(:on_module) }
@listeners[:on_module]&.each { |l| T.unsafe(l).on_module(node) }
super
@listeners.each { |l| T.unsafe(l).after_module(node) if l.registered_for_event?(:after_module) }
@listeners[:after_module]&.each { |l| T.unsafe(l).after_module(node) }
end

sig { override.params(node: SyntaxTree::Command).void }
def visit_command(node)
@listeners.each { |l| T.unsafe(l).on_command(node) if l.registered_for_event?(:on_command) }
@listeners[:on_command]&.each { |l| T.unsafe(l).on_command(node) }
super
@listeners.each { |l| T.unsafe(l).after_command(node) if l.registered_for_event?(:after_command) }
@listeners[:after_command]&.each { |l| T.unsafe(l).after_command(node) }
end

sig { override.params(node: SyntaxTree::CallNode).void }
def visit_call(node)
@listeners.each { |l| T.unsafe(l).on_call(node) if l.registered_for_event?(:on_call) }
@listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
super
@listeners.each { |l| T.unsafe(l).after_call(node) if l.registered_for_event?(:after_call) }
@listeners[:after_call]&.each { |l| T.unsafe(l).after_call(node) }
end

sig { override.params(node: SyntaxTree::VCall).void }
def visit_vcall(node)
@listeners.each { |l| T.unsafe(l).on_vcall(node) if l.registered_for_event?(:on_vcall) }
@listeners[:on_vcall]&.each { |l| T.unsafe(l).on_vcall(node) }
super
end

sig { override.params(node: SyntaxTree::ConstPathField).void }
def visit_const_path_field(node)
@listeners.each { |l| T.unsafe(l).on_const_path_field(node) if l.registered_for_event?(:on_const_path_field) }
@listeners[:on_const_path_field]&.each { |l| T.unsafe(l).on_const_path_field(node) }
super
end

sig { override.params(node: SyntaxTree::TopConstField).void }
def visit_top_const_field(node)
@listeners.each { |l| T.unsafe(l).on_top_const_field(node) if l.registered_for_event?(:on_top_const_field) }
@listeners[:on_top_const_field]&.each { |l| T.unsafe(l).on_top_const_field(node) }
super
end

sig { override.params(node: SyntaxTree::DefNode).void }
def visit_def(node)
@listeners.each { |l| T.unsafe(l).on_def(node) if l.registered_for_event?(:on_def) }
@listeners[:on_def]&.each { |l| T.unsafe(l).on_def(node) }
super
@listeners.each { |l| T.unsafe(l).after_def(node) if l.registered_for_event?(:after_def) }
@listeners[:after_def]&.each { |l| T.unsafe(l).after_def(node) }
end

sig { override.params(node: SyntaxTree::VarField).void }
def visit_var_field(node)
@listeners.each { |l| T.unsafe(l).on_var_field(node) if l.registered_for_event?(:on_var_field) }
@listeners[:on_var_field]&.each { |l| T.unsafe(l).on_var_field(node) }
super
end

sig { override.params(node: SyntaxTree::Comment).void }
def visit_comment(node)
@listeners.each { |l| T.unsafe(l).on_comment(node) if l.registered_for_event?(:on_comment) }
@listeners[:on_comment]&.each { |l| T.unsafe(l).on_comment(node) }
super
end
end
Expand Down
30 changes: 14 additions & 16 deletions lib/ruby_lsp/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,14 @@ def run(request)
return cached_response if cached_response

# Run listeners for the document
document_symbol = Requests::DocumentSymbol.new(uri, @message_queue)
document_link = Requests::DocumentLink.new(uri, @message_queue)
code_lens = Requests::CodeLens.new(uri, @message_queue)
code_lens_extensions_listeners = Requests::CodeLens.listeners.map { |l| l.new(document.uri, @message_queue) }
if document.parsed?
T.unsafe(EventEmitter).new(
document_symbol,
document_link,
code_lens,
*code_lens_extensions_listeners,
).visit(document.tree)
emitter = EventEmitter.new
document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
code_lens = Requests::CodeLens.new(uri, emitter, @message_queue)
code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
T.unsafe(l).new(document.uri, emitter, @message_queue)
end
emitter.visit(document.tree) if document.parsed?

code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }

Expand Down Expand Up @@ -194,11 +190,12 @@ def hover(uri, position)
end

# Instantiate all listeners
base_listener = Requests::Hover.new(document.uri, @message_queue)
listeners = Requests::Hover.listeners.map { |l| l.new(document.uri, @message_queue) }
emitter = EventEmitter.new
base_listener = Requests::Hover.new(emitter, @message_queue)
listeners = Requests::Hover.listeners.map { |l| l.new(emitter, @message_queue) }

# Emit events for all listeners
T.unsafe(EventEmitter).new(base_listener, *listeners).emit_for_target(target)
emitter.emit_for_target(target)

# Merge all responses into a single hover
listeners.each { |ext| base_listener.merge_response!(ext) }
Expand Down Expand Up @@ -405,8 +402,9 @@ def completion(uri, position)

return unless target

listener = Requests::PathCompletion.new(uri, @message_queue)
EventEmitter.new(listener).emit_for_target(target)
emitter = EventEmitter.new
listener = Requests::PathCompletion.new(emitter, @message_queue)
emitter.emit_for_target(target)
listener.response
end

Expand Down
29 changes: 3 additions & 26 deletions lib/ruby_lsp/listener.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,15 @@ class Listener

abstract!

sig { params(uri: String, message_queue: Thread::Queue).void }
def initialize(uri, message_queue)
sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
def initialize(emitter, message_queue)
@emitter = emitter
@message_queue = message_queue
@uri = uri
end

@event_to_listener_map = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[Symbol, T::Array[T.class_of(Listener)]])

sig { params(event: Symbol).returns(T.nilable(T::Boolean)) }
def registered_for_event?(event)
Listener.event_to_listener_map[event]&.include?(self.class)
end

class << self
extend T::Sig

sig { returns(T::Hash[Symbol, T::Array[T.class_of(Listener)]]) }
attr_reader :event_to_listener_map

sig { returns(T::Array[T.class_of(Listener)]) }
def listeners
@listeners ||= T.let([], T.nilable(T::Array[T.class_of(Listener)]))
Expand All @@ -42,19 +32,6 @@ def listeners
def add_listener(listener)
listeners << listener
end

# All listener events must be defined inside of a `listener_events` block. This is to ensure we know which events
# have been registered. Defining an event outside of this block will simply not register it and it'll never be
# invoked
sig { params(block: T.proc.void).void }
def listener_events(&block)
current_methods = instance_methods
block.call

(instance_methods - current_methods).each do |event|
T.must(Listener.event_to_listener_map[event]) << self
end
end
end

# Override this method with an attr_reader that returns the response of your listener. The listener should
Expand Down
104 changes: 52 additions & 52 deletions lib/ruby_lsp/requests/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,78 +31,78 @@ class CodeLens < Listener
sig { override.returns(ResponseType) }
attr_reader :response

sig { params(uri: String, message_queue: Thread::Queue).void }
def initialize(uri, message_queue)
super
sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
def initialize(uri, emitter, message_queue)
super(emitter, message_queue)

@response = T.let([], ResponseType)
@path = T.let(uri.delete_prefix("file://"), String)
@path = T.let(T.must(URI(uri).path), String)
@visibility = T.let("public", String)
@prev_visibility = T.let("public", String)

emitter.register(self, :on_class, :on_def, :on_command, :after_command, :on_call, :after_call, :on_vcall)
end

listener_events do
sig { params(node: SyntaxTree::ClassDeclaration).void }
def on_class(node)
class_name = node.constant.constant.value
if class_name.end_with?("Test")
add_code_lens(node, name: class_name, command: BASE_COMMAND + @path)
end
sig { params(node: SyntaxTree::ClassDeclaration).void }
def on_class(node)
class_name = node.constant.constant.value
if class_name.end_with?("Test")
add_code_lens(node, name: class_name, command: BASE_COMMAND + @path)
end
end

sig { params(node: SyntaxTree::DefNode).void }
def on_def(node)
if @visibility == "public"
method_name = node.name.value
if method_name.start_with?("test_")
add_code_lens(
node,
name: method_name,
command: BASE_COMMAND + @path + " --name " + method_name,
)
end
sig { params(node: SyntaxTree::DefNode).void }
def on_def(node)
if @visibility == "public"
method_name = node.name.value
if method_name.start_with?("test_")
add_code_lens(
node,
name: method_name,
command: BASE_COMMAND + @path + " --name " + method_name,
)
end
end
end

sig { params(node: SyntaxTree::Command).void }
def on_command(node)
if ACCESS_MODIFIERS.include?(node.message.value) && node.arguments.parts.any?
@prev_visibility = @visibility
@visibility = node.message.value
end
sig { params(node: SyntaxTree::Command).void }
def on_command(node)
if ACCESS_MODIFIERS.include?(node.message.value) && node.arguments.parts.any?
@prev_visibility = @visibility
@visibility = node.message.value
end
end

sig { params(node: SyntaxTree::Command).void }
def after_command(node)
@visibility = @prev_visibility
end
sig { params(node: SyntaxTree::Command).void }
def after_command(node)
@visibility = @prev_visibility
end

sig { params(node: SyntaxTree::CallNode).void }
def on_call(node)
ident = node.message if node.message.is_a?(SyntaxTree::Ident)
sig { params(node: SyntaxTree::CallNode).void }
def on_call(node)
ident = node.message if node.message.is_a?(SyntaxTree::Ident)

if ident
ident_value = T.cast(ident, SyntaxTree::Ident).value
if ACCESS_MODIFIERS.include?(ident_value)
@prev_visibility = @visibility
@visibility = ident_value
end
if ident
ident_value = T.cast(ident, SyntaxTree::Ident).value
if ACCESS_MODIFIERS.include?(ident_value)
@prev_visibility = @visibility
@visibility = ident_value
end
end
end

sig { params(node: SyntaxTree::CallNode).void }
def after_call(node)
@visibility = @prev_visibility
end
sig { params(node: SyntaxTree::CallNode).void }
def after_call(node)
@visibility = @prev_visibility
end

sig { params(node: SyntaxTree::VCall).void }
def on_vcall(node)
vcall_value = node.value.value
sig { params(node: SyntaxTree::VCall).void }
def on_vcall(node)
vcall_value = node.value.value

if ACCESS_MODIFIERS.include?(vcall_value)
@prev_visibility = vcall_value
@visibility = vcall_value
end
if ACCESS_MODIFIERS.include?(vcall_value)
@prev_visibility = vcall_value
@visibility = vcall_value
end
end

Expand Down
Loading

0 comments on commit ffc9ce5

Please sign in to comment.