ruby-lsp 0.20.0 → 0.21.0

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