ruby-lsp 0.18.3 → 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 (54) 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 +52 -8
  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 +22 -0
  19. data/lib/ruby_indexer/test/reference_finder_test.rb +242 -0
  20. data/lib/ruby_lsp/addon.rb +79 -17
  21. data/lib/ruby_lsp/base_server.rb +6 -0
  22. data/lib/ruby_lsp/erb_document.rb +9 -3
  23. data/lib/ruby_lsp/global_state.rb +8 -0
  24. data/lib/ruby_lsp/internal.rb +5 -1
  25. data/lib/ruby_lsp/listeners/completion.rb +1 -1
  26. data/lib/ruby_lsp/listeners/hover.rb +57 -0
  27. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +24 -21
  28. data/lib/ruby_lsp/requests/code_action_resolve.rb +9 -3
  29. data/lib/ruby_lsp/requests/completion.rb +1 -0
  30. data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
  31. data/lib/ruby_lsp/requests/definition.rb +1 -0
  32. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  33. data/lib/ruby_lsp/requests/hover.rb +1 -0
  34. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  35. data/lib/ruby_lsp/requests/range_formatting.rb +55 -0
  36. data/lib/ruby_lsp/requests/references.rb +146 -0
  37. data/lib/ruby_lsp/requests/rename.rb +196 -0
  38. data/lib/ruby_lsp/requests/signature_help.rb +6 -1
  39. data/lib/ruby_lsp/requests/support/common.rb +2 -2
  40. data/lib/ruby_lsp/requests/support/formatter.rb +3 -0
  41. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +6 -0
  42. data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
  43. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +8 -0
  44. data/lib/ruby_lsp/ruby_document.rb +23 -8
  45. data/lib/ruby_lsp/scope.rb +47 -0
  46. data/lib/ruby_lsp/server.rb +127 -34
  47. data/lib/ruby_lsp/static_docs.rb +15 -0
  48. data/lib/ruby_lsp/store.rb +12 -0
  49. data/lib/ruby_lsp/test_helper.rb +1 -1
  50. data/lib/ruby_lsp/type_inferrer.rb +6 -1
  51. data/lib/ruby_lsp/utils.rb +3 -6
  52. data/static_docs/yield.md +81 -0
  53. metadata +21 -8
  54. data/lib/ruby_lsp/parameter_scope.rb +0 -33
@@ -27,7 +27,7 @@ module RubyLsp
27
27
  def initialize(dispatcher, response_builder)
28
28
  @response_builder = response_builder
29
29
  @special_methods = T.let(nil, T.nilable(T::Array[String]))
30
- @current_scope = T.let(ParameterScope.new, ParameterScope)
30
+ @current_scope = T.let(Scope.new, Scope)
31
31
  @inside_regex_capture = T.let(false, T::Boolean)
32
32
  @inside_implicit_node = T.let(false, T::Boolean)
33
33
 
@@ -102,7 +102,7 @@ module RubyLsp
102
102
 
103
103
  sig { params(node: Prism::DefNode).void }
104
104
  def on_def_node_enter(node)
105
- @current_scope = ParameterScope.new(@current_scope)
105
+ @current_scope = Scope.new(@current_scope)
106
106
  end
107
107
 
108
108
  sig { params(node: Prism::DefNode).void }
@@ -112,7 +112,7 @@ module RubyLsp
112
112
 
113
113
  sig { params(node: Prism::BlockNode).void }
114
114
  def on_block_node_enter(node)
115
- @current_scope = ParameterScope.new(@current_scope)
115
+ @current_scope = Scope.new(@current_scope)
116
116
  end
117
117
 
118
118
  sig { params(node: Prism::BlockNode).void }
@@ -128,39 +128,39 @@ module RubyLsp
128
128
  sig { params(node: Prism::BlockParameterNode).void }
129
129
  def on_block_parameter_node_enter(node)
130
130
  name = node.name
131
- @current_scope << name.to_sym if name
131
+ @current_scope.add(name.to_sym, :parameter) if name
132
132
  end
133
133
 
134
134
  sig { params(node: Prism::RequiredKeywordParameterNode).void }
135
135
  def on_required_keyword_parameter_node_enter(node)
136
- @current_scope << node.name
136
+ @current_scope.add(node.name, :parameter)
137
137
  end
138
138
 
139
139
  sig { params(node: Prism::OptionalKeywordParameterNode).void }
140
140
  def on_optional_keyword_parameter_node_enter(node)
141
- @current_scope << node.name
141
+ @current_scope.add(node.name, :parameter)
142
142
  end
143
143
 
144
144
  sig { params(node: Prism::KeywordRestParameterNode).void }
145
145
  def on_keyword_rest_parameter_node_enter(node)
146
146
  name = node.name
147
- @current_scope << name.to_sym if name
147
+ @current_scope.add(name.to_sym, :parameter) if name
148
148
  end
149
149
 
150
150
  sig { params(node: Prism::OptionalParameterNode).void }
151
151
  def on_optional_parameter_node_enter(node)
152
- @current_scope << node.name
152
+ @current_scope.add(node.name, :parameter)
153
153
  end
154
154
 
155
155
  sig { params(node: Prism::RequiredParameterNode).void }
156
156
  def on_required_parameter_node_enter(node)
157
- @current_scope << node.name
157
+ @current_scope.add(node.name, :parameter)
158
158
  end
159
159
 
160
160
  sig { params(node: Prism::RestParameterNode).void }
161
161
  def on_rest_parameter_node_enter(node)
162
162
  name = node.name
163
- @current_scope << name.to_sym if name
163
+ @current_scope.add(name.to_sym, :parameter) if name
164
164
  end
165
165
 
166
166
  sig { params(node: Prism::SelfNode).void }
@@ -170,8 +170,8 @@ module RubyLsp
170
170
 
171
171
  sig { params(node: Prism::LocalVariableWriteNode).void }
172
172
  def on_local_variable_write_node_enter(node)
173
- type = @current_scope.type_for(node.name)
174
- @response_builder.add_token(node.name_loc, type) if type == :parameter
173
+ local = @current_scope.lookup(node.name)
174
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
175
175
  end
176
176
 
177
177
  sig { params(node: Prism::LocalVariableReadNode).void }
@@ -184,25 +184,26 @@ module RubyLsp
184
184
  return
185
185
  end
186
186
 
187
- @response_builder.add_token(node.location, @current_scope.type_for(node.name))
187
+ local = @current_scope.lookup(node.name)
188
+ @response_builder.add_token(node.location, local&.type || :variable)
188
189
  end
189
190
 
190
191
  sig { params(node: Prism::LocalVariableAndWriteNode).void }
191
192
  def on_local_variable_and_write_node_enter(node)
192
- type = @current_scope.type_for(node.name)
193
- @response_builder.add_token(node.name_loc, type) if type == :parameter
193
+ local = @current_scope.lookup(node.name)
194
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
194
195
  end
195
196
 
196
197
  sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
197
198
  def on_local_variable_operator_write_node_enter(node)
198
- type = @current_scope.type_for(node.name)
199
- @response_builder.add_token(node.name_loc, type) if type == :parameter
199
+ local = @current_scope.lookup(node.name)
200
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
200
201
  end
201
202
 
202
203
  sig { params(node: Prism::LocalVariableOrWriteNode).void }
203
204
  def on_local_variable_or_write_node_enter(node)
204
- type = @current_scope.type_for(node.name)
205
- @response_builder.add_token(node.name_loc, type) if type == :parameter
205
+ local = @current_scope.lookup(node.name)
206
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
206
207
  end
207
208
 
208
209
  sig { params(node: Prism::LocalVariableTargetNode).void }
@@ -213,7 +214,8 @@ module RubyLsp
213
214
  # prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
214
215
  return if @inside_regex_capture
215
216
 
216
- @response_builder.add_token(node.location, @current_scope.type_for(node.name))
217
+ local = @current_scope.lookup(node.name)
218
+ @response_builder.add_token(node.location, local&.type || :variable)
217
219
  end
218
220
 
219
221
  sig { params(node: Prism::ClassNode).void }
@@ -311,7 +313,8 @@ module RubyLsp
311
313
  capture_name_offset = T.must(content.index("(?<#{name}>")) + 3
312
314
  local_var_loc = loc.copy(start_offset: loc.start_offset + capture_name_offset, length: name.length)
313
315
 
314
- @response_builder.add_token(local_var_loc, @current_scope.type_for(name))
316
+ local = @current_scope.lookup(name)
317
+ @response_builder.add_token(local_var_loc, local&.type || :variable)
315
318
  end
316
319
  end
317
320
  end
@@ -23,10 +23,11 @@ module RubyLsp
23
23
  end
24
24
  end
25
25
 
26
- sig { params(document: RubyDocument, code_action: T::Hash[Symbol, T.untyped]).void }
27
- def initialize(document, code_action)
26
+ sig { params(document: RubyDocument, global_state: GlobalState, code_action: T::Hash[Symbol, T.untyped]).void }
27
+ def initialize(document, global_state, code_action)
28
28
  super()
29
29
  @document = document
30
+ @global_state = global_state
30
31
  @code_action = code_action
31
32
  end
32
33
 
@@ -191,7 +192,12 @@ module RubyLsp
191
192
  extracted_source = T.must(@document.source[start_index...end_index])
192
193
 
193
194
  # Find the closest method declaration node, so that we place the refactor in a valid position
194
- node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
195
+ node_context = RubyDocument.locate(
196
+ @document.parse_result.value,
197
+ start_index,
198
+ node_types: [Prism::DefNode],
199
+ encoding: @global_state.encoding,
200
+ )
195
201
  closest_node = node_context.node
196
202
  return Error::InvalidTargetRange unless closest_node
197
203
 
@@ -57,6 +57,7 @@ module RubyLsp
57
57
  Prism::InstanceVariableTargetNode,
58
58
  Prism::InstanceVariableWriteNode,
59
59
  ],
60
+ encoding: global_state.encoding,
60
61
  )
61
62
  @response_builder = T.let(
62
63
  ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem].new,
@@ -40,6 +40,8 @@ module RubyLsp
40
40
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
41
41
  # `label` for the text edit instead
42
42
  label = @item[:label].dup
43
+ return keyword_resolve(@item) if @item.dig(:data, :keyword)
44
+
43
45
  entries = @index[label] || []
44
46
 
45
47
  owner_name = @item.dig(:data, :owner_name)
@@ -72,6 +74,33 @@ module RubyLsp
72
74
 
73
75
  @item
74
76
  end
77
+
78
+ private
79
+
80
+ sig { params(item: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
81
+ def keyword_resolve(item)
82
+ keyword = item[:label]
83
+ content = KEYWORD_DOCS[keyword]
84
+
85
+ if content
86
+ doc_path = File.join(STATIC_DOCS_PATH, "#{keyword}.md")
87
+
88
+ @item[:documentation] = Interface::MarkupContent.new(
89
+ kind: "markdown",
90
+ value: <<~MARKDOWN.chomp,
91
+ ```ruby
92
+ #{keyword}
93
+ ```
94
+
95
+ [Read more](#{doc_path})
96
+
97
+ #{content}
98
+ MARKDOWN
99
+ )
100
+ end
101
+
102
+ item
103
+ end
75
104
  end
76
105
  end
77
106
  end
@@ -57,6 +57,7 @@ module RubyLsp
57
57
  Prism::SuperNode,
58
58
  Prism::ForwardingSuperNode,
59
59
  ],
60
+ encoding: global_state.encoding,
60
61
  )
61
62
 
62
63
  target = node_context.node
@@ -28,7 +28,7 @@ module RubyLsp
28
28
  char_position = document.create_scanner.find_char_position(position)
29
29
  delegate_request_if_needed!(global_state, document, char_position)
30
30
 
31
- node_context = RubyDocument.locate(document.parse_result.value, char_position)
31
+ node_context = RubyDocument.locate(document.parse_result.value, char_position, encoding: global_state.encoding)
32
32
 
33
33
  @response_builder = T.let(
34
34
  ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight].new,
@@ -41,6 +41,7 @@ module RubyLsp
41
41
  document.parse_result.value,
42
42
  char_position,
43
43
  node_types: Listeners::Hover::ALLOWED_TARGETS,
44
+ encoding: global_state.encoding,
44
45
  )
45
46
  target = node_context.node
46
47
  parent = node_context.parent
@@ -63,7 +63,7 @@ module RubyLsp
63
63
  if (comment_match = @previous_line.match(/^#(\s*)/))
64
64
  handle_comment_line(T.must(comment_match[1]))
65
65
  elsif @document.syntax_error?
66
- match = /(?<=<<(-|~))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
66
+ match = /(<<((-|~)?))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
67
67
  heredoc_delimiter = match && match.named_captures["delimiter"]
68
68
 
69
69
  if heredoc_delimiter
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The [range formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_rangeFormatting)
7
+ # is used to format a selection or to format on paste.
8
+ class RangeFormatting < Request
9
+ extend T::Sig
10
+
11
+ sig { params(global_state: GlobalState, document: RubyDocument, params: T::Hash[Symbol, T.untyped]).void }
12
+ def initialize(global_state, document, params)
13
+ super()
14
+ @document = document
15
+ @uri = T.let(document.uri, URI::Generic)
16
+ @params = params
17
+ @active_formatter = T.let(global_state.active_formatter, T.nilable(Support::Formatter))
18
+ end
19
+
20
+ sig { override.returns(T.nilable(T::Array[Interface::TextEdit])) }
21
+ def perform
22
+ return unless @active_formatter
23
+ return if @document.syntax_error?
24
+
25
+ target = @document.locate_first_within_range(@params[:range])
26
+ return unless target
27
+
28
+ location = target.location
29
+
30
+ formatted_text = @active_formatter.run_range_formatting(
31
+ @uri,
32
+ target.slice,
33
+ location.start_column / 2,
34
+ )
35
+ return unless formatted_text
36
+
37
+ [
38
+ Interface::TextEdit.new(
39
+ range: Interface::Range.new(
40
+ start: Interface::Position.new(
41
+ line: location.start_line - 1,
42
+ character: location.start_code_units_column(@document.encoding),
43
+ ),
44
+ end: Interface::Position.new(
45
+ line: location.end_line - 1,
46
+ character: location.end_code_units_column(@document.encoding),
47
+ ),
48
+ ),
49
+ new_text: formatted_text.strip,
50
+ ),
51
+ ]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,146 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The
7
+ # [references](https://microsoft.github.io/language-server-protocol/specification#textDocument_references)
8
+ # request finds all references for the selected symbol.
9
+ class References < Request
10
+ extend T::Sig
11
+ include Support::Common
12
+
13
+ sig do
14
+ params(
15
+ global_state: GlobalState,
16
+ store: Store,
17
+ document: T.any(RubyDocument, ERBDocument),
18
+ params: T::Hash[Symbol, T.untyped],
19
+ ).void
20
+ end
21
+ def initialize(global_state, store, document, params)
22
+ super()
23
+ @global_state = global_state
24
+ @store = store
25
+ @document = document
26
+ @params = params
27
+ @locations = T.let([], T::Array[Interface::Location])
28
+ end
29
+
30
+ sig { override.returns(T::Array[Interface::Location]) }
31
+ def perform
32
+ position = @params[:position]
33
+ char_position = @document.create_scanner.find_char_position(position)
34
+
35
+ node_context = RubyDocument.locate(
36
+ @document.parse_result.value,
37
+ char_position,
38
+ node_types: [
39
+ Prism::ConstantReadNode,
40
+ Prism::ConstantPathNode,
41
+ Prism::ConstantPathTargetNode,
42
+ Prism::CallNode,
43
+ Prism::DefNode,
44
+ ],
45
+ encoding: @global_state.encoding,
46
+ )
47
+ target = node_context.node
48
+ parent = node_context.parent
49
+ return @locations if !target || target.is_a?(Prism::ProgramNode)
50
+
51
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
52
+ target = determine_target(
53
+ target,
54
+ parent,
55
+ position,
56
+ )
57
+ end
58
+
59
+ target = T.cast(
60
+ target,
61
+ T.any(
62
+ Prism::ConstantReadNode,
63
+ Prism::ConstantPathNode,
64
+ Prism::ConstantPathTargetNode,
65
+ Prism::CallNode,
66
+ Prism::DefNode,
67
+ ),
68
+ )
69
+
70
+ reference_target = create_reference_target(target, node_context)
71
+ return @locations unless reference_target
72
+
73
+ Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
74
+ uri = URI::Generic.from_path(path: path)
75
+ # If the document is being managed by the client, then we should use whatever is present in the store instead
76
+ # of reading from disk
77
+ next if @store.key?(uri)
78
+
79
+ parse_result = Prism.parse_file(path)
80
+ collect_references(reference_target, parse_result, uri)
81
+ end
82
+
83
+ @store.each do |_uri, document|
84
+ collect_references(reference_target, document.parse_result, document.uri)
85
+ end
86
+
87
+ @locations
88
+ end
89
+
90
+ private
91
+
92
+ sig do
93
+ params(
94
+ target_node: T.any(
95
+ Prism::ConstantReadNode,
96
+ Prism::ConstantPathNode,
97
+ Prism::ConstantPathTargetNode,
98
+ Prism::CallNode,
99
+ Prism::DefNode,
100
+ ),
101
+ node_context: NodeContext,
102
+ ).returns(T.nilable(RubyIndexer::ReferenceFinder::Target))
103
+ end
104
+ def create_reference_target(target_node, node_context)
105
+ case target_node
106
+ when Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode
107
+ name = constant_name(target_node)
108
+ return unless name
109
+
110
+ entries = @global_state.index.resolve(name, node_context.nesting)
111
+ return unless entries
112
+
113
+ fully_qualified_name = T.must(entries.first).name
114
+ RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
115
+ when Prism::CallNode, Prism::DefNode
116
+ RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
117
+ end
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ target: RubyIndexer::ReferenceFinder::Target,
123
+ parse_result: Prism::ParseResult,
124
+ uri: URI::Generic,
125
+ ).void
126
+ end
127
+ def collect_references(target, parse_result, uri)
128
+ dispatcher = Prism::Dispatcher.new
129
+ finder = RubyIndexer::ReferenceFinder.new(
130
+ target,
131
+ @global_state.index,
132
+ dispatcher,
133
+ include_declarations: @params.dig(:context, :includeDeclaration) || true,
134
+ )
135
+ dispatcher.visit(parse_result.value)
136
+
137
+ finder.references.each do |reference|
138
+ @locations << Interface::Location.new(
139
+ uri: uri.to_s,
140
+ range: range_from_location(reference.location),
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -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
 
@@ -5,8 +5,8 @@ module RubyLsp
5
5
  module Requests
6
6
  module Support
7
7
  module Common
8
- # WARNING: Methods in this class may be used by Ruby LSP addons such as
9
- # https://github.com/Shopify/ruby-lsp-rails, or addons by created by developers outside of Shopify, so be
8
+ # WARNING: Methods in this class may be used by Ruby LSP add-ons such as
9
+ # https://github.com/Shopify/ruby-lsp-rails, or add-ons by created by developers outside of Shopify, so be
10
10
  # cautious of changing anything.
11
11
  extend T::Sig
12
12
  extend T::Helpers