ruby-lsp 0.14.4 → 0.14.6

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: 4fc74feb692f2c2e902e213dc61a4b3d58f14bcc4520fc7db60bdae022484e6f
4
- data.tar.gz: 98f6d5b939bb945119169e2ca3ce060f275b961d421e752774ebea1c25b74c2d
3
+ metadata.gz: 7c364aee0d80e6187f66e8eb6d50d7cdd21ffb129e0a0b17c9dc7bd5d8a18cad
4
+ data.tar.gz: 65964ec1d89f10adf9bcbf113efed6d43a977b669abe51182e90dd9870032623
5
5
  SHA512:
6
- metadata.gz: 8c33776eb483463d1052cdd5a82d1f66d5d507325b09848d0c5d78cea310c218b056bef834f6f6f03e17d87088b65c8cd77f7c214735390e8b5d244a059ea9e7
7
- data.tar.gz: 7e57548a8dede5695774c27137b6ab23c8029f8ff8c0551bef8b1cf8f6dd920e9bdb423d4905c6a7f9ac360433da99cdec31efc885a120f4c1684acbd3da9582
6
+ metadata.gz: 971af8a3c02903597143a8ec49915eead6bc0f65a0175e4a443e381d671dd4a88f776a26f62af1bd69ee7dc3f478b36bef122febbfaed7656907eb1a6a7a3354
7
+ data.tar.gz: 15148c1c3765390f9bb92dbafe7043ac15b22de6b5c415c424a063f4742026d92831d84bcde2013ab742c4a889dc16d666d4ec5d18863d0db8acf793ce87f0a6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.4
1
+ 0.14.6
@@ -152,6 +152,8 @@ module RubyIndexer
152
152
  handle_attribute(node, reader: false, writer: true)
153
153
  when :attr_accessor
154
154
  handle_attribute(node, reader: true, writer: true)
155
+ when :include
156
+ handle_include(node)
155
157
  end
156
158
  end
157
159
 
@@ -350,5 +352,24 @@ module RubyIndexer
350
352
  @index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer
351
353
  end
352
354
  end
355
+
356
+ sig { params(node: Prism::CallNode).void }
357
+ def handle_include(node)
358
+ return unless @current_owner
359
+
360
+ arguments = node.arguments&.arguments
361
+ return unless arguments
362
+
363
+ names = arguments.filter_map do |node|
364
+ if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
365
+ node.full_name
366
+ end
367
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
368
+ # TO DO: add MissingNodesInConstantPathError when released in Prism
369
+ # If a constant path reference is dynamic or missing parts, we can't
370
+ # index it
371
+ end
372
+ @current_owner.included_modules.concat(names)
373
+ end
353
374
  end
354
375
  end
@@ -20,7 +20,7 @@ module RubyIndexer
20
20
  def initialize
21
21
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
22
22
  @included_gems = T.let([], T::Array[String])
23
- @excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
23
+ @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
24
24
  path = Bundler.settings["path"]
25
25
  @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
26
26
 
@@ -40,6 +40,22 @@ module RubyIndexer
40
40
 
41
41
  abstract!
42
42
 
43
+ sig { returns(T::Array[String]) }
44
+ attr_accessor :included_modules
45
+
46
+ sig do
47
+ params(
48
+ name: String,
49
+ file_path: String,
50
+ location: Prism::Location,
51
+ comments: T::Array[String],
52
+ ).void
53
+ end
54
+ def initialize(name, file_path, location, comments)
55
+ super(name, file_path, location, comments)
56
+ @included_modules = T.let([], T::Array[String])
57
+ end
58
+
43
59
  sig { returns(String) }
44
60
  def short_name
45
61
  T.must(@name.split("::").last)
@@ -191,8 +191,9 @@ module RubyIndexer
191
191
 
192
192
  require_path = indexable_path.require_path
193
193
  @require_paths_tree.insert(require_path, indexable_path) if require_path
194
- rescue Errno::EISDIR
195
- # If `path` is a directory, just ignore it and continue indexing
194
+ rescue Errno::EISDIR, Errno::ENOENT
195
+ # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
196
+ # it
196
197
  end
197
198
 
198
199
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
@@ -281,5 +281,51 @@ module RubyIndexer
281
281
  final_thing = T.must(@index["FinalThing"].first)
282
282
  assert_equal("Something::Baz", final_thing.parent_class)
283
283
  end
284
+
285
+ def test_keeping_track_of_included_modules
286
+ index(<<~RUBY)
287
+ class Foo
288
+ # valid syntaxes that we can index
289
+ include A1
290
+ self.include A2
291
+ include A3, A4
292
+ self.include A5, A6
293
+
294
+ # valid syntaxes that we cannot index because of their dynamic nature
295
+ include some_variable_or_method_call
296
+ self.include some_variable_or_method_call
297
+
298
+ def something
299
+ include A7 # We should not index this because of this dynamic nature
300
+ end
301
+
302
+ # Valid inner class syntax definition with its own modules included
303
+ class Qux
304
+ include Corge
305
+ self.include Corge
306
+ include Baz
307
+
308
+ include some_variable_or_method_call
309
+ end
310
+ end
311
+
312
+ class ConstantPathReferences
313
+ include Foo::Bar
314
+ self.include Foo::Bar2
315
+
316
+ include dynamic::Bar
317
+ include Foo::
318
+ end
319
+ RUBY
320
+
321
+ foo = T.must(@index["Foo"][0])
322
+ assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
323
+
324
+ qux = T.must(@index["Foo::Qux"][0])
325
+ assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
326
+
327
+ constant_path_references = T.must(@index["ConstantPathReferences"][0])
328
+ assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
329
+ end
284
330
  end
285
331
  end
@@ -308,5 +308,10 @@ module RubyIndexer
308
308
 
309
309
  refute_empty(@index.instance_variable_get(:@entries))
310
310
  end
311
+
312
+ def test_index_single_does_not_fail_for_non_existing_file
313
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"))
314
+ assert_empty(@index.instance_variable_get(:@entries))
315
+ end
311
316
  end
312
317
  end
@@ -27,6 +27,8 @@ module RubyLsp
27
27
 
28
28
  @addons = T.let([], T::Array[Addon])
29
29
  @addon_classes = T.let([], T::Array[T.class_of(Addon)])
30
+ # Addon instances that have declared a handler to accept file watcher events
31
+ @file_watcher_addons = T.let([], T::Array[Addon])
30
32
 
31
33
  class << self
32
34
  extend T::Sig
@@ -34,6 +36,9 @@ module RubyLsp
34
36
  sig { returns(T::Array[Addon]) }
35
37
  attr_accessor :addons
36
38
 
39
+ sig { returns(T::Array[Addon]) }
40
+ attr_accessor :file_watcher_addons
41
+
37
42
  sig { returns(T::Array[T.class_of(Addon)]) }
38
43
  attr_reader :addon_classes
39
44
 
@@ -57,6 +62,7 @@ module RubyLsp
57
62
 
58
63
  # Instantiate all discovered addon classes
59
64
  self.addons = addon_classes.map(&:new)
65
+ self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) }
60
66
 
61
67
  # Activate each one of the discovered addons. If any problems occur in the addons, we don't want to
62
68
  # fail to boot the server
@@ -229,20 +229,23 @@ module RubyLsp
229
229
  end
230
230
  end
231
231
 
232
+ Addon.file_watcher_addons.each { |addon| T.unsafe(addon).workspace_did_change_watched_files(changes) }
232
233
  VOID
233
234
  end
234
235
 
235
236
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
236
237
  def workspace_dependencies
237
- definition = Bundler.definition
238
- dep_keys = definition.locked_deps.keys.to_set
239
- definition.specs.map do |spec|
240
- {
241
- name: spec.name,
242
- version: spec.version,
243
- path: spec.full_gem_path,
244
- dependency: dep_keys.include?(spec.name),
245
- }
238
+ Bundler.with_original_env do
239
+ definition = Bundler.definition
240
+ dep_keys = definition.locked_deps.keys.to_set
241
+ definition.specs.map do |spec|
242
+ {
243
+ name: spec.name,
244
+ version: spec.version,
245
+ path: spec.full_gem_path,
246
+ dependency: dep_keys.include?(spec.name),
247
+ }
248
+ end
246
249
  end
247
250
  rescue Bundler::GemfileNotFound
248
251
  []
@@ -22,6 +22,7 @@ module RubyLsp
22
22
  SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
23
23
  DESCRIBE_KEYWORD = T.let(:describe, Symbol)
24
24
  IT_KEYWORD = T.let(:it, Symbol)
25
+ DYNAMIC_REFERENCE_MARKER = T.let("<dynamic_reference>", String)
25
26
 
26
27
  sig do
27
28
  params(
@@ -44,6 +45,8 @@ module RubyLsp
44
45
  self,
45
46
  :on_class_node_enter,
46
47
  :on_class_node_leave,
48
+ :on_module_node_enter,
49
+ :on_module_node_leave,
47
50
  :on_def_node_enter,
48
51
  :on_call_node_enter,
49
52
  :on_call_node_leave,
@@ -60,7 +63,7 @@ module RubyLsp
60
63
  add_test_code_lens(
61
64
  node,
62
65
  name: class_name,
63
- command: generate_test_command(group_name: class_name),
66
+ command: generate_test_command(group_stack: @group_stack),
64
67
  kind: :group,
65
68
  )
66
69
  end
@@ -88,13 +91,27 @@ module RubyLsp
88
91
  add_test_code_lens(
89
92
  node,
90
93
  name: method_name,
91
- command: generate_test_command(method_name: method_name, group_name: class_name),
94
+ command: generate_test_command(method_name: method_name, group_stack: @group_stack),
92
95
  kind: :example,
93
96
  )
94
97
  end
95
98
  end
96
99
  end
97
100
 
101
+ sig { params(node: Prism::ModuleNode).void }
102
+ def on_module_node_enter(node)
103
+ if (path = namespace_constant_name(node))
104
+ @group_stack.push(path)
105
+ else
106
+ @group_stack.push(DYNAMIC_REFERENCE_MARKER)
107
+ end
108
+ end
109
+
110
+ sig { params(node: Prism::ModuleNode).void }
111
+ def on_module_node_leave(node)
112
+ @group_stack.pop
113
+ end
114
+
98
115
  sig { params(node: Prism::CallNode).void }
99
116
  def on_call_node_enter(node)
100
117
  name = node.name
@@ -181,18 +198,45 @@ module RubyLsp
181
198
  )
182
199
  end
183
200
 
184
- sig { params(group_name: String, method_name: T.nilable(String)).returns(String) }
185
- def generate_test_command(group_name:, method_name: nil)
201
+ sig do
202
+ params(
203
+ group_stack: T::Array[String],
204
+ spec_name: T.nilable(String),
205
+ method_name: T.nilable(String),
206
+ ).returns(String)
207
+ end
208
+ def generate_test_command(group_stack: [], spec_name: nil, method_name: nil)
186
209
  command = BASE_COMMAND + T.must(@path)
187
210
 
188
211
  case DependencyDetector.instance.detected_test_library
189
212
  when "minitest"
190
- command += if method_name
191
- " --name " + "/#{Shellwords.escape(group_name + "#" + method_name)}/"
213
+ last_dynamic_reference_index = group_stack.rindex(DYNAMIC_REFERENCE_MARKER)
214
+ command += if last_dynamic_reference_index
215
+ # In cases where the test path looks like `foo::Bar`
216
+ # the best we can do is match everything to the right of it.
217
+ # Tests are classes, dynamic references are only a thing for modules,
218
+ # so there must be something to the left of the available path.
219
+ group_stack = T.must(group_stack[last_dynamic_reference_index + 1..])
220
+ if method_name
221
+ " --name " + "/::#{Shellwords.escape(group_stack.join("::") + "#" + method_name)}$/"
222
+ else
223
+ # When clicking on a CodeLens for `Test`, `(#|::)` will match all tests
224
+ # that are registered on the class itself (matches after `#`) and all tests
225
+ # that are nested inside of that class in other modules/classes (matches after `::`)
226
+ " --name " + "\"/::#{Shellwords.escape(group_stack.join("::"))}(#|::)/\""
227
+ end
228
+ elsif method_name
229
+ # We know the entire path, do an exact match
230
+ " --name " + Shellwords.escape(group_stack.join("::") + "#" + method_name)
231
+ elsif spec_name
232
+ " --name " + "/#{Shellwords.escape(spec_name)}/"
192
233
  else
193
- " --name " + "/#{Shellwords.escape(group_name)}/"
234
+ # Execute all tests of the selected class and tests in
235
+ # modules/classes nested inside of that class
236
+ " --name " + "\"/^#{Shellwords.escape(group_stack.join("::"))}(#|::)/\""
194
237
  end
195
238
  when "test-unit"
239
+ group_name = T.must(group_stack.last)
196
240
  command += " --testcase " + "/#{Shellwords.escape(group_name)}/"
197
241
 
198
242
  if method_name
@@ -214,8 +258,8 @@ module RubyLsp
214
258
  name = case first_argument
215
259
  when Prism::StringNode
216
260
  first_argument.content
217
- when Prism::ConstantReadNode
218
- first_argument.full_name
261
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
262
+ constant_name(first_argument)
219
263
  end
220
264
 
221
265
  return unless name
@@ -223,7 +267,7 @@ module RubyLsp
223
267
  add_test_code_lens(
224
268
  node,
225
269
  name: name,
226
- command: generate_test_command(group_name: name),
270
+ command: generate_test_command(spec_name: name),
227
271
  kind: kind,
228
272
  )
229
273
  end
@@ -100,8 +100,6 @@ module RubyLsp
100
100
 
101
101
  sig { params(node: Prism::CallNode).void }
102
102
  def on_call_node_enter(node)
103
- return if @typechecker_enabled
104
-
105
103
  name = node.message
106
104
  return unless name
107
105
 
@@ -111,7 +109,7 @@ module RubyLsp
111
109
  when "require_relative"
112
110
  complete_require_relative(node)
113
111
  else
114
- complete_self_receiver_method(node, name) if self_receiver?(node)
112
+ complete_self_receiver_method(node, name) if !@typechecker_enabled && self_receiver?(node)
115
113
  end
116
114
  end
117
115
 
@@ -142,14 +140,17 @@ module RubyLsp
142
140
 
143
141
  origin_dir = Pathname.new(@uri.to_standardized_path).dirname
144
142
 
145
- path_query = path_node_to_complete.content
143
+ content = path_node_to_complete.content
146
144
  # if the path is not a directory, glob all possible next characters
147
145
  # for example ../somethi| (where | is the cursor position)
148
146
  # should find files for ../somethi*/
149
- path_query += "*/" unless path_query.end_with?("/")
150
- path_query += "**/*.rb"
147
+ path_query = if content.end_with?("/") || content.empty?
148
+ "#{content}**/*.rb"
149
+ else
150
+ "{#{content}*/**/*.rb,**/#{content}*.rb}"
151
+ end
151
152
 
152
- Dir.glob(path_query, base: origin_dir).sort!.each do |path|
153
+ Dir.glob(path_query, File::FNM_PATHNAME | File::FNM_EXTGLOB, base: origin_dir).sort!.each do |path|
153
154
  @response_builder << build_completion(
154
155
  path.delete_suffix(".rb"),
155
156
  path_node_to_complete,
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  def provider
35
35
  Interface::CompletionOptions.new(
36
36
  resolve_provider: false,
37
- trigger_characters: ["/"],
37
+ trigger_characters: ["/", "\"", "'"],
38
38
  completion_item: {
39
39
  labelDetailsSupport: true,
40
40
  },
@@ -49,7 +49,13 @@ module RubyLsp
49
49
  node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
50
50
  )
51
51
 
52
- target = parent if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
52
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
53
+ target = determine_target(
54
+ target,
55
+ parent,
56
+ position,
57
+ )
58
+ end
53
59
 
54
60
  Listeners::Definition.new(@response_builder, document.uri, nesting, index, dispatcher, typechecker_enabled)
55
61
 
@@ -50,7 +50,11 @@ module RubyLsp
50
50
  if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
51
51
  !Listeners::Hover::ALLOWED_TARGETS.include?(@target.class)) ||
52
52
  (parent.is_a?(Prism::ConstantPathNode) && @target.is_a?(Prism::ConstantReadNode))
53
- @target = parent
53
+ @target = determine_target(
54
+ T.must(@target),
55
+ T.must(parent),
56
+ position,
57
+ )
54
58
  end
55
59
 
56
60
  # Don't need to instantiate any listeners if there's no target
@@ -12,6 +12,57 @@ module RubyLsp
12
12
 
13
13
  sig { abstract.returns(T.anything) }
14
14
  def perform; end
15
+
16
+ private
17
+
18
+ # Checks if a location covers a position
19
+ sig { params(location: Prism::Location, position: T.untyped).returns(T::Boolean) }
20
+ def cover?(location, position)
21
+ start_covered =
22
+ location.start_line - 1 < position[:line] ||
23
+ (
24
+ location.start_line - 1 == position[:line] &&
25
+ location.start_column <= position[:character]
26
+ )
27
+ end_covered =
28
+ location.end_line - 1 > position[:line] ||
29
+ (
30
+ location.end_line - 1 == position[:line] &&
31
+ location.end_column >= position[:character]
32
+ )
33
+ start_covered && end_covered
34
+ end
35
+
36
+ # Based on a constant node target, a constant path node parent and a position, this method will find the exact
37
+ # portion of the constant path that matches the requested position, for higher precision in hover and
38
+ # definition. For example:
39
+ #
40
+ # ```ruby
41
+ # Foo::Bar::Baz
42
+ # # ^ Going to definition here should go to Foo::Bar::Baz
43
+ # # ^ Going to definition here should go to Foo::Bar
44
+ # #^ Going to definition here should go to Foo
45
+ # ```
46
+ sig do
47
+ params(
48
+ target: Prism::Node,
49
+ parent: Prism::Node,
50
+ position: T::Hash[Symbol, Integer],
51
+ ).returns(Prism::Node)
52
+ end
53
+ def determine_target(target, parent, position)
54
+ return target unless parent.is_a?(Prism::ConstantPathNode)
55
+
56
+ target = T.let(parent, Prism::Node)
57
+ parent = T.let(T.cast(target, Prism::ConstantPathNode).parent, T.nilable(Prism::Node))
58
+
59
+ while parent && cover?(parent.location, position)
60
+ target = parent
61
+ parent = target.is_a?(Prism::ConstantPathNode) ? target.parent : nil
62
+ end
63
+
64
+ target
65
+ end
15
66
  end
16
67
  end
17
68
  end
@@ -147,6 +147,15 @@ module RubyLsp
147
147
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
148
148
  nil
149
149
  end
150
+
151
+ sig { params(node: T.any(Prism::ModuleNode, Prism::ClassNode)).returns(T.nilable(String)) }
152
+ def namespace_constant_name(node)
153
+ path = node.constant_path
154
+ case path
155
+ when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode
156
+ constant_name(path)
157
+ end
158
+ end
150
159
  end
151
160
  end
152
161
  end
@@ -40,6 +40,9 @@ module RubyLsp
40
40
 
41
41
  sig { returns(Interface::Diagnostic) }
42
42
  def to_lsp_diagnostic
43
+ # highlighted_area contains the begin and end position of the first line
44
+ # This ensures that multiline offenses don't clutter the editor
45
+ highlighted = @offense.highlighted_area
43
46
  Interface::Diagnostic.new(
44
47
  message: message,
45
48
  source: "RuboCop",
@@ -49,11 +52,11 @@ module RubyLsp
49
52
  range: Interface::Range.new(
50
53
  start: Interface::Position.new(
51
54
  line: @offense.line - 1,
52
- character: @offense.column,
55
+ character: highlighted.begin_pos,
53
56
  ),
54
57
  end: Interface::Position.new(
55
- line: @offense.last_line - 1,
56
- character: @offense.last_column,
58
+ line: @offense.line - 1,
59
+ character: highlighted.end_pos,
57
60
  ),
58
61
  ),
59
62
  data: {
@@ -13,6 +13,10 @@ rescue LoadError
13
13
  raise StandardError, "Incompatible RuboCop version. Ruby LSP requires >= 1.4.0"
14
14
  end
15
15
 
16
+ if RuboCop.const_defined?(:LSP) # This condition will be removed when requiring RuboCop >= 1.61.
17
+ RuboCop::LSP.enable
18
+ end
19
+
16
20
  module RubyLsp
17
21
  module Requests
18
22
  module Support
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.14.4
4
+ version: 0.14.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-04 00:00:00.000000000 Z
11
+ date: 2024-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol