ruby-lsp 0.26.8 → 0.27.0.beta1

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.
@@ -48,6 +48,7 @@ module RubyLsp
48
48
  @response_builder = response_builder
49
49
  @global_state = global_state
50
50
  @index = global_state.index #: RubyIndexer::Index
51
+ @graph = global_state.graph #: Rubydex::Graph
51
52
  @type_inferrer = global_state.type_inferrer #: TypeInferrer
52
53
  @path = uri.to_standardized_path #: String?
53
54
  @node_context = node_context
@@ -178,32 +179,32 @@ module RubyLsp
178
179
 
179
180
  #: (Prism::InstanceVariableReadNode node) -> void
180
181
  def on_instance_variable_read_node_enter(node)
181
- handle_instance_variable_hover(node.name.to_s)
182
+ handle_variable_hover(node.name.to_s)
182
183
  end
183
184
 
184
185
  #: (Prism::InstanceVariableWriteNode node) -> void
185
186
  def on_instance_variable_write_node_enter(node)
186
- handle_instance_variable_hover(node.name.to_s)
187
+ handle_variable_hover(node.name.to_s)
187
188
  end
188
189
 
189
190
  #: (Prism::InstanceVariableAndWriteNode node) -> void
190
191
  def on_instance_variable_and_write_node_enter(node)
191
- handle_instance_variable_hover(node.name.to_s)
192
+ handle_variable_hover(node.name.to_s)
192
193
  end
193
194
 
194
195
  #: (Prism::InstanceVariableOperatorWriteNode node) -> void
195
196
  def on_instance_variable_operator_write_node_enter(node)
196
- handle_instance_variable_hover(node.name.to_s)
197
+ handle_variable_hover(node.name.to_s)
197
198
  end
198
199
 
199
200
  #: (Prism::InstanceVariableOrWriteNode node) -> void
200
201
  def on_instance_variable_or_write_node_enter(node)
201
- handle_instance_variable_hover(node.name.to_s)
202
+ handle_variable_hover(node.name.to_s)
202
203
  end
203
204
 
204
205
  #: (Prism::InstanceVariableTargetNode node) -> void
205
206
  def on_instance_variable_target_node_enter(node)
206
- handle_instance_variable_hover(node.name.to_s)
207
+ handle_variable_hover(node.name.to_s)
207
208
  end
208
209
 
209
210
  #: (Prism::SuperNode node) -> void
@@ -223,32 +224,32 @@ module RubyLsp
223
224
 
224
225
  #: (Prism::ClassVariableAndWriteNode node) -> void
225
226
  def on_class_variable_and_write_node_enter(node)
226
- handle_class_variable_hover(node.name.to_s)
227
+ handle_variable_hover(node.name.to_s)
227
228
  end
228
229
 
229
230
  #: (Prism::ClassVariableOperatorWriteNode node) -> void
230
231
  def on_class_variable_operator_write_node_enter(node)
231
- handle_class_variable_hover(node.name.to_s)
232
+ handle_variable_hover(node.name.to_s)
232
233
  end
233
234
 
234
235
  #: (Prism::ClassVariableOrWriteNode node) -> void
235
236
  def on_class_variable_or_write_node_enter(node)
236
- handle_class_variable_hover(node.name.to_s)
237
+ handle_variable_hover(node.name.to_s)
237
238
  end
238
239
 
239
240
  #: (Prism::ClassVariableTargetNode node) -> void
240
241
  def on_class_variable_target_node_enter(node)
241
- handle_class_variable_hover(node.name.to_s)
242
+ handle_variable_hover(node.name.to_s)
242
243
  end
243
244
 
244
245
  #: (Prism::ClassVariableReadNode node) -> void
245
246
  def on_class_variable_read_node_enter(node)
246
- handle_class_variable_hover(node.name.to_s)
247
+ handle_variable_hover(node.name.to_s)
247
248
  end
248
249
 
249
250
  #: (Prism::ClassVariableWriteNode node) -> void
250
251
  def on_class_variable_write_node_enter(node)
251
- handle_class_variable_hover(node.name.to_s)
252
+ handle_variable_hover(node.name.to_s)
252
253
  end
253
254
 
254
255
  private
@@ -324,62 +325,46 @@ module RubyLsp
324
325
  end
325
326
  end
326
327
 
327
- #: (String name) -> void
328
- def handle_instance_variable_hover(name)
329
- # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
330
- # to provide all features for them
331
- return if @sorbet_level.strict?
332
-
333
- type = @type_inferrer.infer_receiver_type(@node_context)
334
- return unless type
335
-
336
- entries = @index.resolve_instance_variable(name, type.name)
337
- return unless entries
338
-
339
- categorized_markdown_from_index_entries(name, entries).each do |category, content|
340
- @response_builder.push(content, category: category)
341
- end
342
- rescue RubyIndexer::Index::NonExistingNamespaceError
343
- # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
344
- end
345
-
346
328
  #: (String name) -> void
347
329
  def handle_global_variable_hover(name)
348
- entries = @index[name]
349
- return unless entries
330
+ declaration = @graph[name]
331
+ return unless declaration
350
332
 
351
- categorized_markdown_from_index_entries(name, entries).each do |category, content|
333
+ categorized_markdown_from_definitions(name, declaration.definitions).each do |category, content|
352
334
  @response_builder.push(content, category: category)
353
335
  end
354
336
  end
355
337
 
338
+ # Handle class or instance variables. We collect all definitions across the ancestors of the type
339
+ #
356
340
  #: (String name) -> void
357
- def handle_class_variable_hover(name)
341
+ def handle_variable_hover(name)
342
+ # Sorbet enforces that all variables be declared on typed strict or higher, which means it will be able to
343
+ # provide all features for them
344
+ return if @sorbet_level.strict?
345
+
358
346
  type = @type_inferrer.infer_receiver_type(@node_context)
359
347
  return unless type
360
348
 
361
- entries = @index.resolve_class_variable(name, type.name)
362
- return unless entries
349
+ owner = @graph[type.name]
350
+ return unless owner.is_a?(Rubydex::Namespace)
363
351
 
364
- categorized_markdown_from_index_entries(name, entries).each do |category, content|
365
- @response_builder.push(content, category: category)
352
+ owner.ancestors.each do |ancestor|
353
+ member = ancestor.member(name)
354
+ next unless member
355
+
356
+ categorized_markdown_from_definitions(member.name, member.definitions).each do |category, content|
357
+ @response_builder.push(content, category: category)
358
+ end
366
359
  end
367
- rescue RubyIndexer::Index::NonExistingNamespaceError
368
- # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
369
360
  end
370
361
 
371
362
  #: (String name, Prism::Location location) -> void
372
363
  def generate_hover(name, location)
373
- entries = @index.resolve(name, @node_context.nesting)
374
- return unless entries
375
-
376
- # We should only show hover for private constants if the constant is defined in the same namespace as the
377
- # reference
378
- first_entry = entries.first #: as !nil
379
- full_name = first_entry.name
380
- return if first_entry.private? && full_name != "#{@node_context.fully_qualified_name}::#{name}"
364
+ declaration = @graph.resolve_constant(name, @node_context.nesting)
365
+ return unless declaration
381
366
 
382
- categorized_markdown_from_index_entries(full_name, entries).each do |category, content|
367
+ categorized_markdown_from_definitions(declaration.name, declaration.definitions).each do |category, content|
383
368
  @response_builder.push(content, category: category)
384
369
  end
385
370
  end
@@ -162,7 +162,7 @@ module RubyLsp
162
162
 
163
163
  #: (Prism::SelfNode node) -> void
164
164
  def on_self_node_enter(node)
165
- @response_builder.add_token(node.location, :variable, [:default_library])
165
+ @response_builder.add_token(node.location, :variable, [:defaultLibrary])
166
166
  end
167
167
 
168
168
  #: (Prism::LocalVariableWriteNode node) -> void
@@ -266,7 +266,7 @@ module RubyLsp
266
266
  # We only support regular Minitest tests. The declarative syntax provided by ActiveSupport is handled by the
267
267
  # Rails add-on
268
268
  name_parts = fully_qualified_name.split("::")
269
- singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
269
+ singleton_name = "#{name_parts.join("::")}::<#{name_parts.last}>"
270
270
  !@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
271
271
  rescue RubyIndexer::Index::NonExistingNamespaceError
272
272
  true
@@ -62,12 +62,12 @@ module RubyLsp
62
62
  when Prism::ClassNode, Prism::ModuleNode
63
63
  nesting << node.constant_path.slice
64
64
  when Prism::SingletonClassNode
65
- nesting << "<Class:#{nesting.flat_map { |n| n.split("::") }.last}>"
65
+ nesting << "<#{nesting.flat_map { |n| n.split("::") }.last}>"
66
66
  when Prism::DefNode
67
67
  surrounding_method = node.name.to_s
68
68
  next unless node.receiver.is_a?(Prism::SelfNode)
69
69
 
70
- nesting << "<Class:#{nesting.flat_map { |n| n.split("::") }.last}>"
70
+ nesting << "<#{nesting.flat_map { |n| n.split("::") }.last}>"
71
71
  end
72
72
  end
73
73
 
@@ -120,6 +120,10 @@ module RubyLsp
120
120
 
121
121
  return unless END_REGEXES.any? { |regex| regex.match?(@previous_line) }
122
122
 
123
+ # Endless method definitions (e.g., `def foo = 42`) are complete statements
124
+ # and should not have `end` added
125
+ return if @previous_line.match?(/\bdef\s+[\w.]+[!?=]?(\([^)]*\))?\s*=[^=~>]/)
126
+
123
127
  indents = " " * @indentation
124
128
  current_line = @lines[@position[:line]]
125
129
  next_line = @lines[@position[:line] + 1]
@@ -22,6 +22,7 @@ module RubyLsp
22
22
  def initialize(global_state, store, document, params)
23
23
  super()
24
24
  @global_state = global_state
25
+ @graph = global_state.graph #: Rubydex::Graph
25
26
  @store = store
26
27
  @document = document
27
28
  @position = params[:position] #: Hash[Symbol, Integer]
@@ -56,17 +57,14 @@ module RubyLsp
56
57
  name = RubyIndexer::Index.constant_name(target)
57
58
  return unless name
58
59
 
59
- entries = @global_state.index.resolve(name, node_context.nesting)
60
- return unless entries
60
+ declaration = @graph.resolve_constant(name, node_context.nesting)
61
+ return unless declaration
61
62
 
62
- if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
63
- raise InvalidNameError, "The new name is already in use by #{conflict_entries.first&.name}"
63
+ if (conflict = @graph.resolve_constant(@new_name, node_context.nesting))
64
+ raise InvalidNameError, "The new name is already in use by #{conflict.name}"
64
65
  end
65
66
 
66
- fully_qualified_name = entries.first #: as !nil
67
- .name
68
- reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
69
- changes = collect_text_edits(reference_target, name)
67
+ changes = collect_text_edits(declaration, name)
70
68
 
71
69
  # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
72
70
  # text changes
@@ -78,99 +76,93 @@ module RubyLsp
78
76
  # renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
79
77
  document_changes = changes.map do |uri, edits|
80
78
  Interface::TextDocumentEdit.new(
81
- text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
79
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(uri: uri, version: nil),
82
80
  edits: edits,
83
81
  )
84
82
  end
85
83
 
86
- collect_file_renames(fully_qualified_name, document_changes)
84
+ collect_file_renames(declaration, document_changes)
87
85
  Interface::WorkspaceEdit.new(document_changes: document_changes)
88
86
  end
89
87
 
90
88
  private
91
89
 
92
- #: (String fully_qualified_name, Array[(Interface::RenameFile | Interface::TextDocumentEdit)] document_changes) -> void
93
- def collect_file_renames(fully_qualified_name, document_changes)
90
+ #: (Rubydex::Declaration, Array[(Interface::RenameFile | Interface::TextDocumentEdit)]) -> void
91
+ def collect_file_renames(declaration, document_changes)
94
92
  # Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
95
93
  # rename the files for the user.
96
94
  #
97
95
  # We also look for an associated test file and rename it too
98
- short_name = fully_qualified_name.split("::").last #: as !nil
99
96
 
100
- @global_state.index[fully_qualified_name]&.each do |entry|
97
+ unless [
98
+ Rubydex::Class,
99
+ Rubydex::Module,
100
+ Rubydex::Constant,
101
+ Rubydex::ConstantAlias,
102
+ ].any? { |type| declaration.is_a?(type) }
103
+ return
104
+ end
105
+
106
+ short_name = declaration.unqualified_name
107
+
108
+ declaration.definitions.each do |definition|
101
109
  # Do not rename files that are not part of the workspace
102
- uri = entry.uri
110
+ uri = URI(definition.location.uri)
103
111
  file_path = uri.full_path
104
112
  next unless file_path&.start_with?(@global_state.workspace_path)
105
113
 
106
- case entry
107
- when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
108
- RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
109
-
110
- file_name = file_from_constant_name(short_name)
114
+ file_name = file_from_constant_name(short_name)
115
+ next unless "#{file_name}.rb" == File.basename(file_path)
111
116
 
112
- if "#{file_name}.rb" == entry.file_name
113
- new_file_name = file_from_constant_name(
114
- @new_name.split("::").last, #: as !nil
115
- )
117
+ new_file_name = file_from_constant_name(
118
+ @new_name.split("::").last, #: as !nil
119
+ )
116
120
 
117
- new_uri = URI::Generic.from_path(path: File.join(
118
- File.dirname(file_path),
119
- "#{new_file_name}.rb",
120
- )).to_s
121
+ new_uri = URI::Generic.from_path(path: File.join(
122
+ File.dirname(file_path),
123
+ "#{new_file_name}.rb",
124
+ )).to_s
121
125
 
122
- document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
123
- end
124
- end
126
+ document_changes << Interface::RenameFile.new(kind: "rename", old_uri: uri.to_s, new_uri: new_uri)
125
127
  end
126
128
  end
127
129
 
128
- #: (RubyIndexer::ReferenceFinder::Target target, String name) -> Hash[String, Array[Interface::TextEdit]]
129
- def collect_text_edits(target, name)
130
- changes = {}
131
-
132
- Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
133
- uri = URI::Generic.from_path(path: path)
134
- # If the document is being managed by the client, then we should use whatever is present in the store instead
135
- # of reading from disk
136
- next if @store.key?(uri)
137
-
138
- parse_result = Prism.parse_file(path)
139
- edits = collect_changes(target, parse_result.value, name, uri)
140
- changes[uri.to_s] = edits unless edits.empty?
141
- rescue Errno::EISDIR, Errno::ENOENT
142
- # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
143
- end
144
-
145
- @store.each do |uri, document|
146
- next unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
130
+ #: (Rubydex::Declaration declaration, String name) -> Hash[String, Array[Interface::TextEdit]]
131
+ def collect_text_edits(declaration, name)
132
+ changes = {} #: Hash[String, Array[Interface::TextEdit]]
133
+ short_name = name.split("::").last #: as !nil
134
+ new_short_name = @new_name.split("::").last #: as !nil
135
+
136
+ # Collect edits for definition sites (where the constant is declared)
137
+ declaration.definitions.each do |definition|
138
+ name_loc = definition.name_location
139
+ next unless name_loc
140
+
141
+ uri_string = name_loc.uri
142
+ edits = (changes[uri_string] ||= [])
143
+
144
+ # The name_location spans the constant name as written in the definition.
145
+ # We only replace the unqualified name portion (the last segment).
146
+ range = Interface::Range.new(
147
+ start: Interface::Position.new(
148
+ line: name_loc.end_line,
149
+ character: name_loc.end_column - short_name.length,
150
+ ),
151
+ end: Interface::Position.new(line: name_loc.end_line, character: name_loc.end_column),
152
+ )
147
153
 
148
- edits = collect_changes(target, document.ast, name, document.uri)
149
- changes[uri] = edits unless edits.empty?
154
+ edits << Interface::TextEdit.new(range: range, new_text: new_short_name)
150
155
  end
151
156
 
152
- changes
153
- end
154
-
155
- #: (RubyIndexer::ReferenceFinder::Target target, Prism::Node ast, String name, URI::Generic uri) -> Array[Interface::TextEdit]
156
- def collect_changes(target, ast, name, uri)
157
- dispatcher = Prism::Dispatcher.new
158
- finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher, uri)
159
- dispatcher.visit(ast)
160
-
161
- finder.references.map do |reference|
162
- adjust_reference_for_edit(name, reference)
157
+ # Collect edits for reference sites (where the constant is used)
158
+ declaration.references.each do |reference|
159
+ ref = reference #: as Rubydex::ConstantReference
160
+ uri_string = ref.location.uri
161
+ edits = (changes[uri_string] ||= [])
162
+ edits << Interface::TextEdit.new(range: ref.to_lsp_range, new_text: new_short_name)
163
163
  end
164
- end
165
-
166
- #: (String name, RubyIndexer::ReferenceFinder::Reference reference) -> Interface::TextEdit
167
- def adjust_reference_for_edit(name, reference)
168
- # The reference may include a namespace in front. We need to check if the rename new name includes namespaces
169
- # and then adjust both the text and the location to produce the correct edit
170
- location = reference.location
171
- new_text = reference.name.sub(name, @new_name)
172
164
 
173
- Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
165
+ changes
174
166
  end
175
167
 
176
168
  #: (String constant_name) -> String
@@ -64,6 +64,46 @@ module RubyLsp
64
64
  receiver.nil? || receiver.is_a?(Prism::SelfNode)
65
65
  end
66
66
 
67
+ #: (String, Enumerable[Rubydex::Definition], ?Integer?) -> Hash[Symbol, String]
68
+ def categorized_markdown_from_definitions(title, definitions, max_entries = nil)
69
+ markdown_title = "```ruby\n#{title}\n```"
70
+ file_links = []
71
+ content = +""
72
+ defs = max_entries ? definitions.take(max_entries) : definitions
73
+ defs.each do |definition|
74
+ # For Markdown links, we need 1 based display locations
75
+ loc = definition.location.to_display
76
+ uri = URI(loc.uri)
77
+ file_name = if uri.scheme == "untitled"
78
+ uri.opaque #: as !nil
79
+ else
80
+ File.basename(
81
+ uri.full_path, #: as !nil
82
+ )
83
+ end
84
+
85
+ # The format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
86
+ string_uri = "#{loc.uri}#L#{loc.start_line},#{loc.start_column}-#{loc.end_line},#{loc.end_column}"
87
+ file_links << "[#{file_name}](#{string_uri})"
88
+ content << "\n\n#{definition.comments.map { |comment| comment.string.delete_prefix("# ") }.join("\n")}" unless definition.comments.empty?
89
+ end
90
+
91
+ total_definitions = definitions.count
92
+
93
+ additional_entries_text = if max_entries && total_definitions > max_entries
94
+ additional = total_definitions - max_entries
95
+ " | #{additional} other#{additional > 1 ? "s" : ""}"
96
+ else
97
+ ""
98
+ end
99
+
100
+ {
101
+ title: markdown_title,
102
+ links: "**Definitions**: #{file_links.join(" | ")}#{additional_entries_text}",
103
+ documentation: content,
104
+ }
105
+ end
106
+
67
107
  #: (String title, (Array[RubyIndexer::Entry] | RubyIndexer::Entry) entries, ?Integer? max_entries) -> Hash[Symbol, String]
68
108
  def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
69
109
  markdown_title = "```ruby\n#{title}\n```"
@@ -12,54 +12,32 @@ module RubyLsp
12
12
  #: (GlobalState global_state, String? query) -> void
13
13
  def initialize(global_state, query)
14
14
  super()
15
- @global_state = global_state
16
15
  @query = query
17
- @index = global_state.index #: RubyIndexer::Index
16
+ @graph = global_state.graph #: Rubydex::Graph
18
17
  end
19
18
 
20
19
  # @override
21
20
  #: -> Array[Interface::WorkspaceSymbol]
22
21
  def perform
23
- fuzzy_search.filter_map do |entry|
24
- kind = kind_for_entry(entry)
25
- loc = entry.location
22
+ response = []
26
23
 
27
- # We use the namespace as the container name, but we also use the full name as the regular name. The reason we
28
- # do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
29
- # short name `Bar`, then searching for `Foo::Bar` would not return any results
30
- *container, _short_name = entry.name.split("::")
24
+ @graph.fuzzy_search(@query || "").each do |declaration|
25
+ name = declaration.name
31
26
 
32
- Interface::WorkspaceSymbol.new(
33
- name: entry.name,
34
- container_name: container.join("::"),
35
- kind: kind,
36
- location: Interface::Location.new(
37
- uri: entry.uri.to_s,
38
- range: Interface::Range.new(
39
- start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
40
- end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
41
- ),
42
- ),
43
- )
44
- end
45
- end
46
-
47
- private
27
+ declaration.definitions.each do |definition|
28
+ location = definition.location
29
+ uri = URI(location.uri)
30
+ file_path = uri.full_path
48
31
 
49
- #: -> Array[RubyIndexer::Entry]
50
- def fuzzy_search
51
- @index.fuzzy_search(@query) do |entry|
52
- file_path = entry.uri.full_path
32
+ # We only show symbols declared in the workspace
33
+ in_dependencies = file_path && !not_in_dependencies?(file_path)
34
+ next if in_dependencies
53
35
 
54
- # We only show symbols declared in the workspace
55
- in_dependencies = file_path && !not_in_dependencies?(file_path)
56
- next if in_dependencies
57
-
58
- # We should never show private symbols when searching the entire workspace
59
- next if entry.private?
60
-
61
- true
36
+ response << definition.to_lsp_workspace_symbol(name)
37
+ end
62
38
  end
39
+
40
+ response
63
41
  end
64
42
  end
65
43
  end
@@ -43,7 +43,7 @@ module RubyLsp
43
43
  async: 6,
44
44
  modification: 7,
45
45
  documentation: 8,
46
- default_library: 9,
46
+ defaultLibrary: 9,
47
47
  }.freeze #: Hash[Symbol, Integer]
48
48
 
49
49
  #: ((^(Integer arg0) -> Integer | Prism::CodeUnitsCache) code_units_cache) -> void
@@ -184,8 +184,8 @@ module RubyLsp
184
184
  end
185
185
 
186
186
  # Encode an array of modifiers to positions onto a bit flag
187
- # For example, [:default_library] will be encoded as
188
- # 0b1000000000, as :default_library is the 10th bit according
187
+ # For example, [:defaultLibrary] will be encoded as
188
+ # 0b1000000000, as :defaultLibrary is the 10th bit according
189
189
  # to the token modifiers index map.
190
190
  #: (Array[Integer] modifiers) -> Integer
191
191
  def encode_modifiers(modifiers)