ruby-lsp 0.14.6 → 0.16.4

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