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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a300c17567fae60c2ed50c941d8ada19bf6b6fd951b621029880c03afd6d6f5
4
- data.tar.gz: 297d0d2064fbe7e97783e26fbfbdbb2191b6a32a43636e743864ad08665f49dd
3
+ metadata.gz: 36eafde3135250941805bf340b782dfab3002839b66ba4722d261edd7fe1356a
4
+ data.tar.gz: f8dbfe5eb36489223c21d2e816f91324d0928895632a4344c16ecc756c0cb279
5
5
  SHA512:
6
- metadata.gz: ee884d5c7f60e1d3fac9069a170967a761407a6470b7fe7442686bdbdfc1a4cbd626b7bcd4d368072bce0bab349df935bc0e7e9bdcad7b2fa238143afbc78075
7
- data.tar.gz: 317ebe547e3a9730c0161a2ad8b0dd130e819bf92a97c59525e5bc40e1e84bc95e0e610f87f144aebef957a622e141254938954bafd354e7f3d1249e022d2edb
6
+ metadata.gz: 24312d8a758e8388c02165d8f0ee4a8f8ccb93bb71efbd3ed3a4eeeadeb750373856cdcd3842f2cb1138ef7067c7c497260adeee347cc37f81e960a1febb7a44
7
+ data.tar.gz: c637ef4a50da10f0a7d81a15e9fc7af209e6a056642c93b315e0410fbfe732a8f42cd841ca3d70f66fac79d54474253a8febfe60f487c970632e64fd88ce103b
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.4
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[String].new, PrefixTree[String])
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[String]) }
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, require_path) if 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-2")
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-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-2")
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-2")
45
- assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
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-2")
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-2")
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-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-2")
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-2")
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-2")
128
- assert_entry("Foo::Bar", Index::Entry::Class, "/fake/path/foo.rb:1-2:2-4")
129
- assert_entry("Foo::Baz", Index::Entry::Module, "/fake/path/foo.rb:4-2:9-4")
130
- assert_entry("Foo::Baz::Qux", Index::Entry::Class, "/fake/path/foo.rb:5-4:8-6")
131
- assert_entry("Foo::Baz::Qux::Something", Index::Entry::Class, "/fake/path/foo.rb:6-6:7-8")
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-2")
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-6")
18
- assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-8")
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-8")
31
- assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-10")
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-8")
49
- assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-10")
50
- assert_entry("A::B::FOO", Index::Entry::Constant, "/fake/path/foo.rb:5-4:5-10")
51
- assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:9-0:9-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-10")
65
- assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-12")
66
- assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:5-0:5-11")
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
@@ -53,7 +53,8 @@ module RubyLsp
53
53
  # documented
54
54
  features = ObjectSpace.each_object(Class).filter_map do |k|
55
55
  klass = T.unsafe(k)
56
- klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
56
+ klass if klass < RubyLsp::Requests::BaseRequest ||
57
+ (klass < RubyLsp::Listener && klass != RubyLsp::ExtensibleListener)
57
58
  end
58
59
 
59
60
  missing_docs = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
@@ -48,6 +48,8 @@ module RubyLsp
48
48
  @listeners[:on_const_path_ref]&.each { |l| T.unsafe(l).on_const_path_ref(node) }
49
49
  when SyntaxTree::Const
50
50
  @listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
51
+ when SyntaxTree::TopConstRef
52
+ @listeners[:on_top_const_ref]&.each { |l| T.unsafe(l).on_top_const_ref(node) }
51
53
  end
52
54
  end
53
55
 
@@ -103,9 +103,6 @@ module RubyLsp
103
103
  semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
104
104
  emitter.visit(document.tree) if document.parsed?
105
105
 
106
- code_lens.merge_external_listeners_responses!
107
- document_symbol.merge_external_listeners_responses!
108
-
109
106
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
110
107
  # we actually received
111
108
  document.cache_set("textDocument/documentSymbol", document_symbol.response)
@@ -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::PathCompletion.new(@index, emitter, @message_queue)
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
 
@@ -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
@@ -18,13 +18,42 @@ module RubyLsp
18
18
  def initialize(emitter, message_queue)
19
19
  @emitter = emitter
20
20
  @message_queue = message_queue
21
- @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
21
+ end
22
+
23
+ sig { returns(ResponseType) }
24
+ def response
25
+ _response
22
26
  end
23
27
 
24
28
  # Override this method with an attr_reader that returns the response of your listener. The listener should
25
29
  # accumulate results in a @response variable and then provide the reader so that it is accessible
26
30
  sig { abstract.returns(ResponseType) }
27
- def response; end
31
+ def _response; end
32
+ end
33
+
34
+ # ExtensibleListener is an abstract class to be used by requests that accept extensions.
35
+ class ExtensibleListener < Listener
36
+ extend T::Sig
37
+ extend T::Generic
38
+
39
+ ResponseType = type_member
40
+
41
+ abstract!
42
+
43
+ # When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
44
+ # ivars have been initialized. This is because the constructor of ExtensibleListener calls
45
+ # `initialize_external_listener` which may depend on the subclass's ivars.
46
+ sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
47
+ def initialize(emitter, message_queue)
48
+ super
49
+ @response_merged = T.let(false, T::Boolean)
50
+ @external_listeners = T.let(
51
+ Extension.extensions.filter_map do |ext|
52
+ initialize_external_listener(ext)
53
+ end,
54
+ T::Array[RubyLsp::Listener[ResponseType]],
55
+ )
56
+ end
28
57
 
29
58
  # Merge responses from all external listeners into the base listener's response. We do this to return a single
30
59
  # response to the editor including the results of all extensions
@@ -33,11 +62,21 @@ module RubyLsp
33
62
  @external_listeners.each { |l| merge_response!(l) }
34
63
  end
35
64
 
65
+ sig { returns(ResponseType) }
66
+ def response
67
+ merge_external_listeners_responses! unless @response_merged
68
+ super
69
+ end
70
+
71
+ sig do
72
+ abstract.params(extension: RubyLsp::Extension).returns(T.nilable(RubyLsp::Listener[ResponseType]))
73
+ end
74
+ def initialize_external_listener(extension); end
75
+
36
76
  # Does nothing by default. Requests that accept extensions should override this method to define how to merge
37
77
  # responses coming from external listeners
38
- sig { overridable.params(other: Listener[T.untyped]).returns(T.self_type) }
78
+ sig { abstract.params(other: Listener[T.untyped]).returns(T.self_type) }
39
79
  def merge_response!(other)
40
- self
41
80
  end
42
81
  end
43
82
  end
@@ -18,7 +18,7 @@ module RubyLsp
18
18
  # class Test < Minitest::Test
19
19
  # end
20
20
  # ```
21
- class CodeLens < Listener
21
+ class CodeLens < ExtensibleListener
22
22
  extend T::Sig
23
23
  extend T::Generic
24
24
 
@@ -29,23 +29,20 @@ module RubyLsp
29
29
  SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
30
30
 
31
31
  sig { override.returns(ResponseType) }
32
- attr_reader :response
32
+ attr_reader :_response
33
33
 
34
34
  sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
35
35
  def initialize(uri, emitter, message_queue, test_library)
36
- super(emitter, message_queue)
37
-
38
36
  @uri = T.let(uri, URI::Generic)
39
- @external_listeners.concat(
40
- Extension.extensions.filter_map { |ext| ext.create_code_lens_listener(uri, emitter, message_queue) },
41
- )
42
37
  @test_library = T.let(test_library, String)
43
- @response = T.let([], ResponseType)
38
+ @_response = T.let([], ResponseType)
44
39
  @path = T.let(uri.to_standardized_path, T.nilable(String))
45
40
  # visibility_stack is a stack of [current_visibility, previous_visibility]
46
41
  @visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
47
42
  @class_stack = T.let([], T::Array[String])
48
43
 
44
+ super(emitter, message_queue)
45
+
49
46
  emitter.register(
50
47
  self,
51
48
  :on_class,
@@ -149,9 +146,14 @@ module RubyLsp
149
146
  end
150
147
  end
151
148
 
149
+ sig { override.params(extension: RubyLsp::Extension).returns(T.nilable(Listener[ResponseType])) }
150
+ def initialize_external_listener(extension)
151
+ extension.create_code_lens_listener(@uri, @emitter, @message_queue)
152
+ end
153
+
152
154
  sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
153
155
  def merge_response!(other)
154
- @response.concat(other.response)
156
+ @_response.concat(other.response)
155
157
  self
156
158
  end
157
159
 
@@ -174,7 +176,7 @@ module RubyLsp
174
176
  },
175
177
  ]
176
178
 
177
- @response << create_code_lens(
179
+ @_response << create_code_lens(
178
180
  node,
179
181
  title: "Run",
180
182
  command_name: "rubyLsp.runTest",
@@ -182,7 +184,7 @@ module RubyLsp
182
184
  data: { type: "test", kind: kind },
183
185
  )
184
186
 
185
- @response << create_code_lens(
187
+ @_response << create_code_lens(
186
188
  node,
187
189
  title: "Run In Terminal",
188
190
  command_name: "rubyLsp.runTestInTerminal",
@@ -190,7 +192,7 @@ module RubyLsp
190
192
  data: { type: "test_in_terminal", kind: kind },
191
193
  )
192
194
 
193
- @response << create_code_lens(
195
+ @_response << create_code_lens(
194
196
  node,
195
197
  title: "Debug",
196
198
  command_name: "rubyLsp.debugTest",
@@ -239,7 +241,7 @@ module RubyLsp
239
241
 
240
242
  sig { params(node: SyntaxTree::Command, remote: String).void }
241
243
  def add_open_gem_remote_code_lens(node, remote)
242
- @response << create_code_lens(
244
+ @_response << create_code_lens(
243
245
  node,
244
246
  title: "Open remote",
245
247
  command_name: "rubyLsp.openLink",
@@ -0,0 +1,168 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Completion demo](../../completion.gif)
7
+ #
8
+ # The [completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
9
+ # suggests possible completions according to what the developer is typing. Currently, completion is support for
10
+ # - require paths
11
+ # - classes, modules and constant names
12
+ #
13
+ # # Example
14
+ #
15
+ # ```ruby
16
+ # require "ruby_lsp/requests" # --> completion: suggests `base_request`, `code_actions`, ...
17
+ #
18
+ # RubyLsp::Requests:: # --> completion: suggests `Completion`, `Hover`, ...
19
+ # ```
20
+ class Completion < Listener
21
+ extend T::Sig
22
+ extend T::Generic
23
+
24
+ ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
25
+
26
+ sig { override.returns(ResponseType) }
27
+ attr_reader :_response
28
+
29
+ sig do
30
+ params(
31
+ index: RubyIndexer::Index,
32
+ nesting: T::Array[String],
33
+ emitter: EventEmitter,
34
+ message_queue: Thread::Queue,
35
+ ).void
36
+ end
37
+ def initialize(index, nesting, emitter, message_queue)
38
+ super(emitter, message_queue)
39
+ @_response = T.let([], ResponseType)
40
+ @index = index
41
+ @nesting = nesting
42
+
43
+ emitter.register(self, :on_tstring_content, :on_const_path_ref, :on_const, :on_top_const_ref)
44
+ end
45
+
46
+ sig { params(node: SyntaxTree::TStringContent).void }
47
+ def on_tstring_content(node)
48
+ @index.search_require_paths(node.value).map!(&:require_path).sort!.each do |path|
49
+ @_response << build_completion(T.must(path), node)
50
+ end
51
+ end
52
+
53
+ # Handle completion on regular constant references (e.g. `Bar`)
54
+ sig { params(node: SyntaxTree::Const).void }
55
+ def on_const(node)
56
+ return if DependencyDetector::HAS_TYPECHECKER
57
+
58
+ name = node.value
59
+ candidates = @index.prefix_search(name, @nesting)
60
+ candidates.each do |entries|
61
+ @_response << build_entry_completion(name, node, entries, top_level?(T.must(entries.first).name, candidates))
62
+ end
63
+ end
64
+
65
+ # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
66
+ sig { params(node: SyntaxTree::ConstPathRef).void }
67
+ def on_const_path_ref(node)
68
+ return if DependencyDetector::HAS_TYPECHECKER
69
+
70
+ name = full_constant_name(node)
71
+ candidates = @index.prefix_search(name, @nesting)
72
+ candidates.each do |entries|
73
+ @_response << build_entry_completion(name, node, entries, top_level?(T.must(entries.first).name, candidates))
74
+ end
75
+ end
76
+
77
+ # Handle completion on top level constant references (e.g. `::Bar`)
78
+ sig { params(node: SyntaxTree::TopConstRef).void }
79
+ def on_top_const_ref(node)
80
+ return if DependencyDetector::HAS_TYPECHECKER
81
+
82
+ name = full_constant_name(node)
83
+ candidates = @index.prefix_search(name, [])
84
+ candidates.each { |entries| @_response << build_entry_completion(name, node, entries, true) }
85
+ end
86
+
87
+ private
88
+
89
+ sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) }
90
+ def build_completion(label, node)
91
+ Interface::CompletionItem.new(
92
+ label: label,
93
+ text_edit: Interface::TextEdit.new(
94
+ range: range_from_syntax_tree_node(node),
95
+ new_text: label,
96
+ ),
97
+ kind: Constant::CompletionItemKind::REFERENCE,
98
+ )
99
+ end
100
+
101
+ sig do
102
+ params(
103
+ name: String,
104
+ node: SyntaxTree::Node,
105
+ entries: T::Array[RubyIndexer::Index::Entry],
106
+ top_level: T::Boolean,
107
+ ).returns(Interface::CompletionItem)
108
+ end
109
+ def build_entry_completion(name, node, entries, top_level)
110
+ first_entry = T.must(entries.first)
111
+ kind = case first_entry
112
+ when RubyIndexer::Index::Entry::Class
113
+ Constant::CompletionItemKind::CLASS
114
+ when RubyIndexer::Index::Entry::Module
115
+ Constant::CompletionItemKind::MODULE
116
+ when RubyIndexer::Index::Entry::Constant
117
+ Constant::CompletionItemKind::CONSTANT
118
+ else
119
+ Constant::CompletionItemKind::REFERENCE
120
+ end
121
+
122
+ insertion_text = first_entry.name.dup
123
+
124
+ # If we have two entries with the same name inside the current namespace and the user selects the top level
125
+ # option, we have to ensure it's prefixed with `::` or else we're completing the wrong constant. For example:
126
+ # If we have the index with ["Foo::Bar", "Bar"], and we're providing suggestions for `B` inside a `Foo` module,
127
+ # then selecting the `Foo::Bar` option needs to complete to `Bar` and selecting the top level `Bar` option needs
128
+ # to complete to `::Bar`.
129
+ insertion_text.prepend("::") if top_level
130
+
131
+ # If the user is searching for a constant inside the current namespace, then we prefer completing the short name
132
+ # of that constant. E.g.:
133
+ #
134
+ # module Foo
135
+ # class Bar
136
+ # end
137
+ #
138
+ # Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
139
+ # end
140
+ @nesting.each { |namespace| insertion_text.delete_prefix!("#{namespace}::") }
141
+
142
+ # When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
143
+ # For these top level references, we need to include the `::` as part of the filter text or else it won't match
144
+ # the right entries in the index
145
+ Interface::CompletionItem.new(
146
+ label: first_entry.name,
147
+ filter_text: top_level ? "::#{first_entry.name}" : first_entry.name,
148
+ text_edit: Interface::TextEdit.new(
149
+ range: range_from_syntax_tree_node(node),
150
+ new_text: insertion_text,
151
+ ),
152
+ kind: kind,
153
+ label_details: Interface::CompletionItemLabelDetails.new(
154
+ description: entries.map(&:file_name).join(","),
155
+ ),
156
+ documentation: markdown_from_index_entries(first_entry.name, entries),
157
+ )
158
+ end
159
+
160
+ # Check if the `entry_name` has potential conflicts in `candidates`, so that we use a top level reference instead
161
+ # of a short name
162
+ sig { params(entry_name: String, candidates: T::Array[T::Array[RubyIndexer::Index::Entry]]).returns(T::Boolean) }
163
+ def top_level?(entry_name, candidates)
164
+ candidates.any? { |entries| T.must(entries.first).name == "#{@nesting.join("::")}::#{entry_name}" }
165
+ end
166
+ end
167
+ end
168
+ end
@@ -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 < Listener
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 :response
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
- @response = T.let(nil, ResponseType)
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
- candidate = find_file_in_load_path(required_file)
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
- if candidate
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
- if candidate
91
- @response = Interface::Location.new(
92
- uri: URI::Generic.from_path(path: candidate).to_s,
93
- range: Interface::Range.new(
94
- start: Interface::Position.new(line: 0, character: 0),
95
- end: Interface::Position.new(line: 0, character: 0),
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
- @response = entries.filter_map do |entry|
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 :response
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
- @response = T.let([], T::Array[Interface::DocumentHighlight])
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
- @response << Interface::DocumentHighlight.new(range: range, kind: match.type)
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 :response
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
- @response = T.let([], T::Array[Interface::DocumentLink])
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 = T.must(resolve_version(uri))
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
- @response << Interface::DocumentLink.new(
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 < Listener
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 :response
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
- @response = T.let(@root.children, T::Array[Interface::DocumentSymbol])
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
- @external_listeners.concat(
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
- @response.concat(other.response)
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 < Listener
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 :response
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
- @external_listeners.concat(
49
- Extension.extensions.filter_map { |ext| ext.create_hover_listener(emitter, message_queue) },
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 @response.nil?
62
- @response = other.response
63
+ if @_response.nil?
64
+ @_response = other.response
63
65
  else
64
- @response.contents.value << "\n\n" << other_response.contents.value
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
- title = +"```ruby\n#{name}\n```"
93
- definitions = []
94
- content = +""
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 :response
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
- @response = T.let([], ResponseType)
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
- @response << Interface::InlayHint.new(
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 :response
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
- @response = T.let([], ResponseType)
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 @response.last(2).any? { |token| token.location == node.location }
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
- @response.push(
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
@@ -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
- # - [PathCompletion](rdoc-ref:RubyLsp::Requests::PathCompletion)
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 :PathCompletion, "ruby_lsp/requests/path_completion"
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
- if should_bundle_install?
181
- # Install gems using the custom bundle
182
- command << "(bundle check || bundle install) "
183
- else
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 << "bundle update "
185
+ command.prepend("(")
186
+ command << " && bundle update "
186
187
  command << "ruby-lsp " unless @dependencies["ruby-lsp"]
187
- command << "debug " unless @dependencies["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 should_bundle_install?
204
- (!@dependencies["ruby-lsp"].nil? && !@dependencies["debug"].nil?) ||
205
- custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil? ||
206
- (@last_updated_path.exist? && Time.parse(@last_updated_path.read) > (Time.now - FOUR_HOURS))
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.9.4
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-05 00:00:00.000000000 Z
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.9'
67
+ version: '0.11'
68
68
  - - "<"
69
69
  - !ruby/object:Gem::Version
70
- version: '0.11'
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.9'
77
+ version: '0.11'
78
78
  - - "<"
79
79
  - !ruby/object:Gem::Version
80
- version: '0.11'
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