ruby-lsp 0.14.5 → 0.15.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 (41) 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/lib/ruby_indexer/lib/ruby_indexer/collector.rb +21 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +12 -24
  7. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +16 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +3 -2
  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_indexer/test/index_test.rb +5 -0
  12. data/lib/ruby_lsp/addon.rb +18 -5
  13. data/lib/ruby_lsp/base_server.rb +147 -0
  14. data/lib/ruby_lsp/document.rb +0 -5
  15. data/lib/ruby_lsp/{requests/support/dependency_detector.rb → global_state.rb} +30 -9
  16. data/lib/ruby_lsp/internal.rb +2 -1
  17. data/lib/ruby_lsp/listeners/code_lens.rb +66 -18
  18. data/lib/ruby_lsp/listeners/completion.rb +13 -14
  19. data/lib/ruby_lsp/listeners/definition.rb +4 -3
  20. data/lib/ruby_lsp/listeners/document_symbol.rb +91 -3
  21. data/lib/ruby_lsp/listeners/hover.rb +6 -5
  22. data/lib/ruby_lsp/listeners/signature_help.rb +7 -4
  23. data/lib/ruby_lsp/load_sorbet.rb +62 -0
  24. data/lib/ruby_lsp/requests/code_lens.rb +4 -3
  25. data/lib/ruby_lsp/requests/completion.rb +15 -4
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +56 -0
  27. data/lib/ruby_lsp/requests/definition.rb +18 -5
  28. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  29. data/lib/ruby_lsp/requests/hover.rb +9 -5
  30. data/lib/ruby_lsp/requests/request.rb +51 -0
  31. data/lib/ruby_lsp/requests/signature_help.rb +4 -3
  32. data/lib/ruby_lsp/requests/support/common.rb +25 -5
  33. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  34. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  35. data/lib/ruby_lsp/requests.rb +2 -0
  36. data/lib/ruby_lsp/server.rb +756 -142
  37. data/lib/ruby_lsp/store.rb +0 -8
  38. data/lib/ruby_lsp/test_helper.rb +45 -0
  39. data/lib/ruby_lsp/utils.rb +68 -33
  40. metadata +8 -5
  41. data/lib/ruby_lsp/executor.rb +0 -612
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '046081e0caf521f948f3952bd7a356ef6f0beb80c5a9ff72930a6d0c9fd8d9f4'
4
- data.tar.gz: e65b5224342c412a0b28198af0e56b17a421eaacc7748a17d2ed30890c0a2d86
3
+ metadata.gz: 9749ddfd5f25d0f6e3c0636a070d8977c19af973943e33deba4e49cba9b473d6
4
+ data.tar.gz: 1c3f663fd6487e8517154a2595be253659e2f2f9bd6a4df23c8b16a91e159c4d
5
5
  SHA512:
6
- metadata.gz: 682219fbef2c6e1483c0e6b0c36bf2ce8c3bd15aed88e53ee1a05623f5569c7e52227300b1a6fb4968a6ba2b0844a7e985a0b134af9e9498036199673d660117
7
- data.tar.gz: a13eb33a1a41514819f4d0612a5d50eb170a0adb36158b799cebffae34781a5d41c1b07922f0bec8310cf6439ea8356305224bcf1de7423b96ff6e28c9e744c8
6
+ metadata.gz: '039fc58aa31af82930cc2852819208fdd9045ed493cf368702f87e9ffd814487bf63fe522124fa3210336cfc72d806b828e2e338114a66913dbd688ed85980c3'
7
+ data.tar.gz: bb4d62e3ce21fe07cda085f0284b69c356a10694125454b8160bd0c5e87789481b2d4f8036cdb4cdc2b0667c8a78c7e7a1409307722a532122172fcff8c5450c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.5
1
+ 0.15.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..."
@@ -152,6 +152,8 @@ module RubyIndexer
152
152
  handle_attribute(node, reader: false, writer: true)
153
153
  when :attr_accessor
154
154
  handle_attribute(node, reader: true, writer: true)
155
+ when :include
156
+ handle_include(node)
155
157
  end
156
158
  end
157
159
 
@@ -350,5 +352,24 @@ module RubyIndexer
350
352
  @index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer
351
353
  end
352
354
  end
355
+
356
+ sig { params(node: Prism::CallNode).void }
357
+ def handle_include(node)
358
+ return unless @current_owner
359
+
360
+ arguments = node.arguments&.arguments
361
+ return unless arguments
362
+
363
+ names = arguments.filter_map do |node|
364
+ if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
365
+ node.full_name
366
+ end
367
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
368
+ # TO DO: add MissingNodesInConstantPathError when released in Prism
369
+ # If a constant path reference is dynamic or missing parts, we can't
370
+ # index it
371
+ end
372
+ @current_owner.included_modules.concat(names)
373
+ end
353
374
  end
354
375
  end
@@ -20,7 +20,7 @@ module RubyIndexer
20
20
  def initialize
21
21
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
22
22
  @included_gems = T.let([], T::Array[String])
23
- @excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
23
+ @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
24
24
  path = Bundler.settings["path"]
25
25
  @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
26
26
 
@@ -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|
@@ -40,6 +40,22 @@ module RubyIndexer
40
40
 
41
41
  abstract!
42
42
 
43
+ sig { returns(T::Array[String]) }
44
+ attr_accessor :included_modules
45
+
46
+ sig do
47
+ params(
48
+ name: String,
49
+ file_path: String,
50
+ location: Prism::Location,
51
+ comments: T::Array[String],
52
+ ).void
53
+ end
54
+ def initialize(name, file_path, location, comments)
55
+ super(name, file_path, location, comments)
56
+ @included_modules = T.let([], T::Array[String])
57
+ end
58
+
43
59
  sig { returns(String) }
44
60
  def short_name
45
61
  T.must(@name.split("::").last)
@@ -191,8 +191,9 @@ module RubyIndexer
191
191
 
192
192
  require_path = indexable_path.require_path
193
193
  @require_paths_tree.insert(require_path, indexable_path) if require_path
194
- rescue Errno::EISDIR
195
- # If `path` is a directory, just ignore it and continue indexing
194
+ rescue Errno::EISDIR, Errno::ENOENT
195
+ # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
196
+ # it
196
197
  end
197
198
 
198
199
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -281,5 +281,51 @@ module RubyIndexer
281
281
  final_thing = T.must(@index["FinalThing"].first)
282
282
  assert_equal("Something::Baz", final_thing.parent_class)
283
283
  end
284
+
285
+ def test_keeping_track_of_included_modules
286
+ index(<<~RUBY)
287
+ class Foo
288
+ # valid syntaxes that we can index
289
+ include A1
290
+ self.include A2
291
+ include A3, A4
292
+ self.include A5, A6
293
+
294
+ # valid syntaxes that we cannot index because of their dynamic nature
295
+ include some_variable_or_method_call
296
+ self.include some_variable_or_method_call
297
+
298
+ def something
299
+ include A7 # We should not index this because of this dynamic nature
300
+ end
301
+
302
+ # Valid inner class syntax definition with its own modules included
303
+ class Qux
304
+ include Corge
305
+ self.include Corge
306
+ include Baz
307
+
308
+ include some_variable_or_method_call
309
+ end
310
+ end
311
+
312
+ class ConstantPathReferences
313
+ include Foo::Bar
314
+ self.include Foo::Bar2
315
+
316
+ include dynamic::Bar
317
+ include Foo::
318
+ end
319
+ RUBY
320
+
321
+ foo = T.must(@index["Foo"][0])
322
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
323
+
324
+ qux = T.must(@index["Foo::Qux"][0])
325
+ assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
326
+
327
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
328
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
329
+ end
284
330
  end
285
331
  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
 
@@ -308,5 +308,10 @@ module RubyIndexer
308
308
 
309
309
  refute_empty(@index.instance_variable_get(:@entries))
310
310
  end
311
+
312
+ def test_index_single_does_not_fail_for_non_existing_file
313
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"))
314
+ assert_empty(@index.instance_variable_get(:@entries))
315
+ end
311
316
  end
312
317
  end
@@ -121,22 +121,23 @@ module RubyLsp
121
121
  sig do
122
122
  overridable.params(
123
123
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
124
+ global_state: GlobalState,
124
125
  uri: URI::Generic,
125
126
  dispatcher: Prism::Dispatcher,
126
127
  ).void
127
128
  end
128
- def create_code_lens_listener(response_builder, uri, dispatcher); end
129
+ def create_code_lens_listener(response_builder, global_state, uri, dispatcher); end
129
130
 
130
131
  # Creates a new Hover listener. This method is invoked on every Hover request
131
132
  sig do
132
133
  overridable.params(
133
134
  response_builder: ResponseBuilders::Hover,
135
+ global_state: GlobalState,
134
136
  nesting: T::Array[String],
135
- index: RubyIndexer::Index,
136
137
  dispatcher: Prism::Dispatcher,
137
138
  ).void
138
139
  end
139
- def create_hover_listener(response_builder, nesting, index, dispatcher); end
140
+ def create_hover_listener(response_builder, global_state, nesting, dispatcher); end
140
141
 
141
142
  # Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
142
143
  sig do
@@ -159,12 +160,24 @@ module RubyLsp
159
160
  sig do
160
161
  overridable.params(
161
162
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
163
+ global_state: GlobalState,
162
164
  uri: URI::Generic,
163
165
  nesting: T::Array[String],
164
- index: RubyIndexer::Index,
165
166
  dispatcher: Prism::Dispatcher,
166
167
  ).void
167
168
  end
168
- def create_definition_listener(response_builder, uri, nesting, index, dispatcher); end
169
+ def create_definition_listener(response_builder, global_state, uri, nesting, dispatcher); end
170
+
171
+ # Creates a new Completion listener. This method is invoked on every Completion request
172
+ sig do
173
+ overridable.params(
174
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
175
+ global_state: GlobalState,
176
+ nesting: T::Array[String],
177
+ dispatcher: Prism::Dispatcher,
178
+ uri: URI::Generic,
179
+ ).void
180
+ end
181
+ def create_completion_listener(response_builder, global_state, nesting, dispatcher, uri); end
169
182
  end
170
183
  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,41 @@
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
+ end
29
+
30
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
31
+ def apply_options(options)
32
+ workspace_uri = options.dig(:workspaceFolders, 0, :uri)
33
+ @workspace_uri = URI(workspace_uri) if workspace_uri
34
+ end
35
+
36
+ sig { returns(String) }
37
+ def workspace_path
38
+ T.must(@workspace_uri.to_standardized_path)
25
39
  end
26
40
 
27
41
  sig { returns(String) }
@@ -63,8 +77,15 @@ module RubyLsp
63
77
  def detect_typechecker
64
78
  return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
65
79
 
80
+ # We can't read the env from within `Bundle.with_original_env` so we need to set it here.
81
+ ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
66
82
  Bundler.with_original_env do
67
- Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
83
+ sorbet_static_detected = Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
84
+ # Don't show message while running tests, since it's noisy
85
+ if sorbet_static_detected && !ruby_lsp_env_is_test
86
+ $stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
87
+ end
88
+ sorbet_static_detected
68
89
  end
69
90
  rescue Bundler::GemfileNotFound
70
91
  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"