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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +27 -15
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +68 -15
- data/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +29 -0
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +153 -0
- data/lib/ruby_indexer/ruby_indexer.rb +2 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +1 -1
- data/lib/ruby_indexer/test/configuration_test.rb +30 -24
- data/lib/ruby_indexer/test/index_test.rb +42 -9
- data/lib/ruby_indexer/test/prefix_tree_test.rb +150 -0
- data/lib/ruby_indexer/test/test_case.rb +1 -1
- data/lib/ruby_lsp/check_docs.rb +2 -1
- data/lib/ruby_lsp/event_emitter.rb +2 -0
- data/lib/ruby_lsp/executor.rb +33 -16
- data/lib/ruby_lsp/extension.rb +13 -1
- data/lib/ruby_lsp/listener.rb +43 -4
- data/lib/ruby_lsp/requests/code_lens.rb +15 -13
- data/lib/ruby_lsp/requests/completion.rb +168 -0
- data/lib/ruby_lsp/requests/definition.rb +43 -32
- data/lib/ruby_lsp/requests/document_highlight.rb +3 -3
- data/lib/ruby_lsp/requests/document_link.rb +3 -3
- data/lib/ruby_lsp/requests/document_symbol.rb +10 -9
- data/lib/ruby_lsp/requests/hover.rb +16 -33
- data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +4 -4
- data/lib/ruby_lsp/requests/support/common.rb +33 -0
- data/lib/ruby_lsp/requests.rb +2 -3
- metadata +9 -7
- data/lib/ruby_lsp/requests/path_completion.rb +0 -65
- data/lib/ruby_lsp/requests/support/prefix_tree.rb +0 -80
@@ -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
|
data/lib/ruby_lsp/check_docs.rb
CHANGED
@@ -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 ||
|
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
|
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -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(
|
196
|
+
@index.index_single(indexable)
|
197
197
|
when Constant::FileChangeType::CHANGED
|
198
|
-
@index.delete(
|
199
|
-
@index.index_single(
|
198
|
+
@index.delete(indexable)
|
199
|
+
@index.index_single(indexable)
|
200
200
|
when Constant::FileChangeType::DELETED
|
201
|
-
@index.delete(
|
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
|
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::
|
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
|
|
data/lib/ruby_lsp/extension.rb
CHANGED
@@ -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
|
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
|
data/lib/ruby_lsp/listener.rb
CHANGED
@@ -18,13 +18,42 @@ module RubyLsp
|
|
18
18
|
def initialize(emitter, message_queue)
|
19
19
|
@emitter = emitter
|
20
20
|
@message_queue = message_queue
|
21
|
-
|
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
|
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 {
|
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 <
|
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 :
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|