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
@@ -121,7 +121,7 @@ module RubyLsp
121
121
  return Error::InvalidTargetRange if closest_node.is_a?(Prism::MissingNode)
122
122
 
123
123
  closest_node_loc = closest_node.location
124
- # If the parent expression is a single line block, then we have to extract it inside of the oneline block
124
+ # If the parent expression is a single line block, then we have to extract it inside of the one-line block
125
125
  if parent_statements.is_a?(Prism::BlockNode) &&
126
126
  parent_statements.location.start_line == parent_statements.location.end_line
127
127
 
@@ -17,7 +17,7 @@ module RubyLsp
17
17
  def provider
18
18
  Interface::CompletionOptions.new(
19
19
  resolve_provider: true,
20
- trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<"],
20
+ trigger_characters: ["/", "\"", "'", ":", "@", ".", "=", "<", "$"],
21
21
  completion_item: {
22
22
  labelDetailsSupport: true,
23
23
  },
@@ -50,6 +50,12 @@ module RubyLsp
50
50
  Prism::CallNode,
51
51
  Prism::ConstantReadNode,
52
52
  Prism::ConstantPathNode,
53
+ Prism::GlobalVariableAndWriteNode,
54
+ Prism::GlobalVariableOperatorWriteNode,
55
+ Prism::GlobalVariableOrWriteNode,
56
+ Prism::GlobalVariableReadNode,
57
+ Prism::GlobalVariableTargetNode,
58
+ Prism::GlobalVariableWriteNode,
53
59
  Prism::InstanceVariableReadNode,
54
60
  Prism::InstanceVariableAndWriteNode,
55
61
  Prism::InstanceVariableOperatorWriteNode,
@@ -34,7 +34,7 @@ module RubyLsp
34
34
 
35
35
  # Based on the spec https://microsoft.github.io/language-server-protocol/specification#textDocument_completion,
36
36
  # a completion resolve request must always return the original completion item without modifying ANY fields
37
- # other than detail and documentation (NOT labelDetails). If we modify anything, the completion behaviour might
37
+ # other than detail and documentation (NOT labelDetails). If we modify anything, the completion behavior might
38
38
  # be broken.
39
39
  #
40
40
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
@@ -12,12 +12,6 @@ module RubyLsp
12
12
  extend T::Sig
13
13
  extend T::Generic
14
14
 
15
- SPECIAL_METHOD_CALLS = [
16
- :require,
17
- :require_relative,
18
- :autoload,
19
- ].freeze
20
-
21
15
  sig do
22
16
  params(
23
17
  document: T.any(RubyDocument, ERBDocument),
@@ -46,7 +40,12 @@ module RubyLsp
46
40
  Prism::ConstantReadNode,
47
41
  Prism::ConstantPathNode,
48
42
  Prism::BlockArgumentNode,
43
+ Prism::GlobalVariableAndWriteNode,
44
+ Prism::GlobalVariableOperatorWriteNode,
45
+ Prism::GlobalVariableOrWriteNode,
49
46
  Prism::GlobalVariableReadNode,
47
+ Prism::GlobalVariableTargetNode,
48
+ Prism::GlobalVariableWriteNode,
50
49
  Prism::InstanceVariableReadNode,
51
50
  Prism::InstanceVariableAndWriteNode,
52
51
  Prism::InstanceVariableOperatorWriteNode,
@@ -72,11 +71,7 @@ module RubyLsp
72
71
  parent,
73
72
  position,
74
73
  )
75
- elsif target.is_a?(Prism::CallNode) && !SPECIAL_METHOD_CALLS.include?(target.message) && !covers_position?(
76
- target.message_loc, position
77
- )
78
- # If the target is a method call, we need to ensure that the requested position is exactly on top of the
79
- # method identifier. Otherwise, we risk showing definitions for unrelated things
74
+ elsif position_outside_target?(position, target)
80
75
  target = nil
81
76
  # For methods with block arguments using symbol-to-proc
82
77
  elsif target.is_a?(Prism::SymbolNode) && parent.is_a?(Prism::BlockArgumentNode)
@@ -107,6 +102,26 @@ module RubyLsp
107
102
  @dispatcher.dispatch_once(@target) if @target
108
103
  @response_builder.response
109
104
  end
105
+
106
+ private
107
+
108
+ sig { params(position: T::Hash[Symbol, T.untyped], target: T.nilable(Prism::Node)).returns(T::Boolean) }
109
+ def position_outside_target?(position, target)
110
+ case target
111
+ when Prism::GlobalVariableAndWriteNode,
112
+ Prism::GlobalVariableOperatorWriteNode,
113
+ Prism::GlobalVariableOrWriteNode,
114
+ Prism::GlobalVariableWriteNode,
115
+ Prism::InstanceVariableAndWriteNode,
116
+ Prism::InstanceVariableOperatorWriteNode,
117
+ Prism::InstanceVariableOrWriteNode,
118
+ Prism::InstanceVariableWriteNode
119
+
120
+ !covers_position?(target.name_loc, position)
121
+ else
122
+ false
123
+ end
124
+ end
110
125
  end
111
126
  end
112
127
  end
@@ -10,7 +10,8 @@ module RubyLsp
10
10
  # informs the editor of all the important symbols, such as classes, variables, and methods, defined in a file. With
11
11
  # this information, the editor can populate breadcrumbs, file outline and allow for fuzzy symbol searches.
12
12
  #
13
- # In VS Code, fuzzy symbol search can be accessed by opening the command palette and inserting an `@` symbol.
13
+ # In VS Code, symbol search known as 'Go To Symbol in Editor' and can be accessed with Ctrl/Cmd-Shift-O,
14
+ # or by opening the command palette and inserting an `@` symbol.
14
15
  class DocumentSymbol < Request
15
16
  extend T::Sig
16
17
 
@@ -46,17 +46,13 @@ module RubyLsp
46
46
  target = node_context.node
47
47
  parent = node_context.parent
48
48
 
49
- if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
50
- !Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
51
- (parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
49
+ if should_refine_target?(parent, target)
52
50
  target = determine_target(
53
51
  T.must(target),
54
52
  T.must(parent),
55
53
  position,
56
54
  )
57
- elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
58
- !covers_position?(target.message_loc, position)
59
-
55
+ elsif position_outside_target?(position, target)
60
56
  target = nil
61
57
  end
62
58
 
@@ -89,6 +85,28 @@ module RubyLsp
89
85
  ),
90
86
  )
91
87
  end
88
+
89
+ private
90
+
91
+ sig { params(parent: T.nilable(Prism::Node), target: T.nilable(Prism::Node)).returns(T::Boolean) }
92
+ def should_refine_target?(parent, target)
93
+ (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
94
+ !Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
95
+ (parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
96
+ end
97
+
98
+ sig { params(position: T::Hash[Symbol, T.untyped], target: T.nilable(Prism::Node)).returns(T::Boolean) }
99
+ def position_outside_target?(position, target)
100
+ case target
101
+ when Prism::GlobalVariableAndWriteNode,
102
+ Prism::GlobalVariableOperatorWriteNode,
103
+ Prism::GlobalVariableOrWriteNode,
104
+ Prism::GlobalVariableWriteNode
105
+ !covers_position?(target.name_loc, position)
106
+ else
107
+ false
108
+ end
109
+ end
92
110
  end
93
111
  end
94
112
  end
@@ -78,6 +78,8 @@ module RubyLsp
78
78
 
79
79
  parse_result = Prism.parse_file(path)
80
80
  collect_references(reference_target, parse_result, uri)
81
+ rescue Errno::EISDIR, Errno::ENOENT
82
+ # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
81
83
  end
82
84
 
83
85
  @store.each do |_uri, document|
@@ -72,7 +72,7 @@ module RubyLsp
72
72
 
73
73
  # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
74
74
  # text changes
75
- unless @global_state.supported_resource_operations.include?("rename")
75
+ unless @global_state.client_capabilities.supports_rename?
76
76
  return Interface::WorkspaceEdit.new(changes: changes)
77
77
  end
78
78
 
@@ -147,6 +147,8 @@ module RubyLsp
147
147
  parse_result = Prism.parse_file(path)
148
148
  edits = collect_changes(target, parse_result, name, uri)
149
149
  changes[uri.to_s] = edits unless edits.empty?
150
+ rescue Errno::EISDIR, Errno::ENOENT
151
+ # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
150
152
  end
151
153
 
152
154
  @store.each do |uri, document|
@@ -26,7 +26,7 @@ module RubyLsp
26
26
  ).void
27
27
  end
28
28
  def delegate_request_if_needed!(global_state, document, char_position)
29
- if global_state.supports_request_delegation &&
29
+ if global_state.client_capabilities.supports_request_delegation &&
30
30
  document.is_a?(ERBDocument) &&
31
31
  document.inside_host_language?(char_position)
32
32
  raise DelegateRequestError
@@ -1,16 +1,26 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ # If there's no top level Gemfile, don't load RuboCop from a global installation
5
+ begin
6
+ Bundler.with_original_env { Bundler.default_gemfile }
7
+ rescue Bundler::GemfileNotFound
8
+ return
9
+ end
10
+
11
+ # Ensure that RuboCop is available
4
12
  begin
5
13
  require "rubocop"
6
14
  rescue LoadError
7
15
  return
8
16
  end
9
17
 
18
+ # Ensure that RuboCop is at least version 1.4.0
10
19
  begin
11
20
  gem("rubocop", ">= 1.4.0")
12
21
  rescue LoadError
13
- raise StandardError, "Incompatible RuboCop version. Ruby LSP requires >= 1.4.0"
22
+ $stderr.puts "Incompatible RuboCop version. Ruby LSP requires >= 1.4.0"
23
+ return
14
24
  end
15
25
 
16
26
  if RuboCop.const_defined?(:LSP) # This condition will be removed when requiring RuboCop >= 1.61.
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ def compose(raw_initialize)
5
+ require_relative "../setup_bundler"
6
+ require "json"
7
+ require "uri"
8
+ require_relative "../../core_ext/uri"
9
+
10
+ initialize_request = JSON.parse(raw_initialize, symbolize_names: true)
11
+ workspace_uri = initialize_request.dig(:params, :workspaceFolders, 0, :uri)
12
+ workspace_path = workspace_uri && URI(workspace_uri).to_standardized_path
13
+ workspace_path ||= Dir.pwd
14
+
15
+ env = RubyLsp::SetupBundler.new(workspace_path, launcher: true).setup!
16
+ File.write(
17
+ File.join(".ruby-lsp", "bundle_env"),
18
+ env.map { |k, v| "#{k}=#{v}" }.join("\n"),
19
+ )
20
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "compose_bundle"
5
+
6
+ # When this is invoked on Windows, we pass the raw initialize as an argument to this script. On other platforms, we
7
+ # invoke the compose method from inside a forked process
8
+ compose(ARGV.first)
@@ -81,6 +81,8 @@ module RubyLsp
81
81
  workspace_did_change_watched_files(message)
82
82
  when "workspace/symbol"
83
83
  workspace_symbol(message)
84
+ when "window/showMessageRequest"
85
+ window_show_message_request(message)
84
86
  when "rubyLsp/textDocument/showSyntaxTree"
85
87
  text_document_show_syntax_tree(message)
86
88
  when "rubyLsp/workspace/dependencies"
@@ -140,6 +142,11 @@ module RubyLsp
140
142
 
141
143
  sig { params(include_project_addons: T::Boolean).void }
142
144
  def load_addons(include_project_addons: true)
145
+ # If invoking Bundler.setup failed, then the load path will not be configured properly and trying to load add-ons
146
+ # with Gem.find_files will find every single version installed of an add-on, leading to requiring several
147
+ # different versions of the same files. We cannot load add-ons if Bundler.setup failed
148
+ return if @setup_error
149
+
143
150
  errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
144
151
 
145
152
  if errors.any?
@@ -254,12 +261,13 @@ module RubyLsp
254
261
  version: VERSION,
255
262
  },
256
263
  formatter: @global_state.formatter,
264
+ degraded_mode: !!(@install_error || @setup_error),
257
265
  }
258
266
 
259
267
  send_message(Result.new(id: message[:id], response: response))
260
268
 
261
269
  # Not every client supports dynamic registration or file watching
262
- if global_state.supports_watching_files
270
+ if @global_state.client_capabilities.supports_watching_files
263
271
  send_message(
264
272
  Request.new(
265
273
  id: @current_request_id,
@@ -290,6 +298,24 @@ module RubyLsp
290
298
  begin_progress("indexing-progress", "Ruby LSP: indexing files")
291
299
 
292
300
  global_state_notifications.each { |notification| send_message(notification) }
301
+
302
+ if @setup_error
303
+ send_message(Notification.telemetry(
304
+ type: "error",
305
+ errorMessage: @setup_error.message,
306
+ errorClass: @setup_error.class,
307
+ stack: @setup_error.backtrace&.join("\n"),
308
+ ))
309
+ end
310
+
311
+ if @install_error
312
+ send_message(Notification.telemetry(
313
+ type: "error",
314
+ errorMessage: @install_error.message,
315
+ errorClass: @install_error.class,
316
+ stack: @install_error.backtrace&.join("\n"),
317
+ ))
318
+ end
293
319
  end
294
320
 
295
321
  sig { void }
@@ -297,20 +323,22 @@ module RubyLsp
297
323
  load_addons
298
324
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
299
325
 
300
- if defined?(Requests::Support::RuboCopFormatter)
301
- begin
302
- @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
303
- rescue RuboCop::Error => e
304
- # The user may have provided unknown config switches in .rubocop or
305
- # is trying to load a non-existant config file.
306
- send_message(Notification.window_show_message(
307
- "RuboCop configuration error: #{e.message}. Formatting will not be available.",
308
- type: Constant::MessageType::ERROR,
309
- ))
326
+ unless @setup_error
327
+ if defined?(Requests::Support::RuboCopFormatter)
328
+ begin
329
+ @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
330
+ rescue RuboCop::Error => e
331
+ # The user may have provided unknown config switches in .rubocop or
332
+ # is trying to load a non-existent config file.
333
+ send_message(Notification.window_show_message(
334
+ "RuboCop configuration error: #{e.message}. Formatting will not be available.",
335
+ type: Constant::MessageType::ERROR,
336
+ ))
337
+ end
338
+ end
339
+ if defined?(Requests::Support::SyntaxTreeFormatter)
340
+ @global_state.register_formatter("syntax_tree", Requests::Support::SyntaxTreeFormatter.new)
310
341
  end
311
- end
312
- if defined?(Requests::Support::SyntaxTreeFormatter)
313
- @global_state.register_formatter("syntax_tree", Requests::Support::SyntaxTreeFormatter.new)
314
342
  end
315
343
 
316
344
  perform_initial_indexing
@@ -1017,7 +1045,7 @@ module RubyLsp
1017
1045
 
1018
1046
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1019
1047
  def workspace_dependencies(message)
1020
- response = begin
1048
+ response = if @global_state.top_level_bundle
1021
1049
  Bundler.with_original_env do
1022
1050
  definition = Bundler.definition
1023
1051
  dep_keys = definition.locked_deps.keys.to_set
@@ -1031,7 +1059,7 @@ module RubyLsp
1031
1059
  }
1032
1060
  end
1033
1061
  end
1034
- rescue Bundler::GemfileNotFound
1062
+ else
1035
1063
  []
1036
1064
  end
1037
1065
 
@@ -1138,6 +1166,7 @@ module RubyLsp
1138
1166
 
1139
1167
  sig { void }
1140
1168
  def check_formatter_is_available
1169
+ return if @setup_error
1141
1170
  # Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
1142
1171
  # Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
1143
1172
  return unless @global_state.formatter == "rubocop"
@@ -1194,5 +1223,14 @@ module RubyLsp
1194
1223
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1195
1224
  configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1196
1225
  end
1226
+
1227
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
1228
+ def window_show_message_request(message)
1229
+ addon_name = message[:addon_name]
1230
+ addon = Addon.addons.find { |addon| addon.name == addon_name }
1231
+ return unless addon
1232
+
1233
+ addon.handle_window_show_message_response(message[:title])
1234
+ end
1197
1235
  end
1198
1236
  end