ruby-lsp 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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