ruby-lsp 0.9.4 → 0.10.1
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/index.rb +3 -3
- data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +1 -1
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +16 -16
- data/lib/ruby_indexer/test/constant_test.rb +11 -11
- data/lib/ruby_indexer/test/index_test.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 +23 -11
- data/lib/ruby_lsp/extension.rb +12 -0
- 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 +6 -4
- 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 -2
- data/lib/ruby_lsp/setup_bundler.rb +22 -11
- metadata +7 -7
- data/lib/ruby_lsp/requests/path_completion.rb +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36eafde3135250941805bf340b782dfab3002839b66ba4722d261edd7fe1356a
|
4
|
+
data.tar.gz: f8dbfe5eb36489223c21d2e816f91324d0928895632a4344c16ecc756c0cb279
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24312d8a758e8388c02165d8f0ee4a8f8ccb93bb71efbd3ed3a4eeeadeb750373856cdcd3842f2cb1138ef7067c7c497260adeee347cc37f81e960a1febb7a44
|
7
|
+
data.tar.gz: c637ef4a50da10f0a7d81a15e9fc7af209e6a056642c93b315e0410fbfe732a8f42cd841ca3d70f66fac79d54474253a8febfe60f487c970632e64fd88ce103b
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.10.1
|
@@ -28,7 +28,7 @@ module RubyIndexer
|
|
28
28
|
@files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
|
29
29
|
|
30
30
|
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
|
31
|
-
@require_paths_tree = T.let(PrefixTree[
|
31
|
+
@require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
|
32
32
|
end
|
33
33
|
|
34
34
|
sig { params(indexable: IndexablePath).void }
|
@@ -73,7 +73,7 @@ module RubyIndexer
|
|
73
73
|
@entries[fully_qualified_name.delete_prefix("::")]
|
74
74
|
end
|
75
75
|
|
76
|
-
sig { params(query: String).returns(T::Array[
|
76
|
+
sig { params(query: String).returns(T::Array[IndexablePath]) }
|
77
77
|
def search_require_paths(query)
|
78
78
|
@require_paths_tree.search(query)
|
79
79
|
end
|
@@ -147,7 +147,7 @@ module RubyIndexer
|
|
147
147
|
visitor.run
|
148
148
|
|
149
149
|
require_path = indexable_path.require_path
|
150
|
-
@require_paths_tree.insert(require_path,
|
150
|
+
@require_paths_tree.insert(require_path, indexable_path) if require_path
|
151
151
|
rescue Errno::EISDIR
|
152
152
|
# If `path` is a directory, just ignore it and continue indexing
|
153
153
|
end
|
@@ -57,7 +57,7 @@ module RubyIndexer
|
|
57
57
|
end
|
58
58
|
def add_constant(node)
|
59
59
|
comments = collect_comments(node)
|
60
|
-
@index << Index::Entry::Constant.new(fully_qualify_name(node.name), @file_path, node.location, comments)
|
60
|
+
@index << Index::Entry::Constant.new(fully_qualify_name(node.name.to_s), @file_path, node.location, comments)
|
61
61
|
end
|
62
62
|
|
63
63
|
sig do
|
@@ -11,7 +11,7 @@ module RubyIndexer
|
|
11
11
|
end
|
12
12
|
RUBY
|
13
13
|
|
14
|
-
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-
|
14
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_class_with_statements
|
@@ -21,7 +21,7 @@ module RubyIndexer
|
|
21
21
|
end
|
22
22
|
RUBY
|
23
23
|
|
24
|
-
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:2-
|
24
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:2-3")
|
25
25
|
end
|
26
26
|
|
27
27
|
def test_colon_colon_class
|
@@ -30,7 +30,7 @@ module RubyIndexer
|
|
30
30
|
end
|
31
31
|
RUBY
|
32
32
|
|
33
|
-
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-
|
33
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
34
34
|
end
|
35
35
|
|
36
36
|
def test_colon_colon_class_inside_class
|
@@ -41,8 +41,8 @@ module RubyIndexer
|
|
41
41
|
end
|
42
42
|
RUBY
|
43
43
|
|
44
|
-
assert_entry("Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:3-
|
45
|
-
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-
|
44
|
+
assert_entry("Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:3-3")
|
45
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-5")
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_namespaced_class
|
@@ -51,7 +51,7 @@ module RubyIndexer
|
|
51
51
|
end
|
52
52
|
RUBY
|
53
53
|
|
54
|
-
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-
|
54
|
+
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
55
55
|
end
|
56
56
|
|
57
57
|
def test_dynamically_namespaced_class
|
@@ -69,7 +69,7 @@ module RubyIndexer
|
|
69
69
|
end
|
70
70
|
RUBY
|
71
71
|
|
72
|
-
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-
|
72
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
73
73
|
end
|
74
74
|
|
75
75
|
def test_module_with_statements
|
@@ -79,7 +79,7 @@ module RubyIndexer
|
|
79
79
|
end
|
80
80
|
RUBY
|
81
81
|
|
82
|
-
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:2-
|
82
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:2-3")
|
83
83
|
end
|
84
84
|
|
85
85
|
def test_colon_colon_module
|
@@ -88,7 +88,7 @@ module RubyIndexer
|
|
88
88
|
end
|
89
89
|
RUBY
|
90
90
|
|
91
|
-
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-
|
91
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
92
92
|
end
|
93
93
|
|
94
94
|
def test_namespaced_module
|
@@ -97,7 +97,7 @@ module RubyIndexer
|
|
97
97
|
end
|
98
98
|
RUBY
|
99
99
|
|
100
|
-
assert_entry("Foo::Bar", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-
|
100
|
+
assert_entry("Foo::Bar", Index::Entry::Module, "/fake/path/foo.rb:0-0:1-3")
|
101
101
|
end
|
102
102
|
|
103
103
|
def test_dynamically_namespaced_module
|
@@ -124,11 +124,11 @@ module RubyIndexer
|
|
124
124
|
end
|
125
125
|
RUBY
|
126
126
|
|
127
|
-
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:10-
|
128
|
-
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-
|
129
|
-
assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-
|
130
|
-
assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-
|
131
|
-
assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-
|
127
|
+
assert_entry("Foo", Index::Entry::Module, "/fake/path/foo.rb:0-0:10-3")
|
128
|
+
assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-5")
|
129
|
+
assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-5")
|
130
|
+
assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-7")
|
131
|
+
assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-9")
|
132
132
|
end
|
133
133
|
|
134
134
|
def test_deleting_from_index_based_on_file_path
|
@@ -137,7 +137,7 @@ module RubyIndexer
|
|
137
137
|
end
|
138
138
|
RUBY
|
139
139
|
|
140
|
-
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-
|
140
|
+
assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-3")
|
141
141
|
|
142
142
|
@index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
|
143
143
|
refute_entry("Foo")
|
@@ -14,8 +14,8 @@ module RubyIndexer
|
|
14
14
|
end
|
15
15
|
RUBY
|
16
16
|
|
17
|
-
assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-
|
18
|
-
assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-
|
17
|
+
assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-7")
|
18
|
+
assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-9")
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_constant_or_writes
|
@@ -27,8 +27,8 @@ module RubyIndexer
|
|
27
27
|
end
|
28
28
|
RUBY
|
29
29
|
|
30
|
-
assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-
|
31
|
-
assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-
|
30
|
+
assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-9")
|
31
|
+
assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-11")
|
32
32
|
end
|
33
33
|
|
34
34
|
def test_constant_path_writes
|
@@ -45,10 +45,10 @@ module RubyIndexer
|
|
45
45
|
A::BAZ = 1
|
46
46
|
RUBY
|
47
47
|
|
48
|
-
assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-
|
49
|
-
assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-
|
50
|
-
assert_entry("A::B::FOO", Index::Entry::Constant, "/fake/path/foo.rb:5-4:5-
|
51
|
-
assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:9-0:9-
|
48
|
+
assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-9")
|
49
|
+
assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-11")
|
50
|
+
assert_entry("A::B::FOO", Index::Entry::Constant, "/fake/path/foo.rb:5-4:5-11")
|
51
|
+
assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:9-0:9-10")
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_constant_path_or_writes
|
@@ -61,9 +61,9 @@ module RubyIndexer
|
|
61
61
|
A::BAZ ||= 1
|
62
62
|
RUBY
|
63
63
|
|
64
|
-
assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-
|
65
|
-
assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-
|
66
|
-
assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:5-0:5-
|
64
|
+
assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-11")
|
65
|
+
assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-13")
|
66
|
+
assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:5-0:5-12")
|
67
67
|
end
|
68
68
|
|
69
69
|
def test_comments_for_constants
|
@@ -136,7 +136,7 @@ module RubyIndexer
|
|
136
136
|
end
|
137
137
|
RUBY
|
138
138
|
|
139
|
-
assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path"))
|
139
|
+
assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path").map(&:require_path))
|
140
140
|
end
|
141
141
|
|
142
142
|
def test_searching_for_entries_based_on_prefix
|
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)
|
@@ -299,7 +296,6 @@ module RubyLsp
|
|
299
296
|
# Emit events for all listeners
|
300
297
|
emitter.emit_for_target(target)
|
301
298
|
|
302
|
-
hover.merge_external_listeners_responses!
|
303
299
|
hover.response
|
304
300
|
end
|
305
301
|
|
@@ -478,12 +474,18 @@ module RubyLsp
|
|
478
474
|
return unless document.parsed?
|
479
475
|
|
480
476
|
char_position = document.create_scanner.find_char_position(position)
|
481
|
-
matched, parent = document.locate(
|
482
|
-
T.must(document.tree),
|
483
|
-
char_position,
|
484
|
-
node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
|
485
|
-
)
|
486
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)
|
487
489
|
return unless matched && parent
|
488
490
|
|
489
491
|
target = case matched
|
@@ -504,12 +506,19 @@ module RubyLsp
|
|
504
506
|
return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
|
505
507
|
|
506
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
|
507
516
|
end
|
508
517
|
|
509
518
|
return unless target
|
510
519
|
|
511
520
|
emitter = EventEmitter.new
|
512
|
-
listener = Requests::
|
521
|
+
listener = Requests::Completion.new(@index, nesting, emitter, @message_queue)
|
513
522
|
emitter.emit_for_target(target)
|
514
523
|
listener.response
|
515
524
|
end
|
@@ -645,7 +654,10 @@ module RubyLsp
|
|
645
654
|
completion_provider = if enabled_features["completion"]
|
646
655
|
Interface::CompletionOptions.new(
|
647
656
|
resolve_provider: false,
|
648
|
-
trigger_characters: ["/"],
|
657
|
+
trigger_characters: ["/", *"A".."Z"],
|
658
|
+
completion_item: {
|
659
|
+
labelDetailsSupport: true,
|
660
|
+
},
|
649
661
|
)
|
650
662
|
end
|
651
663
|
|
data/lib/ruby_lsp/extension.rb
CHANGED
@@ -133,5 +133,17 @@ module RubyLsp
|
|
133
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
|
@@ -17,14 +17,14 @@ module RubyLsp
|
|
17
17
|
# require "some_gem/file" # <- Request go to definition on this string will take you to the file
|
18
18
|
# Product.new # <- Request go to definition on this class name will take you to its declaration.
|
19
19
|
# ```
|
20
|
-
class Definition <
|
20
|
+
class Definition < ExtensibleListener
|
21
21
|
extend T::Sig
|
22
22
|
extend T::Generic
|
23
23
|
|
24
24
|
ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
|
25
25
|
|
26
26
|
sig { override.returns(ResponseType) }
|
27
|
-
attr_reader :
|
27
|
+
attr_reader :_response
|
28
28
|
|
29
29
|
sig do
|
30
30
|
params(
|
@@ -36,15 +36,37 @@ module RubyLsp
|
|
36
36
|
).void
|
37
37
|
end
|
38
38
|
def initialize(uri, nesting, index, emitter, message_queue)
|
39
|
-
super(emitter, message_queue)
|
40
|
-
|
41
39
|
@uri = uri
|
42
40
|
@nesting = nesting
|
43
41
|
@index = index
|
44
|
-
@
|
42
|
+
@_response = T.let(nil, ResponseType)
|
43
|
+
|
44
|
+
super(emitter, message_queue)
|
45
|
+
|
45
46
|
emitter.register(self, :on_command, :on_const, :on_const_path_ref)
|
46
47
|
end
|
47
48
|
|
49
|
+
sig { override.params(ext: Extension).returns(T.nilable(RubyLsp::Listener[ResponseType])) }
|
50
|
+
def initialize_external_listener(ext)
|
51
|
+
ext.create_definition_listener(@uri, @nesting, @index, @emitter, @message_queue)
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
|
55
|
+
def merge_response!(other)
|
56
|
+
other_response = other._response
|
57
|
+
|
58
|
+
case @_response
|
59
|
+
when Interface::Location
|
60
|
+
@_response = [@_response, *other_response]
|
61
|
+
when Array
|
62
|
+
@_response.concat(Array(other_response))
|
63
|
+
when nil
|
64
|
+
@_response = other_response
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
48
70
|
sig { params(node: SyntaxTree::ConstPathRef).void }
|
49
71
|
def on_const_path_ref(node)
|
50
72
|
name = full_constant_name(node)
|
@@ -67,14 +89,16 @@ module RubyLsp
|
|
67
89
|
string = argument.parts.first
|
68
90
|
return unless string.is_a?(SyntaxTree::TStringContent)
|
69
91
|
|
70
|
-
required_file = "#{string.value}.rb"
|
71
|
-
|
72
92
|
case message
|
73
93
|
when "require"
|
74
|
-
|
94
|
+
entry = @index.search_require_paths(string.value).find do |indexable_path|
|
95
|
+
indexable_path.require_path == string.value
|
96
|
+
end
|
97
|
+
|
98
|
+
if entry
|
99
|
+
candidate = entry.full_path
|
75
100
|
|
76
|
-
|
77
|
-
@response = Interface::Location.new(
|
101
|
+
@_response = Interface::Location.new(
|
78
102
|
uri: URI::Generic.from_path(path: candidate).to_s,
|
79
103
|
range: Interface::Range.new(
|
80
104
|
start: Interface::Position.new(line: 0, character: 0),
|
@@ -83,19 +107,18 @@ module RubyLsp
|
|
83
107
|
)
|
84
108
|
end
|
85
109
|
when "require_relative"
|
110
|
+
required_file = "#{string.value}.rb"
|
86
111
|
path = @uri.to_standardized_path
|
87
112
|
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
|
88
113
|
candidate = File.expand_path(File.join(current_folder, required_file))
|
89
114
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
)
|
98
|
-
end
|
115
|
+
@_response = Interface::Location.new(
|
116
|
+
uri: URI::Generic.from_path(path: candidate).to_s,
|
117
|
+
range: Interface::Range.new(
|
118
|
+
start: Interface::Position.new(line: 0, character: 0),
|
119
|
+
end: Interface::Position.new(line: 0, character: 0),
|
120
|
+
),
|
121
|
+
)
|
99
122
|
end
|
100
123
|
end
|
101
124
|
|
@@ -112,7 +135,7 @@ module RubyLsp
|
|
112
135
|
nil
|
113
136
|
end
|
114
137
|
|
115
|
-
@
|
138
|
+
@_response = entries.filter_map do |entry|
|
116
139
|
location = entry.location
|
117
140
|
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
|
118
141
|
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
|
@@ -133,18 +156,6 @@ module RubyLsp
|
|
133
156
|
)
|
134
157
|
end
|
135
158
|
end
|
136
|
-
|
137
|
-
sig { params(file: String).returns(T.nilable(String)) }
|
138
|
-
def find_file_in_load_path(file)
|
139
|
-
return unless file.include?("/")
|
140
|
-
|
141
|
-
$LOAD_PATH.each do |p|
|
142
|
-
found = Dir.glob("**/#{file}", base: p).first
|
143
|
-
return "#{p}/#{found}" if found
|
144
|
-
end
|
145
|
-
|
146
|
-
nil
|
147
|
-
end
|
148
159
|
end
|
149
160
|
end
|
150
161
|
end
|
@@ -28,7 +28,7 @@ module RubyLsp
|
|
28
28
|
ResponseType = type_member { { fixed: T::Array[Interface::DocumentHighlight] } }
|
29
29
|
|
30
30
|
sig { override.returns(ResponseType) }
|
31
|
-
attr_reader :
|
31
|
+
attr_reader :_response
|
32
32
|
|
33
33
|
sig do
|
34
34
|
params(
|
@@ -41,7 +41,7 @@ module RubyLsp
|
|
41
41
|
def initialize(target, parent, emitter, message_queue)
|
42
42
|
super(emitter, message_queue)
|
43
43
|
|
44
|
-
@
|
44
|
+
@_response = T.let([], T::Array[Interface::DocumentHighlight])
|
45
45
|
|
46
46
|
return unless target && parent
|
47
47
|
|
@@ -83,7 +83,7 @@ module RubyLsp
|
|
83
83
|
sig { params(match: Support::HighlightTarget::HighlightMatch).void }
|
84
84
|
def add_highlight(match)
|
85
85
|
range = range_from_syntax_tree_node(match.node)
|
86
|
-
@
|
86
|
+
@_response << Interface::DocumentHighlight.new(range: range, kind: match.type)
|
87
87
|
end
|
88
88
|
end
|
89
89
|
end
|
@@ -73,7 +73,7 @@ module RubyLsp
|
|
73
73
|
end
|
74
74
|
|
75
75
|
sig { override.returns(ResponseType) }
|
76
|
-
attr_reader :
|
76
|
+
attr_reader :_response
|
77
77
|
|
78
78
|
sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
|
79
79
|
def initialize(uri, emitter, message_queue)
|
@@ -84,7 +84,7 @@ module RubyLsp
|
|
84
84
|
path = uri.to_standardized_path
|
85
85
|
version_match = path ? /(?<=%40)[\d.]+(?=\.rbi$)/.match(path) : nil
|
86
86
|
@gem_version = T.let(version_match && version_match[0], T.nilable(String))
|
87
|
-
@
|
87
|
+
@_response = T.let([], T::Array[Interface::DocumentLink])
|
88
88
|
|
89
89
|
emitter.register(self, :on_comment)
|
90
90
|
end
|
@@ -95,11 +95,13 @@ module RubyLsp
|
|
95
95
|
return unless match
|
96
96
|
|
97
97
|
uri = T.cast(URI(T.must(match[0])), URI::Source)
|
98
|
-
gem_version =
|
98
|
+
gem_version = resolve_version(uri)
|
99
|
+
return if gem_version.nil?
|
100
|
+
|
99
101
|
file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, CGI.unescape(uri.path))
|
100
102
|
return if file_path.nil?
|
101
103
|
|
102
|
-
@
|
104
|
+
@_response << Interface::DocumentLink.new(
|
103
105
|
range: range_from_syntax_tree_node(node),
|
104
106
|
target: "file://#{file_path}##{uri.line_number}",
|
105
107
|
tooltip: "Jump to #{file_path}##{uri.line_number}",
|
@@ -26,7 +26,7 @@ module RubyLsp
|
|
26
26
|
# end
|
27
27
|
# end
|
28
28
|
# ```
|
29
|
-
class DocumentSymbol <
|
29
|
+
class DocumentSymbol < ExtensibleListener
|
30
30
|
extend T::Sig
|
31
31
|
extend T::Generic
|
32
32
|
|
@@ -47,22 +47,18 @@ module RubyLsp
|
|
47
47
|
end
|
48
48
|
|
49
49
|
sig { override.returns(T::Array[Interface::DocumentSymbol]) }
|
50
|
-
attr_reader :
|
50
|
+
attr_reader :_response
|
51
51
|
|
52
52
|
sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
|
53
53
|
def initialize(emitter, message_queue)
|
54
|
-
super
|
55
|
-
|
56
54
|
@root = T.let(SymbolHierarchyRoot.new, SymbolHierarchyRoot)
|
57
|
-
@
|
55
|
+
@_response = T.let(@root.children, T::Array[Interface::DocumentSymbol])
|
58
56
|
@stack = T.let(
|
59
57
|
[@root],
|
60
58
|
T::Array[T.any(SymbolHierarchyRoot, Interface::DocumentSymbol)],
|
61
59
|
)
|
62
60
|
|
63
|
-
|
64
|
-
Extension.extensions.filter_map { |ext| ext.create_document_symbol_listener(emitter, message_queue) },
|
65
|
-
)
|
61
|
+
super
|
66
62
|
|
67
63
|
emitter.register(
|
68
64
|
self,
|
@@ -79,10 +75,15 @@ module RubyLsp
|
|
79
75
|
)
|
80
76
|
end
|
81
77
|
|
78
|
+
sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
|
79
|
+
def initialize_external_listener(extension)
|
80
|
+
extension.create_document_symbol_listener(@emitter, @message_queue)
|
81
|
+
end
|
82
|
+
|
82
83
|
# Merges responses from other listeners
|
83
84
|
sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
|
84
85
|
def merge_response!(other)
|
85
|
-
@
|
86
|
+
@_response.concat(other.response)
|
86
87
|
self
|
87
88
|
end
|
88
89
|
|
@@ -13,7 +13,7 @@ module RubyLsp
|
|
13
13
|
# ```ruby
|
14
14
|
# String # -> Hovering over the class reference will show all declaration locations and the documentation
|
15
15
|
# ```
|
16
|
-
class Hover <
|
16
|
+
class Hover < ExtensibleListener
|
17
17
|
extend T::Sig
|
18
18
|
extend T::Generic
|
19
19
|
|
@@ -30,7 +30,7 @@ module RubyLsp
|
|
30
30
|
)
|
31
31
|
|
32
32
|
sig { override.returns(ResponseType) }
|
33
|
-
attr_reader :
|
33
|
+
attr_reader :_response
|
34
34
|
|
35
35
|
sig do
|
36
36
|
params(
|
@@ -41,27 +41,29 @@ module RubyLsp
|
|
41
41
|
).void
|
42
42
|
end
|
43
43
|
def initialize(index, nesting, emitter, message_queue)
|
44
|
-
super(emitter, message_queue)
|
45
|
-
|
46
44
|
@nesting = nesting
|
47
45
|
@index = index
|
48
|
-
@
|
49
|
-
|
50
|
-
)
|
51
|
-
@response = T.let(nil, ResponseType)
|
46
|
+
@_response = T.let(nil, ResponseType)
|
47
|
+
|
48
|
+
super(emitter, message_queue)
|
52
49
|
emitter.register(self, :on_const_path_ref, :on_const)
|
53
50
|
end
|
54
51
|
|
52
|
+
sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
|
53
|
+
def initialize_external_listener(extension)
|
54
|
+
extension.create_hover_listener(@emitter, @message_queue)
|
55
|
+
end
|
56
|
+
|
55
57
|
# Merges responses from other hover listeners
|
56
58
|
sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
|
57
59
|
def merge_response!(other)
|
58
60
|
other_response = other.response
|
59
61
|
return self unless other_response
|
60
62
|
|
61
|
-
if @
|
62
|
-
@
|
63
|
+
if @_response.nil?
|
64
|
+
@_response = other.response
|
63
65
|
else
|
64
|
-
@
|
66
|
+
@_response.contents.value << "\n\n" << other_response.contents.value
|
65
67
|
end
|
66
68
|
|
67
69
|
self
|
@@ -89,29 +91,10 @@ module RubyLsp
|
|
89
91
|
entries = @index.resolve(name, @nesting)
|
90
92
|
return unless entries
|
91
93
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
entries.each do |entry|
|
96
|
-
loc = entry.location
|
97
|
-
|
98
|
-
# We always handle locations as zero based. However, for file links in Markdown we need them to be one based,
|
99
|
-
# which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to columns. The
|
100
|
-
# format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
|
101
|
-
uri = URI::Generic.from_path(
|
102
|
-
path: entry.file_path,
|
103
|
-
fragment: "L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}",
|
104
|
-
)
|
105
|
-
|
106
|
-
definitions << "[#{entry.file_name}](#{uri})"
|
107
|
-
content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty?
|
108
|
-
end
|
109
|
-
|
110
|
-
contents = Interface::MarkupContent.new(
|
111
|
-
kind: "markdown",
|
112
|
-
value: "#{title}\n\n**Definitions**: #{definitions.join(" | ")}\n\n#{content}",
|
94
|
+
@_response = Interface::Hover.new(
|
95
|
+
range: range_from_syntax_tree_node(node),
|
96
|
+
contents: markdown_from_index_entries(name, entries),
|
113
97
|
)
|
114
|
-
@response = Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
|
115
98
|
end
|
116
99
|
end
|
117
100
|
end
|
@@ -27,13 +27,13 @@ module RubyLsp
|
|
27
27
|
RESCUE_STRING_LENGTH = T.let("rescue".length, Integer)
|
28
28
|
|
29
29
|
sig { override.returns(ResponseType) }
|
30
|
-
attr_reader :
|
30
|
+
attr_reader :_response
|
31
31
|
|
32
32
|
sig { params(range: T::Range[Integer], emitter: EventEmitter, message_queue: Thread::Queue).void }
|
33
33
|
def initialize(range, emitter, message_queue)
|
34
34
|
super(emitter, message_queue)
|
35
35
|
|
36
|
-
@
|
36
|
+
@_response = T.let([], ResponseType)
|
37
37
|
@range = range
|
38
38
|
|
39
39
|
emitter.register(self, :on_rescue)
|
@@ -47,7 +47,7 @@ module RubyLsp
|
|
47
47
|
loc = node.location
|
48
48
|
return unless visible?(node, @range)
|
49
49
|
|
50
|
-
@
|
50
|
+
@_response << Interface::InlayHint.new(
|
51
51
|
position: { line: loc.start_line - 1, character: loc.start_column + RESCUE_STRING_LENGTH },
|
52
52
|
label: "StandardError",
|
53
53
|
padding_left: true,
|
@@ -105,7 +105,7 @@ module RubyLsp
|
|
105
105
|
end
|
106
106
|
|
107
107
|
sig { override.returns(ResponseType) }
|
108
|
-
attr_reader :
|
108
|
+
attr_reader :_response
|
109
109
|
|
110
110
|
sig do
|
111
111
|
params(
|
@@ -117,7 +117,7 @@ module RubyLsp
|
|
117
117
|
def initialize(emitter, message_queue, range: nil)
|
118
118
|
super(emitter, message_queue)
|
119
119
|
|
120
|
-
@
|
120
|
+
@_response = T.let([], ResponseType)
|
121
121
|
@range = range
|
122
122
|
@special_methods = T.let(nil, T.nilable(T::Array[String]))
|
123
123
|
|
@@ -174,7 +174,7 @@ module RubyLsp
|
|
174
174
|
# When finding a module or class definition, we will have already pushed a token related to this constant. We
|
175
175
|
# need to look at the previous two tokens and if they match this locatione exactly, avoid pushing another token
|
176
176
|
# on top of the previous one
|
177
|
-
return if @
|
177
|
+
return if @_response.last(2).any? { |token| token.location == node.location }
|
178
178
|
|
179
179
|
add_token(node.location, :namespace)
|
180
180
|
end
|
@@ -327,7 +327,7 @@ module RubyLsp
|
|
327
327
|
def add_token(location, type, modifiers = [])
|
328
328
|
length = location.end_char - location.start_char
|
329
329
|
modifiers_indices = modifiers.filter_map { |modifier| TOKEN_MODIFIERS[modifier] }
|
330
|
-
@
|
330
|
+
@_response.push(
|
331
331
|
SemanticToken.new(
|
332
332
|
location: location,
|
333
333
|
length: length,
|
@@ -74,6 +74,39 @@ module RubyLsp
|
|
74
74
|
data: data,
|
75
75
|
)
|
76
76
|
end
|
77
|
+
|
78
|
+
sig { params(title: String, entries: T::Array[RubyIndexer::Index::Entry]).returns(Interface::MarkupContent) }
|
79
|
+
def markdown_from_index_entries(title, entries)
|
80
|
+
markdown_title = "```ruby\n#{title}\n```"
|
81
|
+
definitions = []
|
82
|
+
content = +""
|
83
|
+
entries.each do |entry|
|
84
|
+
loc = entry.location
|
85
|
+
|
86
|
+
# We always handle locations as zero based. However, for file links in Markdown we need them to be one
|
87
|
+
# based, which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to
|
88
|
+
# columns. The format for VS Code file URIs is
|
89
|
+
# `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
|
90
|
+
uri = URI::Generic.from_path(
|
91
|
+
path: entry.file_path,
|
92
|
+
fragment: "L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}",
|
93
|
+
)
|
94
|
+
|
95
|
+
definitions << "[#{entry.file_name}](#{uri})"
|
96
|
+
content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty?
|
97
|
+
end
|
98
|
+
|
99
|
+
Interface::MarkupContent.new(
|
100
|
+
kind: "markdown",
|
101
|
+
value: <<~MARKDOWN.chomp,
|
102
|
+
#{markdown_title}
|
103
|
+
|
104
|
+
**Definitions**: #{definitions.join(" | ")}
|
105
|
+
|
106
|
+
#{content}
|
107
|
+
MARKDOWN
|
108
|
+
)
|
109
|
+
end
|
77
110
|
end
|
78
111
|
end
|
79
112
|
end
|
data/lib/ruby_lsp/requests.rb
CHANGED
@@ -17,7 +17,7 @@ module RubyLsp
|
|
17
17
|
# - [CodeActionResolve](rdoc-ref:RubyLsp::Requests::CodeActionResolve)
|
18
18
|
# - [DocumentHighlight](rdoc-ref:RubyLsp::Requests::DocumentHighlight)
|
19
19
|
# - [InlayHint](rdoc-ref:RubyLsp::Requests::InlayHints)
|
20
|
-
# - [
|
20
|
+
# - [Completion](rdoc-ref:RubyLsp::Requests::Completion)
|
21
21
|
# - [CodeLens](rdoc-ref:RubyLsp::Requests::CodeLens)
|
22
22
|
# - [Definition](rdoc-ref:RubyLsp::Requests::Definition)
|
23
23
|
# - [ShowSyntaxTree](rdoc-ref:RubyLsp::Requests::ShowSyntaxTree)
|
@@ -38,7 +38,7 @@ module RubyLsp
|
|
38
38
|
autoload :CodeActionResolve, "ruby_lsp/requests/code_action_resolve"
|
39
39
|
autoload :DocumentHighlight, "ruby_lsp/requests/document_highlight"
|
40
40
|
autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
|
41
|
-
autoload :
|
41
|
+
autoload :Completion, "ruby_lsp/requests/completion"
|
42
42
|
autoload :CodeLens, "ruby_lsp/requests/code_lens"
|
43
43
|
autoload :Definition, "ruby_lsp/requests/definition"
|
44
44
|
autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
|
@@ -175,20 +175,24 @@ module RubyLsp
|
|
175
175
|
# weren't a part of the Gemfile, then we need to run `bundle install` for the first time to generate the
|
176
176
|
# Gemfile.lock with them included or else Bundler will complain that they're missing. We can only update if the
|
177
177
|
# custom `.ruby-lsp/Gemfile.lock` already exists and includes both gems
|
178
|
-
command = +""
|
179
178
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
179
|
+
# When not updating, we run `(bundle check || bundle install)`
|
180
|
+
# When updating, we run `((bundle check && bundle update ruby-lsp debug) || bundle install)`
|
181
|
+
command = +"(bundle check"
|
182
|
+
|
183
|
+
if should_bundle_update?
|
184
184
|
# If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version
|
185
|
-
command
|
185
|
+
command.prepend("(")
|
186
|
+
command << " && bundle update "
|
186
187
|
command << "ruby-lsp " unless @dependencies["ruby-lsp"]
|
187
|
-
command << "debug
|
188
|
+
command << "debug" unless @dependencies["debug"]
|
189
|
+
command << ")"
|
188
190
|
|
189
191
|
@last_updated_path.write(Time.now.iso8601)
|
190
192
|
end
|
191
193
|
|
194
|
+
command << " || bundle install) "
|
195
|
+
|
192
196
|
# Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
|
193
197
|
# responses
|
194
198
|
command << "1>&2"
|
@@ -200,10 +204,17 @@ module RubyLsp
|
|
200
204
|
end
|
201
205
|
|
202
206
|
sig { returns(T::Boolean) }
|
203
|
-
def
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
+
def should_bundle_update?
|
208
|
+
# If both `ruby-lsp` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it will produce
|
209
|
+
# version control changes
|
210
|
+
return false if @dependencies["ruby-lsp"] && @dependencies["debug"]
|
211
|
+
|
212
|
+
# If the custom lockfile doesn't include either the `ruby-lsp` or `debug`, we need to run bundle install before
|
213
|
+
# updating
|
214
|
+
return false if custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil?
|
215
|
+
|
216
|
+
# If the last updated file doesn't exist or was updated more than 4 hours ago, we should update
|
217
|
+
!@last_updated_path.exist? || Time.parse(@last_updated_path.read) < (Time.now - FOUR_HOURS)
|
207
218
|
end
|
208
219
|
end
|
209
220
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-lsp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-09-
|
11
|
+
date: 2023-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|
@@ -64,20 +64,20 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - ">="
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: '0.
|
67
|
+
version: '0.11'
|
68
68
|
- - "<"
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
version: '0.
|
70
|
+
version: '0.12'
|
71
71
|
type: :runtime
|
72
72
|
prerelease: false
|
73
73
|
version_requirements: !ruby/object:Gem::Requirement
|
74
74
|
requirements:
|
75
75
|
- - ">="
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version: '0.
|
77
|
+
version: '0.11'
|
78
78
|
- - "<"
|
79
79
|
- !ruby/object:Gem::Version
|
80
|
-
version: '0.
|
80
|
+
version: '0.12'
|
81
81
|
description: An opinionated language server for Ruby
|
82
82
|
email:
|
83
83
|
- ruby@shopify.com
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- lib/ruby_lsp/requests/code_action_resolve.rb
|
120
120
|
- lib/ruby_lsp/requests/code_actions.rb
|
121
121
|
- lib/ruby_lsp/requests/code_lens.rb
|
122
|
+
- lib/ruby_lsp/requests/completion.rb
|
122
123
|
- lib/ruby_lsp/requests/definition.rb
|
123
124
|
- lib/ruby_lsp/requests/diagnostics.rb
|
124
125
|
- lib/ruby_lsp/requests/document_highlight.rb
|
@@ -129,7 +130,6 @@ files:
|
|
129
130
|
- lib/ruby_lsp/requests/hover.rb
|
130
131
|
- lib/ruby_lsp/requests/inlay_hints.rb
|
131
132
|
- lib/ruby_lsp/requests/on_type_formatting.rb
|
132
|
-
- lib/ruby_lsp/requests/path_completion.rb
|
133
133
|
- lib/ruby_lsp/requests/selection_ranges.rb
|
134
134
|
- lib/ruby_lsp/requests/semantic_highlighting.rb
|
135
135
|
- lib/ruby_lsp/requests/show_syntax_tree.rb
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module RubyLsp
|
5
|
-
module Requests
|
6
|
-
# ![Path completion demo](../../path_completion.gif)
|
7
|
-
#
|
8
|
-
# The [completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
|
9
|
-
# request looks up Ruby files in the $LOAD_PATH to suggest path completion inside `require` statements.
|
10
|
-
#
|
11
|
-
# # Example
|
12
|
-
#
|
13
|
-
# ```ruby
|
14
|
-
# require "ruby_lsp/requests" # --> completion: suggests `base_request`, `code_actions`, ...
|
15
|
-
# ```
|
16
|
-
class PathCompletion < Listener
|
17
|
-
extend T::Sig
|
18
|
-
extend T::Generic
|
19
|
-
|
20
|
-
ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
|
21
|
-
|
22
|
-
sig { override.returns(ResponseType) }
|
23
|
-
attr_reader :response
|
24
|
-
|
25
|
-
sig { params(index: RubyIndexer::Index, emitter: EventEmitter, message_queue: Thread::Queue).void }
|
26
|
-
def initialize(index, emitter, message_queue)
|
27
|
-
super(emitter, message_queue)
|
28
|
-
@response = T.let([], ResponseType)
|
29
|
-
@index = index
|
30
|
-
|
31
|
-
emitter.register(self, :on_tstring_content)
|
32
|
-
end
|
33
|
-
|
34
|
-
sig { params(node: SyntaxTree::TStringContent).void }
|
35
|
-
def on_tstring_content(node)
|
36
|
-
@index.search_require_paths(node.value).sort!.each do |path|
|
37
|
-
@response << build_completion(path, node)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) }
|
44
|
-
def build_completion(label, node)
|
45
|
-
Interface::CompletionItem.new(
|
46
|
-
label: label,
|
47
|
-
text_edit: Interface::TextEdit.new(
|
48
|
-
range: range_from_syntax_tree_node(node),
|
49
|
-
new_text: label,
|
50
|
-
),
|
51
|
-
kind: Constant::CompletionItemKind::REFERENCE,
|
52
|
-
)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|