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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp-check +1 -1
- data/lib/core_ext/uri.rb +9 -4
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +66 -8
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +63 -32
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -5
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +52 -8
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +324 -0
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +23 -0
- data/lib/ruby_indexer/test/constant_test.rb +8 -0
- data/lib/ruby_indexer/test/enhancements_test.rb +2 -0
- data/lib/ruby_indexer/test/index_test.rb +3 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +12 -0
- data/lib/ruby_indexer/test/method_test.rb +10 -0
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +22 -0
- data/lib/ruby_indexer/test/reference_finder_test.rb +242 -0
- data/lib/ruby_lsp/addon.rb +79 -17
- data/lib/ruby_lsp/base_server.rb +6 -0
- data/lib/ruby_lsp/erb_document.rb +9 -3
- data/lib/ruby_lsp/global_state.rb +8 -0
- data/lib/ruby_lsp/internal.rb +5 -1
- data/lib/ruby_lsp/listeners/completion.rb +1 -1
- data/lib/ruby_lsp/listeners/hover.rb +57 -0
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +24 -21
- data/lib/ruby_lsp/requests/code_action_resolve.rb +9 -3
- data/lib/ruby_lsp/requests/completion.rb +1 -0
- data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
- data/lib/ruby_lsp/requests/definition.rb +1 -0
- data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
- data/lib/ruby_lsp/requests/hover.rb +1 -0
- data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
- data/lib/ruby_lsp/requests/range_formatting.rb +55 -0
- data/lib/ruby_lsp/requests/references.rb +146 -0
- data/lib/ruby_lsp/requests/rename.rb +196 -0
- data/lib/ruby_lsp/requests/signature_help.rb +6 -1
- data/lib/ruby_lsp/requests/support/common.rb +2 -2
- data/lib/ruby_lsp/requests/support/formatter.rb +3 -0
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +6 -0
- data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
- data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +8 -0
- data/lib/ruby_lsp/ruby_document.rb +23 -8
- data/lib/ruby_lsp/scope.rb +47 -0
- data/lib/ruby_lsp/server.rb +127 -34
- data/lib/ruby_lsp/static_docs.rb +15 -0
- data/lib/ruby_lsp/store.rb +12 -0
- data/lib/ruby_lsp/test_helper.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +6 -1
- data/lib/ruby_lsp/utils.rb +3 -6
- data/static_docs/yield.md +81 -0
- metadata +21 -8
- 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(
|
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 =
|
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 =
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
174
|
-
@response_builder.add_token(node.name_loc,
|
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
|
-
|
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
|
-
|
193
|
-
@response_builder.add_token(node.name_loc,
|
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
|
-
|
199
|
-
@response_builder.add_token(node.name_loc,
|
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
|
-
|
205
|
-
@response_builder.add_token(node.name_loc,
|
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
|
-
|
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
|
-
|
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(
|
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
|
|
@@ -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
|
@@ -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,
|
@@ -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 = /(
|
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(
|
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
|
9
|
-
# https://github.com/Shopify/ruby-lsp-rails, or
|
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
|