ruby-lsp 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +24 -3
  5. data/exe/ruby-lsp-launcher +127 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +63 -12
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +56 -2
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +21 -6
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -21
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +2 -2
  12. data/lib/ruby_indexer/test/enhancements_test.rb +51 -19
  13. data/lib/ruby_indexer/test/index_test.rb +91 -2
  14. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  15. data/lib/ruby_indexer/test/method_test.rb +26 -0
  16. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  17. data/lib/ruby_lsp/addon.rb +9 -2
  18. data/lib/ruby_lsp/base_server.rb +14 -5
  19. data/lib/ruby_lsp/client_capabilities.rb +60 -0
  20. data/lib/ruby_lsp/document.rb +1 -1
  21. data/lib/ruby_lsp/global_state.rb +20 -19
  22. data/lib/ruby_lsp/internal.rb +2 -0
  23. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  24. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  25. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  26. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  27. data/lib/ruby_lsp/requests/completion.rb +7 -1
  28. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  29. data/lib/ruby_lsp/requests/definition.rb +26 -11
  30. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  31. data/lib/ruby_lsp/requests/hover.rb +24 -6
  32. data/lib/ruby_lsp/requests/references.rb +2 -0
  33. data/lib/ruby_lsp/requests/rename.rb +3 -1
  34. data/lib/ruby_lsp/requests/request.rb +1 -1
  35. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +11 -1
  36. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  37. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  38. data/lib/ruby_lsp/server.rb +54 -16
  39. data/lib/ruby_lsp/setup_bundler.rb +132 -24
  40. data/lib/ruby_lsp/utils.rb +8 -0
  41. metadata +8 -3
@@ -3,10 +3,14 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "bundler"
6
+ require "bundler/cli"
7
+ require "bundler/cli/install"
8
+ require "bundler/cli/update"
6
9
  require "fileutils"
7
10
  require "pathname"
8
11
  require "digest"
9
12
  require "time"
13
+ require "uri"
10
14
 
11
15
  # This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
12
16
  # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
@@ -27,6 +31,8 @@ module RubyLsp
27
31
  def initialize(project_path, **options)
28
32
  @project_path = project_path
29
33
  @branch = T.let(options[:branch], T.nilable(String))
34
+ @launcher = T.let(options[:launcher], T.nilable(T::Boolean))
35
+ patch_thor_to_print_progress_to_stderr! if @launcher
30
36
 
31
37
  # Regular bundle paths
32
38
  @gemfile = T.let(
@@ -47,8 +53,11 @@ module RubyLsp
47
53
  @custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
48
54
  @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
49
55
  @last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
56
+ @error_path = T.let(@custom_dir + "install_error", Pathname)
50
57
 
51
- @dependencies = T.let(load_dependencies, T::Hash[String, T.untyped])
58
+ dependencies, bundler_version = load_dependencies
59
+ @dependencies = T.let(dependencies, T::Hash[String, T.untyped])
60
+ @bundler_version = T.let(bundler_version, T.nilable(Gem::Version))
52
61
  @rails_app = T.let(rails_app?, T::Boolean)
53
62
  @retry = T.let(false, T::Boolean)
54
63
  end
@@ -57,7 +66,12 @@ module RubyLsp
57
66
  # used for running the server
58
67
  sig { returns(T::Hash[String, String]) }
59
68
  def setup!
60
- raise BundleNotLocked if @gemfile&.exist? && !@lockfile&.exist?
69
+ raise BundleNotLocked if !@launcher && @gemfile&.exist? && !@lockfile&.exist?
70
+
71
+ # Automatically create and ignore the .ruby-lsp folder for users
72
+ @custom_dir.mkpath unless @custom_dir.exist?
73
+ ignore_file = @custom_dir + ".gitignore"
74
+ ignore_file.write("*") unless ignore_file.exist?
61
75
 
62
76
  # Do not set up a custom bundle if LSP dependencies are already in the Gemfile
63
77
  if @dependencies["ruby-lsp"] &&
@@ -67,19 +81,9 @@ module RubyLsp
67
81
  "Ruby LSP> Skipping custom bundle setup since LSP dependencies are already in #{@gemfile}",
68
82
  )
69
83
 
70
- # If the user decided to add `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) to their Gemfile after
71
- # having already run the Ruby LSP, then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle
72
- # install` for the top level and try to execute the Ruby LSP using the custom bundle, which will fail since the
73
- # gems are not installed there
74
- @custom_dir.rmtree if @custom_dir.exist?
75
84
  return run_bundle_install
76
85
  end
77
86
 
78
- # Automatically create and ignore the .ruby-lsp folder for users
79
- @custom_dir.mkpath unless @custom_dir.exist?
80
- ignore_file = @custom_dir + ".gitignore"
81
- ignore_file.write("*") unless ignore_file.exist?
82
-
83
87
  write_custom_gemfile
84
88
 
85
89
  unless @gemfile&.exist? && @lockfile&.exist?
@@ -109,17 +113,19 @@ module RubyLsp
109
113
  def custom_bundle_dependencies
110
114
  @custom_bundle_dependencies ||= T.let(
111
115
  begin
116
+ original_bundle_gemfile = ENV["BUNDLE_GEMFILE"]
117
+
112
118
  if @custom_lockfile.exist?
113
119
  ENV["BUNDLE_GEMFILE"] = @custom_gemfile.to_s
114
120
  Bundler::LockfileParser.new(@custom_lockfile.read).dependencies
115
121
  else
116
122
  {}
117
123
  end
124
+ ensure
125
+ ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
118
126
  end,
119
127
  T.nilable(T::Hash[String, T.untyped]),
120
128
  )
121
- ensure
122
- ENV.delete("BUNDLE_GEMFILE")
123
129
  end
124
130
 
125
131
  sig { void }
@@ -132,7 +138,7 @@ module RubyLsp
132
138
 
133
139
  # If there's a top level Gemfile, we want to evaluate from the custom bundle. We get the source from the top level
134
140
  # Gemfile, so if there isn't one we need to add a default source
135
- if @gemfile&.exist?
141
+ if @gemfile&.exist? && @lockfile&.exist?
136
142
  parts << "eval_gemfile(File.expand_path(\"../#{@gemfile_name}\", __dir__))"
137
143
  else
138
144
  parts.unshift('source "https://rubygems.org"')
@@ -156,14 +162,15 @@ module RubyLsp
156
162
  @custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content
157
163
  end
158
164
 
159
- sig { returns(T::Hash[String, T.untyped]) }
165
+ sig { returns([T::Hash[String, T.untyped], T.nilable(Gem::Version)]) }
160
166
  def load_dependencies
161
- return {} unless @lockfile&.exist?
167
+ return [{}, nil] unless @lockfile&.exist?
162
168
 
163
169
  # We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more
164
170
  # convenient, we may end up with issues when the globally installed `ruby-lsp` version mismatches the one included
165
171
  # in the `Gemfile`
166
- dependencies = Bundler::LockfileParser.new(@lockfile.read).dependencies
172
+ lockfile_parser = Bundler::LockfileParser.new(@lockfile.read)
173
+ dependencies = lockfile_parser.dependencies
167
174
 
168
175
  # When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we
169
176
  # check those as well or else we may get version mismatch errors. Notice that bundler allows more than one
@@ -172,7 +179,7 @@ module RubyLsp
172
179
  dependencies.merge!(Bundler.load_gemspec(path).dependencies.to_h { |dep| [dep.name, dep] })
173
180
  end
174
181
 
175
- dependencies
182
+ [dependencies, lockfile_parser.bundler_version]
176
183
  end
177
184
 
178
185
  sig { params(bundle_gemfile: T.nilable(Pathname)).returns(T::Hash[String, String]) }
@@ -188,6 +195,54 @@ module RubyLsp
188
195
  env["BUNDLE_PATH"] = File.expand_path(env["BUNDLE_PATH"], @project_path)
189
196
  end
190
197
 
198
+ return run_bundle_install_through_command(env) unless @launcher
199
+
200
+ # This same check happens conditionally when running through the command. For invoking the CLI directly, it's
201
+ # important that we ensure the Bundler version is set to avoid restarts
202
+ if @bundler_version
203
+ env["BUNDLER_VERSION"] = @bundler_version.to_s
204
+ install_bundler_if_needed
205
+ end
206
+
207
+ begin
208
+ run_bundle_install_directly(env)
209
+ # If no error occurred, then clear previous errors
210
+ @error_path.delete if @error_path.exist?
211
+ $stderr.puts("Ruby LSP> Composed bundle installation complete")
212
+ rescue => e
213
+ # Write the error object to a file so that we can read it from the parent process
214
+ @error_path.write(Marshal.dump(e))
215
+ end
216
+
217
+ env
218
+ end
219
+
220
+ sig { params(env: T::Hash[String, String]).returns(T::Hash[String, String]) }
221
+ def run_bundle_install_directly(env)
222
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
223
+ T.unsafe(ENV).merge!(env)
224
+
225
+ unless should_bundle_update?
226
+ Bundler::CLI::Install.new({}).run
227
+ correct_relative_remote_paths if @custom_lockfile.exist?
228
+ return env
229
+ end
230
+
231
+ # Try to auto upgrade the gems we depend on, unless they are in the Gemfile as that would result in undesired
232
+ # source control changes
233
+ gems = ["ruby-lsp", "debug", "prism"].reject { |dep| @dependencies[dep] }
234
+ gems << "ruby-lsp-rails" if @rails_app && !@dependencies["ruby-lsp-rails"]
235
+
236
+ Bundler::CLI::Update.new({ conservative: true }, gems).run
237
+ correct_relative_remote_paths if @custom_lockfile.exist?
238
+ @last_updated_path.write(Time.now.iso8601)
239
+ env
240
+ end
241
+
242
+ sig { params(env: T::Hash[String, String]).returns(T::Hash[String, String]) }
243
+ def run_bundle_install_through_command(env)
244
+ base_bundle = base_bundle_command(env)
245
+
191
246
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
192
247
  # to upgrade them or else we'll produce undesired source control changes. If the custom bundle was just created
193
248
  # and any of `ruby-lsp`, `ruby-lsp-rails` or `debug` weren't a part of the Gemfile, then we need to run `bundle
@@ -196,13 +251,13 @@ module RubyLsp
196
251
 
197
252
  # When not updating, we run `(bundle check || bundle install)`
198
253
  # When updating, we run `((bundle check && bundle update ruby-lsp debug) || bundle install)`
199
- command = +"(bundle check"
254
+ command = +"(#{base_bundle} check"
200
255
 
201
256
  if should_bundle_update?
202
257
  # If any of `ruby-lsp`, `ruby-lsp-rails` or `debug` are not in the Gemfile, try to update them to the latest
203
258
  # version
204
259
  command.prepend("(")
205
- command << " && bundle update "
260
+ command << " && #{base_bundle} update "
206
261
  command << "ruby-lsp " unless @dependencies["ruby-lsp"]
207
262
  command << "debug " unless @dependencies["debug"]
208
263
  command << "ruby-lsp-rails " if @rails_app && !@dependencies["ruby-lsp-rails"]
@@ -212,7 +267,7 @@ module RubyLsp
212
267
  @last_updated_path.write(Time.now.iso8601)
213
268
  end
214
269
 
215
- command << " || bundle install) "
270
+ command << " || #{base_bundle} install) "
216
271
 
217
272
  # Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
218
273
  # responses
@@ -225,7 +280,7 @@ module RubyLsp
225
280
  # Try to run the bundle install or update command. If that fails, it normally means that the custom lockfile is in
226
281
  # a bad state that no longer reflects the top level one. In that case, we can remove the whole directory, try
227
282
  # another time and give up if it fails again
228
- if !system(env, command) && !@retry && @custom_dir.exist?
283
+ if !system(env, command) && !@retry && @custom_gemfile.exist?
229
284
  @retry = true
230
285
  @custom_dir.rmtree
231
286
  $stderr.puts("Ruby LSP> Running bundle install failed. Trying to re-generate the custom bundle from scratch")
@@ -259,6 +314,15 @@ module RubyLsp
259
314
  end
260
315
  end
261
316
 
317
+ sig { void }
318
+ def install_bundler_if_needed
319
+ # Try to find the bundler version specified in the lockfile in installed gems. If not found, install it
320
+ requirement = Gem::Requirement.new(@bundler_version.to_s)
321
+ return if Gem::Specification.any? { |s| s.name == "bundler" && requirement =~ s.version }
322
+
323
+ Gem.install("bundler", @bundler_version.to_s)
324
+ end
325
+
262
326
  sig { returns(T::Boolean) }
263
327
  def should_bundle_update?
264
328
  # If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
@@ -290,7 +354,7 @@ module RubyLsp
290
354
 
291
355
  # We should only apply the correction if the remote is a relative path. It might also be a URI, like
292
356
  # `https://rubygems.org` or an absolute path, in which case we shouldn't do anything
293
- if path&.start_with?(".")
357
+ if path && !URI(path).scheme
294
358
  "remote: #{File.expand_path(path, T.must(@gemfile).dirname)}"
295
359
  else
296
360
  match
@@ -309,5 +373,49 @@ module RubyLsp
309
373
 
310
374
  /class .* < (::)?Rails::Application/.match?(application_contents)
311
375
  end
376
+
377
+ # Returns the base bundle command we should use for this project, which will be:
378
+ # - `bundle` if there's no locked Bundler version and no `bin/bundle` binstub in the $PATH
379
+ # - `bundle _<version>_` if there's a locked Bundler version
380
+ # - `bin/bundle` if there's a `bin/bundle` binstub in the $PATH
381
+ sig { params(env: T::Hash[String, String]).returns(String) }
382
+ def base_bundle_command(env)
383
+ path_parts = if Gem.win_platform?
384
+ ENV["Path"] || ENV["PATH"] || ENV["path"] || ""
385
+ else
386
+ ENV["PATH"] || ""
387
+ end.split(File::PATH_SEPARATOR)
388
+
389
+ bin_dir = File.expand_path("bin", @project_path)
390
+ bundle_binstub = File.join(@project_path, "bin", "bundle")
391
+
392
+ if File.exist?(bundle_binstub) && path_parts.any? { |path| File.expand_path(path, @project_path) == bin_dir }
393
+ return bundle_binstub
394
+ end
395
+
396
+ if @bundler_version
397
+ env["BUNDLER_VERSION"] = @bundler_version.to_s
398
+ install_bundler_if_needed
399
+ return "bundle _#{@bundler_version}_"
400
+ end
401
+
402
+ "bundle"
403
+ end
404
+
405
+ sig { void }
406
+ def patch_thor_to_print_progress_to_stderr!
407
+ return unless defined?(Bundler::Thor::Shell::Basic)
408
+
409
+ Bundler::Thor::Shell::Basic.prepend(Module.new do
410
+ extend T::Sig
411
+
412
+ sig { returns(IO) }
413
+ def stdout
414
+ $stderr
415
+ end
416
+ end)
417
+
418
+ Bundler.ui.level = :info
419
+ end
312
420
  end
313
421
  end
@@ -79,6 +79,14 @@ module RubyLsp
79
79
  params: Interface::LogMessageParams.new(type: type, message: message),
80
80
  )
81
81
  end
82
+
83
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(Notification) }
84
+ def telemetry(data)
85
+ new(
86
+ method: "telemetry/event",
87
+ params: data,
88
+ )
89
+ end
82
90
  end
83
91
 
84
92
  extend T::Sig
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-11 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -84,6 +84,7 @@ email:
84
84
  executables:
85
85
  - ruby-lsp
86
86
  - ruby-lsp-check
87
+ - ruby-lsp-launcher
87
88
  extensions: []
88
89
  extra_rdoc_files: []
89
90
  files:
@@ -92,6 +93,7 @@ files:
92
93
  - VERSION
93
94
  - exe/ruby-lsp
94
95
  - exe/ruby-lsp-check
96
+ - exe/ruby-lsp-launcher
95
97
  - lib/core_ext/uri.rb
96
98
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
97
99
  - lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb
@@ -121,6 +123,7 @@ files:
121
123
  - lib/ruby_indexer/test/test_case.rb
122
124
  - lib/ruby_lsp/addon.rb
123
125
  - lib/ruby_lsp/base_server.rb
126
+ - lib/ruby_lsp/client_capabilities.rb
124
127
  - lib/ruby_lsp/document.rb
125
128
  - lib/ruby_lsp/erb_document.rb
126
129
  - lib/ruby_lsp/global_state.rb
@@ -183,6 +186,8 @@ files:
183
186
  - lib/ruby_lsp/response_builders/signature_help.rb
184
187
  - lib/ruby_lsp/ruby_document.rb
185
188
  - lib/ruby_lsp/scope.rb
189
+ - lib/ruby_lsp/scripts/compose_bundle.rb
190
+ - lib/ruby_lsp/scripts/compose_bundle_windows.rb
186
191
  - lib/ruby_lsp/server.rb
187
192
  - lib/ruby_lsp/setup_bundler.rb
188
193
  - lib/ruby_lsp/static_docs.rb
@@ -212,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
217
  - !ruby/object:Gem::Version
213
218
  version: '0'
214
219
  requirements: []
215
- rubygems_version: 3.5.21
220
+ rubygems_version: 3.5.22
216
221
  signing_key:
217
222
  specification_version: 4
218
223
  summary: An opinionated language server for Ruby