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.
- 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
|