ruby-lsp 0.4.4 → 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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -86
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -1
  5. data/lib/ruby_lsp/check_docs.rb +112 -0
  6. data/lib/ruby_lsp/document.rb +13 -2
  7. data/lib/ruby_lsp/event_emitter.rb +86 -18
  8. data/lib/ruby_lsp/executor.rb +146 -45
  9. data/lib/ruby_lsp/extension.rb +104 -0
  10. data/lib/ruby_lsp/internal.rb +2 -0
  11. data/lib/ruby_lsp/listener.rb +14 -11
  12. data/lib/ruby_lsp/requests/base_request.rb +0 -5
  13. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  14. data/lib/ruby_lsp/requests/code_actions.rb +1 -1
  15. data/lib/ruby_lsp/requests/code_lens.rb +82 -66
  16. data/lib/ruby_lsp/requests/diagnostics.rb +2 -2
  17. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  18. data/lib/ruby_lsp/requests/document_link.rb +17 -15
  19. data/lib/ruby_lsp/requests/document_symbol.rb +51 -31
  20. data/lib/ruby_lsp/requests/folding_ranges.rb +1 -1
  21. data/lib/ruby_lsp/requests/formatting.rb +10 -11
  22. data/lib/ruby_lsp/requests/hover.rb +34 -19
  23. data/lib/ruby_lsp/requests/inlay_hints.rb +1 -1
  24. data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
  25. data/lib/ruby_lsp/requests/path_completion.rb +21 -57
  26. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  27. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  28. data/lib/ruby_lsp/requests/support/common.rb +36 -0
  29. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
  31. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +5 -2
  32. data/lib/ruby_lsp/requests.rb +15 -15
  33. data/lib/ruby_lsp/server.rb +44 -11
  34. data/lib/ruby_lsp/store.rb +1 -1
  35. data/lib/ruby_lsp/utils.rb +9 -8
  36. metadata +5 -3
@@ -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,6 +38,24 @@ 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"
@@ -47,7 +65,7 @@ module RubyLsp
47
65
  request.dig(:params, :textDocument, :version),
48
66
  )
49
67
  when "textDocument/didClose"
50
- @notifications << Notification.new(
68
+ @message_queue << Notification.new(
51
69
  message: "textDocument/publishDiagnostics",
52
70
  params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
53
71
  )
@@ -61,12 +79,33 @@ module RubyLsp
61
79
  )
62
80
  when "textDocument/foldingRange"
63
81
  folding_range(uri)
64
- when "textDocument/documentLink"
65
- document_link(uri)
66
82
  when "textDocument/selectionRange"
67
83
  selection_range(uri, request.dig(:params, :positions))
68
- when "textDocument/documentSymbol"
69
- 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])
70
109
  when "textDocument/semanticTokens/full"
71
110
  semantic_tokens_full(uri)
72
111
  when "textDocument/semanticTokens/range"
@@ -75,7 +114,7 @@ module RubyLsp
75
114
  begin
76
115
  formatting(uri)
77
116
  rescue Requests::Formatting::InvalidFormatter => error
78
- @notifications << Notification.new(
117
+ @message_queue << Notification.new(
79
118
  message: "window/showMessage",
80
119
  params: Interface::ShowMessageParams.new(
81
120
  type: Constant::MessageType::ERROR,
@@ -85,7 +124,7 @@ module RubyLsp
85
124
 
86
125
  nil
87
126
  rescue StandardError => error
88
- @notifications << Notification.new(
127
+ @message_queue << Notification.new(
89
128
  message: "window/showMessage",
90
129
  params: Interface::ShowMessageParams.new(
91
130
  type: Constant::MessageType::ERROR,
@@ -111,7 +150,7 @@ module RubyLsp
111
150
  begin
112
151
  diagnostic(uri)
113
152
  rescue StandardError => error
114
- @notifications << Notification.new(
153
+ @message_queue << Notification.new(
115
154
  message: "window/showMessage",
116
155
  params: Interface::ShowMessageParams.new(
117
156
  type: Constant::MessageType::ERROR,
@@ -123,25 +162,16 @@ module RubyLsp
123
162
  end
124
163
  when "textDocument/completion"
125
164
  completion(uri, request.dig(:params, :position))
126
- when "textDocument/codeLens"
127
- code_lens(uri)
128
165
  end
129
166
  end
130
167
 
131
168
  sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
132
169
  def folding_range(uri)
133
- @store.cache_fetch(uri, :folding_ranges) do |document|
170
+ @store.cache_fetch(uri, "textDocument/foldingRange") do |document|
134
171
  Requests::FoldingRanges.new(document).run
135
172
  end
136
173
  end
137
174
 
138
- sig { params(uri: String).returns(T::Array[Interface::CodeLens]) }
139
- def code_lens(uri)
140
- @store.cache_fetch(uri, :code_lens) do |document|
141
- Requests::CodeLens.new(document).run
142
- end
143
- end
144
-
145
175
  sig do
146
176
  params(
147
177
  uri: String,
@@ -150,7 +180,6 @@ module RubyLsp
150
180
  end
151
181
  def hover(uri, position)
152
182
  document = @store.get(uri)
153
- document.parse
154
183
  return if document.syntax_error?
155
184
 
156
185
  target, parent = document.locate_node(position)
@@ -160,23 +189,17 @@ module RubyLsp
160
189
  target = parent
161
190
  end
162
191
 
163
- listener = RubyLsp::Requests::Hover.new
164
- EventEmitter.new(listener).emit_for_target(target)
165
- listener.response
166
- end
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) }
167
196
 
168
- sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
169
- def document_link(uri)
170
- @store.cache_fetch(uri, :document_link) do |document|
171
- RubyLsp::Requests::DocumentLink.new(document).run
172
- end
173
- end
197
+ # Emit events for all listeners
198
+ emitter.emit_for_target(target)
174
199
 
175
- sig { params(uri: String).returns(T::Array[Interface::DocumentSymbol]) }
176
- def document_symbol(uri)
177
- @store.cache_fetch(uri, :document_symbol) do |document|
178
- Requests::DocumentSymbol.new(document).run
179
- end
200
+ # Merge all responses into a single hover
201
+ listeners.each { |ext| base_listener.merge_response!(ext) }
202
+ base_listener.response
180
203
  end
181
204
 
182
205
  sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
@@ -204,7 +227,7 @@ module RubyLsp
204
227
  ).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
205
228
  end
206
229
  def selection_range(uri, positions)
207
- ranges = @store.cache_fetch(uri, :selection_ranges) do |document|
230
+ ranges = @store.cache_fetch(uri, "textDocument/selectionRange") do |document|
208
231
  Requests::SelectionRanges.new(document).run
209
232
  end
210
233
 
@@ -224,7 +247,7 @@ module RubyLsp
224
247
 
225
248
  sig { params(uri: String).returns(Interface::SemanticTokens) }
226
249
  def semantic_tokens_full(uri)
227
- @store.cache_fetch(uri, :semantic_highlighting) do |document|
250
+ @store.cache_fetch(uri, "textDocument/semanticTokens/full") do |document|
228
251
  T.cast(
229
252
  Requests::SemanticHighlighting.new(
230
253
  document,
@@ -237,6 +260,9 @@ module RubyLsp
237
260
 
238
261
  sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
239
262
  def formatting(uri)
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
+
240
266
  Requests::Formatting.new(@store.get(uri), formatter: @store.formatter).run
241
267
  end
242
268
 
@@ -291,7 +317,7 @@ module RubyLsp
291
317
 
292
318
  case result
293
319
  when Requests::CodeActionResolve::Error::EmptySelection
294
- @notifications << Notification.new(
320
+ @message_queue << Notification.new(
295
321
  message: "window/showMessage",
296
322
  params: Interface::ShowMessageParams.new(
297
323
  type: Constant::MessageType::ERROR,
@@ -300,7 +326,7 @@ module RubyLsp
300
326
  )
301
327
  raise Requests::CodeActionResolve::CodeActionError
302
328
  when Requests::CodeActionResolve::Error::InvalidTargetRange
303
- @notifications << Notification.new(
329
+ @message_queue << Notification.new(
304
330
  message: "window/showMessage",
305
331
  params: Interface::ShowMessageParams.new(
306
332
  type: Constant::MessageType::ERROR,
@@ -315,7 +341,7 @@ module RubyLsp
315
341
 
316
342
  sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
317
343
  def diagnostic(uri)
318
- response = @store.cache_fetch(uri, :diagnostics) do |document|
344
+ response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
319
345
  Requests::Diagnostics.new(document).run
320
346
  end
321
347
 
@@ -342,7 +368,44 @@ module RubyLsp
342
368
  params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
343
369
  end
344
370
  def completion(uri, position)
345
- 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
346
409
  end
347
410
 
348
411
  sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
@@ -359,7 +422,11 @@ module RubyLsp
359
422
  end
360
423
 
361
424
  formatter = options.dig(:initializationOptions, :formatter)
362
- @store.formatter = formatter unless formatter.nil?
425
+ @store.formatter = if formatter == "auto"
426
+ detected_formatter
427
+ else
428
+ formatter
429
+ end
363
430
 
364
431
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
365
432
  experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
@@ -468,5 +535,39 @@ module RubyLsp
468
535
  ),
469
536
  )
470
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
471
572
  end
472
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
@@ -15,3 +15,5 @@ require "ruby_lsp/event_emitter"
15
15
  require "ruby_lsp/requests"
16
16
  require "ruby_lsp/listener"
17
17
  require "ruby_lsp/store"
18
+ require "ruby_lsp/extension"
19
+ require "ruby_lsp/requests/support/rubocop_runner"
@@ -14,20 +14,23 @@ module RubyLsp
14
14
 
15
15
  abstract!
16
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
+
17
23
  class << self
18
24
  extend T::Sig
19
25
 
20
- sig { returns(T.nilable(T::Array[Symbol])) }
21
- attr_reader :events
22
-
23
- # All listener events must be defined inside of a `listener_events` block. This is to ensure we know which events
24
- # have been registered. Defining an event outside of this block will simply not register it and it'll never be
25
- # invoked
26
- sig { params(block: T.proc.void).void }
27
- def listener_events(&block)
28
- current_methods = instance_methods
29
- block.call
30
- @events = T.let(instance_methods - current_methods, T.nilable(T::Array[Symbol]))
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
31
34
  end
32
35
  end
33
36
 
@@ -18,11 +18,6 @@ module RubyLsp
18
18
  sig { params(document: Document, _kwargs: T.untyped).void }
19
19
  def initialize(document, **_kwargs)
20
20
  @document = document
21
-
22
- # Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
23
- # is received by the server. This happens because {Document#parse} remembers if there are new edits to be parsed
24
- @document.parse
25
-
26
21
  super()
27
22
  end
28
23
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Code action resolve demo](../../misc/code_action_resolve.gif)
6
+ # ![Code action resolve demo](../../code_action_resolve.gif)
7
7
  #
8
8
  # The [code action resolve](https://microsoft.github.io/language-server-protocol/specification#codeAction_resolve)
9
9
  # request is used to to resolve the edit field for a given code action, if it is not already provided in the
@@ -3,7 +3,7 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Code actions demo](../../misc/code_actions.gif)
6
+ # ![Code actions demo](../../code_actions.gif)
7
7
  #
8
8
  # The [code actions](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
9
9
  # request informs the editor of RuboCop quick fixes that can be applied. These are accessible by hovering over a