ruby-lsp 0.9.3 → 0.10.0

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