ruby-lsp 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,150 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class PrefixTreeTest < Minitest::Test
8
+ def test_empty
9
+ tree = PrefixTree.new
10
+
11
+ assert_empty(tree.search(""))
12
+ assert_empty(tree.search("foo"))
13
+ end
14
+
15
+ def test_single_item
16
+ tree = PrefixTree.new
17
+ tree.insert("foo", "foo")
18
+
19
+ assert_equal(["foo"], tree.search(""))
20
+ assert_equal(["foo"], tree.search("foo"))
21
+ assert_empty(tree.search("bar"))
22
+ end
23
+
24
+ def test_multiple_items
25
+ tree = PrefixTree[String].new
26
+ ["foo", "bar", "baz"].each { |item| tree.insert(item, item) }
27
+
28
+ assert_equal(["foo", "bar", "baz"], tree.search(""))
29
+ assert_equal(["bar", "baz"], tree.search("b"))
30
+ assert_equal(["foo"], tree.search("fo"))
31
+ assert_equal(["bar", "baz"], tree.search("ba"))
32
+ assert_equal(["baz"], tree.search("baz"))
33
+ assert_empty(tree.search("qux"))
34
+ end
35
+
36
+ def test_multiple_prefixes
37
+ tree = PrefixTree[String].new
38
+ ["fo", "foo"].each { |item| tree.insert(item, item) }
39
+
40
+ assert_equal(["fo", "foo"], tree.search(""))
41
+ assert_equal(["fo", "foo"], tree.search("f"))
42
+ assert_equal(["fo", "foo"], tree.search("fo"))
43
+ assert_equal(["foo"], tree.search("foo"))
44
+ assert_empty(tree.search("fooo"))
45
+ end
46
+
47
+ def test_multiple_prefixes_with_shuffled_order
48
+ tree = PrefixTree[String].new
49
+ [
50
+ "foo/bar/base",
51
+ "foo/bar/on",
52
+ "foo/bar/support/selection",
53
+ "foo/bar/support/runner",
54
+ "foo/internal",
55
+ "foo/bar/document",
56
+ "foo/bar/code",
57
+ "foo/bar/support/rails",
58
+ "foo/bar/diagnostics",
59
+ "foo/bar/document2",
60
+ "foo/bar/support/runner2",
61
+ "foo/bar/support/diagnostic",
62
+ "foo/document",
63
+ "foo/bar/formatting",
64
+ "foo/bar/support/highlight",
65
+ "foo/bar/semantic",
66
+ "foo/bar/support/prefix",
67
+ "foo/bar/folding",
68
+ "foo/bar/selection",
69
+ "foo/bar/support/syntax",
70
+ "foo/bar/document3",
71
+ "foo/bar/hover",
72
+ "foo/bar/support/semantic",
73
+ "foo/bar/support/source",
74
+ "foo/bar/inlay",
75
+ "foo/requests",
76
+ "foo/bar/support/formatting",
77
+ "foo/bar/path",
78
+ "foo/executor",
79
+ ].each { |item| tree.insert(item, item) }
80
+
81
+ assert_equal(
82
+ [
83
+ "foo/bar/support/selection",
84
+ "foo/bar/support/semantic",
85
+ "foo/bar/support/syntax",
86
+ "foo/bar/support/source",
87
+ "foo/bar/support/runner",
88
+ "foo/bar/support/runner2",
89
+ "foo/bar/support/rails",
90
+ "foo/bar/support/diagnostic",
91
+ "foo/bar/support/highlight",
92
+ "foo/bar/support/prefix",
93
+ "foo/bar/support/formatting",
94
+ ],
95
+ tree.search("foo/bar/support"),
96
+ )
97
+ end
98
+
99
+ def test_deletion
100
+ tree = PrefixTree[String].new
101
+ ["foo/bar", "foo/baz"].each { |item| tree.insert(item, item) }
102
+ assert_equal(["foo/bar", "foo/baz"], tree.search("foo"))
103
+
104
+ tree.delete("foo/bar")
105
+ assert_empty(tree.search("foo/bar"))
106
+ assert_equal(["foo/baz"], tree.search("foo"))
107
+ end
108
+
109
+ def test_delete_does_not_impact_other_keys_with_the_same_value
110
+ tree = PrefixTree[String].new
111
+ tree.insert("key1", "value")
112
+ tree.insert("key2", "value")
113
+ assert_equal(["value", "value"], tree.search("key"))
114
+
115
+ tree.delete("key2")
116
+ assert_empty(tree.search("key2"))
117
+ assert_equal(["value"], tree.search("key1"))
118
+ end
119
+
120
+ def test_deleted_node_is_removed_from_the_tree
121
+ tree = PrefixTree[String].new
122
+ tree.insert("foo/bar", "foo/bar")
123
+ assert_equal(["foo/bar"], tree.search("foo"))
124
+
125
+ tree.delete("foo/bar")
126
+ root = tree.instance_variable_get(:@root)
127
+ assert_empty(root.children)
128
+ end
129
+
130
+ def test_deleting_non_terminal_nodes
131
+ tree = PrefixTree[String].new
132
+ tree.insert("abc", "value1")
133
+ tree.insert("abcdef", "value2")
134
+
135
+ tree.delete("abcdef")
136
+ assert_empty(tree.search("abcdef"))
137
+ assert_equal(["value1"], tree.search("abc"))
138
+ end
139
+
140
+ def test_overriding_values
141
+ tree = PrefixTree[Integer].new
142
+
143
+ tree.insert("foo/bar", 123)
144
+ assert_equal([123], tree.search("foo/bar"))
145
+
146
+ tree.insert("foo/bar", 456)
147
+ assert_equal([456], tree.search("foo/bar"))
148
+ end
149
+ end
150
+ end
@@ -12,7 +12,7 @@ module RubyIndexer
12
12
  private
13
13
 
14
14
  def index(source)
15
- @index.index_single("/fake/path/foo.rb", source)
15
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), source)
16
16
  end
17
17
 
18
18
  def assert_entry(expected_name, type, expected_location)
@@ -53,7 +53,8 @@ module RubyLsp
53
53
  # documented
54
54
  features = ObjectSpace.each_object(Class).filter_map do |k|
55
55
  klass = T.unsafe(k)
56
- klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
56
+ klass if klass < RubyLsp::Requests::BaseRequest ||
57
+ (klass < RubyLsp::Listener && klass != RubyLsp::ExtensibleListener)
57
58
  end
58
59
 
59
60
  missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
@@ -48,6 +48,8 @@ module RubyLsp
48
48
  @listeners[:on_const_path_ref]&.each { |l| T.unsafe(l).on_const_path_ref(node) }
49
49
  when SyntaxTree::Const
50
50
  @listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
51
+ when SyntaxTree::TopConstRef
52
+ @listeners[:on_top_const_ref]&.each { |l| T.unsafe(l).on_top_const_ref(node) }
51
53
  end
52
54
  end
53
55
 
@@ -103,9 +103,6 @@ module RubyLsp
103
103
  semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
104
104
  emitter.visit(document.tree) if document.parsed?
105
105
 
106
- code_lens.merge_external_listeners_responses!
107
- document_symbol.merge_external_listeners_responses!
108
-
109
106
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
110
107
  # we actually received
111
108
  document.cache_set("textDocument/documentSymbol", document_symbol.response)
@@ -191,14 +188,17 @@ module RubyLsp
191
188
  file_path = uri.to_standardized_path
192
189
  next if file_path.nil? || File.directory?(file_path)
193
190
 
191
+ load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
192
+ indexable = RubyIndexer::IndexablePath.new(load_path_entry, file_path)
193
+
194
194
  case change[:type]
195
195
  when Constant::FileChangeType::CREATED
196
- @index.index_single(file_path)
196
+ @index.index_single(indexable)
197
197
  when Constant::FileChangeType::CHANGED
198
- @index.delete(file_path)
199
- @index.index_single(file_path)
198
+ @index.delete(indexable)
199
+ @index.index_single(indexable)
200
200
  when Constant::FileChangeType::DELETED
201
- @index.delete(file_path)
201
+ @index.delete(indexable)
202
202
  end
203
203
  end
204
204
 
@@ -296,11 +296,12 @@ module RubyLsp
296
296
  # Emit events for all listeners
297
297
  emitter.emit_for_target(target)
298
298
 
299
- hover.merge_external_listeners_responses!
300
299
  hover.response
301
300
  end
302
301
 
303
- sig { params(uri: URI::Generic, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
302
+ sig do
303
+ params(uri: URI::Generic, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object)
304
+ end
304
305
  def text_document_did_change(uri, content_changes, version)
305
306
  @store.push_edits(uri: uri, edits: content_changes, version: version)
306
307
  VOID
@@ -473,12 +474,18 @@ module RubyLsp
473
474
  return unless document.parsed?
474
475
 
475
476
  char_position = document.create_scanner.find_char_position(position)
476
- matched, parent = document.locate(
477
- T.must(document.tree),
478
- char_position,
479
- node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
480
- )
481
477
 
478
+ # When the user types in the first letter of a constant name, we actually receive the position of the next
479
+ # immediate character. We check to see if the character is uppercase and then remove the offset to try to locate
480
+ # the node, as it could not be a constant
481
+ target_node_types = if ("A".."Z").cover?(document.source[char_position - 1])
482
+ char_position -= 1
483
+ [SyntaxTree::Const, SyntaxTree::ConstPathRef, SyntaxTree::TopConstRef]
484
+ else
485
+ [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode]
486
+ end
487
+
488
+ matched, parent, nesting = document.locate(T.must(document.tree), char_position, node_types: target_node_types)
482
489
  return unless matched && parent
483
490
 
484
491
  target = case matched
@@ -499,12 +506,19 @@ module RubyLsp
499
506
  return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
500
507
 
501
508
  path_node
509
+ when SyntaxTree::Const, SyntaxTree::ConstPathRef
510
+ if (parent.is_a?(SyntaxTree::ConstPathRef) || parent.is_a?(SyntaxTree::TopConstRef)) &&
511
+ matched.is_a?(SyntaxTree::Const)
512
+ parent
513
+ else
514
+ matched
515
+ end
502
516
  end
503
517
 
504
518
  return unless target
505
519
 
506
520
  emitter = EventEmitter.new
507
- listener = Requests::PathCompletion.new(emitter, @message_queue)
521
+ listener = Requests::Completion.new(@index, nesting, emitter, @message_queue)
508
522
  emitter.emit_for_target(target)
509
523
  listener.response
510
524
  end
@@ -640,7 +654,10 @@ module RubyLsp
640
654
  completion_provider = if enabled_features["completion"]
641
655
  Interface::CompletionOptions.new(
642
656
  resolve_provider: false,
643
- trigger_characters: ["/"],
657
+ trigger_characters: ["/", *"A".."Z"],
658
+ completion_item: {
659
+ labelDetailsSupport: true,
660
+ },
644
661
  )
645
662
  end
646
663
 
@@ -130,8 +130,20 @@ module RubyLsp
130
130
  overridable.params(
131
131
  emitter: EventEmitter,
132
132
  message_queue: Thread::Queue,
133
- ).returns(T.nilable(Listener[T.nilable(Interface::DocumentSymbol)]))
133
+ ).returns(T.nilable(Listener[T::Array[Interface::DocumentSymbol]]))
134
134
  end
135
135
  def create_document_symbol_listener(emitter, message_queue); end
136
+
137
+ # Creates a new Definition listener. This method is invoked on every Definition request
138
+ sig do
139
+ overridable.params(
140
+ uri: URI::Generic,
141
+ nesting: T::Array[String],
142
+ index: RubyIndexer::Index,
143
+ emitter: EventEmitter,
144
+ message_queue: Thread::Queue,
145
+ ).returns(T.nilable(Listener[T.nilable(T.any(T::Array[Interface::Location], Interface::Location))]))
146
+ end
147
+ def create_definition_listener(uri, nesting, index, emitter, message_queue); end
136
148
  end
137
149
  end
@@ -18,13 +18,42 @@ module RubyLsp
18
18
  def initialize(emitter, message_queue)
19
19
  @emitter = emitter
20
20
  @message_queue = message_queue
21
- @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
21
+ end
22
+
23
+ sig { returns(ResponseType) }
24
+ def response
25
+ _response
22
26
  end
23
27
 
24
28
  # Override this method with an attr_reader that returns the response of your listener. The listener should
25
29
  # accumulate results in a @response variable and then provide the reader so that it is accessible
26
30
  sig { abstract.returns(ResponseType) }
27
- def response; end
31
+ def _response; end
32
+ end
33
+
34
+ # ExtensibleListener is an abstract class to be used by requests that accept extensions.
35
+ class ExtensibleListener < Listener
36
+ extend T::Sig
37
+ extend T::Generic
38
+
39
+ ResponseType = type_member
40
+
41
+ abstract!
42
+
43
+ # When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
44
+ # ivars have been initialized. This is because the constructor of ExtensibleListener calls
45
+ # `initialize_external_listener` which may depend on the subclass's ivars.
46
+ sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
47
+ def initialize(emitter, message_queue)
48
+ super
49
+ @response_merged = T.let(false, T::Boolean)
50
+ @external_listeners = T.let(
51
+ Extension.extensions.filter_map do |ext|
52
+ initialize_external_listener(ext)
53
+ end,
54
+ T::Array[RubyLsp::Listener[ResponseType]],
55
+ )
56
+ end
28
57
 
29
58
  # Merge responses from all external listeners into the base listener's response. We do this to return a single
30
59
  # response to the editor including the results of all extensions
@@ -33,11 +62,21 @@ module RubyLsp
33
62
  @external_listeners.each { |l| merge_response!(l) }
34
63
  end
35
64
 
65
+ sig { returns(ResponseType) }
66
+ def response
67
+ merge_external_listeners_responses! unless @response_merged
68
+ super
69
+ end
70
+
71
+ sig do
72
+ abstract.params(extension: RubyLsp::Extension).returns(T.nilable(RubyLsp::Listener[ResponseType]))
73
+ end
74
+ def initialize_external_listener(extension); end
75
+
36
76
  # Does nothing by default. Requests that accept extensions should override this method to define how to merge
37
77
  # responses coming from external listeners
38
- sig { overridable.params(other: Listener[T.untyped]).returns(T.self_type) }
78
+ sig { abstract.params(other: Listener[T.untyped]).returns(T.self_type) }
39
79
  def merge_response!(other)
40
- self
41
80
  end
42
81
  end
43
82
  end
@@ -18,7 +18,7 @@ module RubyLsp
18
18
  # class Test < Minitest::Test
19
19
  # end
20
20
  # ```
21
- class CodeLens < Listener
21
+ class CodeLens < ExtensibleListener
22
22
  extend T::Sig
23
23
  extend T::Generic
24
24
 
@@ -29,23 +29,20 @@ module RubyLsp
29
29
  SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
30
30
 
31
31
  sig { override.returns(ResponseType) }
32
- attr_reader :response
32
+ attr_reader :_response
33
33
 
34
34
  sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
35
35
  def initialize(uri, emitter, message_queue, test_library)
36
- super(emitter, message_queue)
37
-
38
36
  @uri = T.let(uri, URI::Generic)
39
- @external_listeners.concat(
40
- Extension.extensions.filter_map { |ext| ext.create_code_lens_listener(uri, emitter, message_queue) },
41
- )
42
37
  @test_library = T.let(test_library, String)
43
- @response = T.let([], ResponseType)
38
+ @_response = T.let([], ResponseType)
44
39
  @path = T.let(uri.to_standardized_path, T.nilable(String))
45
40
  # visibility_stack is a stack of [current_visibility, previous_visibility]
46
41
  @visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
47
42
  @class_stack = T.let([], T::Array[String])
48
43
 
44
+ super(emitter, message_queue)
45
+
49
46
  emitter.register(
50
47
  self,
51
48
  :on_class,
@@ -149,9 +146,14 @@ module RubyLsp
149
146
  end
150
147
  end
151
148
 
149
+ sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
150
+ def initialize_external_listener(extension)
151
+ extension.create_code_lens_listener(@uri, @emitter, @message_queue)
152
+ end
153
+
152
154
  sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
153
155
  def merge_response!(other)
154
- @response.concat(other.response)
156
+ @_response.concat(other.response)
155
157
  self
156
158
  end
157
159
 
@@ -174,7 +176,7 @@ module RubyLsp
174
176
  },
175
177
  ]
176
178
 
177
- @response << create_code_lens(
179
+ @_response << create_code_lens(
178
180
  node,
179
181
  title: "Run",
180
182
  command_name: "rubyLsp.runTest",
@@ -182,7 +184,7 @@ module RubyLsp
182
184
  data: { type: "test", kind: kind },
183
185
  )
184
186
 
185
- @response << create_code_lens(
187
+ @_response << create_code_lens(
186
188
  node,
187
189
  title: "Run In Terminal",
188
190
  command_name: "rubyLsp.runTestInTerminal",
@@ -190,7 +192,7 @@ module RubyLsp
190
192
  data: { type: "test_in_terminal", kind: kind },
191
193
  )
192
194
 
193
- @response << create_code_lens(
195
+ @_response << create_code_lens(
194
196
  node,
195
197
  title: "Debug",
196
198
  command_name: "rubyLsp.debugTest",
@@ -239,7 +241,7 @@ module RubyLsp
239
241
 
240
242
  sig { params(node: SyntaxTree::Command, remote: String).void }
241
243
  def add_open_gem_remote_code_lens(node, remote)
242
- @response << create_code_lens(
244
+ @_response << create_code_lens(
243
245
  node,
244
246
  title: "Open remote",
245
247
  command_name: "rubyLsp.openLink",
@@ -0,0 +1,168 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Completion demo](../../completion.gif)
7
+ #
8
+ # The [completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
9
+ # suggests possible completions according to what the developer is typing. Currently, completion is support for
10
+ # - require paths
11
+ # - classes, modules and constant names
12
+ #
13
+ # # Example
14
+ #
15
+ # ```ruby
16
+ # require "ruby_lsp/requests" # --> completion: suggests `base_request`, `code_actions`, ...
17
+ #
18
+ # RubyLsp::Requests:: # --> completion: suggests `Completion`, `Hover`, ...
19
+ # ```
20
+ class Completion < Listener
21
+ extend T::Sig
22
+ extend T::Generic
23
+
24
+ ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
25
+
26
+ sig { override.returns(ResponseType) }
27
+ attr_reader :_response
28
+
29
+ sig do
30
+ params(
31
+ index: RubyIndexer::Index,
32
+ nesting: T::Array[String],
33
+ emitter: EventEmitter,
34
+ message_queue: Thread::Queue,
35
+ ).void
36
+ end
37
+ def initialize(index, nesting, emitter, message_queue)
38
+ super(emitter, message_queue)
39
+ @_response = T.let([], ResponseType)
40
+ @index = index
41
+ @nesting = nesting
42
+
43
+ emitter.register(self, :on_tstring_content, :on_const_path_ref, :on_const, :on_top_const_ref)
44
+ end
45
+
46
+ sig { params(node: SyntaxTree::TStringContent).void }
47
+ def on_tstring_content(node)
48
+ @index.search_require_paths(node.value).map!(&:require_path).sort!.each do |path|
49
+ @_response << build_completion(T.must(path), node)
50
+ end
51
+ end
52
+
53
+ # Handle completion on regular constant references (e.g. `Bar`)
54
+ sig { params(node: SyntaxTree::Const).void }
55
+ def on_const(node)
56
+ return if DependencyDetector::HAS_TYPECHECKER
57
+
58
+ name = node.value
59
+ candidates = @index.prefix_search(name, @nesting)
60
+ candidates.each do |entries|
61
+ @_response << build_entry_completion(name, node, entries, top_level?(T.must(entries.first).name, candidates))
62
+ end
63
+ end
64
+
65
+ # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
66
+ sig { params(node: SyntaxTree::ConstPathRef).void }
67
+ def on_const_path_ref(node)
68
+ return if DependencyDetector::HAS_TYPECHECKER
69
+
70
+ name = full_constant_name(node)
71
+ candidates = @index.prefix_search(name, @nesting)
72
+ candidates.each do |entries|
73
+ @_response << build_entry_completion(name, node, entries, top_level?(T.must(entries.first).name, candidates))
74
+ end
75
+ end
76
+
77
+ # Handle completion on top level constant references (e.g. `::Bar`)
78
+ sig { params(node: SyntaxTree::TopConstRef).void }
79
+ def on_top_const_ref(node)
80
+ return if DependencyDetector::HAS_TYPECHECKER
81
+
82
+ name = full_constant_name(node)
83
+ candidates = @index.prefix_search(name, [])
84
+ candidates.each { |entries| @_response << build_entry_completion(name, node, entries, true) }
85
+ end
86
+
87
+ private
88
+
89
+ sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) }
90
+ def build_completion(label, node)
91
+ Interface::CompletionItem.new(
92
+ label: label,
93
+ text_edit: Interface::TextEdit.new(
94
+ range: range_from_syntax_tree_node(node),
95
+ new_text: label,
96
+ ),
97
+ kind: Constant::CompletionItemKind::REFERENCE,
98
+ )
99
+ end
100
+
101
+ sig do
102
+ params(
103
+ name: String,
104
+ node: SyntaxTree::Node,
105
+ entries: T::Array[RubyIndexer::Index::Entry],
106
+ top_level: T::Boolean,
107
+ ).returns(Interface::CompletionItem)
108
+ end
109
+ def build_entry_completion(name, node, entries, top_level)
110
+ first_entry = T.must(entries.first)
111
+ kind = case first_entry
112
+ when RubyIndexer::Index::Entry::Class
113
+ Constant::CompletionItemKind::CLASS
114
+ when RubyIndexer::Index::Entry::Module
115
+ Constant::CompletionItemKind::MODULE
116
+ when RubyIndexer::Index::Entry::Constant
117
+ Constant::CompletionItemKind::CONSTANT
118
+ else
119
+ Constant::CompletionItemKind::REFERENCE
120
+ end
121
+
122
+ insertion_text = first_entry.name.dup
123
+
124
+ # If we have two entries with the same name inside the current namespace and the user selects the top level
125
+ # option, we have to ensure it's prefixed with `::` or else we're completing the wrong constant. For example:
126
+ # If we have the index with ["Foo::Bar", "Bar"], and we're providing suggestions for `B` inside a `Foo` module,
127
+ # then selecting the `Foo::Bar` option needs to complete to `Bar` and selecting the top level `Bar` option needs
128
+ # to complete to `::Bar`.
129
+ insertion_text.prepend("::") if top_level
130
+
131
+ # If the user is searching for a constant inside the current namespace, then we prefer completing the short name
132
+ # of that constant. E.g.:
133
+ #
134
+ # module Foo
135
+ # class Bar
136
+ # end
137
+ #
138
+ # Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
139
+ # end
140
+ @nesting.each { |namespace| insertion_text.delete_prefix!("#{namespace}::") }
141
+
142
+ # When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
143
+ # For these top level references, we need to include the `::` as part of the filter text or else it won't match
144
+ # the right entries in the index
145
+ Interface::CompletionItem.new(
146
+ label: first_entry.name,
147
+ filter_text: top_level ? "::#{first_entry.name}" : first_entry.name,
148
+ text_edit: Interface::TextEdit.new(
149
+ range: range_from_syntax_tree_node(node),
150
+ new_text: insertion_text,
151
+ ),
152
+ kind: kind,
153
+ label_details: Interface::CompletionItemLabelDetails.new(
154
+ description: entries.map(&:file_name).join(","),
155
+ ),
156
+ documentation: markdown_from_index_entries(first_entry.name, entries),
157
+ )
158
+ end
159
+
160
+ # Check if the `entry_name` has potential conflicts in `candidates`, so that we use a top level reference instead
161
+ # of a short name
162
+ sig { params(entry_name: String, candidates: T::Array[T::Array[RubyIndexer::Index::Entry]]).returns(T::Boolean) }
163
+ def top_level?(entry_name, candidates)
164
+ candidates.any? { |entries| T.must(entries.first).name == "#{@nesting.join("::")}::#{entry_name}" }
165
+ end
166
+ end
167
+ end
168
+ end