ruby-lsp 0.18.4 → 0.19.1

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