ruby-lsp 0.12.3 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b053184e8e593901a5344dfd3d34d20962fd2ed773900e00112be751008dae7d
4
- data.tar.gz: '087be2ed7ace3fdb9eed751215a806deab096c6a5aa415d170eceaa45286c4d9'
3
+ metadata.gz: 70a84356e392893c6f7b20e1701ef31621d9902af3a120c7013a13f80ec09568
4
+ data.tar.gz: bd360c2a5426631816f227ff115af967813ae7284d0a61845daf743b0a99d948
5
5
  SHA512:
6
- metadata.gz: e29d0633e023d4d3ce3efe5347479d9e2c16b1cbf02638e29b8f617158c08d7aae84ae4ad1f5898f6bd34033e841070d8cd2ffe8d67180ad4c554f3d5f80b564
7
- data.tar.gz: 7a74cc5acca390661a196e21760f6c82e00478a02539c4c5cc2d17089948287762886c8a169d2384b59aa7e3324dda08fc3fa52a1aa6112951a2a8bc6b9253ab
6
+ metadata.gz: 598494207add9412e909836e65f6288403189133711dbb17e39663eecc719089dba9f8e758069fda91666715d8c8fc5326055192ba5558f6d8151bbc21f40338
7
+ data.tar.gz: b86880d5b342e817d8cda46c4977a4229896d19b1fa495c6235f4d86ba74fa1a8ec737b7bad8a19af8859f315980f491b7bd8bcd8f25c1ad21d1ec3f119b9aaf
data/README.md CHANGED
@@ -52,6 +52,7 @@ The Ruby LSP provides an addon system that allows other gems to enhance the base
52
52
  features. This is the mechanism that powers addons like
53
53
 
54
54
  - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
55
+ - [Ruby LSP RSpec](https://github.com/st0012/ruby-lsp-rspec)
55
56
 
56
57
  For instructions on how to create addons, see the [addons documentation](ADDONS.md).
57
58
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.3
1
+ 0.12.4
data/exe/ruby-lsp-doctor CHANGED
@@ -10,6 +10,6 @@ RubyIndexer.configuration.indexables.each do |indexable|
10
10
  puts "indexing: #{indexable.full_path}"
11
11
  content = File.read(indexable.full_path)
12
12
  result = Prism.parse(content)
13
- visitor = RubyIndexer::IndexVisitor.new(index, result, indexable.full_path)
14
- result.value.accept(visitor)
13
+ collector = RubyIndexer::Collector.new(index, result, indexable.full_path)
14
+ collector.collect(result.value)
15
15
  end
@@ -2,9 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyIndexer
5
- class IndexVisitor < Prism::Visitor
5
+ class Collector
6
6
  extend T::Sig
7
7
 
8
+ LEAVE_EVENT = T.let(Object.new.freeze, Object)
9
+
8
10
  sig { params(index: Index, parse_result: Prism::ParseResult, file_path: String).void }
9
11
  def initialize(index, parse_result, file_path)
10
12
  @index = index
@@ -16,22 +18,63 @@ module RubyIndexer
16
18
  end,
17
19
  T::Hash[Integer, Prism::Comment],
18
20
  )
21
+ @queue = T.let([], T::Array[Object])
22
+ @current_owner = T.let(nil, T.nilable(Entry::Namespace))
19
23
 
20
24
  super()
21
25
  end
22
26
 
23
- sig { override.params(node: Prism::ClassNode).void }
24
- def visit_class_node(node)
25
- add_class_entry(node)
27
+ sig { params(node: Prism::Node).void }
28
+ def collect(node)
29
+ @queue = [node]
30
+
31
+ until @queue.empty?
32
+ node_or_event = @queue.shift
33
+
34
+ case node_or_event
35
+ when Prism::ProgramNode
36
+ @queue << node_or_event.statements
37
+ when Prism::StatementsNode
38
+ T.unsafe(@queue).prepend(*node_or_event.body)
39
+ when Prism::ClassNode
40
+ add_class_entry(node_or_event)
41
+ when Prism::ModuleNode
42
+ add_module_entry(node_or_event)
43
+ when Prism::MultiWriteNode
44
+ handle_multi_write_node(node_or_event)
45
+ when Prism::ConstantPathWriteNode
46
+ handle_constant_path_write_node(node_or_event)
47
+ when Prism::ConstantPathOrWriteNode
48
+ handle_constant_path_or_write_node(node_or_event)
49
+ when Prism::ConstantPathOperatorWriteNode
50
+ handle_constant_path_operator_write_node(node_or_event)
51
+ when Prism::ConstantPathAndWriteNode
52
+ handle_constant_path_and_write_node(node_or_event)
53
+ when Prism::ConstantWriteNode
54
+ handle_constant_write_node(node_or_event)
55
+ when Prism::ConstantOrWriteNode
56
+ name = fully_qualify_name(node_or_event.name.to_s)
57
+ add_constant(node_or_event, name)
58
+ when Prism::ConstantAndWriteNode
59
+ name = fully_qualify_name(node_or_event.name.to_s)
60
+ add_constant(node_or_event, name)
61
+ when Prism::ConstantOperatorWriteNode
62
+ name = fully_qualify_name(node_or_event.name.to_s)
63
+ add_constant(node_or_event, name)
64
+ when Prism::CallNode
65
+ handle_call_node(node_or_event)
66
+ when Prism::DefNode
67
+ handle_def_node(node_or_event)
68
+ when LEAVE_EVENT
69
+ @stack.pop
70
+ end
71
+ end
26
72
  end
27
73
 
28
- sig { override.params(node: Prism::ModuleNode).void }
29
- def visit_module_node(node)
30
- add_module_entry(node)
31
- end
74
+ private
32
75
 
33
- sig { override.params(node: Prism::MultiWriteNode).void }
34
- def visit_multi_write_node(node)
76
+ sig { params(node: Prism::MultiWriteNode).void }
77
+ def handle_multi_write_node(node)
35
78
  value = node.value
36
79
  values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : []
37
80
 
@@ -50,8 +93,8 @@ module RubyIndexer
50
93
  end
51
94
  end
52
95
 
53
- sig { override.params(node: Prism::ConstantPathWriteNode).void }
54
- def visit_constant_path_write_node(node)
96
+ sig { params(node: Prism::ConstantPathWriteNode).void }
97
+ def handle_constant_path_write_node(node)
55
98
  # ignore variable constants like `var::FOO` or `self.class::FOO`
56
99
  target = node.target
57
100
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -60,8 +103,8 @@ module RubyIndexer
60
103
  add_constant(node, name)
61
104
  end
62
105
 
63
- sig { override.params(node: Prism::ConstantPathOrWriteNode).void }
64
- def visit_constant_path_or_write_node(node)
106
+ sig { params(node: Prism::ConstantPathOrWriteNode).void }
107
+ def handle_constant_path_or_write_node(node)
65
108
  # ignore variable constants like `var::FOO` or `self.class::FOO`
66
109
  target = node.target
67
110
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -70,8 +113,8 @@ module RubyIndexer
70
113
  add_constant(node, name)
71
114
  end
72
115
 
73
- sig { override.params(node: Prism::ConstantPathOperatorWriteNode).void }
74
- def visit_constant_path_operator_write_node(node)
116
+ sig { params(node: Prism::ConstantPathOperatorWriteNode).void }
117
+ def handle_constant_path_operator_write_node(node)
75
118
  # ignore variable constants like `var::FOO` or `self.class::FOO`
76
119
  target = node.target
77
120
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -80,8 +123,8 @@ module RubyIndexer
80
123
  add_constant(node, name)
81
124
  end
82
125
 
83
- sig { override.params(node: Prism::ConstantPathAndWriteNode).void }
84
- def visit_constant_path_and_write_node(node)
126
+ sig { params(node: Prism::ConstantPathAndWriteNode).void }
127
+ def handle_constant_path_and_write_node(node)
85
128
  # ignore variable constants like `var::FOO` or `self.class::FOO`
86
129
  target = node.target
87
130
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
@@ -90,50 +133,44 @@ module RubyIndexer
90
133
  add_constant(node, name)
91
134
  end
92
135
 
93
- sig { override.params(node: Prism::ConstantWriteNode).void }
94
- def visit_constant_write_node(node)
95
- name = fully_qualify_name(node.name.to_s)
96
- add_constant(node, name)
97
- end
98
-
99
- sig { override.params(node: Prism::ConstantOrWriteNode).void }
100
- def visit_constant_or_write_node(node)
101
- name = fully_qualify_name(node.name.to_s)
102
- add_constant(node, name)
103
- end
104
-
105
- sig { override.params(node: Prism::ConstantAndWriteNode).void }
106
- def visit_constant_and_write_node(node)
136
+ sig { params(node: Prism::ConstantWriteNode).void }
137
+ def handle_constant_write_node(node)
107
138
  name = fully_qualify_name(node.name.to_s)
108
139
  add_constant(node, name)
109
140
  end
110
141
 
111
- sig { override.params(node: Prism::ConstantOperatorWriteNode).void }
112
- def visit_constant_operator_write_node(node)
113
- name = fully_qualify_name(node.name.to_s)
114
- add_constant(node, name)
115
- end
116
-
117
- sig { override.params(node: Prism::CallNode).void }
118
- def visit_call_node(node)
142
+ sig { params(node: Prism::CallNode).void }
143
+ def handle_call_node(node)
119
144
  message = node.message
120
145
  handle_private_constant(node) if message == "private_constant"
121
146
  end
122
147
 
123
- sig { override.params(node: Prism::DefNode).void }
124
- def visit_def_node(node)
148
+ sig { params(node: Prism::DefNode).void }
149
+ def handle_def_node(node)
125
150
  method_name = node.name.to_s
126
151
  comments = collect_comments(node)
127
152
  case node.receiver
128
153
  when nil
129
- @index << Entry::InstanceMethod.new(method_name, @file_path, node.location, comments, node.parameters)
154
+ @index << Entry::InstanceMethod.new(
155
+ method_name,
156
+ @file_path,
157
+ node.location,
158
+ comments,
159
+ node.parameters,
160
+ @current_owner,
161
+ )
130
162
  when Prism::SelfNode
131
- @index << Entry::SingletonMethod.new(method_name, @file_path, node.location, comments, node.parameters)
163
+ @index << Entry::SingletonMethod.new(
164
+ method_name,
165
+ @file_path,
166
+ node.location,
167
+ comments,
168
+ node.parameters,
169
+ @current_owner,
170
+ )
132
171
  end
133
172
  end
134
173
 
135
- private
136
-
137
174
  sig { params(node: Prism::CallNode).void }
138
175
  def handle_private_constant(node)
139
176
  arguments = node.arguments&.arguments
@@ -189,12 +226,12 @@ module RubyIndexer
189
226
 
190
227
  # If the right hand side is another constant assignment, we need to visit it because that constant has to be
191
228
  # indexed too
192
- visit(value)
229
+ @queue.prepend(value)
193
230
  Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments)
194
231
  when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
195
232
  Prism::ConstantPathAndWriteNode
196
233
 
197
- visit(value)
234
+ @queue.prepend(value)
198
235
  Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments)
199
236
  else
200
237
  Entry::Constant.new(name, @file_path, node.location, comments)
@@ -204,20 +241,26 @@ module RubyIndexer
204
241
  sig { params(node: Prism::ModuleNode).void }
205
242
  def add_module_entry(node)
206
243
  name = node.constant_path.location.slice
207
- return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
244
+ unless /^[A-Z:]/.match?(name)
245
+ @queue << node.body
246
+ return
247
+ end
208
248
 
209
249
  comments = collect_comments(node)
210
-
211
- @index << Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments)
250
+ @current_owner = Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments)
251
+ @index << @current_owner
212
252
  @stack << name
213
- visit_child_nodes(node)
214
- @stack.pop
253
+ @queue.prepend(node.body, LEAVE_EVENT)
215
254
  end
216
255
 
217
256
  sig { params(node: Prism::ClassNode).void }
218
257
  def add_class_entry(node)
219
258
  name = node.constant_path.location.slice
220
- return visit_child_nodes(node) unless /^[A-Z:]/.match?(name)
259
+
260
+ unless /^[A-Z:]/.match?(name)
261
+ @queue << node.body
262
+ return
263
+ end
221
264
 
222
265
  comments = collect_comments(node)
223
266
 
@@ -227,10 +270,16 @@ module RubyIndexer
227
270
  superclass.slice
228
271
  end
229
272
 
230
- @index << Entry::Class.new(fully_qualify_name(name), @file_path, node.location, comments, parent_class)
273
+ @current_owner = Entry::Class.new(
274
+ fully_qualify_name(name),
275
+ @file_path,
276
+ node.location,
277
+ comments,
278
+ parent_class,
279
+ )
280
+ @index << @current_owner
231
281
  @stack << name
232
- visit(node.body)
233
- @stack.pop
282
+ @queue.prepend(node.body, LEAVE_EVENT)
234
283
  end
235
284
 
236
285
  sig { params(node: Prism::Node).returns(T::Array[String]) }
@@ -249,7 +298,7 @@ module RubyIndexer
249
298
 
250
299
  comment_content.delete_prefix!("#")
251
300
  comment_content.delete_prefix!(" ")
252
- comments.unshift(comment_content)
301
+ comments.prepend(comment_content)
253
302
  end
254
303
 
255
304
  comments
@@ -71,8 +71,13 @@ module RubyIndexer
71
71
 
72
72
  Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
73
73
  # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
74
- # entry is expensive, we memoize it for the entire pattern
75
- load_path_entry ||= $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
74
+ # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
75
+ # on repositories that define multiple gems, like Rails. All frameworks are defined inside the Dir.pwd, but
76
+ # each one of them belongs to a different $LOAD_PATH entry
77
+ if load_path_entry.nil? || !path.start_with?(load_path_entry)
78
+ load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
79
+ end
80
+
76
81
  IndexablePath.new(load_path_entry, path)
77
82
  end
78
83
  end
@@ -144,7 +149,7 @@ module RubyIndexer
144
149
  # just ignore if they're missing
145
150
  end
146
151
 
147
- indexables.uniq!
152
+ indexables.uniq!(&:full_path)
148
153
  indexables
149
154
  end
150
155
 
@@ -101,6 +101,9 @@ module RubyIndexer
101
101
  sig { returns(T::Array[Parameter]) }
102
102
  attr_reader :parameters
103
103
 
104
+ sig { returns(T.nilable(Entry::Namespace)) }
105
+ attr_reader :owner
106
+
104
107
  sig do
105
108
  params(
106
109
  name: String,
@@ -108,11 +111,13 @@ module RubyIndexer
108
111
  location: Prism::Location,
109
112
  comments: T::Array[String],
110
113
  parameters_node: T.nilable(Prism::ParametersNode),
114
+ owner: T.nilable(Entry::Namespace),
111
115
  ).void
112
116
  end
113
- def initialize(name, file_path, location, comments, parameters_node)
117
+ def initialize(name, file_path, location, comments, parameters_node, owner) # rubocop:disable Metrics/ParameterLists
114
118
  super(name, file_path, location, comments)
115
119
  @parameters = T.let(list_params(parameters_node), T::Array[Parameter])
120
+ @owner = owner
116
121
  end
117
122
 
118
123
  private
@@ -93,8 +93,14 @@ module RubyIndexer
93
93
  # [#<Entry::Class name="Foo::Baz">],
94
94
  # ]
95
95
  # ```
96
- sig { params(query: String, nesting: T::Array[String]).returns(T::Array[T::Array[Entry]]) }
97
- def prefix_search(query, nesting)
96
+ sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) }
97
+ def prefix_search(query, nesting = nil)
98
+ unless nesting
99
+ results = @entries_tree.search(query)
100
+ results.uniq!
101
+ return results
102
+ end
103
+
98
104
  results = nesting.length.downto(0).flat_map do |i|
99
105
  prefix = T.must(nesting[0...i]).join("::")
100
106
  namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
@@ -180,8 +186,8 @@ module RubyIndexer
180
186
  def index_single(indexable_path, source = nil)
181
187
  content = source || File.read(indexable_path.full_path)
182
188
  result = Prism.parse(content)
183
- visitor = IndexVisitor.new(self, result, indexable_path.full_path)
184
- result.value.accept(visitor)
189
+ collector = Collector.new(self, result, indexable_path.full_path)
190
+ collector.collect(result.value)
185
191
 
186
192
  require_path = indexable_path.require_path
187
193
  @require_paths_tree.insert(require_path, indexable_path) if require_path
@@ -231,6 +237,18 @@ module RubyIndexer
231
237
  real_parts.join("::")
232
238
  end
233
239
 
240
+ # Attempts to find a given method for a resolved fully qualified receiver name. Returns `nil` if the method does not
241
+ # exist on that receiver
242
+ sig { params(method_name: String, receiver_name: String).returns(T.nilable(Entry::Method)) }
243
+ def resolve_method(method_name, receiver_name)
244
+ method_entries = T.cast(self[method_name], T.nilable(T::Array[Entry::Method]))
245
+ owner_entries = self[receiver_name]
246
+ return unless owner_entries && method_entries
247
+
248
+ owner_name = T.must(owner_entries.first).name
249
+ method_entries.find { |entry| entry.owner&.name == owner_name }
250
+ end
251
+
234
252
  private
235
253
 
236
254
  # Attempts to resolve an UnresolvedAlias into a resolved Alias. If the unresolved alias is pointing to a constant
@@ -5,7 +5,7 @@ require "yaml"
5
5
  require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/indexable_path"
8
- require "ruby_indexer/lib/ruby_indexer/visitor"
8
+ require "ruby_indexer/lib/ruby_indexer/collector"
9
9
  require "ruby_indexer/lib/ruby_indexer/index"
10
10
  require "ruby_indexer/lib/ruby_indexer/entry"
11
11
  require "ruby_indexer/lib/ruby_indexer/configuration"
@@ -193,5 +193,58 @@ module RubyIndexer
193
193
 
194
194
  assert_instance_of(Entry::UnresolvedAlias, entry)
195
195
  end
196
+
197
+ def test_visitor_does_not_visit_unnecessary_nodes
198
+ concats = (0...10_000).map do |i|
199
+ <<~STRING
200
+ "string#{i}" \\
201
+ STRING
202
+ end.join
203
+
204
+ index(<<~RUBY)
205
+ module Foo
206
+ local_var = #{concats}
207
+ "final"
208
+ @class_instance_var = #{concats}
209
+ "final"
210
+ @@class_var = #{concats}
211
+ "final"
212
+ $global_var = #{concats}
213
+ "final"
214
+ CONST = #{concats}
215
+ "final"
216
+ end
217
+ RUBY
218
+ end
219
+
220
+ def test_resolve_method_with_known_receiver
221
+ index(<<~RUBY)
222
+ module Foo
223
+ module Bar
224
+ def baz; end
225
+ end
226
+ end
227
+ RUBY
228
+
229
+ entry = T.must(@index.resolve_method("baz", "Foo::Bar"))
230
+ assert_equal("baz", entry.name)
231
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
232
+ end
233
+
234
+ def test_prefix_search_for_methods
235
+ index(<<~RUBY)
236
+ module Foo
237
+ module Bar
238
+ def baz; end
239
+ end
240
+ end
241
+ RUBY
242
+
243
+ entries = @index.prefix_search("ba")
244
+ refute_empty(entries)
245
+
246
+ entry = T.must(entries.first).first
247
+ assert_equal("baz", entry.name)
248
+ end
196
249
  end
197
250
  end
@@ -69,5 +69,19 @@ module RubyIndexer
69
69
  assert_equal(:"(a, (b, ))", parameter.name)
70
70
  assert_instance_of(Entry::RequiredParameter, parameter)
71
71
  end
72
+
73
+ def test_keeps_track_of_method_owner
74
+ index(<<~RUBY)
75
+ class Foo
76
+ def bar
77
+ end
78
+ end
79
+ RUBY
80
+
81
+ entry = T.must(@index["bar"].first)
82
+ owner_name = T.must(entry.owner).name
83
+
84
+ assert_equal("Foo", owner_name)
85
+ end
72
86
  end
73
87
  end
@@ -4,6 +4,9 @@
4
4
  module RubyLsp
5
5
  class Document
6
6
  extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
7
10
 
8
11
  PositionShape = T.type_alias { { line: Integer, character: Integer } }
9
12
  RangeShape = T.type_alias { { start: PositionShape, end: PositionShape } }
@@ -28,8 +31,8 @@ module RubyLsp
28
31
  @source = T.let(source, String)
29
32
  @version = T.let(version, Integer)
30
33
  @uri = T.let(uri, URI::Generic)
31
- @needs_parsing = T.let(false, T::Boolean)
32
- @parse_result = T.let(Prism.parse(@source), Prism::ParseResult)
34
+ @needs_parsing = T.let(true, T::Boolean)
35
+ @parse_result = T.let(parse, Prism::ParseResult)
33
36
  end
34
37
 
35
38
  sig { returns(Prism::ProgramNode) }
@@ -91,13 +94,8 @@ module RubyLsp
91
94
  @cache.clear
92
95
  end
93
96
 
94
- sig { void }
95
- def parse
96
- return unless @needs_parsing
97
-
98
- @needs_parsing = false
99
- @parse_result = Prism.parse(@source)
100
- end
97
+ sig { abstract.returns(Prism::ParseResult) }
98
+ def parse; end
101
99
 
102
100
  sig { returns(T::Boolean) }
103
101
  def syntax_error?
@@ -57,6 +57,8 @@ module RubyLsp
57
57
  warn(errored_addons.map(&:backtraces).join("\n\n"))
58
58
  end
59
59
 
60
+ RubyVM::YJIT.enable if defined? RubyVM::YJIT.enable
61
+
60
62
  perform_initial_indexing
61
63
  check_formatter_is_available
62
64
 
@@ -474,34 +476,32 @@ module RubyLsp
474
476
  def completion(uri, position)
475
477
  document = @store.get(uri)
476
478
 
477
- char_position = document.create_scanner.find_char_position(position)
478
-
479
- # When the user types in the first letter of a constant name, we actually receive the position of the next
480
- # immediate character. We check to see if the character is uppercase and then remove the offset to try to locate
481
- # the node, as it could not be a constant
482
- target_node_types = if ("A".."Z").cover?(document.source[char_position - 1])
483
- char_position -= 1
484
- [Prism::ConstantReadNode, Prism::ConstantPathNode]
485
- else
486
- [Prism::CallNode]
487
- end
488
-
489
- matched, parent, nesting = document.locate(document.tree, char_position, node_types: target_node_types)
479
+ # Completion always receives the position immediately after the character that was just typed. Here we adjust it
480
+ # back by 1, so that we find the right node
481
+ char_position = document.create_scanner.find_char_position(position) - 1
482
+ matched, parent, nesting = document.locate(
483
+ document.tree,
484
+ char_position,
485
+ node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
486
+ )
490
487
  return unless matched && parent
491
488
 
492
489
  target = case matched
493
490
  when Prism::CallNode
494
491
  message = matched.message
495
- return unless message == "require"
496
492
 
497
- args = matched.arguments&.arguments
498
- return if args.nil? || args.is_a?(Prism::ForwardingArgumentsNode)
493
+ if message == "require"
494
+ args = matched.arguments&.arguments
495
+ return if args.nil? || args.is_a?(Prism::ForwardingArgumentsNode)
499
496
 
500
- argument = args.first
501
- return unless argument.is_a?(Prism::StringNode)
502
- return unless (argument.location.start_offset..argument.location.end_offset).cover?(char_position)
497
+ argument = args.first
498
+ return unless argument.is_a?(Prism::StringNode)
499
+ return unless (argument.location.start_offset..argument.location.end_offset).cover?(char_position)
503
500
 
504
- argument
501
+ argument
502
+ else
503
+ matched
504
+ end
505
505
  when Prism::ConstantReadNode, Prism::ConstantPathNode
506
506
  if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
507
507
  parent
@@ -579,7 +579,7 @@ module RubyLsp
579
579
  # notification
580
580
  end
581
581
 
582
- sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
582
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
583
583
  def initialize_request(options)
584
584
  @store.clear
585
585
 
@@ -680,7 +680,7 @@ module RubyLsp
680
680
  completion_provider = if enabled_features["completion"]
681
681
  Interface::CompletionOptions.new(
682
682
  resolve_provider: false,
683
- trigger_characters: ["/", *"A".."Z"],
683
+ trigger_characters: ["/"],
684
684
  completion_item: {
685
685
  labelDetailsSupport: true,
686
686
  },
@@ -716,7 +716,7 @@ module RubyLsp
716
716
 
717
717
  begin_progress("indexing-progress", "Ruby LSP: indexing files")
718
718
 
719
- Interface::InitializeResult.new(
719
+ {
720
720
  capabilities: Interface::ServerCapabilities.new(
721
721
  text_document_sync: Interface::TextDocumentSyncOptions.new(
722
722
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
@@ -740,7 +740,12 @@ module RubyLsp
740
740
  definition_provider: enabled_features["definition"],
741
741
  workspace_symbol_provider: enabled_features["workspaceSymbol"],
742
742
  ),
743
- )
743
+ serverInfo: {
744
+ name: "Ruby LSP",
745
+ version: VERSION,
746
+ },
747
+ formatter: @store.formatter,
748
+ }
744
749
  end
745
750
 
746
751
  sig { void }
@@ -24,6 +24,10 @@ require "ruby_lsp/server"
24
24
  require "ruby_lsp/executor"
25
25
  require "ruby_lsp/requests"
26
26
  require "ruby_lsp/listener"
27
+ require "ruby_lsp/document"
28
+ require "ruby_lsp/ruby_document"
27
29
  require "ruby_lsp/store"
28
30
  require "ruby_lsp/addon"
29
31
  require "ruby_lsp/requests/support/rubocop_runner"
32
+
33
+ Bundler.ui.level = :silent
@@ -6,9 +6,14 @@ module RubyLsp
6
6
  # ![Completion demo](../../completion.gif)
7
7
  #
8
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
9
+ # suggests possible completions according to what the developer is typing.
10
+ #
11
+ # Currently supported targets:
12
+ # - Classes
13
+ # - Modules
14
+ # - Constants
15
+ # - Require paths
16
+ # - Methods invoked on self only
12
17
  #
13
18
  # # Example
14
19
  #
@@ -45,6 +50,7 @@ module RubyLsp
45
50
  :on_string_node_enter,
46
51
  :on_constant_path_node_enter,
47
52
  :on_constant_read_node_enter,
53
+ :on_call_node_enter,
48
54
  )
49
55
  end
50
56
 
@@ -118,8 +124,48 @@ module RubyLsp
118
124
  end
119
125
  end
120
126
 
127
+ sig { params(node: Prism::CallNode).void }
128
+ def on_call_node_enter(node)
129
+ return if DependencyDetector.instance.typechecker
130
+ return unless self_receiver?(node)
131
+
132
+ name = node.message
133
+ return unless name
134
+
135
+ receiver_entries = @index[@nesting.join("::")]
136
+ return unless receiver_entries
137
+
138
+ receiver = T.must(receiver_entries.first)
139
+
140
+ candidates = T.cast(@index.prefix_search(name), T::Array[T::Array[RubyIndexer::Entry::Method]])
141
+ candidates.each do |entries|
142
+ entry = entries.find { |e| e.owner&.name == receiver.name }
143
+ next unless entry
144
+
145
+ @_response << build_method_completion(entry, node)
146
+ end
147
+ end
148
+
121
149
  private
122
150
 
151
+ sig { params(entry: RubyIndexer::Entry::Method, node: Prism::CallNode).returns(Interface::CompletionItem) }
152
+ def build_method_completion(entry, node)
153
+ name = entry.name
154
+ parameters = entry.parameters
155
+ new_text = parameters.empty? ? name : "#{name}(#{parameters.map(&:name).join(", ")})"
156
+
157
+ Interface::CompletionItem.new(
158
+ label: name,
159
+ filter_text: name,
160
+ text_edit: Interface::TextEdit.new(range: range_from_node(node), new_text: new_text),
161
+ kind: Constant::CompletionItemKind::METHOD,
162
+ label_details: Interface::CompletionItemLabelDetails.new(
163
+ description: entry.file_name,
164
+ ),
165
+ documentation: markdown_from_index_entries(name, entry),
166
+ )
167
+ end
168
+
123
169
  sig { params(label: String, node: Prism::StringNode).returns(Interface::CompletionItem) }
124
170
  def build_completion(label, node)
125
171
  Interface::CompletionItem.new(
@@ -9,7 +9,12 @@ module RubyLsp
9
9
  # request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
10
10
  # definition of the symbol under the cursor.
11
11
  #
12
- # Currently, only jumping to classes, modules and required files is supported.
12
+ # Currently supported targets:
13
+ # - Classes
14
+ # - Modules
15
+ # - Constants
16
+ # - Require paths
17
+ # - Methods invoked on self only
13
18
  #
14
19
  # # Example
15
20
  #
@@ -75,8 +80,52 @@ module RubyLsp
75
80
  sig { params(node: Prism::CallNode).void }
76
81
  def on_call_node_enter(node)
77
82
  message = node.name
78
- return unless message == :require || message == :require_relative
79
83
 
84
+ if message == :require || message == :require_relative
85
+ handle_require_definition(node)
86
+ else
87
+ handle_method_definition(node)
88
+ end
89
+ end
90
+
91
+ sig { params(node: Prism::ConstantPathNode).void }
92
+ def on_constant_path_node_enter(node)
93
+ find_in_index(node.slice)
94
+ end
95
+
96
+ sig { params(node: Prism::ConstantReadNode).void }
97
+ def on_constant_read_node_enter(node)
98
+ find_in_index(node.slice)
99
+ end
100
+
101
+ private
102
+
103
+ sig { params(node: Prism::CallNode).void }
104
+ def handle_method_definition(node)
105
+ return unless self_receiver?(node)
106
+
107
+ message = node.message
108
+ return unless message
109
+
110
+ target_method = @index.resolve_method(message, @nesting.join("::"))
111
+ return unless target_method
112
+
113
+ location = target_method.location
114
+ file_path = target_method.file_path
115
+ return if defined_in_gem?(file_path)
116
+
117
+ @_response = Interface::Location.new(
118
+ uri: URI::Generic.from_path(path: file_path).to_s,
119
+ range: Interface::Range.new(
120
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
121
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
122
+ ),
123
+ )
124
+ end
125
+
126
+ sig { params(node: Prism::CallNode).void }
127
+ def handle_require_definition(node)
128
+ message = node.name
80
129
  arguments = node.arguments
81
130
  return unless arguments
82
131
 
@@ -116,18 +165,6 @@ module RubyLsp
116
165
  end
117
166
  end
118
167
 
119
- sig { params(node: Prism::ConstantPathNode).void }
120
- def on_constant_path_node_enter(node)
121
- find_in_index(node.slice)
122
- end
123
-
124
- sig { params(node: Prism::ConstantReadNode).void }
125
- def on_constant_read_node_enter(node)
126
- find_in_index(node.slice)
127
- end
128
-
129
- private
130
-
131
168
  sig { params(value: String).void }
132
169
  def find_in_index(value)
133
170
  entries = @index.resolve(value, @nesting)
@@ -138,23 +175,13 @@ module RubyLsp
138
175
  first_entry = T.must(entries.first)
139
176
  return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
140
177
 
141
- bundle_path = begin
142
- Bundler.bundle_path.to_s
143
- rescue Bundler::GemfileNotFound
144
- nil
145
- end
146
-
147
178
  @_response = entries.filter_map do |entry|
148
179
  location = entry.location
149
180
  # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
150
181
  # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
151
182
  # in the project, even if the files are typed false
152
183
  file_path = entry.file_path
153
- if DependencyDetector.instance.typechecker && bundle_path && !file_path.start_with?(bundle_path) &&
154
- !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
155
-
156
- next
157
- end
184
+ next if defined_in_gem?(file_path)
158
185
 
159
186
  Interface::Location.new(
160
187
  uri: URI::Generic.from_path(path: file_path).to_s,
@@ -51,6 +51,7 @@ module RubyLsp
51
51
  :on_constant_read_node_enter,
52
52
  :on_constant_write_node_enter,
53
53
  :on_constant_path_node_enter,
54
+ :on_call_node_enter,
54
55
  )
55
56
  end
56
57
 
@@ -95,6 +96,25 @@ module RubyLsp
95
96
  generate_hover(node.slice, node.location)
96
97
  end
97
98
 
99
+ sig { params(node: Prism::CallNode).void }
100
+ def on_call_node_enter(node)
101
+ return if DependencyDetector.instance.typechecker
102
+ return unless self_receiver?(node)
103
+
104
+ message = node.message
105
+ return unless message
106
+
107
+ target_method = @index.resolve_method(message, @nesting.join("::"))
108
+ return unless target_method
109
+
110
+ location = target_method.location
111
+
112
+ @_response = Interface::Hover.new(
113
+ range: range_from_location(location),
114
+ contents: markdown_from_index_entries(message, target_method),
115
+ )
116
+ end
117
+
98
118
  private
99
119
 
100
120
  sig { params(name: String, location: Prism::Location).void }
@@ -18,6 +18,16 @@ module RubyLsp
18
18
  # puts "handle some rescue"
19
19
  # end
20
20
  # ```
21
+ #
22
+ # # Example
23
+ #
24
+ # ```ruby
25
+ # var = "foo"
26
+ # {
27
+ # var: var, # Label "var" goes here in cases where the value is omitted
28
+ # a: "hello",
29
+ # }
30
+ # ```
21
31
  class InlayHints < Listener
22
32
  extend T::Sig
23
33
  extend T::Generic
@@ -36,7 +46,7 @@ module RubyLsp
36
46
  @_response = T.let([], ResponseType)
37
47
  @range = range
38
48
 
39
- dispatcher.register(self, :on_rescue_node_enter)
49
+ dispatcher.register(self, :on_rescue_node_enter, :on_implicit_node_enter)
40
50
  end
41
51
 
42
52
  sig { params(node: Prism::RescueNode).void }
@@ -53,6 +63,34 @@ module RubyLsp
53
63
  tooltip: "StandardError is implied in a bare rescue",
54
64
  )
55
65
  end
66
+
67
+ sig { params(node: Prism::ImplicitNode).void }
68
+ def on_implicit_node_enter(node)
69
+ return unless visible?(node, @range)
70
+
71
+ node_value = node.value
72
+ loc = node.location
73
+ tooltip = ""
74
+ node_name = ""
75
+ case node_value
76
+ when Prism::CallNode
77
+ node_name = node_value.name
78
+ tooltip = "This is a method call. Method name: #{node_name}"
79
+ when Prism::ConstantReadNode
80
+ node_name = node_value.name
81
+ tooltip = "This is a constant: #{node_name}"
82
+ when Prism::LocalVariableReadNode
83
+ node_name = node_value.name
84
+ tooltip = "This is a local variable: #{node_name}"
85
+ end
86
+
87
+ @_response << Interface::InlayHint.new(
88
+ position: { line: loc.start_line - 1, character: loc.start_column + node_name.length + 1 },
89
+ label: node_name,
90
+ padding_left: true,
91
+ tooltip: tooltip,
92
+ )
93
+ end
56
94
  end
57
95
  end
58
96
  end
@@ -51,7 +51,14 @@ module RubyLsp
51
51
  if (comment_match = @previous_line.match(/^#(\s*)/))
52
52
  handle_comment_line(T.must(comment_match[1]))
53
53
  elsif @document.syntax_error?
54
- handle_statement_end
54
+ match = /(?<=<<(-|~))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
55
+ heredoc_delimiter = match && match.named_captures["delimiter"]
56
+
57
+ if heredoc_delimiter
58
+ handle_heredoc_end(heredoc_delimiter)
59
+ else
60
+ handle_statement_end
61
+ end
55
62
  end
56
63
  end
57
64
 
@@ -121,6 +128,14 @@ module RubyLsp
121
128
  end
122
129
  end
123
130
 
131
+ sig { params(delimiter: String).void }
132
+ def handle_heredoc_end(delimiter)
133
+ indents = " " * @indentation
134
+ add_edit_with_text("\n")
135
+ add_edit_with_text("#{indents}#{delimiter}")
136
+ move_cursor_to(@position[:line], @indentation + 2)
137
+ end
138
+
124
139
  sig { params(spaces: String).void }
125
140
  def handle_comment_line(spaces)
126
141
  add_edit_with_text("##{spaces}")
@@ -9,6 +9,9 @@ module RubyLsp
9
9
  # https://github.com/Shopify/ruby-lsp-rails, or addons by created by developers outside of Shopify, so be
10
10
  # cautious of changing anything.
11
11
  extend T::Sig
12
+ extend T::Helpers
13
+
14
+ requires_ancestor { Kernel }
12
15
 
13
16
  sig { params(node: Prism::Node).returns(Interface::Range) }
14
17
  def range_from_node(node)
@@ -66,12 +69,29 @@ module RubyLsp
66
69
  )
67
70
  end
68
71
 
69
- sig { params(title: String, entries: T::Array[RubyIndexer::Entry]).returns(Interface::MarkupContent) }
72
+ sig { params(file_path: String).returns(T.nilable(T::Boolean)) }
73
+ def defined_in_gem?(file_path)
74
+ DependencyDetector.instance.typechecker && BUNDLE_PATH && !file_path.start_with?(T.must(BUNDLE_PATH)) &&
75
+ !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
76
+ end
77
+
78
+ sig { params(node: Prism::CallNode).returns(T::Boolean) }
79
+ def self_receiver?(node)
80
+ receiver = node.receiver
81
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
82
+ end
83
+
84
+ sig do
85
+ params(
86
+ title: String,
87
+ entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry),
88
+ ).returns(Interface::MarkupContent)
89
+ end
70
90
  def markdown_from_index_entries(title, entries)
71
91
  markdown_title = "```ruby\n#{title}\n```"
72
92
  definitions = []
73
93
  content = +""
74
- entries.each do |entry|
94
+ Array(entries).each do |entry|
75
95
  loc = entry.location
76
96
 
77
97
  # We always handle locations as zero based. However, for file links in Markdown we need them to be one
@@ -66,7 +66,6 @@ module RubyLsp
66
66
 
67
67
  sig { returns(T::Array[String]) }
68
68
  def dependencies
69
- # NOTE: If changing this behaviour, it's likely that the VS Code extension will also need changed.
70
69
  @dependencies ||= T.let(
71
70
  begin
72
71
  Bundler.with_original_env { Bundler.default_gemfile }
@@ -20,6 +20,7 @@ module RubyLsp
20
20
  #
21
21
  class WorkspaceSymbol
22
22
  extend T::Sig
23
+ include Support::Common
23
24
 
24
25
  sig { params(query: T.nilable(String), index: RubyIndexer::Index).void }
25
26
  def initialize(query, index)
@@ -29,21 +30,11 @@ module RubyLsp
29
30
 
30
31
  sig { returns(T::Array[Interface::WorkspaceSymbol]) }
31
32
  def run
32
- bundle_path = begin
33
- Bundler.bundle_path.to_s
34
- rescue Bundler::GemfileNotFound
35
- nil
36
- end
37
-
38
33
  @index.fuzzy_search(@query).filter_map do |entry|
39
34
  # If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but
40
35
  # we still return entries defined in gems to allow developers to jump directly to the source
41
36
  file_path = entry.file_path
42
- if DependencyDetector.instance.typechecker && bundle_path && !file_path.start_with?(bundle_path) &&
43
- !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
44
-
45
- next
46
- end
37
+ next if defined_in_gem?(file_path)
47
38
 
48
39
  # We should never show private symbols when searching the entire workspace
49
40
  next if entry.visibility == :private
@@ -82,6 +73,8 @@ module RubyLsp
82
73
  Constant::SymbolKind::NAMESPACE
83
74
  when RubyIndexer::Entry::Constant
84
75
  Constant::SymbolKind::CONSTANT
76
+ when RubyIndexer::Entry::Method
77
+ entry.name == "initialize" ? Constant::SymbolKind::CONSTRUCTOR : Constant::SymbolKind::METHOD
85
78
  end
86
79
  end
87
80
  end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class RubyDocument < Document
6
+ sig { override.returns(Prism::ParseResult) }
7
+ def parse
8
+ return @parse_result unless @needs_parsing
9
+
10
+ @needs_parsing = false
11
+ @parse_result = Prism.parse(@source)
12
+ end
13
+ end
14
+ end
@@ -12,6 +12,8 @@ require "time"
12
12
  # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
13
13
  # exact locked versions of dependencies.
14
14
 
15
+ Bundler.ui.level = :silent
16
+
15
17
  module RubyLsp
16
18
  class SetupBundler
17
19
  extend T::Sig
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "ruby_lsp/document"
5
-
6
4
  module RubyLsp
7
5
  class Store
8
6
  extend T::Sig
@@ -40,7 +38,7 @@ module RubyLsp
40
38
 
41
39
  sig { params(uri: URI::Generic, source: String, version: Integer).void }
42
40
  def set(uri:, source:, version:)
43
- document = Document.new(source: source, version: version, uri: uri, encoding: @encoding)
41
+ document = RubyDocument.new(source: source, version: version, uri: uri, encoding: @encoding)
44
42
  @state[uri.to_s] = document
45
43
  end
46
44
 
@@ -8,6 +8,15 @@ module RubyLsp
8
8
  # This freeze is not redundant since the interpolated string is mutable
9
9
  WORKSPACE_URI = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
10
10
 
11
+ BUNDLE_PATH = T.let(
12
+ begin
13
+ Bundler.bundle_path.to_s
14
+ rescue Bundler::GemfileNotFound
15
+ nil
16
+ end,
17
+ T.nilable(String),
18
+ )
19
+
11
20
  # A notification to be sent to the client
12
21
  class Message
13
22
  extend T::Sig
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.12.3
4
+ version: 0.12.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-06 00:00:00.000000000 Z
11
+ date: 2023-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -78,12 +78,12 @@ files:
78
78
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
79
79
  - lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb
80
80
  - lib/ruby-lsp.rb
81
+ - lib/ruby_indexer/lib/ruby_indexer/collector.rb
81
82
  - lib/ruby_indexer/lib/ruby_indexer/configuration.rb
82
83
  - lib/ruby_indexer/lib/ruby_indexer/entry.rb
83
84
  - lib/ruby_indexer/lib/ruby_indexer/index.rb
84
85
  - lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb
85
86
  - lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb
86
- - lib/ruby_indexer/lib/ruby_indexer/visitor.rb
87
87
  - lib/ruby_indexer/ruby_indexer.rb
88
88
  - lib/ruby_indexer/test/classes_and_modules_test.rb
89
89
  - lib/ruby_indexer/test/configuration_test.rb
@@ -132,6 +132,7 @@ files:
132
132
  - lib/ruby_lsp/requests/support/source_uri.rb
133
133
  - lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb
134
134
  - lib/ruby_lsp/requests/workspace_symbol.rb
135
+ - lib/ruby_lsp/ruby_document.rb
135
136
  - lib/ruby_lsp/server.rb
136
137
  - lib/ruby_lsp/setup_bundler.rb
137
138
  - lib/ruby_lsp/store.rb