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
@@ -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