ruby-lsp 0.23.5 → 0.23.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 924e57270229eb4d0b709b01023d16f414591f8ea40c3b8a51ae6fb6cd09cc4f
4
- data.tar.gz: 72f4f544baf412f8580fe28287ba5324c7f646a9436797ebea59f34f3712d62e
3
+ metadata.gz: 46b72d1b9ba142496f64035b6b1ad54d096c0a28ae52311612bd118bc49e2956
4
+ data.tar.gz: 525dc46dcfc71ee57da51955a4034ceace3bbdd1eca7942b542ab2cd32d42ad4
5
5
  SHA512:
6
- metadata.gz: 650cf172b8b1408f5498cb2010c2f94b3ef0a4bc93212ef63e24c2177ef5996455625a1bb64fe51f75322a42625e9ebcf156f95f5412c44839fe4957df4c957f
7
- data.tar.gz: ce9b866443836810fe5884c2f977d4fd91fe020023ff547a9d60e525715ce388429aa7a901b0acc84876d68cada8f30dac2836494b7403fd8c2e845bea86ce79
6
+ metadata.gz: '06690b76d284ca6abc05fc0e836af32a54c0a3cedea1fd53071648330a3b3b6e6d1a59ef205127f54a88e8cd3ca07c3840f25f1293e37737d705b2ea41719cb1'
7
+ data.tar.gz: bd6104ef180f122d0bb9d6d751213c43f0d19d0f203deb50e5d94583ff0aaf85559a040ca0371fc24506ff11a0d2742c14f1277226cc34d3a09d02730d46aed7
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)](https://github.com/Shopify/ruby-lsp/actions/workflows/ci.yml)
6
6
  [![Ruby LSP extension](https://img.shields.io/badge/VS%20Code-Ruby%20LSP-success?logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
7
- [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q)
7
+ [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2yd77ayis-yAiVc1TX_kH0mHMBbi89dA)
8
8
 
9
9
  # Ruby LSP
10
10
 
@@ -13,7 +13,7 @@ for Ruby, used to improve rich features in editors. It is a part of a wider goal
13
13
  experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
14
14
 
15
15
  Want to discuss Ruby developer experience? Consider joining the public
16
- [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q).
16
+ [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-2yd77ayis-yAiVc1TX_kH0mHMBbi89dA).
17
17
 
18
18
  ## Getting Started
19
19
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.5
1
+ 0.23.7
data/exe/ruby-lsp CHANGED
@@ -64,9 +64,9 @@ if ENV["BUNDLE_GEMFILE"].nil?
64
64
  # which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
65
65
  # install rather than failing to boot the server completely
66
66
  if options[:launcher]
67
- command = +"#{Gem.ruby} #{File.expand_path("ruby-lsp-launcher", __dir__)}"
68
- command << " --debug" if options[:debug]
69
- exit exec(command)
67
+ flags = []
68
+ flags << "--debug" if options[:debug]
69
+ exit exec(Gem.ruby, File.expand_path("ruby-lsp-launcher", __dir__), *flags)
70
70
  end
71
71
 
72
72
  require_relative "../lib/ruby_lsp/setup_bundler"
@@ -8,14 +8,24 @@
8
8
 
9
9
  setup_error = nil
10
10
  install_error = nil
11
+ reboot = false
11
12
 
12
- # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
13
- # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
14
- # to ensure that we're setting up the bundle in the right place
15
- $stdin.binmode
16
- headers = $stdin.gets("\r\n\r\n")
17
- content_length = headers[/Content-Length: (\d+)/i, 1].to_i
18
- raw_initialize = $stdin.read(content_length)
13
+ workspace_uri = ARGV.first
14
+
15
+ raw_initialize = if workspace_uri && !workspace_uri.start_with?("--")
16
+ # If there's an argument without `--`, then it's the server asking to compose the bundle and passing to this
17
+ # executable the workspace URI. We can't require gems at this point, so we built a fake initialize request manually
18
+ reboot = true
19
+ "{\"params\":{\"workspaceFolders\":[{\"uri\":\"#{workspace_uri}\"}]}}"
20
+ else
21
+ # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
22
+ # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
23
+ # to ensure that we're setting up the bundle in the right place
24
+ $stdin.binmode
25
+ headers = $stdin.gets("\r\n\r\n")
26
+ content_length = headers[/Content-Length: (\d+)/i, 1].to_i
27
+ $stdin.read(content_length)
28
+ end
19
29
 
20
30
  # Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
21
31
  # `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
@@ -91,6 +101,13 @@ rescue StandardError => e
91
101
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
92
102
  end
93
103
 
104
+ # When performing a lockfile re-boot, this executable is invoked to set up the composed bundle ahead of time. In this
105
+ # flow, we are not booting the LSP yet, just checking if the bundle is valid before rebooting
106
+ if reboot
107
+ # Use the exit status to signal to the server if composing the bundle succeeded
108
+ exit(install_error || setup_error ? 1 : 0)
109
+ end
110
+
94
111
  # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
95
112
  # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
96
113
  # paths into the load path manually or we may end up requiring the wrong version of the gem
@@ -143,7 +143,7 @@ module RubyIndexer
143
143
 
144
144
  if current_owner
145
145
  expression = node.expression
146
- name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
146
+ name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{last_name_in_stack}>" : "<Class:#{expression.slice}>")
147
147
  real_nesting = actual_nesting(name)
148
148
 
149
149
  existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
@@ -376,7 +376,6 @@ module RubyIndexer
376
376
  ))
377
377
 
378
378
  @owner_stack << singleton
379
- @stack << "<Class:#{@stack.last}>"
380
379
  end
381
380
  end
382
381
 
@@ -386,7 +385,6 @@ module RubyIndexer
386
385
 
387
386
  if node.receiver.is_a?(Prism::SelfNode)
388
387
  @owner_stack.pop
389
- @stack.pop
390
388
  end
391
389
  end
392
390
 
@@ -1127,5 +1125,15 @@ module RubyIndexer
1127
1125
  @index.add(entry)
1128
1126
  @stack << short_name
1129
1127
  end
1128
+
1129
+ # Returns the last name in the stack not as we found it, but in terms of declared constants. For example, if the
1130
+ # last entry in the stack is a compact namespace like `Foo::Bar`, then the last name is `Bar`
1131
+ sig { returns(T.nilable(String)) }
1132
+ def last_name_in_stack
1133
+ name = @stack.last
1134
+ return unless name
1135
+
1136
+ name.split("::").last
1137
+ end
1130
1138
  end
1131
1139
  end
@@ -37,6 +37,19 @@ module RubyIndexer
37
37
  end
38
38
  end
39
39
 
40
+ class InstanceVariableTarget < Target
41
+ extend T::Sig
42
+
43
+ sig { returns(String) }
44
+ attr_reader :name
45
+
46
+ sig { params(name: String).void }
47
+ def initialize(name)
48
+ super()
49
+ @name = name
50
+ end
51
+ end
52
+
40
53
  class Reference
41
54
  extend T::Sig
42
55
 
@@ -94,6 +107,12 @@ module RubyIndexer
94
107
  :on_constant_or_write_node_enter,
95
108
  :on_constant_and_write_node_enter,
96
109
  :on_constant_operator_write_node_enter,
110
+ :on_instance_variable_read_node_enter,
111
+ :on_instance_variable_write_node_enter,
112
+ :on_instance_variable_and_write_node_enter,
113
+ :on_instance_variable_operator_write_node_enter,
114
+ :on_instance_variable_or_write_node_enter,
115
+ :on_instance_variable_target_node_enter,
97
116
  :on_call_node_enter,
98
117
  )
99
118
  end
@@ -262,6 +281,36 @@ module RubyIndexer
262
281
  end
263
282
  end
264
283
 
284
+ sig { params(node: Prism::InstanceVariableReadNode).void }
285
+ def on_instance_variable_read_node_enter(node)
286
+ collect_instance_variable_references(node.name.to_s, node.location, false)
287
+ end
288
+
289
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
290
+ def on_instance_variable_write_node_enter(node)
291
+ collect_instance_variable_references(node.name.to_s, node.name_loc, true)
292
+ end
293
+
294
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
295
+ def on_instance_variable_and_write_node_enter(node)
296
+ collect_instance_variable_references(node.name.to_s, node.name_loc, true)
297
+ end
298
+
299
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
300
+ def on_instance_variable_operator_write_node_enter(node)
301
+ collect_instance_variable_references(node.name.to_s, node.name_loc, true)
302
+ end
303
+
304
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
305
+ def on_instance_variable_or_write_node_enter(node)
306
+ collect_instance_variable_references(node.name.to_s, node.name_loc, true)
307
+ end
308
+
309
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
310
+ def on_instance_variable_target_node_enter(node)
311
+ collect_instance_variable_references(node.name.to_s, node.location, true)
312
+ end
313
+
265
314
  sig { params(node: Prism::CallNode).void }
266
315
  def on_call_node_enter(node)
267
316
  if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
@@ -305,6 +354,13 @@ module RubyIndexer
305
354
  end
306
355
  end
307
356
 
357
+ sig { params(name: String, location: Prism::Location, declaration: T::Boolean).void }
358
+ def collect_instance_variable_references(name, location, declaration)
359
+ return unless @target.is_a?(InstanceVariableTarget) && name == @target.name
360
+
361
+ @references << Reference.new(name, location, declaration: declaration)
362
+ end
363
+
308
364
  sig do
309
365
  params(
310
366
  node: T.any(
@@ -647,5 +647,24 @@ module RubyIndexer
647
647
  entry = @index["Foo"].first
648
648
  assert_empty(entry.comments)
649
649
  end
650
+
651
+ def test_singleton_inside_compact_namespace
652
+ index(<<~RUBY)
653
+ module Foo::Bar
654
+ class << self
655
+ def baz; end
656
+ end
657
+ end
658
+ RUBY
659
+
660
+ # Verify we didn't index the incorrect name
661
+ assert_nil(@index["Foo::Bar::<Class:Foo::Bar>"])
662
+
663
+ # Verify we indexed the correct name
664
+ assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:1-2:3-5")
665
+
666
+ method = @index["baz"]&.first
667
+ assert_equal("Foo::Bar::<Class:Bar>", method.owner.name)
668
+ end
650
669
  end
651
670
  end
@@ -216,6 +216,64 @@ module RubyIndexer
216
216
  assert_equal(11, refs[2].location.start_line)
217
217
  end
218
218
 
219
+ def test_finds_instance_variable_read_references
220
+ refs = find_instance_variable_references("@foo", <<~RUBY)
221
+ class Foo
222
+ def foo
223
+ @foo
224
+ end
225
+ end
226
+ RUBY
227
+ assert_equal(1, refs.size)
228
+
229
+ assert_equal("@foo", refs[0].name)
230
+ assert_equal(3, refs[0].location.start_line)
231
+ end
232
+
233
+ def test_finds_instance_variable_write_references
234
+ refs = find_instance_variable_references("@foo", <<~RUBY)
235
+ class Foo
236
+ def write
237
+ @foo = 1
238
+ @foo &&= 2
239
+ @foo ||= 3
240
+ @foo += 4
241
+ @foo, @bar = []
242
+ end
243
+ end
244
+ RUBY
245
+ assert_equal(5, refs.size)
246
+
247
+ assert_equal(["@foo"], refs.map(&:name).uniq)
248
+ assert_equal(3, refs[0].location.start_line)
249
+ assert_equal(4, refs[1].location.start_line)
250
+ assert_equal(5, refs[2].location.start_line)
251
+ assert_equal(6, refs[3].location.start_line)
252
+ assert_equal(7, refs[4].location.start_line)
253
+ end
254
+
255
+ def test_finds_instance_variable_references_ignore_context
256
+ refs = find_instance_variable_references("@name", <<~RUBY)
257
+ class Foo
258
+ def name
259
+ @name = "foo"
260
+ end
261
+ end
262
+ class Bar
263
+ def name
264
+ @name = "bar"
265
+ end
266
+ end
267
+ RUBY
268
+ assert_equal(2, refs.size)
269
+
270
+ assert_equal("@name", refs[0].name)
271
+ assert_equal(3, refs[0].location.start_line)
272
+
273
+ assert_equal("@name", refs[1].name)
274
+ assert_equal(8, refs[1].location.start_line)
275
+ end
276
+
219
277
  private
220
278
 
221
279
  def find_const_references(const_name, source)
@@ -228,6 +286,11 @@ module RubyIndexer
228
286
  find_references(target, source)
229
287
  end
230
288
 
289
+ def find_instance_variable_references(instance_variable_name, source)
290
+ target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name)
291
+ find_references(target, source)
292
+ end
293
+
231
294
  def find_references(target, source)
232
295
  file_path = "/fake.rb"
233
296
  index = Index.new
@@ -11,7 +11,8 @@ module RubyLsp
11
11
  attr_reader :supports_watching_files,
12
12
  :supports_request_delegation,
13
13
  :window_show_message_supports_extra_properties,
14
- :supports_progress
14
+ :supports_progress,
15
+ :supports_diagnostic_refresh
15
16
 
16
17
  sig { void }
17
18
  def initialize
@@ -32,6 +33,9 @@ module RubyLsp
32
33
 
33
34
  # The editor supports displaying progress requests
34
35
  @supports_progress = T.let(false, T::Boolean)
36
+
37
+ # The editor supports server initiated refresh for diagnostics
38
+ @supports_diagnostic_refresh = T.let(false, T::Boolean)
35
39
  end
36
40
 
37
41
  sig { params(capabilities: T::Hash[Symbol, T.untyped]).void }
@@ -57,6 +61,8 @@ module RubyLsp
57
61
 
58
62
  progress = capabilities.dig(:window, :workDoneProgress)
59
63
  @supports_progress = progress if progress
64
+
65
+ @supports_diagnostic_refresh = workspace_capabilities.dig(:diagnostics, :refreshSupport) || false
60
66
  end
61
67
 
62
68
  sig { returns(T::Boolean) }
@@ -94,6 +94,8 @@ module RubyLsp
94
94
  @workspace_uri = URI(workspace_uri) if workspace_uri
95
95
 
96
96
  specified_formatter = options.dig(:initializationOptions, :formatter)
97
+ rubocop_has_addon = defined?(::RuboCop::Version::STRING) &&
98
+ Gem::Requirement.new(">= 1.70.0").satisfied_by?(Gem::Version.new(::RuboCop::Version::STRING))
97
99
 
98
100
  if specified_formatter
99
101
  @formatter = specified_formatter
@@ -101,6 +103,12 @@ module RubyLsp
101
103
  if specified_formatter != "auto"
102
104
  notifications << Notification.window_log_message("Using formatter specified by user: #{@formatter}")
103
105
  end
106
+
107
+ # If the user had originally configured to use `rubocop`, but their version doesn't provide the add-on yet,
108
+ # fallback to the internal integration
109
+ if specified_formatter == "rubocop" && !rubocop_has_addon
110
+ @formatter = "rubocop_internal"
111
+ end
104
112
  end
105
113
 
106
114
  if @formatter == "auto"
@@ -109,6 +117,23 @@ module RubyLsp
109
117
  end
110
118
 
111
119
  specified_linters = options.dig(:initializationOptions, :linters)
120
+
121
+ if specified_formatter == "rubocop" || specified_linters&.include?("rubocop")
122
+ notifications << Notification.window_log_message(<<~MESSAGE, type: Constant::MessageType::WARNING)
123
+ Formatter is configured to be `rubocop`. As of RuboCop v1.70.0, this identifier activates the add-on
124
+ implemented in the rubocop gem itself instead of the internal integration provided by the Ruby LSP.
125
+
126
+ If you wish to use the internal integration, please configure the formatter as `rubocop_internal`.
127
+ MESSAGE
128
+ end
129
+
130
+ # If the user had originally configured to use `rubocop`, but their version doesn't provide the add-on yet,
131
+ # fall back to the internal integration
132
+ if specified_linters&.include?("rubocop") && !rubocop_has_addon
133
+ specified_linters.delete("rubocop")
134
+ specified_linters << "rubocop_internal"
135
+ end
136
+
112
137
  @linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)
113
138
 
114
139
  notifications << if specified_linters
@@ -185,13 +210,13 @@ module RubyLsp
185
210
  sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
186
211
  def detect_formatter(direct_dependencies, all_dependencies)
187
212
  # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
188
- return "rubocop" if direct_dependencies.any?(/^rubocop/)
213
+ return "rubocop_internal" if direct_dependencies.any?(/^rubocop/)
189
214
 
190
215
  syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
191
216
  return "syntax_tree" if syntax_tree_is_direct_dependency
192
217
 
193
218
  rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
194
- return "rubocop" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
219
+ return "rubocop_internal" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
195
220
 
196
221
  "none"
197
222
  end
@@ -203,7 +228,7 @@ module RubyLsp
203
228
  linters = []
204
229
 
205
230
  if dependencies.any?(/^rubocop/) || (all_dependencies.include?("rubocop") && dot_rubocop_yml_present)
206
- linters << "rubocop"
231
+ linters << "rubocop_internal"
207
232
  end
208
233
 
209
234
  linters
@@ -120,7 +120,7 @@ module RubyLsp
120
120
  [target, node_value(target)]
121
121
  when Prism::ModuleNode, Prism::ClassNode, Prism::SingletonClassNode, Prism::DefNode, Prism::CaseNode,
122
122
  Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::IfNode, Prism::UnlessNode
123
- target
123
+ [target, nil]
124
124
  end
125
125
 
126
126
  @target = T.let(highlight_target, T.nilable(Prism::Node))
@@ -620,7 +620,8 @@ module RubyLsp
620
620
 
621
621
  sig { params(keyword_loc: T.nilable(Prism::Location), end_loc: T.nilable(Prism::Location)).void }
622
622
  def add_matching_end_highlights(keyword_loc, end_loc)
623
- return unless keyword_loc && end_loc && end_loc.length.positive?
623
+ return unless keyword_loc && end_loc
624
+ return unless end_loc.length.positive?
624
625
  return unless covers_target_position?(keyword_loc) || covers_target_position?(end_loc)
625
626
 
626
627
  add_highlight(Constant::DocumentHighlightKind::TEXT, keyword_loc)
@@ -128,7 +128,13 @@ module RubyLsp
128
128
  gem_version = resolve_version(uri)
129
129
  return if gem_version.nil?
130
130
 
131
- file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, CGI.unescape(uri.path))
131
+ path = uri.path
132
+ return unless path
133
+
134
+ gem_name = uri.gem_name
135
+ return unless gem_name
136
+
137
+ file_path = self.class.gem_paths.dig(gem_name, gem_version, CGI.unescape(path))
132
138
  return if file_path.nil?
133
139
 
134
140
  @response_builder << Interface::DocumentLink.new(
@@ -149,7 +155,10 @@ module RubyLsp
149
155
 
150
156
  return @gem_version unless @gem_version.nil? || @gem_version.empty?
151
157
 
152
- GEM_TO_VERSION_MAP[uri.gem_name]
158
+ gem_name = uri.gem_name
159
+ return unless gem_name
160
+
161
+ GEM_TO_VERSION_MAP[gem_name]
153
162
  end
154
163
  end
155
164
  end
@@ -6,11 +6,11 @@ require "sorbet-runtime"
6
6
  begin
7
7
  T::Configuration.default_checked_level = :never
8
8
  # Suppresses call validation errors
9
- T::Configuration.call_validation_error_handler = ->(*) {}
9
+ T::Configuration.call_validation_error_handler = ->(*arg) {}
10
10
  # Suppresses errors caused by T.cast, T.let, T.must, etc.
11
- T::Configuration.inline_type_error_handler = ->(*) {}
11
+ T::Configuration.inline_type_error_handler = ->(*arg) {}
12
12
  # Suppresses errors caused by incorrect parameter ordering
13
- T::Configuration.sig_validation_error_handler = ->(*) {}
13
+ T::Configuration.sig_validation_error_handler = ->(*arg) {}
14
14
  rescue
15
15
  # Need this rescue so that if another gem has
16
16
  # already set the checked level by the time we
@@ -19,7 +19,7 @@ module RubyLsp
19
19
  sig { returns(Interface::CodeActionRegistrationOptions) }
20
20
  def provider
21
21
  Interface::CodeActionRegistrationOptions.new(
22
- document_selector: [Interface::DocumentFilter.new(language: "ruby")],
22
+ document_selector: nil,
23
23
  resolve_provider: true,
24
24
  )
25
25
  end
@@ -15,7 +15,7 @@ module RubyLsp
15
15
  sig { returns(Interface::DiagnosticRegistrationOptions) }
16
16
  def provider
17
17
  Interface::DiagnosticRegistrationOptions.new(
18
- document_selector: [Interface::DocumentFilter.new(language: "ruby")],
18
+ document_selector: nil,
19
19
  inter_file_dependencies: false,
20
20
  workspace_diagnostics: false,
21
21
  )
@@ -13,13 +13,9 @@ module RubyLsp
13
13
  class << self
14
14
  extend T::Sig
15
15
 
16
- sig { returns(Interface::FoldingRangeRegistrationOptions) }
16
+ sig { returns(TrueClass) }
17
17
  def provider
18
- Interface::FoldingRangeRegistrationOptions.new(
19
- document_selector: [
20
- Interface::DocumentFilter.new(language: "ruby"),
21
- ],
22
- )
18
+ true
23
19
  end
24
20
  end
25
21
 
@@ -14,13 +14,9 @@ module RubyLsp
14
14
  class << self
15
15
  extend T::Sig
16
16
 
17
- sig { returns(Interface::DocumentFormattingRegistrationOptions) }
17
+ sig { returns(TrueClass) }
18
18
  def provider
19
- Interface::DocumentFormattingRegistrationOptions.new(
20
- document_selector: [
21
- Interface::DocumentFilter.new(language: "ruby"),
22
- ],
23
- )
19
+ true
24
20
  end
25
21
  end
26
22
 
@@ -14,7 +14,7 @@ module RubyLsp
14
14
  sig { returns(Interface::DocumentOnTypeFormattingRegistrationOptions) }
15
15
  def provider
16
16
  Interface::DocumentOnTypeFormattingRegistrationOptions.new(
17
- document_selector: [Interface::DocumentFilter.new(language: "ruby")],
17
+ document_selector: nil,
18
18
  first_trigger_character: "{",
19
19
  more_trigger_character: ["\n", "|", "d"],
20
20
  )
@@ -39,6 +39,12 @@ module RubyLsp
39
39
  Prism::ConstantReadNode,
40
40
  Prism::ConstantPathNode,
41
41
  Prism::ConstantPathTargetNode,
42
+ Prism::InstanceVariableAndWriteNode,
43
+ Prism::InstanceVariableOperatorWriteNode,
44
+ Prism::InstanceVariableOrWriteNode,
45
+ Prism::InstanceVariableReadNode,
46
+ Prism::InstanceVariableTargetNode,
47
+ Prism::InstanceVariableWriteNode,
42
48
  Prism::CallNode,
43
49
  Prism::DefNode,
44
50
  ],
@@ -62,6 +68,12 @@ module RubyLsp
62
68
  Prism::ConstantReadNode,
63
69
  Prism::ConstantPathNode,
64
70
  Prism::ConstantPathTargetNode,
71
+ Prism::InstanceVariableAndWriteNode,
72
+ Prism::InstanceVariableOperatorWriteNode,
73
+ Prism::InstanceVariableOrWriteNode,
74
+ Prism::InstanceVariableReadNode,
75
+ Prism::InstanceVariableTargetNode,
76
+ Prism::InstanceVariableWriteNode,
65
77
  Prism::CallNode,
66
78
  Prism::DefNode,
67
79
  ),
@@ -97,6 +109,12 @@ module RubyLsp
97
109
  Prism::ConstantReadNode,
98
110
  Prism::ConstantPathNode,
99
111
  Prism::ConstantPathTargetNode,
112
+ Prism::InstanceVariableAndWriteNode,
113
+ Prism::InstanceVariableOperatorWriteNode,
114
+ Prism::InstanceVariableOrWriteNode,
115
+ Prism::InstanceVariableReadNode,
116
+ Prism::InstanceVariableTargetNode,
117
+ Prism::InstanceVariableWriteNode,
100
118
  Prism::CallNode,
101
119
  Prism::DefNode,
102
120
  ),
@@ -114,6 +132,14 @@ module RubyLsp
114
132
 
115
133
  fully_qualified_name = T.must(entries.first).name
116
134
  RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
135
+ when
136
+ Prism::InstanceVariableAndWriteNode,
137
+ Prism::InstanceVariableOperatorWriteNode,
138
+ Prism::InstanceVariableOrWriteNode,
139
+ Prism::InstanceVariableReadNode,
140
+ Prism::InstanceVariableTargetNode,
141
+ Prism::InstanceVariableWriteNode
142
+ RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s)
117
143
  when Prism::CallNode, Prism::DefNode
118
144
  RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
119
145
  end
@@ -17,7 +17,7 @@ module RubyLsp
17
17
  sig { returns(Interface::SemanticTokensRegistrationOptions) }
18
18
  def provider
19
19
  Interface::SemanticTokensRegistrationOptions.new(
20
- document_selector: [{ language: "ruby" }, { language: "erb" }],
20
+ document_selector: nil,
21
21
  legend: Interface::SemanticTokensLegend.new(
22
22
  token_types: ResponseBuilders::SemanticHighlighting::TOKEN_TYPES.keys,
23
23
  token_modifiers: ResponseBuilders::SemanticHighlighting::TOKEN_MODIFIERS.keys,
@@ -106,6 +106,8 @@ module RubyLsp
106
106
  end,
107
107
  ),
108
108
  )
109
+ when "rubyLsp/composeBundle"
110
+ compose_bundle(message)
109
111
  when "$/cancelRequest"
110
112
  @global_state.synchronize { @cancelled_requests << message[:params][:id] }
111
113
  when nil
@@ -283,6 +285,7 @@ module RubyLsp
283
285
  document_range_formatting_provider: true,
284
286
  experimental: {
285
287
  addon_detection: true,
288
+ compose_bundle: true,
286
289
  },
287
290
  ),
288
291
  serverInfo: {
@@ -338,7 +341,7 @@ module RubyLsp
338
341
  unless @setup_error
339
342
  if defined?(Requests::Support::RuboCopFormatter)
340
343
  begin
341
- @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
344
+ @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
342
345
  rescue ::RuboCop::Error => e
343
346
  # The user may have provided unknown config switches in .rubocop or
344
347
  # is trying to load a non-existent config file.
@@ -1039,16 +1042,20 @@ module RubyLsp
1039
1042
  def handle_rubocop_config_change(uri)
1040
1043
  return unless defined?(Requests::Support::RuboCopFormatter)
1041
1044
 
1042
- send_log_message("Reloading RuboCop since #{uri} changed")
1043
- @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
1045
+ # Register a new runner to reload configurations
1046
+ @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
1044
1047
 
1045
- # Clear all existing diagnostics since the config changed. This has to happen under a mutex because the `state`
1046
- # hash cannot be mutated during iteration or that will throw an error
1048
+ # Clear all document caches for pull diagnostics
1047
1049
  @global_state.synchronize do
1048
- @store.each do |uri, _document|
1049
- send_message(Notification.publish_diagnostics(uri.to_s, []))
1050
+ @store.each do |_uri, document|
1051
+ document.cache_set("textDocument/diagnostic", Document::EMPTY_CACHE)
1050
1052
  end
1051
1053
  end
1054
+
1055
+ # Request a pull diagnostic refresh from the editor
1056
+ if @global_state.client_capabilities.supports_diagnostic_refresh
1057
+ send_message(Request.new(id: @current_request_id, method: "workspace/diagnostic/refresh", params: nil))
1058
+ end
1052
1059
  end
1053
1060
 
1054
1061
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
@@ -1211,16 +1218,15 @@ module RubyLsp
1211
1218
  sig { void }
1212
1219
  def check_formatter_is_available
1213
1220
  return if @setup_error
1214
- # Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
1215
- # Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
1216
- return unless @global_state.formatter == "rubocop"
1221
+ # Warn of an unavailable `formatter` setting, e.g. `rubocop_internal` on a project which doesn't have RuboCop.
1222
+ return unless @global_state.formatter == "rubocop_internal"
1217
1223
 
1218
1224
  unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
1219
1225
  @global_state.formatter = "none"
1220
1226
 
1221
1227
  send_message(
1222
1228
  Notification.window_show_message(
1223
- "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
1229
+ "Ruby LSP formatter is set to `rubocop_internal` but RuboCop was not found in the Gemfile or gemspec.",
1224
1230
  type: Constant::MessageType::ERROR,
1225
1231
  ),
1226
1232
  )
@@ -1279,5 +1285,37 @@ module RubyLsp
1279
1285
 
1280
1286
  addon.handle_window_show_message_response(result[:title])
1281
1287
  end
1288
+
1289
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
1290
+ def compose_bundle(message)
1291
+ already_composed_path = File.join(@global_state.workspace_path, ".ruby-lsp", "bundle_is_composed")
1292
+ command = "#{Gem.ruby} #{File.expand_path("../../exe/ruby-lsp-launcher", __dir__)} #{@global_state.workspace_uri}"
1293
+ id = message[:id]
1294
+
1295
+ begin
1296
+ Bundler::LockfileParser.new(Bundler.default_lockfile.read)
1297
+ rescue Bundler::LockfileError => e
1298
+ send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: e.message))
1299
+ return
1300
+ end
1301
+
1302
+ # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1303
+ # we return the response back to the editor, then the restart is triggered
1304
+ Thread.new do
1305
+ send_log_message("Recomposing the bundle ahead of restart")
1306
+ pid = Process.spawn(command)
1307
+ _, status = Process.wait2(pid)
1308
+
1309
+ if status&.exitstatus == 0
1310
+ # Create a signal for the restart that it can skip composing the bundle and launch directly
1311
+ FileUtils.touch(already_composed_path)
1312
+ send_message(Result.new(id: id, response: { success: true }))
1313
+ else
1314
+ # This special error code makes the extension avoid restarting in case we already know that the composed
1315
+ # bundle is not valid
1316
+ send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle"))
1317
+ end
1318
+ end
1319
+ end
1282
1320
  end
1283
1321
  end
@@ -57,6 +57,7 @@ module RubyLsp
57
57
  @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
58
58
  @last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
59
59
  @error_path = T.let(@custom_dir + "install_error", Pathname)
60
+ @already_composed_path = T.let(@custom_dir + "bundle_is_composed", Pathname)
60
61
 
61
62
  dependencies, bundler_version = load_dependencies
62
63
  @dependencies = T.let(dependencies, T::Hash[String, T.untyped])
@@ -71,6 +72,23 @@ module RubyLsp
71
72
  def setup!
72
73
  raise BundleNotLocked if !@launcher && @gemfile&.exist? && !@lockfile&.exist?
73
74
 
75
+ # If the bundle was composed ahead of time using our custom `rubyLsp/composeBundle` request, then we can skip the
76
+ # entire process and just return the composed environment
77
+ if @already_composed_path.exist?
78
+ $stderr.puts("Ruby LSP> Composed bundle was set up ahead of time. Skipping...")
79
+ @already_composed_path.delete
80
+
81
+ env = bundler_settings_as_env
82
+ env["BUNDLE_GEMFILE"] = @custom_gemfile.exist? ? @custom_gemfile.to_s : @gemfile.to_s
83
+
84
+ if env["BUNDLE_PATH"]
85
+ env["BUNDLE_PATH"] = File.expand_path(env["BUNDLE_PATH"], @project_path)
86
+ end
87
+
88
+ env["BUNDLER_VERSION"] = @bundler_version.to_s if @bundler_version
89
+ return env
90
+ end
91
+
74
92
  # Automatically create and ignore the .ruby-lsp folder for users
75
93
  @custom_dir.mkpath unless @custom_dir.exist?
76
94
  ignore_file = @custom_dir + ".gitignore"
@@ -25,7 +25,7 @@ module RubyLsp
25
25
  end,
26
26
  String,
27
27
  )
28
- GUESSED_TYPES_URL = "https://shopify.github.io/ruby-lsp/design-and-roadmap.html#guessed-types"
28
+ GUESSED_TYPES_URL = "https://shopify.github.io/ruby-lsp/#guessed-types"
29
29
 
30
30
  # Request delegation for embedded languages is not yet standardized into the language server specification. Here we
31
31
  # use this custom error class as a way to return a signal to the client that the request should be delegated to the
@@ -37,6 +37,8 @@ module RubyLsp
37
37
  CODE = -32900
38
38
  end
39
39
 
40
+ BUNDLE_COMPOSE_FAILED_CODE = -33000
41
+
40
42
  # A notification to be sent to the client
41
43
  class Message
42
44
  extend T::Sig
@@ -162,7 +164,9 @@ module RubyLsp
162
164
 
163
165
  sig { override.returns(T::Hash[Symbol, T.untyped]) }
164
166
  def to_hash
165
- { method: @method, params: T.unsafe(@params).to_hash }
167
+ hash = { method: @method }
168
+ hash[:params] = T.unsafe(@params).to_hash if @params
169
+ hash
166
170
  end
167
171
  end
168
172
 
@@ -206,7 +210,9 @@ module RubyLsp
206
210
 
207
211
  sig { override.returns(T::Hash[Symbol, T.untyped]) }
208
212
  def to_hash
209
- { id: @id, method: @method, params: T.unsafe(@params).to_hash }
213
+ hash = { id: @id, method: @method }
214
+ hash[:params] = T.unsafe(@params).to_hash if @params
215
+ hash
210
216
  end
211
217
  end
212
218
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.5
4
+ version: 0.23.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-10 00:00:00.000000000 Z
10
+ date: 2025-01-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: language_server-protocol
@@ -218,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
218
  - !ruby/object:Gem::Version
219
219
  version: '0'
220
220
  requirements: []
221
- rubygems_version: 3.6.2
221
+ rubygems_version: 3.6.3
222
222
  specification_version: 4
223
223
  summary: An opinionated language server for Ruby
224
224
  test_files: []