ruby-lsp 0.18.4 → 0.19.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp-check +1 -1
  4. data/lib/core_ext/uri.rb +9 -4
  5. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +66 -8
  7. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +63 -32
  8. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -5
  9. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +38 -4
  10. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +324 -0
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +23 -0
  13. data/lib/ruby_indexer/test/constant_test.rb +8 -0
  14. data/lib/ruby_indexer/test/enhancements_test.rb +2 -0
  15. data/lib/ruby_indexer/test/index_test.rb +3 -0
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +12 -0
  17. data/lib/ruby_indexer/test/method_test.rb +10 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +14 -0
  19. data/lib/ruby_indexer/test/reference_finder_test.rb +242 -0
  20. data/lib/ruby_lsp/addon.rb +69 -7
  21. data/lib/ruby_lsp/erb_document.rb +9 -3
  22. data/lib/ruby_lsp/global_state.rb +8 -0
  23. data/lib/ruby_lsp/internal.rb +4 -0
  24. data/lib/ruby_lsp/listeners/completion.rb +1 -1
  25. data/lib/ruby_lsp/listeners/hover.rb +19 -0
  26. data/lib/ruby_lsp/requests/code_action_resolve.rb +9 -3
  27. data/lib/ruby_lsp/requests/completion.rb +1 -0
  28. data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
  29. data/lib/ruby_lsp/requests/definition.rb +1 -0
  30. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  31. data/lib/ruby_lsp/requests/hover.rb +1 -0
  32. data/lib/ruby_lsp/requests/range_formatting.rb +55 -0
  33. data/lib/ruby_lsp/requests/references.rb +146 -0
  34. data/lib/ruby_lsp/requests/rename.rb +196 -0
  35. data/lib/ruby_lsp/requests/signature_help.rb +6 -1
  36. data/lib/ruby_lsp/requests/support/formatter.rb +3 -0
  37. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +6 -0
  38. data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
  39. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +8 -0
  40. data/lib/ruby_lsp/ruby_document.rb +23 -8
  41. data/lib/ruby_lsp/server.rb +98 -10
  42. data/lib/ruby_lsp/static_docs.rb +15 -0
  43. data/lib/ruby_lsp/store.rb +12 -0
  44. data/lib/ruby_lsp/test_helper.rb +1 -1
  45. data/lib/ruby_lsp/type_inferrer.rb +6 -1
  46. data/static_docs/yield.md +81 -0
  47. metadata +20 -7
@@ -0,0 +1,196 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The
7
+ # [rename](https://microsoft.github.io/language-server-protocol/specification#textDocument_rename)
8
+ # request renames all instances of a symbol in a document.
9
+ class Rename < Request
10
+ extend T::Sig
11
+ include Support::Common
12
+
13
+ class InvalidNameError < StandardError; end
14
+
15
+ sig do
16
+ params(
17
+ global_state: GlobalState,
18
+ store: Store,
19
+ document: T.any(RubyDocument, ERBDocument),
20
+ params: T::Hash[Symbol, T.untyped],
21
+ ).void
22
+ end
23
+ def initialize(global_state, store, document, params)
24
+ super()
25
+ @global_state = global_state
26
+ @store = store
27
+ @document = document
28
+ @position = T.let(params[:position], T::Hash[Symbol, Integer])
29
+ @new_name = T.let(params[:newName], String)
30
+ end
31
+
32
+ sig { override.returns(T.nilable(Interface::WorkspaceEdit)) }
33
+ def perform
34
+ char_position = @document.create_scanner.find_char_position(@position)
35
+
36
+ node_context = RubyDocument.locate(
37
+ @document.parse_result.value,
38
+ char_position,
39
+ node_types: [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode],
40
+ encoding: @global_state.encoding,
41
+ )
42
+ target = node_context.node
43
+ parent = node_context.parent
44
+ return if !target || target.is_a?(Prism::ProgramNode)
45
+
46
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
47
+ target = determine_target(
48
+ target,
49
+ parent,
50
+ @position,
51
+ )
52
+ end
53
+
54
+ target = T.cast(
55
+ target,
56
+ T.any(Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode),
57
+ )
58
+
59
+ name = constant_name(target)
60
+ return unless name
61
+
62
+ entries = @global_state.index.resolve(name, node_context.nesting)
63
+ return unless entries
64
+
65
+ if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
66
+ raise InvalidNameError, "The new name is already in use by #{T.must(conflict_entries.first).name}"
67
+ end
68
+
69
+ fully_qualified_name = T.must(entries.first).name
70
+ reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
71
+ changes = collect_text_edits(reference_target, name)
72
+
73
+ # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
74
+ # text changes
75
+ unless @global_state.supported_resource_operations.include?("rename")
76
+ return Interface::WorkspaceEdit.new(changes: changes)
77
+ end
78
+
79
+ # Text edits must be applied before any resource operations, such as renaming files. Otherwise, the file is
80
+ # renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
81
+ document_changes = changes.map do |uri, edits|
82
+ Interface::TextDocumentEdit.new(
83
+ text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
84
+ edits: edits,
85
+ )
86
+ end
87
+
88
+ collect_file_renames(fully_qualified_name, document_changes)
89
+ Interface::WorkspaceEdit.new(document_changes: document_changes)
90
+ end
91
+
92
+ private
93
+
94
+ sig do
95
+ params(
96
+ fully_qualified_name: String,
97
+ document_changes: T::Array[T.any(Interface::RenameFile, Interface::TextDocumentEdit)],
98
+ ).void
99
+ end
100
+ def collect_file_renames(fully_qualified_name, document_changes)
101
+ # Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
102
+ # rename the files for the user.
103
+ #
104
+ # We also look for an associated test file and rename it too
105
+ short_name = T.must(fully_qualified_name.split("::").last)
106
+
107
+ T.must(@global_state.index[fully_qualified_name]).each do |entry|
108
+ # Do not rename files that are not part of the workspace
109
+ next unless entry.file_path.start_with?(@global_state.workspace_path)
110
+
111
+ case entry
112
+ when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
113
+ RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
114
+
115
+ file_name = file_from_constant_name(short_name)
116
+
117
+ if "#{file_name}.rb" == entry.file_name
118
+ new_file_name = file_from_constant_name(T.must(@new_name.split("::").last))
119
+
120
+ old_uri = URI::Generic.from_path(path: entry.file_path).to_s
121
+ new_uri = URI::Generic.from_path(path: File.join(
122
+ File.dirname(entry.file_path),
123
+ "#{new_file_name}.rb",
124
+ )).to_s
125
+
126
+ document_changes << Interface::RenameFile.new(kind: "rename", old_uri: old_uri, new_uri: new_uri)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ sig do
133
+ params(
134
+ target: RubyIndexer::ReferenceFinder::Target,
135
+ name: String,
136
+ ).returns(T::Hash[String, T::Array[Interface::TextEdit]])
137
+ end
138
+ def collect_text_edits(target, name)
139
+ changes = {}
140
+
141
+ Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
142
+ uri = URI::Generic.from_path(path: path)
143
+ # If the document is being managed by the client, then we should use whatever is present in the store instead
144
+ # of reading from disk
145
+ next if @store.key?(uri)
146
+
147
+ parse_result = Prism.parse_file(path)
148
+ edits = collect_changes(target, parse_result, name, uri)
149
+ changes[uri.to_s] = edits unless edits.empty?
150
+ end
151
+
152
+ @store.each do |uri, document|
153
+ edits = collect_changes(target, document.parse_result, name, document.uri)
154
+ changes[uri] = edits unless edits.empty?
155
+ end
156
+
157
+ changes
158
+ end
159
+
160
+ sig do
161
+ params(
162
+ target: RubyIndexer::ReferenceFinder::Target,
163
+ parse_result: Prism::ParseResult,
164
+ name: String,
165
+ uri: URI::Generic,
166
+ ).returns(T::Array[Interface::TextEdit])
167
+ end
168
+ def collect_changes(target, parse_result, name, uri)
169
+ dispatcher = Prism::Dispatcher.new
170
+ finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher)
171
+ dispatcher.visit(parse_result.value)
172
+
173
+ finder.references.map do |reference|
174
+ adjust_reference_for_edit(name, reference)
175
+ end
176
+ end
177
+
178
+ sig { params(name: String, reference: RubyIndexer::ReferenceFinder::Reference).returns(Interface::TextEdit) }
179
+ def adjust_reference_for_edit(name, reference)
180
+ # The reference may include a namespace in front. We need to check if the rename new name includes namespaces
181
+ # and then adjust both the text and the location to produce the correct edit
182
+ location = reference.location
183
+ new_text = reference.name.sub(name, @new_name)
184
+
185
+ Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
186
+ end
187
+
188
+ sig { params(constant_name: String).returns(String) }
189
+ def file_from_constant_name(constant_name)
190
+ constant_name
191
+ .gsub(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/, '\1\3_\2\4')
192
+ .downcase
193
+ end
194
+ end
195
+ end
196
+ end
@@ -39,7 +39,12 @@ module RubyLsp
39
39
  char_position = document.create_scanner.find_char_position(position)
40
40
  delegate_request_if_needed!(global_state, document, char_position)
41
41
 
42
- node_context = RubyDocument.locate(document.parse_result.value, char_position, node_types: [Prism::CallNode])
42
+ node_context = RubyDocument.locate(
43
+ document.parse_result.value,
44
+ char_position,
45
+ node_types: [Prism::CallNode],
46
+ encoding: global_state.encoding,
47
+ )
43
48
 
44
49
  target = adjust_for_nested_target(node_context.node, node_context.parent, position)
45
50
 
@@ -13,6 +13,9 @@ module RubyLsp
13
13
  sig { abstract.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) }
14
14
  def run_formatting(uri, document); end
15
15
 
16
+ sig { abstract.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
17
+ def run_range_formatting(uri, source, base_indentation); end
18
+
16
19
  sig do
17
20
  abstract.params(
18
21
  uri: URI::Generic,
@@ -28,6 +28,12 @@ module RubyLsp
28
28
  @format_runner.formatted_source
29
29
  end
30
30
 
31
+ # RuboCop does not support range formatting
32
+ sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
33
+ def run_range_formatting(uri, source, base_indentation)
34
+ nil
35
+ end
36
+
31
37
  sig do
32
38
  override.params(
33
39
  uri: URI::Generic,
@@ -19,6 +19,13 @@ module URI
19
19
  T::Array[Symbol],
20
20
  )
21
21
 
22
+ # `uri` for Ruby 3.4 switched the default parser from RFC2396 to RFC3986. The new parser emits a deprecation
23
+ # warning on a few methods and delegates them to RFC2396, namely `extract`/`make_regexp`/`escape`/`unescape`.
24
+ # On earlier versions of the uri gem, the RFC2396_PARSER constant doesn't exist, so it needs some special
25
+ # handling to select a parser that doesn't emit deprecations. While it was backported to Ruby 3.1, users may
26
+ # have the uri gem in their own bundle and thus not use a compatible version.
27
+ PARSER = T.let(const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER, RFC2396_Parser)
28
+
22
29
  T.unsafe(self).alias_method(:gem_name, :host)
23
30
  T.unsafe(self).alias_method(:line_number, :fragment)
24
31
 
@@ -41,7 +48,7 @@ module URI
41
48
  {
42
49
  scheme: "source",
43
50
  host: gem_name,
44
- path: DEFAULT_PARSER.escape("/#{gem_version}/#{path}"),
51
+ path: PARSER.escape("/#{gem_version}/#{path}"),
45
52
  fragment: line_number,
46
53
  }
47
54
  )
@@ -37,6 +37,14 @@ module RubyLsp
37
37
  SyntaxTree.format(document.source, @options.print_width, options: @options.formatter_options)
38
38
  end
39
39
 
40
+ sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
41
+ def run_range_formatting(uri, source, base_indentation)
42
+ path = uri.to_standardized_path
43
+ return if path && @options.ignore_files.any? { |pattern| File.fnmatch?("*/#{pattern}", path) }
44
+
45
+ SyntaxTree.format(source, @options.print_width, base_indentation, options: @options.formatter_options)
46
+ end
47
+
40
48
  sig do
41
49
  override.params(
42
50
  uri: URI::Generic,
@@ -26,9 +26,10 @@ module RubyLsp
26
26
  node: Prism::Node,
27
27
  char_position: Integer,
28
28
  node_types: T::Array[T.class_of(Prism::Node)],
29
+ encoding: Encoding,
29
30
  ).returns(NodeContext)
30
31
  end
31
- def locate(node, char_position, node_types: [])
32
+ def locate(node, char_position, node_types: [], encoding: Encoding::UTF_8)
32
33
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
33
34
  closest = node
34
35
  parent = T.let(nil, T.nilable(Prism::Node))
@@ -61,16 +62,21 @@ module RubyLsp
61
62
 
62
63
  # Skip if the current node doesn't cover the desired position
63
64
  loc = candidate.location
64
- next unless (loc.start_offset...loc.end_offset).cover?(char_position)
65
+ loc_start_offset = loc.start_code_units_offset(encoding)
66
+ loc_end_offset = loc.end_code_units_offset(encoding)
67
+ next unless (loc_start_offset...loc_end_offset).cover?(char_position)
65
68
 
66
69
  # If the node's start character is already past the position, then we should've found the closest node
67
70
  # already
68
- break if char_position < loc.start_offset
71
+ break if char_position < loc_start_offset
69
72
 
70
73
  # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
71
74
  # and need to pop the stack
72
75
  previous_level = nesting_nodes.last
73
- nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
76
+ if previous_level &&
77
+ (loc_start_offset > previous_level.location.end_code_units_offset(encoding))
78
+ nesting_nodes.pop
79
+ end
74
80
 
75
81
  # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
76
82
  # the target when it is a constant
@@ -83,8 +89,10 @@ module RubyLsp
83
89
  if candidate.is_a?(Prism::CallNode)
84
90
  arg_loc = candidate.arguments&.location
85
91
  blk_loc = candidate.block&.location
86
- if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
87
- (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
92
+ if (arg_loc && (arg_loc.start_code_units_offset(encoding)...
93
+ arg_loc.end_code_units_offset(encoding)).cover?(char_position)) ||
94
+ (blk_loc && (blk_loc.start_code_units_offset(encoding)...
95
+ blk_loc.end_code_units_offset(encoding)).cover?(char_position))
88
96
  call_node = candidate
89
97
  end
90
98
  end
@@ -94,7 +102,9 @@ module RubyLsp
94
102
 
95
103
  # If the current node is narrower than or equal to the previous closest node, then it is more precise
96
104
  closest_loc = closest.location
97
- if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
105
+ closest_node_start_offset = closest_loc.start_code_units_offset(encoding)
106
+ closest_node_end_offset = closest_loc.end_code_units_offset(encoding)
107
+ if loc_end_offset - loc_start_offset <= closest_node_end_offset - closest_node_start_offset
98
108
  parent = closest
99
109
  closest = candidate
100
110
  end
@@ -201,7 +211,12 @@ module RubyLsp
201
211
  ).returns(NodeContext)
202
212
  end
203
213
  def locate_node(position, node_types: [])
204
- RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
214
+ RubyDocument.locate(
215
+ @parse_result.value,
216
+ create_scanner.find_char_position(position),
217
+ node_types: node_types,
218
+ encoding: @encoding,
219
+ )
205
220
  end
206
221
  end
207
222
  end
@@ -43,6 +43,8 @@ module RubyLsp
43
43
  text_document_semantic_tokens_range(message)
44
44
  when "textDocument/formatting"
45
45
  text_document_formatting(message)
46
+ when "textDocument/rangeFormatting"
47
+ text_document_range_formatting(message)
46
48
  when "textDocument/documentHighlight"
47
49
  text_document_document_highlight(message)
48
50
  when "textDocument/onTypeFormatting"
@@ -67,6 +69,10 @@ module RubyLsp
67
69
  text_document_definition(message)
68
70
  when "textDocument/prepareTypeHierarchy"
69
71
  text_document_prepare_type_hierarchy(message)
72
+ when "textDocument/rename"
73
+ text_document_rename(message)
74
+ when "textDocument/references"
75
+ text_document_references(message)
70
76
  when "typeHierarchy/supertypes"
71
77
  type_hierarchy_supertypes(message)
72
78
  when "typeHierarchy/subtypes"
@@ -85,7 +91,16 @@ module RubyLsp
85
91
  id: message[:id],
86
92
  response:
87
93
  Addon.addons.map do |addon|
88
- { name: addon.name, errored: addon.error? }
94
+ version_method = addon.method(:version)
95
+
96
+ # If the add-on doesn't define a `version` method, we'd be calling the abstract method defined by
97
+ # Sorbet, which would raise an error.
98
+ # Therefore, we only call the method if it's defined by the add-on itself
99
+ if version_method.owner != Addon
100
+ version = addon.version
101
+ end
102
+
103
+ { name: addon.name, version: version, errored: addon.error? }
89
104
  end,
90
105
  ),
91
106
  )
@@ -123,9 +138,9 @@ module RubyLsp
123
138
  send_log_message("Error processing #{message[:method]}: #{e.full_message}", type: Constant::MessageType::ERROR)
124
139
  end
125
140
 
126
- sig { void }
127
- def load_addons
128
- errors = Addon.load_addons(@global_state, @outgoing_queue)
141
+ sig { params(include_project_addons: T::Boolean).void }
142
+ def load_addons(include_project_addons: true)
143
+ errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
129
144
 
130
145
  if errors.any?
131
146
  send_log_message(
@@ -227,6 +242,9 @@ module RubyLsp
227
242
  workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.has_type_checker,
228
243
  signature_help_provider: signature_help_provider,
229
244
  type_hierarchy_provider: type_hierarchy_provider,
245
+ rename_provider: !@global_state.has_type_checker,
246
+ references_provider: !@global_state.has_type_checker,
247
+ document_range_formatting_provider: true,
230
248
  experimental: {
231
249
  addon_detection: true,
232
250
  },
@@ -320,14 +338,18 @@ module RubyLsp
320
338
  language_id: language_id,
321
339
  )
322
340
 
323
- if document.past_expensive_limit?
341
+ if document.past_expensive_limit? && text_document[:uri].scheme == "file"
342
+ log_message = <<~MESSAGE
343
+ The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
344
+ diagnostics will be disabled.
345
+ MESSAGE
346
+
324
347
  send_message(
325
348
  Notification.new(
326
- method: "window/showMessage",
327
- params: Interface::ShowMessageParams.new(
349
+ method: "window/logMessage",
350
+ params: Interface::LogMessageParams.new(
328
351
  type: Constant::MessageType::WARNING,
329
- message: "This file is too long. For performance reasons, semantic highlighting and " \
330
- "diagnostics will be disabled",
352
+ message: log_message,
331
353
  ),
332
354
  ),
333
355
  )
@@ -506,6 +528,34 @@ module RubyLsp
506
528
  send_message(Result.new(id: message[:id], response: request.perform))
507
529
  end
508
530
 
531
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
532
+ def text_document_range_formatting(message)
533
+ # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
534
+ if @global_state.formatter == "none"
535
+ send_empty_response(message[:id])
536
+ return
537
+ end
538
+
539
+ params = message[:params]
540
+ uri = params.dig(:textDocument, :uri)
541
+ # Do not format files outside of the workspace. For example, if someone is looking at a gem's source code, we
542
+ # don't want to format it
543
+ path = uri.to_standardized_path
544
+ unless path.nil? || path.start_with?(@global_state.workspace_path)
545
+ send_empty_response(message[:id])
546
+ return
547
+ end
548
+
549
+ document = @store.get(uri)
550
+ unless document.is_a?(RubyDocument)
551
+ send_empty_response(message[:id])
552
+ return
553
+ end
554
+
555
+ response = Requests::RangeFormatting.new(@global_state, document, params).perform
556
+ send_message(Result.new(id: message[:id], response: response))
557
+ end
558
+
509
559
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
510
560
  def text_document_formatting(message)
511
561
  # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
@@ -609,6 +659,44 @@ module RubyLsp
609
659
  )
610
660
  end
611
661
 
662
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
663
+ def text_document_rename(message)
664
+ params = message[:params]
665
+ document = @store.get(params.dig(:textDocument, :uri))
666
+
667
+ unless document.is_a?(RubyDocument)
668
+ send_empty_response(message[:id])
669
+ return
670
+ end
671
+
672
+ send_message(
673
+ Result.new(
674
+ id: message[:id],
675
+ response: Requests::Rename.new(@global_state, @store, document, params).perform,
676
+ ),
677
+ )
678
+ rescue Requests::Rename::InvalidNameError => e
679
+ send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::REQUEST_FAILED, message: e.message))
680
+ end
681
+
682
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
683
+ def text_document_references(message)
684
+ params = message[:params]
685
+ document = @store.get(params.dig(:textDocument, :uri))
686
+
687
+ unless document.is_a?(RubyDocument)
688
+ send_empty_response(message[:id])
689
+ return
690
+ end
691
+
692
+ send_message(
693
+ Result.new(
694
+ id: message[:id],
695
+ response: Requests::References.new(@global_state, @store, document, params).perform,
696
+ ),
697
+ )
698
+ end
699
+
612
700
  sig { params(document: Document[T.untyped]).returns(RubyDocument::SorbetLevel) }
613
701
  def sorbet_level(document)
614
702
  return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
@@ -684,7 +772,7 @@ module RubyLsp
684
772
  return
685
773
  end
686
774
 
687
- result = Requests::CodeActionResolve.new(document, params).perform
775
+ result = Requests::CodeActionResolve.new(document, @global_state, params).perform
688
776
 
689
777
  case result
690
778
  when Requests::CodeActionResolve::Error::EmptySelection
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # The path to the `static_docs` directory, where we keep long-form static documentation
6
+ STATIC_DOCS_PATH = T.let(File.join(File.dirname(File.dirname(T.must(__dir__))), "static_docs"), String)
7
+
8
+ # A map of keyword => short documentation to be displayed on hover or completion
9
+ KEYWORD_DOCS = T.let(
10
+ {
11
+ "yield" => "Invokes the passed block with the given arguments",
12
+ }.freeze,
13
+ T::Hash[String, String],
14
+ )
15
+ end
@@ -99,6 +99,18 @@ module RubyLsp
99
99
  @state.delete(uri.to_s)
100
100
  end
101
101
 
102
+ sig { params(uri: URI::Generic).returns(T::Boolean) }
103
+ def key?(uri)
104
+ @state.key?(uri.to_s)
105
+ end
106
+
107
+ sig { params(block: T.proc.params(uri: String, document: Document[T.untyped]).void).void }
108
+ def each(&block)
109
+ @state.each do |uri, document|
110
+ block.call(uri, document)
111
+ end
112
+ end
113
+
102
114
  sig do
103
115
  type_parameters(:T)
104
116
  .params(
@@ -42,7 +42,7 @@ module RubyLsp
42
42
  RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)),
43
43
  source,
44
44
  )
45
- server.load_addons if load_addons
45
+ server.load_addons(include_project_addons: false) if load_addons
46
46
  block.call(server, uri)
47
47
  ensure
48
48
  if load_addons
@@ -89,7 +89,12 @@ module RubyLsp
89
89
 
90
90
  Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
91
91
  else
92
- raw_receiver = node.receiver&.slice
92
+
93
+ raw_receiver = if receiver.is_a?(Prism::CallNode)
94
+ receiver.message
95
+ else
96
+ receiver&.slice
97
+ end
93
98
 
94
99
  if raw_receiver
95
100
  guessed_name = raw_receiver
@@ -0,0 +1,81 @@
1
+ # Yield
2
+
3
+ In Ruby, every method implicitly accepts a block, even when not included in the parameters list.
4
+
5
+ ```ruby
6
+ def foo
7
+ end
8
+
9
+ foo { 123 } # works!
10
+ ```
11
+
12
+ The `yield` keyword is used to invoke the block that was passed with arguments.
13
+
14
+ ```ruby
15
+ # Consider this method call. The block being passed to the method `foo` accepts an argument called `a`.
16
+ # It then takes whatever argument was passed and multiplies it by 2
17
+ foo do |a|
18
+ a * 2
19
+ end
20
+
21
+ # In the `foo` method declaration, we can use `yield` to invoke the block that was passed and provide the block
22
+ # with the value for the `a` argument
23
+ def foo
24
+ # Invoke the block passed to `foo` with the number 10 as the argument `a`
25
+ result = yield(10)
26
+ puts result # Will print 20
27
+ end
28
+ ```
29
+
30
+ If `yield` is used to invoke the block, but no block was passed, that will result in a local jump error.
31
+
32
+ ```ruby
33
+ # If we invoke `foo` without a block, trying to `yield` will fail
34
+ foo
35
+
36
+ # `foo': no block given (yield) (LocalJumpError)
37
+ ```
38
+
39
+ We can decide to use `yield` conditionally by using Ruby's `block_given?` method, which will return `true` if a block
40
+ was passed to the method.
41
+
42
+ ```ruby
43
+ def foo
44
+ # If a block is passed when invoking `foo`, call the block with argument 10 and print the result.
45
+ # Otherwise, just print that no block was passed
46
+ if block_given?
47
+ result = yield(10)
48
+ puts result
49
+ else
50
+ puts "No block passed!"
51
+ end
52
+ end
53
+
54
+ foo do |a|
55
+ a * 2
56
+ end
57
+ # => 20
58
+
59
+ foo
60
+ # => No block passed!
61
+ ```
62
+
63
+ ## Block parameter
64
+
65
+ In addition to implicit blocks, Ruby also allows developers to use explicit block parameters as part of the method's
66
+ signature. In this scenario, we can use the reference to the block directly instead of relying on the `yield` keyword.
67
+
68
+ ```ruby
69
+ # Block parameters are prefixed with & and a name
70
+ def foo(&my_block_param)
71
+ # If a block was passed to `foo`, `my_block_param` will be a `Proc` object. Otherwise, it will be `nil`. We can use
72
+ # that to check for its presence
73
+ if my_block_param
74
+ # Explicit block parameters are invoked using the method `call`, which is present in all `Proc` objects
75
+ result = my_block_param.call(10)
76
+ puts result
77
+ else
78
+ puts "No block passed!"
79
+ end
80
+ end
81
+ ```