ruby-lsp 0.18.3 → 0.19.1

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