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.
- 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
|
+
# 
|
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
|