ruby-lsp 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -116
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -1
  5. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +62 -0
  6. data/lib/ruby_lsp/check_docs.rb +112 -0
  7. data/lib/ruby_lsp/document.rb +87 -8
  8. data/lib/ruby_lsp/event_emitter.rb +120 -0
  9. data/lib/ruby_lsp/executor.rb +191 -44
  10. data/lib/ruby_lsp/extension.rb +104 -0
  11. data/lib/ruby_lsp/internal.rb +4 -0
  12. data/lib/ruby_lsp/listener.rb +42 -0
  13. data/lib/ruby_lsp/requests/base_request.rb +2 -90
  14. data/lib/ruby_lsp/requests/code_action_resolve.rb +47 -20
  15. data/lib/ruby_lsp/requests/code_actions.rb +6 -5
  16. data/lib/ruby_lsp/requests/code_lens.rb +151 -0
  17. data/lib/ruby_lsp/requests/diagnostics.rb +5 -5
  18. data/lib/ruby_lsp/requests/document_highlight.rb +8 -10
  19. data/lib/ruby_lsp/requests/document_link.rb +17 -15
  20. data/lib/ruby_lsp/requests/document_symbol.rb +63 -40
  21. data/lib/ruby_lsp/requests/folding_ranges.rb +14 -10
  22. data/lib/ruby_lsp/requests/formatting.rb +15 -15
  23. data/lib/ruby_lsp/requests/hover.rb +45 -34
  24. data/lib/ruby_lsp/requests/inlay_hints.rb +6 -5
  25. data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
  26. data/lib/ruby_lsp/requests/path_completion.rb +21 -51
  27. data/lib/ruby_lsp/requests/selection_ranges.rb +4 -4
  28. data/lib/ruby_lsp/requests/semantic_highlighting.rb +30 -16
  29. data/lib/ruby_lsp/requests/support/common.rb +91 -0
  30. data/lib/ruby_lsp/requests/support/highlight_target.rb +5 -4
  31. data/lib/ruby_lsp/requests/support/rails_document_client.rb +7 -6
  32. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
  34. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
  35. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
  36. data/lib/ruby_lsp/requests/support/sorbet.rb +5 -15
  37. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +42 -0
  38. data/lib/ruby_lsp/requests.rb +17 -14
  39. data/lib/ruby_lsp/server.rb +45 -9
  40. data/lib/ruby_lsp/store.rb +11 -11
  41. data/lib/ruby_lsp/utils.rb +9 -8
  42. metadata +13 -5
@@ -6,12 +6,12 @@ module RubyLsp
6
6
  class Executor
7
7
  extend T::Sig
8
8
 
9
- sig { params(store: Store).void }
10
- def initialize(store)
9
+ sig { params(store: Store, message_queue: Thread::Queue).void }
10
+ def initialize(store, message_queue)
11
11
  # Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
12
12
  # store
13
13
  @store = store
14
- @notifications = T.let([], T::Array[Notification])
14
+ @message_queue = message_queue
15
15
  end
16
16
 
17
17
  sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
@@ -25,7 +25,7 @@ module RubyLsp
25
25
  error = e
26
26
  end
27
27
 
28
- Result.new(response: response, error: error, request_time: request_time, notifications: @notifications)
28
+ Result.new(response: response, error: error, request_time: request_time)
29
29
  end
30
30
 
31
31
  private
@@ -38,27 +38,74 @@ module RubyLsp
38
38
  when "initialize"
39
39
  initialize_request(request.dig(:params))
40
40
  when "initialized"
41
+ Extension.load_extensions
42
+
43
+ errored_extensions = Extension.extensions.select(&:error?)
44
+
45
+ if errored_extensions.any?
46
+ @message_queue << Notification.new(
47
+ message: "window/showMessage",
48
+ params: Interface::ShowMessageParams.new(
49
+ type: Constant::MessageType::WARNING,
50
+ message: "Error loading extensions:\n\n#{errored_extensions.map(&:formatted_errors).join("\n\n")}",
51
+ ),
52
+ )
53
+
54
+ warn(errored_extensions.map(&:backtraces).join("\n\n"))
55
+ end
56
+
57
+ check_formatter_is_available
58
+
41
59
  warn("Ruby LSP is ready")
42
60
  VOID
43
61
  when "textDocument/didOpen"
44
- text_document_did_open(uri, request.dig(:params, :textDocument, :text))
62
+ text_document_did_open(
63
+ uri,
64
+ request.dig(:params, :textDocument, :text),
65
+ request.dig(:params, :textDocument, :version),
66
+ )
45
67
  when "textDocument/didClose"
46
- @notifications << Notification.new(
68
+ @message_queue << Notification.new(
47
69
  message: "textDocument/publishDiagnostics",
48
70
  params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
49
71
  )
50
72
 
51
73
  text_document_did_close(uri)
52
74
  when "textDocument/didChange"
53
- text_document_did_change(uri, request.dig(:params, :contentChanges))
75
+ text_document_did_change(
76
+ uri,
77
+ request.dig(:params, :contentChanges),
78
+ request.dig(:params, :textDocument, :version),
79
+ )
54
80
  when "textDocument/foldingRange"
55
81
  folding_range(uri)
56
- when "textDocument/documentLink"
57
- document_link(uri)
58
82
  when "textDocument/selectionRange"
59
83
  selection_range(uri, request.dig(:params, :positions))
60
- when "textDocument/documentSymbol"
61
- document_symbol(uri)
84
+ when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens"
85
+ document = @store.get(uri)
86
+
87
+ # If the response has already been cached by another request, return it
88
+ cached_response = document.cache_get(request[:method])
89
+ return cached_response if cached_response
90
+
91
+ # Run listeners for the document
92
+ emitter = EventEmitter.new
93
+ document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
94
+ document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
95
+ code_lens = Requests::CodeLens.new(uri, emitter, @message_queue)
96
+ code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
97
+ T.unsafe(l).new(document.uri, emitter, @message_queue)
98
+ end
99
+ emitter.visit(document.tree) if document.parsed?
100
+
101
+ code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
102
+
103
+ # Store all responses retrieve in this round of visits in the cache and then return the response for the request
104
+ # we actually received
105
+ document.cache_set("textDocument/documentSymbol", document_symbol.response)
106
+ document.cache_set("textDocument/documentLink", document_link.response)
107
+ document.cache_set("textDocument/codeLens", code_lens.response)
108
+ document.cache_get(request[:method])
62
109
  when "textDocument/semanticTokens/full"
63
110
  semantic_tokens_full(uri)
64
111
  when "textDocument/semanticTokens/range"
@@ -67,7 +114,7 @@ module RubyLsp
67
114
  begin
68
115
  formatting(uri)
69
116
  rescue Requests::Formatting::InvalidFormatter => error
70
- @notifications << Notification.new(
117
+ @message_queue << Notification.new(
71
118
  message: "window/showMessage",
72
119
  params: Interface::ShowMessageParams.new(
73
120
  type: Constant::MessageType::ERROR,
@@ -77,7 +124,7 @@ module RubyLsp
77
124
 
78
125
  nil
79
126
  rescue StandardError => error
80
- @notifications << Notification.new(
127
+ @message_queue << Notification.new(
81
128
  message: "window/showMessage",
82
129
  params: Interface::ShowMessageParams.new(
83
130
  type: Constant::MessageType::ERROR,
@@ -103,7 +150,7 @@ module RubyLsp
103
150
  begin
104
151
  diagnostic(uri)
105
152
  rescue StandardError => error
106
- @notifications << Notification.new(
153
+ @message_queue << Notification.new(
107
154
  message: "window/showMessage",
108
155
  params: Interface::ShowMessageParams.new(
109
156
  type: Constant::MessageType::ERROR,
@@ -120,7 +167,7 @@ module RubyLsp
120
167
 
121
168
  sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
122
169
  def folding_range(uri)
123
- @store.cache_fetch(uri, :folding_ranges) do |document|
170
+ @store.cache_fetch(uri, "textDocument/foldingRange") do |document|
124
171
  Requests::FoldingRanges.new(document).run
125
172
  end
126
173
  end
@@ -132,32 +179,38 @@ module RubyLsp
132
179
  ).returns(T.nilable(Interface::Hover))
133
180
  end
134
181
  def hover(uri, position)
135
- RubyLsp::Requests::Hover.new(@store.get(uri), position).run
136
- end
182
+ document = @store.get(uri)
183
+ return if document.syntax_error?
137
184
 
138
- sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
139
- def document_link(uri)
140
- @store.cache_fetch(uri, :document_link) do |document|
141
- RubyLsp::Requests::DocumentLink.new(uri, document).run
142
- end
143
- end
185
+ target, parent = document.locate_node(position)
144
186
 
145
- sig { params(uri: String).returns(T::Array[Interface::DocumentSymbol]) }
146
- def document_symbol(uri)
147
- @store.cache_fetch(uri, :document_symbol) do |document|
148
- Requests::DocumentSymbol.new(document).run
187
+ if !Requests::Hover::ALLOWED_TARGETS.include?(target.class) &&
188
+ Requests::Hover::ALLOWED_TARGETS.include?(parent.class)
189
+ target = parent
149
190
  end
191
+
192
+ # Instantiate all listeners
193
+ emitter = EventEmitter.new
194
+ base_listener = Requests::Hover.new(emitter, @message_queue)
195
+ listeners = Requests::Hover.listeners.map { |l| l.new(emitter, @message_queue) }
196
+
197
+ # Emit events for all listeners
198
+ emitter.emit_for_target(target)
199
+
200
+ # Merge all responses into a single hover
201
+ listeners.each { |ext| base_listener.merge_response!(ext) }
202
+ base_listener.response
150
203
  end
151
204
 
152
- sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
153
- def text_document_did_change(uri, content_changes)
154
- @store.push_edits(uri, content_changes)
205
+ sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
206
+ def text_document_did_change(uri, content_changes, version)
207
+ @store.push_edits(uri: uri, edits: content_changes, version: version)
155
208
  VOID
156
209
  end
157
210
 
158
- sig { params(uri: String, text: String).returns(Object) }
159
- def text_document_did_open(uri, text)
160
- @store.set(uri, text)
211
+ sig { params(uri: String, text: String, version: Integer).returns(Object) }
212
+ def text_document_did_open(uri, text, version)
213
+ @store.set(uri: uri, source: text, version: version)
161
214
  VOID
162
215
  end
163
216
 
@@ -174,7 +227,7 @@ module RubyLsp
174
227
  ).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
175
228
  end
176
229
  def selection_range(uri, positions)
177
- ranges = @store.cache_fetch(uri, :selection_ranges) do |document|
230
+ ranges = @store.cache_fetch(uri, "textDocument/selectionRange") do |document|
178
231
  Requests::SelectionRanges.new(document).run
179
232
  end
180
233
 
@@ -194,7 +247,7 @@ module RubyLsp
194
247
 
195
248
  sig { params(uri: String).returns(Interface::SemanticTokens) }
196
249
  def semantic_tokens_full(uri)
197
- @store.cache_fetch(uri, :semantic_highlighting) do |document|
250
+ @store.cache_fetch(uri, "textDocument/semanticTokens/full") do |document|
198
251
  T.cast(
199
252
  Requests::SemanticHighlighting.new(
200
253
  document,
@@ -207,7 +260,10 @@ module RubyLsp
207
260
 
208
261
  sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
209
262
  def formatting(uri)
210
- Requests::Formatting.new(uri, @store.get(uri), formatter: @store.formatter).run
263
+ # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
264
+ return if @store.formatter == "none"
265
+
266
+ Requests::Formatting.new(@store.get(uri), formatter: @store.formatter).run
211
267
  end
212
268
 
213
269
  sig do
@@ -250,7 +306,7 @@ module RubyLsp
250
306
  def code_action(uri, range, context)
251
307
  document = @store.get(uri)
252
308
 
253
- Requests::CodeActions.new(uri, document, range, context).run
309
+ Requests::CodeActions.new(document, range, context).run
254
310
  end
255
311
 
256
312
  sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
@@ -261,7 +317,7 @@ module RubyLsp
261
317
 
262
318
  case result
263
319
  when Requests::CodeActionResolve::Error::EmptySelection
264
- @notifications << Notification.new(
320
+ @message_queue << Notification.new(
265
321
  message: "window/showMessage",
266
322
  params: Interface::ShowMessageParams.new(
267
323
  type: Constant::MessageType::ERROR,
@@ -270,7 +326,7 @@ module RubyLsp
270
326
  )
271
327
  raise Requests::CodeActionResolve::CodeActionError
272
328
  when Requests::CodeActionResolve::Error::InvalidTargetRange
273
- @notifications << Notification.new(
329
+ @message_queue << Notification.new(
274
330
  message: "window/showMessage",
275
331
  params: Interface::ShowMessageParams.new(
276
332
  type: Constant::MessageType::ERROR,
@@ -285,8 +341,8 @@ module RubyLsp
285
341
 
286
342
  sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
287
343
  def diagnostic(uri)
288
- response = @store.cache_fetch(uri, :diagnostics) do |document|
289
- Requests::Diagnostics.new(uri, document).run
344
+ response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
345
+ Requests::Diagnostics.new(document).run
290
346
  end
291
347
 
292
348
  Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
@@ -312,17 +368,68 @@ module RubyLsp
312
368
  params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
313
369
  end
314
370
  def completion(uri, position)
315
- Requests::PathCompletion.new(@store.get(uri), position).run
371
+ document = @store.get(uri)
372
+ return unless document.parsed?
373
+
374
+ char_position = document.create_scanner.find_char_position(position)
375
+ matched, parent = document.locate(
376
+ T.must(document.tree),
377
+ char_position,
378
+ node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
379
+ )
380
+
381
+ return unless matched && parent
382
+
383
+ target = case matched
384
+ when SyntaxTree::Command, SyntaxTree::CallNode, SyntaxTree::CommandCall
385
+ message = matched.message
386
+ return if message.is_a?(Symbol)
387
+ return unless message.value == "require"
388
+
389
+ args = matched.arguments
390
+ args = args.arguments if args.is_a?(SyntaxTree::ArgParen)
391
+ return if args.nil? || args.is_a?(SyntaxTree::ArgsForward)
392
+
393
+ argument = args.parts.first
394
+ return unless argument.is_a?(SyntaxTree::StringLiteral)
395
+
396
+ path_node = argument.parts.first
397
+ return unless path_node.is_a?(SyntaxTree::TStringContent)
398
+ return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
399
+
400
+ path_node
401
+ end
402
+
403
+ return unless target
404
+
405
+ emitter = EventEmitter.new
406
+ listener = Requests::PathCompletion.new(emitter, @message_queue)
407
+ emitter.emit_for_target(target)
408
+ listener.response
316
409
  end
317
410
 
318
411
  sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
319
412
  def initialize_request(options)
320
413
  @store.clear
321
- @store.encoding = options.dig(:capabilities, :general, :positionEncodings)
414
+
415
+ encodings = options.dig(:capabilities, :general, :positionEncodings)
416
+ @store.encoding = if encodings.nil? || encodings.empty?
417
+ Constant::PositionEncodingKind::UTF16
418
+ elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
419
+ Constant::PositionEncodingKind::UTF8
420
+ else
421
+ encodings.first
422
+ end
423
+
322
424
  formatter = options.dig(:initializationOptions, :formatter)
323
- @store.formatter = formatter unless formatter.nil?
425
+ @store.formatter = if formatter == "auto"
426
+ detected_formatter
427
+ else
428
+ formatter
429
+ end
324
430
 
325
431
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
432
+ experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
326
433
 
327
434
  enabled_features = case configured_features
328
435
  when Array
@@ -351,6 +458,10 @@ module RubyLsp
351
458
  Interface::DocumentLinkOptions.new(resolve_provider: false)
352
459
  end
353
460
 
461
+ code_lens_provider = if experimental_features
462
+ Interface::CodeLensOptions.new(resolve_provider: false)
463
+ end
464
+
354
465
  hover_provider = if enabled_features["hover"]
355
466
  Interface::HoverClientCapabilities.new(dynamic_registration: false)
356
467
  end
@@ -406,6 +517,7 @@ module RubyLsp
406
517
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
407
518
  open_close: true,
408
519
  ),
520
+ position_encoding: @store.encoding,
409
521
  selection_range_provider: enabled_features["selectionRanges"],
410
522
  hover_provider: hover_provider,
411
523
  document_symbol_provider: document_symbol_provider,
@@ -419,8 +531,43 @@ module RubyLsp
419
531
  diagnostic_provider: diagnostics_provider,
420
532
  inlay_hint_provider: inlay_hint_provider,
421
533
  completion_provider: completion_provider,
534
+ code_lens_provider: code_lens_provider,
422
535
  ),
423
536
  )
424
537
  end
538
+
539
+ sig { returns(String) }
540
+ def detected_formatter
541
+ # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
542
+ if direct_dependency?(/^rubocop/)
543
+ "rubocop"
544
+ elsif direct_dependency?(/^syntax_tree$/)
545
+ "syntax_tree"
546
+ else
547
+ "none"
548
+ end
549
+ end
550
+
551
+ sig { params(gem_pattern: Regexp).returns(T::Boolean) }
552
+ def direct_dependency?(gem_pattern)
553
+ Bundler.locked_gems.dependencies.keys.grep(gem_pattern).any?
554
+ end
555
+
556
+ sig { void }
557
+ def check_formatter_is_available
558
+ # Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
559
+ # Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
560
+ return unless @store.formatter == "rubocop"
561
+
562
+ unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
563
+ @message_queue << Notification.new(
564
+ message: "window/showMessage",
565
+ params: Interface::ShowMessageParams.new(
566
+ type: Constant::MessageType::ERROR,
567
+ message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the bundle.",
568
+ ),
569
+ )
570
+ end
571
+ end
425
572
  end
426
573
  end
@@ -0,0 +1,104 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # To register an extension, inherit from this class and implement both `name` and `activate`
6
+ #
7
+ # # Example
8
+ #
9
+ # ```ruby
10
+ # module MyGem
11
+ # class MyExtension < Extension
12
+ # def activate
13
+ # # Perform any relevant initialization
14
+ # end
15
+ #
16
+ # def name
17
+ # "My extension name"
18
+ # end
19
+ # end
20
+ # end
21
+ # ```
22
+ class Extension
23
+ extend T::Sig
24
+ extend T::Helpers
25
+
26
+ abstract!
27
+
28
+ class << self
29
+ extend T::Sig
30
+
31
+ # Automatically track and instantiate extension classes
32
+ sig { params(child_class: T.class_of(Extension)).void }
33
+ def inherited(child_class)
34
+ extensions << child_class.new
35
+ super
36
+ end
37
+
38
+ sig { returns(T::Array[Extension]) }
39
+ def extensions
40
+ @extensions ||= T.let([], T.nilable(T::Array[Extension]))
41
+ end
42
+
43
+ # Discovers and loads all extensions. Returns the list of activated extensions
44
+ sig { returns(T::Array[Extension]) }
45
+ def load_extensions
46
+ # Require all extensions entry points, which should be placed under
47
+ # `some_gem/lib/ruby_lsp/your_gem_name/extension.rb`
48
+ Gem.find_files("ruby_lsp/**/extension.rb").each do |extension|
49
+ require File.expand_path(extension)
50
+ rescue => e
51
+ warn(e.message)
52
+ warn(e.backtrace.to_s) # rubocop:disable Lint/RedundantStringCoercion
53
+ end
54
+
55
+ # Activate each one of the discovered extensions. If any problems occur in the extensions, we don't want to
56
+ # fail to boot the server
57
+ extensions.each do |extension|
58
+ extension.activate
59
+ nil
60
+ rescue => e
61
+ extension.add_error(e)
62
+ end
63
+ end
64
+ end
65
+
66
+ sig { void }
67
+ def initialize
68
+ @errors = T.let([], T::Array[StandardError])
69
+ end
70
+
71
+ sig { params(error: StandardError).returns(T.self_type) }
72
+ def add_error(error)
73
+ @errors << error
74
+ self
75
+ end
76
+
77
+ sig { returns(T::Boolean) }
78
+ def error?
79
+ @errors.any?
80
+ end
81
+
82
+ sig { returns(String) }
83
+ def formatted_errors
84
+ <<~ERRORS
85
+ #{name}:
86
+ #{@errors.map(&:message).join("\n")}
87
+ ERRORS
88
+ end
89
+
90
+ sig { returns(String) }
91
+ def backtraces
92
+ @errors.filter_map(&:backtrace).join("\n\n")
93
+ end
94
+
95
+ # Each extension should implement `MyExtension#activate` and use to perform any sort of initialization, such as
96
+ # reading information into memory or even spawning a separate process
97
+ sig { abstract.void }
98
+ def activate; end
99
+
100
+ # Extensions should override the `name` method to return the extension name
101
+ sig { abstract.returns(String) }
102
+ def name; end
103
+ end
104
+ end
@@ -11,5 +11,9 @@ require "ruby-lsp"
11
11
  require "ruby_lsp/utils"
12
12
  require "ruby_lsp/server"
13
13
  require "ruby_lsp/executor"
14
+ require "ruby_lsp/event_emitter"
14
15
  require "ruby_lsp/requests"
16
+ require "ruby_lsp/listener"
15
17
  require "ruby_lsp/store"
18
+ require "ruby_lsp/extension"
19
+ require "ruby_lsp/requests/support/rubocop_runner"
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # Listener is an abstract class to be used by requests for listening to events emitted when visiting an AST using the
6
+ # EventEmitter.
7
+ class Listener
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ extend T::Generic
11
+ include Requests::Support::Common
12
+
13
+ ResponseType = type_member
14
+
15
+ abstract!
16
+
17
+ sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
18
+ def initialize(emitter, message_queue)
19
+ @emitter = emitter
20
+ @message_queue = message_queue
21
+ end
22
+
23
+ class << self
24
+ extend T::Sig
25
+
26
+ sig { returns(T::Array[T.class_of(Listener)]) }
27
+ def listeners
28
+ @listeners ||= T.let([], T.nilable(T::Array[T.class_of(Listener)]))
29
+ end
30
+
31
+ sig { params(listener: T.class_of(Listener)).void }
32
+ def add_listener(listener)
33
+ listeners << listener
34
+ end
35
+ end
36
+
37
+ # Override this method with an attr_reader that returns the response of your listener. The listener should
38
+ # accumulate results in a @response variable and then provide the reader so that it is accessible
39
+ sig { abstract.returns(ResponseType) }
40
+ def response; end
41
+ end
42
+ end
@@ -7,6 +7,7 @@ module RubyLsp
7
7
  class BaseRequest < SyntaxTree::Visitor
8
8
  extend T::Sig
9
9
  extend T::Helpers
10
+ include Support::Common
10
11
 
11
12
  abstract!
12
13
 
@@ -17,11 +18,6 @@ module RubyLsp
17
18
  sig { params(document: Document, _kwargs: T.untyped).void }
18
19
  def initialize(document, **_kwargs)
19
20
  @document = document
20
-
21
- # Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
22
- # is received by the server. This happens because {Document#parse} remembers if there are new edits to be parsed
23
- @document.parse
24
-
25
21
  super()
26
22
  end
27
23
 
@@ -31,94 +27,10 @@ module RubyLsp
31
27
  # Syntax Tree implements `visit_all` using `map` instead of `each` for users who want to use the pattern
32
28
  # `result = visitor.visit(tree)`. However, we don't use that pattern and should avoid producing a new array for
33
29
  # every single node visited
34
- sig { params(nodes: T::Array[SyntaxTree::Node]).void }
30
+ sig { params(nodes: T::Array[T.nilable(SyntaxTree::Node)]).void }
35
31
  def visit_all(nodes)
36
32
  nodes.each { |node| visit(node) }
37
33
  end
38
-
39
- sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
40
- def range_from_syntax_tree_node(node)
41
- loc = node.location
42
-
43
- LanguageServer::Protocol::Interface::Range.new(
44
- start: LanguageServer::Protocol::Interface::Position.new(
45
- line: loc.start_line - 1,
46
- character: loc.start_column,
47
- ),
48
- end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
49
- )
50
- end
51
-
52
- sig do
53
- params(node: T.any(SyntaxTree::ConstPathRef, SyntaxTree::ConstRef, SyntaxTree::TopConstRef)).returns(String)
54
- end
55
- def full_constant_name(node)
56
- name = +node.constant.value
57
- constant = T.let(node, SyntaxTree::Node)
58
-
59
- while constant.is_a?(SyntaxTree::ConstPathRef)
60
- constant = constant.parent
61
-
62
- case constant
63
- when SyntaxTree::ConstPathRef
64
- name.prepend("#{constant.constant.value}::")
65
- when SyntaxTree::VarRef
66
- name.prepend("#{constant.value.value}::")
67
- end
68
- end
69
-
70
- name
71
- end
72
-
73
- sig do
74
- params(
75
- node: SyntaxTree::Node,
76
- position: Integer,
77
- node_types: T::Array[T.class_of(SyntaxTree::Node)],
78
- ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
79
- end
80
- def locate(node, position, node_types: [])
81
- queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
82
- closest = node
83
-
84
- until queue.empty?
85
- candidate = queue.shift
86
-
87
- # Skip nil child nodes
88
- next if candidate.nil?
89
-
90
- # Add the next child_nodes to the queue to be processed
91
- queue.concat(candidate.child_nodes)
92
-
93
- # Skip if the current node doesn't cover the desired position
94
- loc = candidate.location
95
- next unless (loc.start_char...loc.end_char).cover?(position)
96
-
97
- # If the node's start character is already past the position, then we should've found the closest node already
98
- break if position < loc.start_char
99
-
100
- # If there are node types to filter by, and the current node is not one of those types, then skip it
101
- next if node_types.any? && node_types.none? { |type| candidate.is_a?(type) }
102
-
103
- # If the current node is narrower than or equal to the previous closest node, then it is more precise
104
- closest_loc = closest.location
105
- if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
106
- parent = T.let(closest, SyntaxTree::Node)
107
- closest = candidate
108
- end
109
- end
110
-
111
- [closest, parent]
112
- end
113
-
114
- sig { params(node: T.nilable(SyntaxTree::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
115
- def visible?(node, range)
116
- return true if range.nil?
117
- return false if node.nil?
118
-
119
- loc = node.location
120
- range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
121
- end
122
34
  end
123
35
  end
124
36
  end