ruby-lsp 0.27.0.beta1 → 0.27.0.beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +0 -46
  4. data/exe/ruby-lsp-check +0 -15
  5. data/lib/ruby_lsp/addon.rb +19 -19
  6. data/lib/ruby_lsp/global_state.rb +1 -6
  7. data/lib/ruby_lsp/internal.rb +3 -2
  8. data/lib/ruby_lsp/listeners/code_lens.rb +1 -1
  9. data/lib/ruby_lsp/listeners/completion.rb +246 -382
  10. data/lib/ruby_lsp/listeners/definition.rb +7 -10
  11. data/lib/ruby_lsp/listeners/document_link.rb +4 -0
  12. data/lib/ruby_lsp/listeners/hover.rb +234 -82
  13. data/lib/ruby_lsp/listeners/signature_help.rb +11 -12
  14. data/lib/ruby_lsp/listeners/spec_style.rb +6 -1
  15. data/lib/ruby_lsp/listeners/test_discovery.rb +38 -15
  16. data/lib/ruby_lsp/listeners/test_style.rb +21 -9
  17. data/lib/ruby_lsp/node_context.rb +31 -8
  18. data/lib/ruby_lsp/requests/completion_resolve.rb +55 -39
  19. data/lib/ruby_lsp/requests/discover_tests.rb +5 -41
  20. data/lib/ruby_lsp/requests/hover.rb +2 -5
  21. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +66 -22
  22. data/lib/ruby_lsp/requests/references.rb +180 -66
  23. data/lib/ruby_lsp/requests/rename.rb +1 -1
  24. data/lib/ruby_lsp/requests/request.rb +3 -33
  25. data/lib/ruby_lsp/requests/support/common.rb +82 -68
  26. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +82 -46
  27. data/lib/ruby_lsp/ruby_document.rb +0 -73
  28. data/lib/ruby_lsp/rubydex/declaration.rb +174 -0
  29. data/lib/ruby_lsp/rubydex/definition.rb +73 -0
  30. data/lib/ruby_lsp/rubydex/reference.rb +6 -1
  31. data/lib/ruby_lsp/rubydex/signature.rb +107 -0
  32. data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
  33. data/lib/ruby_lsp/server.rb +56 -171
  34. data/lib/ruby_lsp/test_helper.rb +0 -1
  35. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +89 -11
  37. metadata +12 -18
  38. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +0 -276
  39. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +0 -1101
  40. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +0 -44
  41. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +0 -605
  42. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +0 -1077
  43. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +0 -37
  44. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +0 -149
  45. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +0 -294
  46. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +0 -335
  47. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +0 -32
  48. data/lib/ruby_indexer/ruby_indexer.rb +0 -20
  49. data/lib/ruby_lsp/static_docs.rb +0 -20
  50. data/static_docs/break.md +0 -103
  51. data/static_docs/yield.md +0 -81
  52. /data/lib/{ruby_indexer/lib/ruby_indexer → ruby_lsp}/uri.rb +0 -0
@@ -174,9 +174,10 @@ module RubyLsp
174
174
  #: (Prism::ClassNode node) -> void
175
175
  def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
176
176
  with_test_ancestor_tracking(node) do |name, ancestors|
177
- @framework = :test_unit if ancestors.include?("Test::Unit::TestCase")
177
+ is_test_unit = test_unit_group?(ancestors, name)
178
+ @framework = :test_unit if is_test_unit
178
179
 
179
- if @framework == :test_unit || non_declarative_minitest?(ancestors, name)
180
+ if is_test_unit || non_declarative_minitest?(ancestors, name)
180
181
  test_item = Requests::Support::TestItem.new(
181
182
  name,
182
183
  name,
@@ -219,7 +220,7 @@ module RubyLsp
219
220
  name = node.name.to_s
220
221
  return unless name.start_with?("test_")
221
222
 
222
- current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
223
+ current_group_name = calc_fully_qualified_name(nil)
223
224
  parent = @parent_stack.last
224
225
  return unless parent.is_a?(Requests::Support::TestItem)
225
226
 
@@ -259,17 +260,28 @@ module RubyLsp
259
260
  @parent_stack[index] #: as Requests::Support::TestItem | ResponseBuilders::TestCollection
260
261
  end
261
262
 
262
- #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
263
+ #: (Array[String], String) -> bool
264
+ def test_unit_group?(ancestors, fully_qualified_name)
265
+ fully_qualified_name != "Test::Unit::TestCase" && ancestors.include?("Test::Unit::TestCase")
266
+ end
267
+
268
+ #: (Array[String], String) -> bool
263
269
  def non_declarative_minitest?(attached_ancestors, fully_qualified_name)
270
+ return false if ["Minitest::Spec", "Minitest::Test", "ActiveSupport::TestCase"].include?(fully_qualified_name)
264
271
  return false unless attached_ancestors.include?("Minitest::Test")
265
272
 
266
273
  # We only support regular Minitest tests. The declarative syntax provided by ActiveSupport is handled by the
267
274
  # Rails add-on
268
- name_parts = fully_qualified_name.split("::")
269
- singleton_name = "#{name_parts.join("::")}::<#{name_parts.last}>"
270
- !@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
271
- rescue RubyIndexer::Index::NonExistingNamespaceError
272
- true
275
+
276
+ declaration = @graph[fully_qualified_name]
277
+ # If we don't find the fully qualified name in the graph, it means there's a dynamic portion in the test class
278
+ # definition. In that case, if the ancestors did include `Minitest::Test`, we always assume it's a test
279
+ return true unless declaration.is_a?(Rubydex::Namespace)
280
+
281
+ singleton = declaration.singleton_class
282
+ return !singleton.ancestors.map(&:name).include?("ActiveSupport::Testing::Declarative") if singleton
283
+
284
+ !attached_ancestors.include?("ActiveSupport::TestCase")
273
285
  end
274
286
  end
275
287
  end
@@ -5,6 +5,21 @@ module RubyLsp
5
5
  # This class allows listeners to access contextual information about a node in the AST, such as its parent,
6
6
  # its namespace nesting, and the surrounding CallNode (e.g. a method call).
7
7
  class NodeContext
8
+ # Represents the surrounding method definition context, tracking both the method name and its receiver
9
+ class MethodDef
10
+ #: String
11
+ attr_reader :name
12
+
13
+ #: String?
14
+ attr_reader :receiver
15
+
16
+ #: (String name, String? receiver) -> void
17
+ def initialize(name, receiver)
18
+ @name = name
19
+ @receiver = receiver
20
+ end
21
+ end
22
+
8
23
  #: Prism::Node?
9
24
  attr_reader :node, :parent
10
25
 
@@ -14,7 +29,7 @@ module RubyLsp
14
29
  #: Prism::CallNode?
15
30
  attr_reader :call_node
16
31
 
17
- #: String?
32
+ #: MethodDef?
18
33
  attr_reader :surrounding_method
19
34
 
20
35
  #: (Prism::Node? node, Prism::Node? parent, Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nesting_nodes, Prism::CallNode? call_node) -> void
@@ -26,7 +41,7 @@ module RubyLsp
26
41
 
27
42
  nesting, surrounding_method = handle_nesting_nodes(nesting_nodes)
28
43
  @nesting = nesting #: Array[String]
29
- @surrounding_method = surrounding_method #: String?
44
+ @surrounding_method = surrounding_method #: MethodDef?
30
45
  end
31
46
 
32
47
  #: -> String
@@ -52,10 +67,10 @@ module RubyLsp
52
67
 
53
68
  private
54
69
 
55
- #: (Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nodes) -> [Array[String], String?]
70
+ #: (Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] nodes) -> [Array[String], MethodDef?]
56
71
  def handle_nesting_nodes(nodes)
57
72
  nesting = []
58
- surrounding_method = nil #: String?
73
+ surrounding_method = nil #: MethodDef?
59
74
 
60
75
  @nesting_nodes.each do |node|
61
76
  case node
@@ -64,10 +79,18 @@ module RubyLsp
64
79
  when Prism::SingletonClassNode
65
80
  nesting << "<#{nesting.flat_map { |n| n.split("::") }.last}>"
66
81
  when Prism::DefNode
67
- surrounding_method = node.name.to_s
68
- next unless node.receiver.is_a?(Prism::SelfNode)
69
-
70
- nesting << "<#{nesting.flat_map { |n| n.split("::") }.last}>"
82
+ receiver = node.receiver
83
+
84
+ surrounding_method = case receiver
85
+ when nil
86
+ MethodDef.new(node.name.to_s, "none")
87
+ when Prism::SelfNode
88
+ MethodDef.new(node.name.to_s, "self")
89
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
90
+ MethodDef.new(node.name.to_s, receiver.slice)
91
+ else
92
+ MethodDef.new(node.name.to_s, nil)
93
+ end
71
94
  end
72
95
  end
73
96
 
@@ -16,6 +16,12 @@ module RubyLsp
16
16
  class CompletionResolve < Request
17
17
  include Requests::Support::Common
18
18
 
19
+ METHOD_KINDS = [
20
+ Constant::CompletionItemKind::METHOD,
21
+ Constant::CompletionItemKind::CONSTRUCTOR,
22
+ Constant::CompletionItemKind::FUNCTION,
23
+ ].freeze #: Array[Integer]
24
+
19
25
  # set a limit on the number of documentation entries returned, to avoid rendering performance issues
20
26
  # https://github.com/Shopify/ruby-lsp/pull/1798
21
27
  MAX_DOCUMENTATION_ENTRIES = 10
@@ -23,7 +29,7 @@ module RubyLsp
23
29
  #: (GlobalState global_state, Hash[Symbol, untyped] item) -> void
24
30
  def initialize(global_state, item)
25
31
  super()
26
- @index = global_state.index #: RubyIndexer::Index
32
+ @graph = global_state.graph #: Rubydex::Graph
27
33
  @item = item
28
34
  end
29
35
 
@@ -31,6 +37,8 @@ module RubyLsp
31
37
  #: -> Hash[Symbol, untyped]
32
38
  def perform
33
39
  return @item if @item.dig(:data, :skip_resolve)
40
+ return keyword_resolve if @item.dig(:data, :keyword)
41
+ return @item if @item[:kind] == Constant::CompletionItemKind::FILE
34
42
 
35
43
  # Based on the spec https://microsoft.github.io/language-server-protocol/specification#textDocument_completion,
36
44
  # a completion resolve request must always return the original completion item without modifying ANY fields
@@ -39,70 +47,78 @@ module RubyLsp
39
47
  #
40
48
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
41
49
  # `label` for the text edit instead
42
- label = @item[:label].dup
43
- return keyword_resolve(@item) if @item.dig(:data, :keyword)
44
-
45
- entries = @index[label] || []
46
-
47
- owner_name = @item.dig(:data, :owner_name)
48
-
49
- if owner_name
50
- entries = entries.select do |entry|
51
- (entry.is_a?(RubyIndexer::Entry::Member) || entry.is_a?(RubyIndexer::Entry::InstanceVariable) ||
52
- entry.is_a?(RubyIndexer::Entry::MethodAlias) || entry.is_a?(RubyIndexer::Entry::ClassVariable)) &&
53
- entry.owner&.name == owner_name
54
- end
55
- end
50
+ declaration = resolve_declaration
51
+ return @item unless declaration
56
52
 
57
- first_entry = entries.first #: as !nil
53
+ guessed_type = @item.dig(:data, :guessed_type)
54
+ title = @item[:label].dup
58
55
 
59
- if first_entry.is_a?(RubyIndexer::Entry::Member)
60
- label = +"#{label}#{first_entry.decorated_parameters}"
61
- label << first_entry.formatted_signatures
56
+ if declaration.is_a?(Rubydex::Method)
57
+ title << declaration.decorated_parameters
58
+ title << declaration.formatted_signatures
62
59
  end
63
60
 
64
- guessed_type = @item.dig(:data, :guessed_type)
65
-
66
61
  extra_links = if guessed_type
67
- label << "\n\nGuessed receiver: #{guessed_type}"
62
+ title << "\n\nGuessed receiver: #{guessed_type}"
68
63
  "[Learn more about guessed types](#{GUESSED_TYPES_URL})"
69
64
  end
70
65
 
71
- unless @item[:kind] == Constant::CompletionItemKind::FILE
72
- @item[:documentation] = Interface::MarkupContent.new(
73
- kind: "markdown",
74
- value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES, extra_links: extra_links),
75
- )
76
- end
66
+ @item[:documentation] = Interface::MarkupContent.new(
67
+ kind: "markdown",
68
+ value: markdown_from_definitions(
69
+ title,
70
+ declaration.definitions,
71
+ MAX_DOCUMENTATION_ENTRIES,
72
+ extra_links: extra_links,
73
+ ),
74
+ )
77
75
 
78
76
  @item
79
77
  end
80
78
 
81
79
  private
82
80
 
83
- #: (Hash[Symbol, untyped] item) -> Hash[Symbol, untyped]
84
- def keyword_resolve(item)
85
- keyword = item[:label]
86
- content = KEYWORD_DOCS[keyword]
81
+ # Find the Rubydex declaration that matches the completion item. Constants are looked up by their fully qualified
82
+ # name (set when the completion was produced); members (methods, instance/class variables) are resolved by walking
83
+ # the owner namespace and its ancestors so that inherited and aliased members are surfaced correctly.
84
+ #: -> Rubydex::Declaration?
85
+ def resolve_declaration
86
+ data = @item[:data] || {}
87
+
88
+ if (fully_qualified_name = data[:fully_qualified_name])
89
+ @graph[fully_qualified_name]
90
+ elsif (owner_name = data[:owner_name])
91
+ owner = @graph[owner_name]
92
+ return unless owner.is_a?(Rubydex::Namespace)
93
+
94
+ member_name = if METHOD_KINDS.include?(@item[:kind])
95
+ "#{@item[:label]}()"
96
+ else
97
+ @item[:label]
98
+ end
99
+
100
+ owner.find_member(member_name)
101
+ end
102
+ end
87
103
 
88
- if content
89
- doc_uri = URI::Generic.from_path(path: File.join(STATIC_DOCS_PATH, "#{keyword}.md"))
104
+ #: -> Hash[Symbol, untyped]
105
+ def keyword_resolve
106
+ keyword = @graph.keyword(@item[:label])
90
107
 
108
+ if keyword
91
109
  @item[:documentation] = Interface::MarkupContent.new(
92
110
  kind: "markdown",
93
111
  value: <<~MARKDOWN.chomp,
94
112
  ```ruby
95
- #{keyword}
113
+ #{keyword.name}
96
114
  ```
97
115
 
98
- [Read more](#{doc_uri})
99
-
100
- #{content}
116
+ #{keyword.documentation}
101
117
  MARKDOWN
102
118
  )
103
119
  end
104
120
 
105
- item
121
+ @item
106
122
  end
107
123
  end
108
124
  end
@@ -19,55 +19,19 @@ module RubyLsp
19
19
  @document = document
20
20
  @dispatcher = dispatcher
21
21
  @response_builder = ResponseBuilders::TestCollection.new #: ResponseBuilders::TestCollection
22
- @index = global_state.index #: RubyIndexer::Index
23
22
  end
24
23
 
25
24
  # @override
26
25
  #: -> Array[Support::TestItem]
27
26
  def perform
28
- uri = @document.uri
27
+ Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
28
+ Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
29
29
 
30
- # We normally only index test files once they are opened in the editor to save memory and avoid doing
31
- # unnecessary work. If the file is already opened and we already indexed it, then we can just discover the tests
32
- # straight away.
33
- #
34
- # However, if the user navigates to a specific test file from the explorer with nothing opened in the UI, then
35
- # we will not have indexed the test file yet and trying to linearize the ancestor of the class will fail. In
36
- # this case, we have to instantiate the indexer listener first, so that we insert classes, modules and methods
37
- # in the index first and then discover the tests, all in the same traversal.
38
- if @index.entries_for(uri.to_s)
39
- Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
40
- Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
41
-
42
- Addon.addons.each do |addon|
43
- addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
44
- end
45
-
46
- @dispatcher.visit(@document.ast)
47
- else
48
- @global_state.synchronize do
49
- RubyIndexer::DeclarationListener.new(
50
- @index,
51
- @dispatcher,
52
- @document.parse_result,
53
- uri,
54
- collect_comments: true,
55
- )
56
-
57
- Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
58
- Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
59
-
60
- Addon.addons.each do |addon|
61
- addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
62
- end
63
-
64
- # Dispatch the events both for indexing the test file and discovering the tests. The order here is
65
- # important because we need the index to be aware of the existing classes/modules/methods before the test
66
- # listeners can do their work
67
- @dispatcher.visit(@document.ast)
68
- end
30
+ Addon.addons.each do |addon|
31
+ addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
69
32
  end
70
33
 
34
+ @dispatcher.visit(@document.ast)
71
35
  @response_builder.response
72
36
  end
73
37
  end
@@ -26,7 +26,6 @@ module RubyLsp
26
26
  node_context = RubyDocument.locate(
27
27
  document.ast,
28
28
  char_position,
29
- node_types: Listeners::Hover::ALLOWED_TARGETS,
30
29
  code_units_cache: document.code_units_cache,
31
30
  )
32
31
  target = node_context.node
@@ -48,7 +47,7 @@ module RubyLsp
48
47
  @target = target #: Prism::Node?
49
48
  uri = document.uri
50
49
  @response_builder = ResponseBuilders::Hover.new #: ResponseBuilders::Hover
51
- Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, sorbet_level)
50
+ Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, sorbet_level, position)
52
51
  Addon.addons.each do |addon|
53
52
  addon.create_hover_listener(@response_builder, node_context, dispatcher)
54
53
  end
@@ -77,9 +76,7 @@ module RubyLsp
77
76
 
78
77
  #: (Prism::Node? parent, Prism::Node? target) -> bool
79
78
  def should_refine_target?(parent, target)
80
- (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
81
- !Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
82
- (parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
79
+ parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode)
83
80
  end
84
81
 
85
82
  #: (Hash[Symbol, untyped] position, Prism::Node? target) -> bool
@@ -5,9 +5,7 @@ module RubyLsp
5
5
  module Requests
6
6
  # The [prepare type hierarchy
7
7
  # request](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareTypeHierarchy)
8
- # displays the list of ancestors (supertypes) and descendants (subtypes) for the selected type.
9
- #
10
- # Currently only supports supertypes due to a limitation of the index.
8
+ # displays the list of direct ancestors (supertypes) and descendants (subtypes) for the selected type.
11
9
  class PrepareTypeHierarchy < Request
12
10
  include Support::Common
13
11
 
@@ -18,12 +16,12 @@ module RubyLsp
18
16
  end
19
17
  end
20
18
 
21
- #: ((RubyDocument | ERBDocument) document, RubyIndexer::Index index, Hash[Symbol, untyped] position) -> void
22
- def initialize(document, index, position)
19
+ #: ((RubyDocument | ERBDocument) document, GlobalState global_state, Hash[Symbol, untyped] position) -> void
20
+ def initialize(document, global_state, position)
23
21
  super()
24
22
 
25
23
  @document = document
26
- @index = index
24
+ @graph = global_state.graph #: Rubydex::Graph
27
25
  @position = position
28
26
  end
29
27
 
@@ -36,32 +34,78 @@ module RubyLsp
36
34
  Prism::ConstantReadNode,
37
35
  Prism::ConstantWriteNode,
38
36
  Prism::ConstantPathNode,
37
+ Prism::SingletonClassNode,
39
38
  ],
40
39
  )
41
40
 
42
- node = context.node
43
- parent = context.parent
44
- return unless node && parent
41
+ node = context.node #: as (Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode)?
42
+ return unless node
43
+
44
+ pair = name_and_nesting(node, context)
45
+ return unless pair
45
46
 
46
- target = determine_target(node, parent, @position)
47
- entries = @index.resolve(target.slice, context.nesting)
48
- return unless entries
47
+ declaration = @graph.resolve_constant(pair.first, pair.last)
48
+ return unless declaration.is_a?(Rubydex::Namespace)
49
49
 
50
- # While the spec allows for multiple entries, VSCode seems to only support one
51
- # We'll just return the first one for now
52
- first_entry = entries.first #: as !nil
53
- range = range_from_location(first_entry.location)
50
+ primary = declaration.definitions.first
51
+ return unless primary
54
52
 
55
53
  [
56
- Interface::TypeHierarchyItem.new(
57
- name: first_entry.name,
58
- kind: kind_for_entry(first_entry),
59
- uri: first_entry.uri.to_s,
60
- range: range,
61
- selection_range: range,
54
+ primary.to_lsp_type_hierarchy_item(
55
+ declaration.name,
56
+ detail: declaration.lsp_type_hierarchy_detail,
62
57
  ),
63
58
  ]
64
59
  end
60
+
61
+ private
62
+
63
+ # Returns the `(name, nesting)` pair to pass to `Rubydex::Graph#resolve_constant`, covering three cases:
64
+ #
65
+ #: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), NodeContext) -> [String, Array[String]]?
66
+ def name_and_nesting(node, context)
67
+ parent = context.parent
68
+ nesting = context.nesting
69
+
70
+ singleton_node = singleton_class_node_for(node, parent)
71
+ return singleton_lookup(singleton_node, nesting) if singleton_node
72
+
73
+ target = parent ? determine_target(node, parent, @position) : node
74
+ [target.slice, nesting]
75
+ end
76
+
77
+ # Ensures that we're returning the target of the singleton class block regardless of whether the cursor is on the
78
+ # `class` keyword or the constant reference for the target
79
+ #: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), Prism::Node?) -> Prism::SingletonClassNode?
80
+ def singleton_class_node_for(node, parent)
81
+ return node if node.is_a?(Prism::SingletonClassNode)
82
+ return unless parent.is_a?(Prism::SingletonClassNode) && parent.expression == node
83
+
84
+ parent
85
+ end
86
+
87
+ # Builds the synthesized singleton class name (e.g. `Foo::<Foo>`) for a `class << X` block, together with the
88
+ # outer lexical nesting. `NodeContext` already appends a `<ClassName>` marker as the last element of the nesting
89
+ # whenever the cursor sits inside (or on) a `SingletonClassNode`, so we drop that marker to obtain the scope in
90
+ # which the singleton should be resolved.
91
+ #: (Prism::SingletonClassNode, Array[String]) -> [String, Array[String]]?
92
+ def singleton_lookup(singleton_node, nesting)
93
+ outer = nesting[0...-1] || []
94
+
95
+ case expression = singleton_node.expression
96
+ when Prism::SelfNode
97
+ name = nesting.last
98
+ return unless name
99
+
100
+ [name, outer]
101
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
102
+ name = constant_name(expression)
103
+ return unless name
104
+
105
+ unqualified = name.split("::").last #: as !nil
106
+ ["#{name}::<#{unqualified}>", outer]
107
+ end
108
+ end
65
109
  end
66
110
  end
67
111
  end