Skip to content

Commit 7bd2869

Browse files
authored
Make requests easier to extend (#972)
* Introduce Listener::Extensible module * Introduce ExtensibleListener to simplify response merging * Use `_response` as Listeners' internal interface This will allow `ExternalListener` to encapsulate response merging logic under `response`, which means we can always call `response` on the listener regardless of whether it's an extensible or not.
1 parent 71b31db commit 7bd2869

16 files changed

+116
-73
lines changed

SERVER_EXTENSIONS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ module RubyLsp
134134
ResponseType = type_member { { fixed: T.nilable(::RubyLsp::Interface::Hover) } }
135135

136136
sig { override.returns(ResponseType) }
137-
attr_reader :response
137+
attr_reader :_response
138138

139139
# Listeners are initialized with the EventEmitter. This object is used by the Ruby LSP to emit the events when it
140140
# finds nodes during AST analysis. Listeners must register which nodes they want to handle with the emitter (see
@@ -145,7 +145,7 @@ module RubyLsp
145145
def initialize(config, emitter, message_queue)
146146
super
147147

148-
@response = T.let(nil, ResponseType)
148+
@_response = T.let(nil, ResponseType)
149149
@config = config
150150

151151
# Register that this listener will handle `on_const` events (i.e.: whenever a constant is found in the code)
@@ -159,7 +159,7 @@ module RubyLsp
159159
# Certain helpers are made available to listeners to build LSP responses. The classes under `RubyLsp::Interface`
160160
# are generally used to build responses and they match exactly what the specification requests.
161161
contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: "Hello!")
162-
@response = RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
162+
@_response = RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
163163
end
164164
end
165165
end

lib/ruby_lsp/check_docs.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ def run_task
5353
# documented
5454
features = ObjectSpace.each_object(Class).filter_map do |k|
5555
klass = T.unsafe(k)
56-
klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
56+
klass if klass < RubyLsp::Requests::BaseRequest ||
57+
(klass < RubyLsp::Listener && klass != RubyLsp::ExtensibleListener)
5758
end
5859

5960
missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])

lib/ruby_lsp/executor.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ def run(request)
103103
semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
104104
emitter.visit(document.tree) if document.parsed?
105105

106-
code_lens.merge_external_listeners_responses!
107-
document_symbol.merge_external_listeners_responses!
108-
109106
# Store all responses retrieve in this round of visits in the cache and then return the response for the request
110107
# we actually received
111108
document.cache_set("textDocument/documentSymbol", document_symbol.response)
@@ -299,7 +296,6 @@ def hover(uri, position)
299296
# Emit events for all listeners
300297
emitter.emit_for_target(target)
301298

302-
hover.merge_external_listeners_responses!
303299
hover.response
304300
end
305301

lib/ruby_lsp/listener.rb

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,42 @@ class Listener
1818
def initialize(emitter, message_queue)
1919
@emitter = emitter
2020
@message_queue = message_queue
21-
@external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
21+
end
22+
23+
sig { returns(ResponseType) }
24+
def response
25+
_response
2226
end
2327

2428
# Override this method with an attr_reader that returns the response of your listener. The listener should
2529
# accumulate results in a @response variable and then provide the reader so that it is accessible
2630
sig { abstract.returns(ResponseType) }
27-
def response; end
31+
def _response; end
32+
end
33+
34+
# ExtensibleListener is an abstract class to be used by requests that accept extensions.
35+
class ExtensibleListener < Listener
36+
extend T::Sig
37+
extend T::Generic
38+
39+
ResponseType = type_member
40+
41+
abstract!
42+
43+
# When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
44+
# ivars have been initialized. This is because the constructor of ExtensibleListener calls
45+
# `initialize_external_listener` which may depend on the subclass's ivars.
46+
sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
47+
def initialize(emitter, message_queue)
48+
super
49+
@response_merged = T.let(false, T::Boolean)
50+
@external_listeners = T.let(
51+
Extension.extensions.filter_map do |ext|
52+
initialize_external_listener(ext)
53+
end,
54+
T::Array[RubyLsp::Listener[ResponseType]],
55+
)
56+
end
2857

2958
# Merge responses from all external listeners into the base listener's response. We do this to return a single
3059
# response to the editor including the results of all extensions
@@ -33,11 +62,21 @@ def merge_external_listeners_responses!
3362
@external_listeners.each { |l| merge_response!(l) }
3463
end
3564

65+
sig { returns(ResponseType) }
66+
def response
67+
merge_external_listeners_responses! unless @response_merged
68+
super
69+
end
70+
71+
sig do
72+
abstract.params(extension: RubyLsp::Extension).returns(T.nilable(RubyLsp::Listener[ResponseType]))
73+
end
74+
def initialize_external_listener(extension); end
75+
3676
# Does nothing by default. Requests that accept extensions should override this method to define how to merge
3777
# responses coming from external listeners
38-
sig { overridable.params(other: Listener[T.untyped]).returns(T.self_type) }
78+
sig { abstract.params(other: Listener[T.untyped]).returns(T.self_type) }
3979
def merge_response!(other)
40-
self
4180
end
4281
end
4382
end

lib/ruby_lsp/requests/code_lens.rb

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Requests
1818
# class Test < Minitest::Test
1919
# end
2020
# ```
21-
class CodeLens < Listener
21+
class CodeLens < ExtensibleListener
2222
extend T::Sig
2323
extend T::Generic
2424

@@ -29,23 +29,20 @@ class CodeLens < Listener
2929
SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
3030

3131
sig { override.returns(ResponseType) }
32-
attr_reader :response
32+
attr_reader :_response
3333

3434
sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
3535
def initialize(uri, emitter, message_queue, test_library)
36-
super(emitter, message_queue)
37-
3836
@uri = T.let(uri, URI::Generic)
39-
@external_listeners.concat(
40-
Extension.extensions.filter_map { |ext| ext.create_code_lens_listener(uri, emitter, message_queue) },
41-
)
4237
@test_library = T.let(test_library, String)
43-
@response = T.let([], ResponseType)
38+
@_response = T.let([], ResponseType)
4439
@path = T.let(uri.to_standardized_path, T.nilable(String))
4540
# visibility_stack is a stack of [current_visibility, previous_visibility]
4641
@visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
4742
@class_stack = T.let([], T::Array[String])
4843

44+
super(emitter, message_queue)
45+
4946
emitter.register(
5047
self,
5148
:on_class,
@@ -149,9 +146,14 @@ def on_vcall(node)
149146
end
150147
end
151148

149+
sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
150+
def initialize_external_listener(extension)
151+
extension.create_code_lens_listener(@uri, @emitter, @message_queue)
152+
end
153+
152154
sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
153155
def merge_response!(other)
154-
@response.concat(other.response)
156+
@_response.concat(other.response)
155157
self
156158
end
157159

@@ -174,23 +176,23 @@ def add_test_code_lens(node, name:, command:, kind:)
174176
},
175177
]
176178

177-
@response << create_code_lens(
179+
@_response << create_code_lens(
178180
node,
179181
title: "Run",
180182
command_name: "rubyLsp.runTest",
181183
arguments: arguments,
182184
data: { type: "test", kind: kind },
183185
)
184186

185-
@response << create_code_lens(
187+
@_response << create_code_lens(
186188
node,
187189
title: "Run In Terminal",
188190
command_name: "rubyLsp.runTestInTerminal",
189191
arguments: arguments,
190192
data: { type: "test_in_terminal", kind: kind },
191193
)
192194

193-
@response << create_code_lens(
195+
@_response << create_code_lens(
194196
node,
195197
title: "Debug",
196198
command_name: "rubyLsp.debugTest",
@@ -239,7 +241,7 @@ def generate_test_command(class_name:, method_name: nil)
239241

240242
sig { params(node: SyntaxTree::Command, remote: String).void }
241243
def add_open_gem_remote_code_lens(node, remote)
242-
@response << create_code_lens(
244+
@_response << create_code_lens(
243245
node,
244246
title: "Open remote",
245247
command_name: "rubyLsp.openLink",

lib/ruby_lsp/requests/definition.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Definition < Listener
2424
ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
2525

2626
sig { override.returns(ResponseType) }
27-
attr_reader :response
27+
attr_reader :_response
2828

2929
sig do
3030
params(
@@ -41,7 +41,7 @@ def initialize(uri, nesting, index, emitter, message_queue)
4141
@uri = uri
4242
@nesting = nesting
4343
@index = index
44-
@response = T.let(nil, ResponseType)
44+
@_response = T.let(nil, ResponseType)
4545
emitter.register(self, :on_command, :on_const, :on_const_path_ref)
4646
end
4747

@@ -76,7 +76,7 @@ def on_command(node)
7676
if entry
7777
candidate = entry.full_path
7878

79-
@response = Interface::Location.new(
79+
@_response = Interface::Location.new(
8080
uri: URI::Generic.from_path(path: candidate).to_s,
8181
range: Interface::Range.new(
8282
start: Interface::Position.new(line: 0, character: 0),
@@ -90,7 +90,7 @@ def on_command(node)
9090
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
9191
candidate = File.expand_path(File.join(current_folder, required_file))
9292

93-
@response = Interface::Location.new(
93+
@_response = Interface::Location.new(
9494
uri: URI::Generic.from_path(path: candidate).to_s,
9595
range: Interface::Range.new(
9696
start: Interface::Position.new(line: 0, character: 0),
@@ -113,7 +113,7 @@ def find_in_index(value)
113113
nil
114114
end
115115

116-
@response = entries.filter_map do |entry|
116+
@_response = entries.filter_map do |entry|
117117
location = entry.location
118118
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
119119
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants

lib/ruby_lsp/requests/document_highlight.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class DocumentHighlight < Listener
2828
ResponseType = type_member { { fixed: T::Array[Interface::DocumentHighlight] } }
2929

3030
sig { override.returns(ResponseType) }
31-
attr_reader :response
31+
attr_reader :_response
3232

3333
sig do
3434
params(
@@ -41,7 +41,7 @@ class DocumentHighlight < Listener
4141
def initialize(target, parent, emitter, message_queue)
4242
super(emitter, message_queue)
4343

44-
@response = T.let([], T::Array[Interface::DocumentHighlight])
44+
@_response = T.let([], T::Array[Interface::DocumentHighlight])
4545

4646
return unless target && parent
4747

@@ -83,7 +83,7 @@ def on_node(node)
8383
sig { params(match: Support::HighlightTarget::HighlightMatch).void }
8484
def add_highlight(match)
8585
range = range_from_syntax_tree_node(match.node)
86-
@response << Interface::DocumentHighlight.new(range: range, kind: match.type)
86+
@_response << Interface::DocumentHighlight.new(range: range, kind: match.type)
8787
end
8888
end
8989
end

lib/ruby_lsp/requests/document_link.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def gem_paths
7373
end
7474

7575
sig { override.returns(ResponseType) }
76-
attr_reader :response
76+
attr_reader :_response
7777

7878
sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
7979
def initialize(uri, emitter, message_queue)
@@ -84,7 +84,7 @@ def initialize(uri, emitter, message_queue)
8484
path = uri.to_standardized_path
8585
version_match = path ? /(?<=%40)[\d.]+(?=\.rbi$)/.match(path) : nil
8686
@gem_version = T.let(version_match && version_match[0], T.nilable(String))
87-
@response = T.let([], T::Array[Interface::DocumentLink])
87+
@_response = T.let([], T::Array[Interface::DocumentLink])
8888

8989
emitter.register(self, :on_comment)
9090
end
@@ -99,7 +99,7 @@ def on_comment(node)
9999
file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, CGI.unescape(uri.path))
100100
return if file_path.nil?
101101

102-
@response << Interface::DocumentLink.new(
102+
@_response << Interface::DocumentLink.new(
103103
range: range_from_syntax_tree_node(node),
104104
target: "file://#{file_path}##{uri.line_number}",
105105
tooltip: "Jump to #{file_path}##{uri.line_number}",

lib/ruby_lsp/requests/document_symbol.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module Requests
2626
# end
2727
# end
2828
# ```
29-
class DocumentSymbol < Listener
29+
class DocumentSymbol < ExtensibleListener
3030
extend T::Sig
3131
extend T::Generic
3232

@@ -47,22 +47,18 @@ def initialize
4747
end
4848

4949
sig { override.returns(T::Array[Interface::DocumentSymbol]) }
50-
attr_reader :response
50+
attr_reader :_response
5151

5252
sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
5353
def initialize(emitter, message_queue)
54-
super
55-
5654
@root = T.let(SymbolHierarchyRoot.new, SymbolHierarchyRoot)
57-
@response = T.let(@root.children, T::Array[Interface::DocumentSymbol])
55+
@_response = T.let(@root.children, T::Array[Interface::DocumentSymbol])
5856
@stack = T.let(
5957
[@root],
6058
T::Array[T.any(SymbolHierarchyRoot, Interface::DocumentSymbol)],
6159
)
6260

63-
@external_listeners.concat(
64-
Extension.extensions.filter_map { |ext| ext.create_document_symbol_listener(emitter, message_queue) },
65-
)
61+
super
6662

6763
emitter.register(
6864
self,
@@ -79,10 +75,15 @@ def initialize(emitter, message_queue)
7975
)
8076
end
8177

78+
sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
79+
def initialize_external_listener(extension)
80+
extension.create_document_symbol_listener(@emitter, @message_queue)
81+
end
82+
8283
# Merges responses from other listeners
8384
sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
8485
def merge_response!(other)
85-
@response.concat(other.response)
86+
@_response.concat(other.response)
8687
self
8788
end
8889

0 commit comments

Comments
 (0)