ruby-lsp 0.14.6 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +1 -16
  4. data/exe/ruby-lsp-check +13 -22
  5. data/exe/ruby-lsp-doctor +9 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +14 -1
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +11 -23
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +4 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
  10. data/lib/ruby_indexer/test/configuration_test.rb +2 -11
  11. data/lib/ruby_lsp/addon.rb +18 -9
  12. data/lib/ruby_lsp/base_server.rb +147 -0
  13. data/lib/ruby_lsp/document.rb +0 -5
  14. data/lib/ruby_lsp/{requests/support/dependency_detector.rb → global_state.rb} +41 -9
  15. data/lib/ruby_lsp/internal.rb +4 -1
  16. data/lib/ruby_lsp/listeners/code_lens.rb +13 -9
  17. data/lib/ruby_lsp/listeners/completion.rb +13 -14
  18. data/lib/ruby_lsp/listeners/definition.rb +4 -3
  19. data/lib/ruby_lsp/listeners/document_symbol.rb +91 -3
  20. data/lib/ruby_lsp/listeners/hover.rb +6 -5
  21. data/lib/ruby_lsp/listeners/signature_help.rb +7 -4
  22. data/lib/ruby_lsp/load_sorbet.rb +62 -0
  23. data/lib/ruby_lsp/requests/code_lens.rb +3 -2
  24. data/lib/ruby_lsp/requests/completion.rb +15 -4
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +56 -0
  26. data/lib/ruby_lsp/requests/definition.rb +11 -4
  27. data/lib/ruby_lsp/requests/diagnostics.rb +6 -12
  28. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  29. data/lib/ruby_lsp/requests/formatting.rb +7 -43
  30. data/lib/ruby_lsp/requests/hover.rb +4 -4
  31. data/lib/ruby_lsp/requests/request.rb +2 -0
  32. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  33. data/lib/ruby_lsp/requests/signature_help.rb +4 -3
  34. data/lib/ruby_lsp/requests/support/common.rb +16 -5
  35. data/lib/ruby_lsp/requests/support/formatter.rb +26 -0
  36. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +50 -0
  37. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  38. data/lib/ruby_lsp/requests/support/{syntax_tree_formatting_runner.rb → syntax_tree_formatter.rb} +13 -3
  39. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  40. data/lib/ruby_lsp/requests.rb +3 -1
  41. data/lib/ruby_lsp/server.rb +770 -142
  42. data/lib/ruby_lsp/store.rb +0 -8
  43. data/lib/ruby_lsp/test_helper.rb +52 -0
  44. data/lib/ruby_lsp/utils.rb +68 -33
  45. metadata +11 -9
  46. data/lib/ruby_lsp/executor.rb +0 -614
  47. data/lib/ruby_lsp/requests/support/formatter_runner.rb +0 -18
  48. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -34
  49. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c364aee0d80e6187f66e8eb6d50d7cdd21ffb129e0a0b17c9dc7bd5d8a18cad
4
- data.tar.gz: 65964ec1d89f10adf9bcbf113efed6d43a977b669abe51182e90dd9870032623
3
+ metadata.gz: faf267ef6ce1eb7c9471d0a63b07c8da3d60ae1aeb656b63eedb64c493746e18
4
+ data.tar.gz: 56cd98100e674d8e3de391cb107efb7c4b713715494f212f879e687d2c26cda4
5
5
  SHA512:
6
- metadata.gz: 971af8a3c02903597143a8ec49915eead6bc0f65a0175e4a443e381d671dd4a88f776a26f62af1bd69ee7dc3f478b36bef122febbfaed7656907eb1a6a7a3354
7
- data.tar.gz: 15148c1c3765390f9bb92dbafe7043ac15b22de6b5c415c424a063f4742026d92831d84bcde2013ab742c4a889dc16d666d4ec5d18863d0db8acf793ce87f0a6
6
+ metadata.gz: 75077a56844015e9915c1232102eca5e5bcc40687207579367b9ce836525a369e4d87e2a3e89223ca4af3bad1b8fdf09648d2839a756d171fefdc2c7d38c9df4
7
+ data.tar.gz: '025321865d6799a4ca874296d2552d8ec71f7ffa3023a104f74633ae9b858ab757dd5babee429aa38b04c1820ce64c4f7416aa626267189ac43d262744698369'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.6
1
+ 0.16.0
data/exe/ruby-lsp CHANGED
@@ -68,22 +68,7 @@ if ENV["BUNDLE_GEMFILE"].nil?
68
68
  exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
69
69
  end
70
70
 
71
- require "sorbet-runtime"
72
-
73
- begin
74
- T::Configuration.default_checked_level = :never
75
- # Suppresses call validation errors
76
- T::Configuration.call_validation_error_handler = ->(*) {}
77
- # Suppresses errors caused by T.cast, T.let, T.must, etc.
78
- T::Configuration.inline_type_error_handler = ->(*) {}
79
- # Suppresses errors caused by incorrect parameter ordering
80
- T::Configuration.sig_validation_error_handler = ->(*) {}
81
- rescue
82
- # Need this rescue so that if another gem has
83
- # already set the checked level by the time we
84
- # get to it, we don't fail outright.
85
- nil
86
- end
71
+ require "ruby_lsp/load_sorbet"
87
72
 
88
73
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
89
74
  require "ruby_lsp/internal"
data/exe/ruby-lsp-check CHANGED
@@ -3,16 +3,7 @@
3
3
 
4
4
  # This executable checks if all automatic LSP requests run successfully on every Ruby file under the current directory
5
5
 
6
- require "sorbet-runtime"
7
-
8
- begin
9
- T::Configuration.default_checked_level = :never
10
- T::Configuration.call_validation_error_handler = ->(*) {}
11
- T::Configuration.inline_type_error_handler = ->(*) {}
12
- T::Configuration.sig_validation_error_handler = ->(*) {}
13
- rescue
14
- nil
15
- end
6
+ require "ruby_lsp/load_sorbet"
16
7
 
17
8
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
18
9
  require "ruby_lsp/internal"
@@ -24,30 +15,30 @@ files = Dir.glob("#{Dir.pwd}/**/*.rb")
24
15
  puts "Verifying that all automatic LSP requests execute successfully. This may take a while..."
25
16
 
26
17
  errors = {}
27
- store = RubyLsp::Store.new
28
- message_queue = Thread::Queue.new
29
- RubyLsp::Addon.load_addons(message_queue)
30
- executor = RubyLsp::Executor.new(store, message_queue)
18
+ server = RubyLsp::Server.new(test_mode: true)
31
19
 
32
20
  files.each_with_index do |file, index|
33
21
  uri = URI("file://#{file}")
34
- store.set(uri: uri, source: File.read(file), version: 1)
22
+ server.process_message({
23
+ method: "textDocument/didOpen",
24
+ params: { textDocument: { uri: uri, text: File.read(file), version: 1 } },
25
+ })
35
26
 
36
27
  # Executing any of the automatic requests will execute all of them, so here we just pick one
37
- result = executor.execute({
28
+ server.process_message({
29
+ id: 1,
38
30
  method: "textDocument/documentSymbol",
39
- params: { textDocument: { uri: uri.to_s } },
31
+ params: { textDocument: { uri: uri } },
40
32
  })
41
33
 
42
- error = result.error
43
- errors[file] = error if error
34
+ result = server.pop_response
35
+ errors[file] = result.message if result.is_a?(RubyLsp::Error)
44
36
  ensure
45
- store.delete(uri)
37
+ server.process_message({ method: "textDocument/didClose", params: { textDocument: { uri: uri } } })
38
+ server.pop_response
46
39
  print("\033[M\033[0KCompleted #{index + 1}/#{files.length}") unless ENV["CI"]
47
40
  end
48
-
49
41
  puts "\n"
50
- message_queue.close
51
42
 
52
43
  # Indexing
53
44
  puts "Verifying that indexing executes successfully. This may take a while..."
data/exe/ruby-lsp-doctor CHANGED
@@ -4,6 +4,15 @@
4
4
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
5
  require "ruby_lsp/internal"
6
6
 
7
+ if File.exist?(".index.yml")
8
+ begin
9
+ config = YAML.parse_file(".index.yml").to_ruby
10
+ rescue => e
11
+ abort("Error parsing config: #{e.message}")
12
+ end
13
+ RubyIndexer.configuration.apply_config(config)
14
+ end
15
+
7
16
  index = RubyIndexer::Index.new
8
17
 
9
18
  puts "Globbing for indexable files"
@@ -154,6 +154,8 @@ module RubyIndexer
154
154
  handle_attribute(node, reader: true, writer: true)
155
155
  when :include
156
156
  handle_include(node)
157
+ when :prepend
158
+ handle_prepend(node)
157
159
  end
158
160
  end
159
161
 
@@ -355,6 +357,16 @@ module RubyIndexer
355
357
 
356
358
  sig { params(node: Prism::CallNode).void }
357
359
  def handle_include(node)
360
+ handle_module_operation(node, :included_modules)
361
+ end
362
+
363
+ sig { params(node: Prism::CallNode).void }
364
+ def handle_prepend(node)
365
+ handle_module_operation(node, :prepended_modules)
366
+ end
367
+
368
+ sig { params(node: Prism::CallNode, operation: Symbol).void }
369
+ def handle_module_operation(node, operation)
358
370
  return unless @current_owner
359
371
 
360
372
  arguments = node.arguments&.arguments
@@ -369,7 +381,8 @@ module RubyIndexer
369
381
  # If a constant path reference is dynamic or missing parts, we can't
370
382
  # index it
371
383
  end
372
- @current_owner.included_modules.concat(names)
384
+ collection = operation == :included_modules ? @current_owner.included_modules : @current_owner.prepended_modules
385
+ collection.concat(names)
373
386
  end
374
387
  end
375
388
  end
@@ -43,20 +43,6 @@ module RubyIndexer
43
43
  )
44
44
  end
45
45
 
46
- sig { void }
47
- def load_config
48
- return unless File.exist?(".index.yml")
49
-
50
- config = YAML.parse_file(".index.yml")
51
- return unless config
52
-
53
- config_hash = config.to_ruby
54
- validate_config!(config_hash)
55
- apply_config(config_hash)
56
- rescue Psych::SyntaxError => e
57
- raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
58
- end
59
-
60
46
  sig { returns(T::Array[IndexablePath]) }
61
47
  def indexables
62
48
  excluded_gems = @excluded_gems - @included_gems
@@ -158,6 +144,17 @@ module RubyIndexer
158
144
  @magic_comment_regex ||= T.let(/^#\s*#{@excluded_magic_comments.join("|")}/, T.nilable(Regexp))
159
145
  end
160
146
 
147
+ sig { params(config: T::Hash[String, T.untyped]).void }
148
+ def apply_config(config)
149
+ validate_config!(config)
150
+
151
+ @excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
152
+ @included_gems.concat(config["included_gems"]) if config["included_gems"]
153
+ @excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
154
+ @included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
155
+ @excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
156
+ end
157
+
161
158
  private
162
159
 
163
160
  sig { params(config: T::Hash[String, T.untyped]).void }
@@ -175,15 +172,6 @@ module RubyIndexer
175
172
  raise ArgumentError, errors.join("\n") if errors.any?
176
173
  end
177
174
 
178
- sig { params(config: T::Hash[String, T.untyped]).void }
179
- def apply_config(config)
180
- @excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
181
- @included_gems.concat(config["included_gems"]) if config["included_gems"]
182
- @excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
183
- @included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
184
- @excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
185
- end
186
-
187
175
  sig { returns(T::Array[String]) }
188
176
  def initial_excluded_gems
189
177
  excluded, others = Bundler.definition.dependencies.partition do |dependency|
@@ -43,6 +43,9 @@ module RubyIndexer
43
43
  sig { returns(T::Array[String]) }
44
44
  attr_accessor :included_modules
45
45
 
46
+ sig { returns(T::Array[String]) }
47
+ attr_accessor :prepended_modules
48
+
46
49
  sig do
47
50
  params(
48
51
  name: String,
@@ -54,6 +57,7 @@ module RubyIndexer
54
57
  def initialize(name, file_path, location, comments)
55
58
  super(name, file_path, location, comments)
56
59
  @included_modules = T.let([], T::Array[String])
60
+ @prepended_modules = T.let([], T::Array[String])
57
61
  end
58
62
 
59
63
  sig { returns(String) }
@@ -327,5 +327,51 @@ module RubyIndexer
327
327
  constant_path_references = T.must(@index["ConstantPathReferences"][0])
328
328
  assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
329
329
  end
330
+
331
+ def test_keeping_track_of_prepended_modules
332
+ index(<<~RUBY)
333
+ class Foo
334
+ # valid syntaxes that we can index
335
+ prepend A1
336
+ self.prepend A2
337
+ prepend A3, A4
338
+ self.prepend A5, A6
339
+
340
+ # valid syntaxes that we cannot index because of their dynamic nature
341
+ prepend some_variable_or_method_call
342
+ self.prepend some_variable_or_method_call
343
+
344
+ def something
345
+ prepend A7 # We should not index this because of this dynamic nature
346
+ end
347
+
348
+ # Valid inner class syntax definition with its own modules prepended
349
+ class Qux
350
+ prepend Corge
351
+ self.prepend Corge
352
+ prepend Baz
353
+
354
+ prepend some_variable_or_method_call
355
+ end
356
+ end
357
+
358
+ class ConstantPathReferences
359
+ prepend Foo::Bar
360
+ self.prepend Foo::Bar2
361
+
362
+ prepend dynamic::Bar
363
+ prepend Foo::
364
+ end
365
+ RUBY
366
+
367
+ foo = T.must(@index["Foo"][0])
368
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules)
369
+
370
+ qux = T.must(@index["Foo::Qux"][0])
371
+ assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules)
372
+
373
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
374
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules)
375
+ end
330
376
  end
331
377
  end
@@ -10,7 +10,7 @@ module RubyIndexer
10
10
  end
11
11
 
12
12
  def test_load_configuration_executes_configure_block
13
- @config.load_config
13
+ @config.apply_config({ "excluded_patterns" => ["**/test/fixtures/**/*.rb"] })
14
14
  indexables = @config.indexables
15
15
 
16
16
  assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
@@ -21,7 +21,6 @@ module RubyIndexer
21
21
  end
22
22
 
23
23
  def test_indexables_only_includes_gem_require_paths
24
- @config.load_config
25
24
  indexables = @config.indexables
26
25
 
27
26
  Bundler.locked_gems.specs.each do |lazy_spec|
@@ -35,7 +34,6 @@ module RubyIndexer
35
34
  end
36
35
 
37
36
  def test_indexables_does_not_include_default_gem_path_when_in_bundle
38
- @config.load_config
39
37
  indexables = @config.indexables
40
38
 
41
39
  assert(
@@ -44,7 +42,6 @@ module RubyIndexer
44
42
  end
45
43
 
46
44
  def test_indexables_includes_default_gems
47
- @config.load_config
48
45
  indexables = @config.indexables.map(&:full_path)
49
46
 
50
47
  assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")
@@ -53,7 +50,6 @@ module RubyIndexer
53
50
  end
54
51
 
55
52
  def test_indexables_includes_project_files
56
- @config.load_config
57
53
  indexables = @config.indexables.map(&:full_path)
58
54
 
59
55
  Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path|
@@ -66,7 +62,6 @@ module RubyIndexer
66
62
  def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
67
63
  Bundler.settings.set_global("path", "vendor/bundle")
68
64
  config = Configuration.new
69
- config.load_config
70
65
 
71
66
  assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
72
67
  ensure
@@ -74,7 +69,6 @@ module RubyIndexer
74
69
  end
75
70
 
76
71
  def test_indexables_does_not_include_gems_own_installed_files
77
- @config.load_config
78
72
  indexables = @config.indexables
79
73
 
80
74
  assert(
@@ -95,17 +89,14 @@ module RubyIndexer
95
89
  end
96
90
 
97
91
  def test_paths_are_unique
98
- @config.load_config
99
92
  indexables = @config.indexables
100
93
 
101
94
  assert_equal(indexables.uniq.length, indexables.length)
102
95
  end
103
96
 
104
97
  def test_configuration_raises_for_unknown_keys
105
- Psych::Nodes::Document.any_instance.expects(:to_ruby).returns({ "unknown_config" => 123 })
106
-
107
98
  assert_raises(ArgumentError) do
108
- @config.load_config
99
+ @config.apply_config({ "unknown_config" => 123 })
109
100
  end
110
101
  end
111
102
 
@@ -50,8 +50,8 @@ module RubyLsp
50
50
  end
51
51
 
52
52
  # Discovers and loads all addons. Returns the list of activated addons
53
- sig { params(message_queue: Thread::Queue).returns(T::Array[Addon]) }
54
- def load_addons(message_queue)
53
+ sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).returns(T::Array[Addon]) }
54
+ def load_addons(global_state, outgoing_queue)
55
55
  # Require all addons entry points, which should be placed under
56
56
  # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb`
57
57
  Gem.find_files("ruby_lsp/**/addon.rb").each do |addon|
@@ -67,7 +67,7 @@ module RubyLsp
67
67
  # Activate each one of the discovered addons. If any problems occur in the addons, we don't want to
68
68
  # fail to boot the server
69
69
  addons.each do |addon|
70
- addon.activate(message_queue)
70
+ addon.activate(global_state, outgoing_queue)
71
71
  rescue => e
72
72
  addon.add_error(e)
73
73
  end
@@ -105,8 +105,8 @@ module RubyLsp
105
105
 
106
106
  # Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
107
107
  # reading information into memory or even spawning a separate process
108
- sig { abstract.params(message_queue: Thread::Queue).void }
109
- def activate(message_queue); end
108
+ sig { abstract.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
109
+ def activate(global_state, outgoing_queue); end
110
110
 
111
111
  # Each addon should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
112
112
  # child process
@@ -132,11 +132,10 @@ module RubyLsp
132
132
  overridable.params(
133
133
  response_builder: ResponseBuilders::Hover,
134
134
  nesting: T::Array[String],
135
- index: RubyIndexer::Index,
136
135
  dispatcher: Prism::Dispatcher,
137
136
  ).void
138
137
  end
139
- def create_hover_listener(response_builder, nesting, index, dispatcher); end
138
+ def create_hover_listener(response_builder, nesting, dispatcher); end
140
139
 
141
140
  # Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
142
141
  sig do
@@ -161,10 +160,20 @@ module RubyLsp
161
160
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
162
161
  uri: URI::Generic,
163
162
  nesting: T::Array[String],
164
- index: RubyIndexer::Index,
165
163
  dispatcher: Prism::Dispatcher,
166
164
  ).void
167
165
  end
168
- def create_definition_listener(response_builder, uri, nesting, index, dispatcher); end
166
+ def create_definition_listener(response_builder, uri, nesting, dispatcher); end
167
+
168
+ # Creates a new Completion listener. This method is invoked on every Completion request
169
+ sig do
170
+ overridable.params(
171
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
172
+ nesting: T::Array[String],
173
+ dispatcher: Prism::Dispatcher,
174
+ uri: URI::Generic,
175
+ ).void
176
+ end
177
+ def create_completion_listener(response_builder, nesting, dispatcher, uri); end
169
178
  end
170
179
  end
@@ -0,0 +1,147 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class BaseServer
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
11
+ sig { params(test_mode: T::Boolean).void }
12
+ def initialize(test_mode: false)
13
+ @test_mode = T.let(test_mode, T::Boolean)
14
+ @writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
15
+ @reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
16
+ @incoming_queue = T.let(Thread::Queue.new, Thread::Queue)
17
+ @outgoing_queue = T.let(Thread::Queue.new, Thread::Queue)
18
+ @cancelled_requests = T.let([], T::Array[Integer])
19
+ @mutex = T.let(Mutex.new, Mutex)
20
+ @worker = T.let(new_worker, Thread)
21
+ @current_request_id = T.let(1, Integer)
22
+ @store = T.let(Store.new, Store)
23
+ @outgoing_dispatcher = T.let(
24
+ Thread.new do
25
+ unless test_mode
26
+ while (message = @outgoing_queue.pop)
27
+ @mutex.synchronize { @writer.write(message.to_hash) }
28
+ end
29
+ end
30
+ end,
31
+ Thread,
32
+ )
33
+
34
+ Thread.main.priority = 1
35
+ end
36
+
37
+ sig { void }
38
+ def start
39
+ @reader.read do |message|
40
+ method = message[:method]
41
+
42
+ # We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
43
+ # source. Altering the source reference during parsing will put the parser in an invalid internal state, since
44
+ # it started parsing with one source but then it changed in the middle. We don't want to do this for text
45
+ # synchronization notifications
46
+ @mutex.synchronize do
47
+ uri = message.dig(:params, :textDocument, :uri)
48
+
49
+ if uri
50
+ begin
51
+ parsed_uri = URI(uri)
52
+ message[:params][:textDocument][:uri] = parsed_uri
53
+
54
+ # We don't want to try to parse documents on text synchronization notifications
55
+ @store.get(parsed_uri).parse unless method.start_with?("textDocument/did")
56
+ rescue Errno::ENOENT
57
+ # If we receive a request for a file that no longer exists, we don't want to fail
58
+ end
59
+ end
60
+ end
61
+
62
+ # We need to process shutdown and exit from the main thread in order to close queues and wait for other threads
63
+ # to finish. Everything else is pushed into the incoming queue
64
+ case method
65
+ when "shutdown"
66
+ $stderr.puts("Shutting down Ruby LSP...")
67
+
68
+ shutdown
69
+
70
+ @mutex.synchronize do
71
+ run_shutdown
72
+ @writer.write(Result.new(id: message[:id], response: nil).to_hash)
73
+ end
74
+ when "exit"
75
+ @mutex.synchronize do
76
+ status = @incoming_queue.closed? ? 0 : 1
77
+ $stderr.puts("Shutdown complete with status #{status}")
78
+ exit(status)
79
+ end
80
+ else
81
+ @incoming_queue << message
82
+ end
83
+ end
84
+ end
85
+
86
+ sig { void }
87
+ def run_shutdown
88
+ @incoming_queue.clear
89
+ @outgoing_queue.clear
90
+ @incoming_queue.close
91
+ @outgoing_queue.close
92
+ @cancelled_requests.clear
93
+
94
+ @worker.join
95
+ @outgoing_dispatcher.join
96
+ @store.clear
97
+ end
98
+
99
+ # This method is only intended to be used in tests! Pops the latest response that would be sent to the client
100
+ sig { returns(T.untyped) }
101
+ def pop_response
102
+ @outgoing_queue.pop
103
+ end
104
+
105
+ sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
106
+ def process_message(message); end
107
+
108
+ sig { abstract.void }
109
+ def shutdown; end
110
+
111
+ sig { returns(Thread) }
112
+ def new_worker
113
+ Thread.new do
114
+ while (message = T.let(@incoming_queue.pop, T.nilable(T::Hash[Symbol, T.untyped])))
115
+ id = message[:id]
116
+
117
+ # Check if the request was cancelled before trying to process it
118
+ @mutex.synchronize do
119
+ if id && @cancelled_requests.include?(id)
120
+ send_message(Result.new(id: id, response: nil))
121
+ @cancelled_requests.delete(id)
122
+ next
123
+ end
124
+ end
125
+
126
+ process_message(message)
127
+ end
128
+ end
129
+ end
130
+
131
+ sig { params(message: T.any(Result, Error, Notification, Request)).void }
132
+ def send_message(message)
133
+ # When we're shutting down the server, there's a small race condition between closing the thread queues and
134
+ # finishing remaining requests. We may close the queue in the middle of processing a request, which will then fail
135
+ # when trying to send a response back
136
+ return if @outgoing_queue.closed?
137
+
138
+ @outgoing_queue << message
139
+ @current_request_id += 1 if message.is_a?(Request)
140
+ end
141
+
142
+ sig { params(id: Integer).void }
143
+ def send_empty_response(id)
144
+ send_message(Result.new(id: id, response: nil))
145
+ end
146
+ end
147
+ end
@@ -180,11 +180,6 @@ module RubyLsp
180
180
  end
181
181
  end
182
182
 
183
- sig { returns(T::Boolean) }
184
- def typechecker_enabled?
185
- DependencyDetector.instance.typechecker && sorbet_sigil_is_true_or_higher
186
- end
187
-
188
183
  class Scanner
189
184
  extend T::Sig
190
185
 
@@ -1,27 +1,52 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "singleton"
5
-
6
4
  module RubyLsp
7
- class DependencyDetector
8
- include Singleton
5
+ class GlobalState
9
6
  extend T::Sig
10
7
 
11
8
  sig { returns(String) }
12
- attr_reader :detected_formatter
9
+ attr_reader :test_library
13
10
 
14
11
  sig { returns(String) }
15
- attr_reader :detected_test_library
12
+ attr_accessor :formatter
16
13
 
17
14
  sig { returns(T::Boolean) }
18
15
  attr_reader :typechecker
19
16
 
17
+ sig { returns(RubyIndexer::Index) }
18
+ attr_reader :index
19
+
20
20
  sig { void }
21
21
  def initialize
22
- @detected_formatter = T.let(detect_formatter, String)
23
- @detected_test_library = T.let(detect_test_library, String)
22
+ @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
23
+
24
+ @formatter = T.let(detect_formatter, String)
25
+ @test_library = T.let(detect_test_library, String)
24
26
  @typechecker = T.let(detect_typechecker, T::Boolean)
27
+ @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
28
+ @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
29
+ end
30
+
31
+ sig { params(identifier: String, instance: Requests::Support::Formatter).void }
32
+ def register_formatter(identifier, instance)
33
+ @supported_formatters[identifier] = instance
34
+ end
35
+
36
+ sig { returns(T.nilable(Requests::Support::Formatter)) }
37
+ def active_formatter
38
+ @supported_formatters[@formatter]
39
+ end
40
+
41
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
42
+ def apply_options(options)
43
+ workspace_uri = options.dig(:workspaceFolders, 0, :uri)
44
+ @workspace_uri = URI(workspace_uri) if workspace_uri
45
+ end
46
+
47
+ sig { returns(String) }
48
+ def workspace_path
49
+ T.must(@workspace_uri.to_standardized_path)
25
50
  end
26
51
 
27
52
  sig { returns(String) }
@@ -63,8 +88,15 @@ module RubyLsp
63
88
  def detect_typechecker
64
89
  return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
65
90
 
91
+ # We can't read the env from within `Bundle.with_original_env` so we need to set it here.
92
+ ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
66
93
  Bundler.with_original_env do
67
- Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
94
+ sorbet_static_detected = Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
95
+ # Don't show message while running tests, since it's noisy
96
+ if sorbet_static_detected && !ruby_lsp_env_is_test
97
+ $stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
98
+ end
99
+ sorbet_static_detected
68
100
  end
69
101
  rescue Bundler::GemfileNotFound
70
102
  false
@@ -20,12 +20,13 @@ require "prism/visitor"
20
20
  require "language_server-protocol"
21
21
 
22
22
  require "ruby-lsp"
23
+ require "ruby_lsp/base_server"
23
24
  require "ruby_indexer/ruby_indexer"
24
25
  require "core_ext/uri"
25
26
  require "ruby_lsp/utils"
26
27
  require "ruby_lsp/parameter_scope"
28
+ require "ruby_lsp/global_state"
27
29
  require "ruby_lsp/server"
28
- require "ruby_lsp/executor"
29
30
  require "ruby_lsp/requests"
30
31
  require "ruby_lsp/response_builders"
31
32
  require "ruby_lsp/document"
@@ -33,3 +34,5 @@ require "ruby_lsp/ruby_document"
33
34
  require "ruby_lsp/store"
34
35
  require "ruby_lsp/addon"
35
36
  require "ruby_lsp/requests/support/rubocop_runner"
37
+ require "ruby_lsp/requests/support/rubocop_formatter"
38
+ require "ruby_lsp/requests/support/syntax_tree_formatter"