ruby-lsp 0.23.5 → 0.23.7

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