ruby-lsp 0.14.6 → 0.16.4

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +1 -16
  5. data/exe/ruby-lsp-check +13 -22
  6. data/exe/ruby-lsp-doctor +9 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +14 -1
  8. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +11 -23
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +4 -0
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
  11. data/lib/ruby_indexer/test/configuration_test.rb +2 -11
  12. data/lib/ruby_lsp/addon.rb +18 -9
  13. data/lib/ruby_lsp/base_server.rb +149 -0
  14. data/lib/ruby_lsp/document.rb +6 -11
  15. data/lib/ruby_lsp/global_state.rb +169 -0
  16. data/lib/ruby_lsp/internal.rb +4 -1
  17. data/lib/ruby_lsp/listeners/code_lens.rb +22 -13
  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 +3 -2
  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 +11 -4
  28. data/lib/ruby_lsp/requests/diagnostics.rb +6 -12
  29. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  30. data/lib/ruby_lsp/requests/formatting.rb +7 -43
  31. data/lib/ruby_lsp/requests/hover.rb +4 -4
  32. data/lib/ruby_lsp/requests/request.rb +2 -0
  33. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  34. data/lib/ruby_lsp/requests/signature_help.rb +4 -3
  35. data/lib/ruby_lsp/requests/support/common.rb +16 -5
  36. data/lib/ruby_lsp/requests/support/formatter.rb +26 -0
  37. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +1 -1
  38. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +47 -0
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  40. data/lib/ruby_lsp/requests/support/{syntax_tree_formatting_runner.rb → syntax_tree_formatter.rb} +13 -6
  41. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  42. data/lib/ruby_lsp/requests.rb +3 -1
  43. data/lib/ruby_lsp/server.rb +763 -142
  44. data/lib/ruby_lsp/setup_bundler.rb +13 -1
  45. data/lib/ruby_lsp/store.rb +3 -15
  46. data/lib/ruby_lsp/test_helper.rb +52 -0
  47. data/lib/ruby_lsp/utils.rb +68 -33
  48. metadata +11 -9
  49. data/lib/ruby_lsp/executor.rb +0 -614
  50. data/lib/ruby_lsp/requests/support/dependency_detector.rb +0 -93
  51. data/lib/ruby_lsp/requests/support/formatter_runner.rb +0 -18
  52. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -34
  53. 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: 94d4dfcda701bdf2ea1dd04e2a271572a139fe3d716e1bd535e2c05bcc9b3d23
4
+ data.tar.gz: b79dfc816bf027e733fa5f44db3e2d1039ae860f9dee88fe996ab1a27089d467
5
5
  SHA512:
6
- metadata.gz: 971af8a3c02903597143a8ec49915eead6bc0f65a0175e4a443e381d671dd4a88f776a26f62af1bd69ee7dc3f478b36bef122febbfaed7656907eb1a6a7a3354
7
- data.tar.gz: 15148c1c3765390f9bb92dbafe7043ac15b22de6b5c415c424a063f4742026d92831d84bcde2013ab742c4a889dc16d666d4ec5d18863d0db8acf793ce87f0a6
6
+ metadata.gz: 9de1f73feba2284c75294542a0e9ae1196f092004fe83ef815b2e08a1b404daceebc0b3f6f8f59149f936c8f7414821942556bee522776a909c071765dcc3443
7
+ data.tar.gz: b7617a6d508a94e5a06b8d39acb4d2bcc28d02832b19ab8567ed58b6b297aa3fc450dd0a116257fe91fc9545683550456c0d712fc32dc3f3ff716e2cdca2985e
data/README.md CHANGED
@@ -29,7 +29,7 @@ The Ruby LSP features include
29
29
  - Debugging support
30
30
  - Running and debugging tests through VS Code's UI
31
31
  - Go to definition for classes, modules, constants and required files
32
- - Showing documentaton on hover for classes, modules and constants
32
+ - Showing documentation on hover for classes, modules and constants
33
33
  - Completion for classes, modules, constants and require paths
34
34
  - Fuzzy search classes, modules and constants anywhere in the project and its dependencies (workspace symbol)
35
35
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.6
1
+ 0.16.4
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,149 @@
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
+ # The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
63
+ # else is pushed into the incoming queue
64
+ case method
65
+ when "initialize", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
66
+ process_message(message)
67
+ when "shutdown"
68
+ $stderr.puts("Shutting down Ruby LSP...")
69
+
70
+ shutdown
71
+
72
+ @mutex.synchronize do
73
+ run_shutdown
74
+ @writer.write(Result.new(id: message[:id], response: nil).to_hash)
75
+ end
76
+ when "exit"
77
+ @mutex.synchronize do
78
+ status = @incoming_queue.closed? ? 0 : 1
79
+ $stderr.puts("Shutdown complete with status #{status}")
80
+ exit(status)
81
+ end
82
+ else
83
+ @incoming_queue << message
84
+ end
85
+ end
86
+ end
87
+
88
+ sig { void }
89
+ def run_shutdown
90
+ @incoming_queue.clear
91
+ @outgoing_queue.clear
92
+ @incoming_queue.close
93
+ @outgoing_queue.close
94
+ @cancelled_requests.clear
95
+
96
+ @worker.join
97
+ @outgoing_dispatcher.join
98
+ @store.clear
99
+ end
100
+
101
+ # This method is only intended to be used in tests! Pops the latest response that would be sent to the client
102
+ sig { returns(T.untyped) }
103
+ def pop_response
104
+ @outgoing_queue.pop
105
+ end
106
+
107
+ sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
108
+ def process_message(message); end
109
+
110
+ sig { abstract.void }
111
+ def shutdown; end
112
+
113
+ sig { returns(Thread) }
114
+ def new_worker
115
+ Thread.new do
116
+ while (message = T.let(@incoming_queue.pop, T.nilable(T::Hash[Symbol, T.untyped])))
117
+ id = message[:id]
118
+
119
+ # Check if the request was cancelled before trying to process it
120
+ @mutex.synchronize do
121
+ if id && @cancelled_requests.include?(id)
122
+ send_message(Result.new(id: id, response: nil))
123
+ @cancelled_requests.delete(id)
124
+ next
125
+ end
126
+ end
127
+
128
+ process_message(message)
129
+ end
130
+ end
131
+ end
132
+
133
+ sig { params(message: T.any(Result, Error, Notification, Request)).void }
134
+ def send_message(message)
135
+ # When we're shutting down the server, there's a small race condition between closing the thread queues and
136
+ # finishing remaining requests. We may close the queue in the middle of processing a request, which will then fail
137
+ # when trying to send a response back
138
+ return if @outgoing_queue.closed?
139
+
140
+ @outgoing_queue << message
141
+ @current_request_id += 1 if message.is_a?(Request)
142
+ end
143
+
144
+ sig { params(id: Integer).void }
145
+ def send_empty_response(id)
146
+ send_message(Result.new(id: id, response: nil))
147
+ end
148
+ end
149
+ end
@@ -20,13 +20,13 @@ module RubyLsp
20
20
  sig { returns(URI::Generic) }
21
21
  attr_reader :uri
22
22
 
23
- sig { returns(String) }
23
+ sig { returns(Encoding) }
24
24
  attr_reader :encoding
25
25
 
26
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: String).void }
27
- def initialize(source:, version:, uri:, encoding: Constant::PositionEncodingKind::UTF8)
26
+ sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
27
+ def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
28
28
  @cache = T.let({}, T::Hash[String, T.untyped])
29
- @encoding = T.let(encoding, String)
29
+ @encoding = T.let(encoding, Encoding)
30
30
  @source = T.let(source, String)
31
31
  @version = T.let(version, Integer)
32
32
  @uri = T.let(uri, URI::Generic)
@@ -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
 
@@ -192,7 +187,7 @@ module RubyLsp
192
187
  # After character 0xFFFF, UTF-16 considers characters to have length 2 and we have to account for that
193
188
  SURROGATE_PAIR_START = T.let(0xFFFF, Integer)
194
189
 
195
- sig { params(source: String, encoding: String).void }
190
+ sig { params(source: String, encoding: Encoding).void }
196
191
  def initialize(source, encoding)
197
192
  @current_line = T.let(0, Integer)
198
193
  @pos = T.let(0, Integer)
@@ -214,7 +209,7 @@ module RubyLsp
214
209
  # need to adjust for surrogate pairs
215
210
  requested_position = @pos + position[:character]
216
211
 
217
- if @encoding == Constant::PositionEncodingKind::UTF16
212
+ if @encoding == Encoding::UTF_16LE
218
213
  requested_position -= utf_16_character_position_correction(@pos, requested_position)
219
214
  end
220
215