ruby-lsp 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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