ruby-lsp 0.17.13 → 0.17.15

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +2 -0
  4. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +28 -9
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +5 -3
  6. data/lib/ruby_indexer/test/classes_and_modules_test.rb +21 -0
  7. data/lib/ruby_indexer/test/configuration_test.rb +41 -7
  8. data/lib/ruby_lsp/document.rb +9 -114
  9. data/lib/ruby_lsp/erb_document.rb +16 -2
  10. data/lib/ruby_lsp/global_state.rb +1 -1
  11. data/lib/ruby_lsp/internal.rb +1 -0
  12. data/lib/ruby_lsp/listeners/definition.rb +8 -5
  13. data/lib/ruby_lsp/rbs_document.rb +41 -0
  14. data/lib/ruby_lsp/requests/code_action_resolve.rb +34 -18
  15. data/lib/ruby_lsp/requests/code_actions.rb +1 -1
  16. data/lib/ruby_lsp/requests/completion.rb +2 -2
  17. data/lib/ruby_lsp/requests/definition.rb +1 -1
  18. data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
  19. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  20. data/lib/ruby_lsp/requests/formatting.rb +1 -1
  21. data/lib/ruby_lsp/requests/hover.rb +1 -1
  22. data/lib/ruby_lsp/requests/inlay_hints.rb +1 -1
  23. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  24. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -1
  25. data/lib/ruby_lsp/requests/selection_ranges.rb +2 -1
  26. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  27. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  28. data/lib/ruby_lsp/requests/support/formatter.rb +2 -2
  29. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +1 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  31. data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +2 -2
  32. data/lib/ruby_lsp/ruby_document.rb +119 -1
  33. data/lib/ruby_lsp/server.rb +96 -8
  34. data/lib/ruby_lsp/setup_bundler.rb +18 -7
  35. data/lib/ruby_lsp/store.rb +10 -6
  36. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 042725d7afce428b5c024933a7101151f2f69c57cbb40b4f938384c13d6c974b
4
- data.tar.gz: bec8636d402451e1009e87ddd98e6fdedc062643e614b596a0698ffcfcc9b271
3
+ metadata.gz: 43d715172510bba67801670dbfc574eed9851b9852aaea3f6e210acd74734af4
4
+ data.tar.gz: b35b3a55c3d65987390e4b125a090ff8f6da902bfa7ab9c08eb9eb8c72d5157f
5
5
  SHA512:
6
- metadata.gz: 6a6adb40a9ccaaf916f46c041f3eb2cc7a81be73c5d3bf7c05a45472cd9d08e5a9dd04d804b8e15f091645d420037e324368ccbcc619311a1c60c81f241c1b89
7
- data.tar.gz: d6b19c8d02cfab8dca9e4d675e8ee549e8b392631a6080757c0413da29a07d1db411d9360b7553a798805d9e6faac7318863e35ba664488c5eb41c869a7e734a
6
+ metadata.gz: ef8a2ca24755eef8c5ed1146f1f6c40ec725988e9dd6a51efb326583e5606a221f733625f08d4dad5bb6091675c39fb18a485ff8c0bed1dc0b2e221a4390c585
7
+ data.tar.gz: 16101462585105a9a201f6341908f0b238f77f4acaa6dac723c95e35fcddc7c7b68d937bead5beecc14ae6bcac276b2f813523304f7eaded3132d3aa07eb943b
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.13
1
+ 0.17.15
data/exe/ruby-lsp CHANGED
@@ -81,6 +81,8 @@ require "ruby_lsp/load_sorbet"
81
81
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
82
82
  require "ruby_lsp/internal"
83
83
 
84
+ T::Utils.run_all_sig_blocks
85
+
84
86
  if options[:debug]
85
87
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
86
88
  $stderr.puts "Debugging is not supported on Windows"
@@ -16,15 +16,24 @@ module RubyIndexer
16
16
  T::Hash[String, T.untyped],
17
17
  )
18
18
 
19
+ sig { params(workspace_path: String).void }
20
+ attr_writer :workspace_path
21
+
19
22
  sig { void }
20
23
  def initialize
24
+ @workspace_path = T.let(Dir.pwd, String)
21
25
  @excluded_gems = T.let(initial_excluded_gems, T::Array[String])
22
26
  @included_gems = T.let([], T::Array[String])
23
- @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
27
+ @excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("tmp", "**", "*")], T::Array[String])
28
+
24
29
  path = Bundler.settings["path"]
25
- @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
30
+ if path
31
+ # Substitute Windows backslashes into forward slashes, which are used in glob patterns
32
+ glob = path.gsub(/[\\]+/, "/")
33
+ @excluded_patterns << File.join(glob, "**", "*.rb")
34
+ end
26
35
 
27
- @included_patterns = T.let([File.join(Dir.pwd, "**", "*.rb")], T::Array[String])
36
+ @included_patterns = T.let([File.join("**", "*.rb")], T::Array[String])
28
37
  @excluded_magic_comments = T.let(
29
38
  [
30
39
  "frozen_string_literal:",
@@ -55,12 +64,12 @@ module RubyIndexer
55
64
  indexables = @included_patterns.flat_map do |pattern|
56
65
  load_path_entry = T.let(nil, T.nilable(String))
57
66
 
58
- Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
67
+ Dir.glob(File.join(@workspace_path, pattern), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
59
68
  path = File.expand_path(path)
60
69
  # All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
61
70
  # entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
62
- # on repositories that define multiple gems, like Rails. All frameworks are defined inside the Dir.pwd, but
63
- # each one of them belongs to a different $LOAD_PATH entry
71
+ # on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
72
+ # workspace directory, but each one of them belongs to a different $LOAD_PATH entry
64
73
  if load_path_entry.nil? || !path.start_with?(load_path_entry)
65
74
  load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
66
75
  end
@@ -69,9 +78,19 @@ module RubyIndexer
69
78
  end
70
79
  end
71
80
 
81
+ # If the patterns are relative, we make it relative to the workspace path. If they are absolute, then we shouldn't
82
+ # concatenate anything
83
+ excluded_patterns = @excluded_patterns.map do |pattern|
84
+ if File.absolute_path?(pattern)
85
+ pattern
86
+ else
87
+ File.join(@workspace_path, pattern)
88
+ end
89
+ end
90
+
72
91
  # Remove user specified patterns
73
92
  indexables.reject! do |indexable|
74
- @excluded_patterns.any? do |pattern|
93
+ excluded_patterns.any? do |pattern|
75
94
  File.fnmatch?(pattern, indexable.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
76
95
  end
77
96
  end
@@ -122,7 +141,7 @@ module RubyIndexer
122
141
  # When working on a gem, it will be included in the locked_gems list. Since these are the project's own files,
123
142
  # we have already included and handled exclude patterns for it and should not re-include or it'll lead to
124
143
  # duplicates or accidentally ignoring exclude patterns
125
- next if spec.full_gem_path == Dir.pwd
144
+ next if spec.full_gem_path == @workspace_path
126
145
 
127
146
  indexables.concat(
128
147
  spec.require_paths.flat_map do |require_path|
@@ -185,7 +204,7 @@ module RubyIndexer
185
204
  # If the dependency is prerelease, `to_spec` may return `nil` due to a bug in older version of Bundler/RubyGems:
186
205
  # https://github.com/Shopify/ruby-lsp/issues/1246
187
206
  this_gem = Bundler.definition.dependencies.find do |d|
188
- d.to_spec&.full_gem_path == Dir.pwd
207
+ d.to_spec&.full_gem_path == @workspace_path
189
208
  rescue Gem::MissingSpecError
190
209
  false
191
210
  end
@@ -152,16 +152,17 @@ module RubyIndexer
152
152
 
153
153
  if current_owner
154
154
  expression = node.expression
155
- @stack << (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
155
+ name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
156
+ real_nesting = actual_nesting(name)
156
157
 
157
- existing_entries = T.cast(@index[@stack.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
158
+ existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
158
159
 
159
160
  if existing_entries
160
161
  entry = T.must(existing_entries.first)
161
162
  entry.update_singleton_information(node.location, expression.location, collect_comments(node))
162
163
  else
163
164
  entry = Entry::SingletonClass.new(
164
- @stack,
165
+ real_nesting,
165
166
  @file_path,
166
167
  node.location,
167
168
  expression.location,
@@ -172,6 +173,7 @@ module RubyIndexer
172
173
  end
173
174
 
174
175
  @owner_stack << entry
176
+ @stack << name
175
177
  end
176
178
  end
177
179
 
@@ -564,6 +564,27 @@ module RubyIndexer
564
564
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
565
565
  end
566
566
 
567
+ def test_indexing_singletons_inside_top_level_references
568
+ index(<<~RUBY)
569
+ module ::Foo
570
+ class Bar
571
+ class << self
572
+ end
573
+ end
574
+ end
575
+ RUBY
576
+
577
+ # We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the
578
+ # prefix when we use `refute_entry`
579
+ entries = @index.instance_variable_get(:@entries)
580
+ refute(entries.key?("::Foo"))
581
+ refute(entries.key?("::Foo::Bar"))
582
+ refute(entries.key?("::Foo::Bar::<Class:Bar>"))
583
+ assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:5-3")
584
+ assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:4-5")
585
+ assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:2-4:3-7")
586
+ end
587
+
567
588
  def test_indexing_namespaces_inside_nested_top_level_references
568
589
  index(<<~RUBY)
569
590
  class Baz
@@ -7,10 +7,12 @@ module RubyIndexer
7
7
  class ConfigurationTest < Minitest::Test
8
8
  def setup
9
9
  @config = Configuration.new
10
+ @workspace_path = File.expand_path(File.join("..", "..", ".."), __dir__)
11
+ @config.workspace_path = @workspace_path
10
12
  end
11
13
 
12
14
  def test_load_configuration_executes_configure_block
13
- @config.apply_config({ "excluded_patterns" => ["**/test/fixtures/**/*.rb"] })
15
+ @config.apply_config({ "excluded_patterns" => ["**/fixtures/**/*.rb"] })
14
16
  indexables = @config.indexables
15
17
 
16
18
  assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
@@ -25,7 +27,7 @@ module RubyIndexer
25
27
  indexables = @config.indexables
26
28
 
27
29
  # All paths should be expanded
28
- assert(indexables.none? { |indexable| indexable.full_path.start_with?("lib/") })
30
+ assert(indexables.all? { |indexable| File.absolute_path?(indexable.full_path) })
29
31
  end
30
32
 
31
33
  def test_indexables_only_includes_gem_require_paths
@@ -71,17 +73,20 @@ module RubyIndexer
71
73
  Bundler.settings.temporary(path: "vendor/bundle") do
72
74
  config = Configuration.new
73
75
 
74
- assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
76
+ assert_includes(config.instance_variable_get(:@excluded_patterns), "vendor/bundle/**/*.rb")
75
77
  end
76
78
  end
77
79
 
78
80
  def test_indexables_does_not_include_gems_own_installed_files
79
81
  indexables = @config.indexables
82
+ indexables_inside_bundled_lsp = indexables.select do |indexable|
83
+ indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
84
+ end
80
85
 
81
- assert(
82
- indexables.none? do |indexable|
83
- indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
84
- end,
86
+ assert_empty(
87
+ indexables_inside_bundled_lsp,
88
+ "Indexables should not include files from the gem currently being worked on. " \
89
+ "Included: #{indexables_inside_bundled_lsp.map(&:full_path)}",
85
90
  )
86
91
  end
87
92
 
@@ -126,5 +131,34 @@ module RubyIndexer
126
131
  assert_match(regex, comment)
127
132
  end
128
133
  end
134
+
135
+ def test_indexables_respect_given_workspace_path
136
+ Dir.mktmpdir do |dir|
137
+ FileUtils.mkdir(File.join(dir, "ignore"))
138
+ FileUtils.touch(File.join(dir, "ignore", "file0.rb"))
139
+ FileUtils.touch(File.join(dir, "file1.rb"))
140
+ FileUtils.touch(File.join(dir, "file2.rb"))
141
+
142
+ @config.apply_config({ "excluded_patterns" => ["ignore/**/*.rb"] })
143
+ @config.workspace_path = dir
144
+ indexables = @config.indexables
145
+
146
+ assert(indexables.none? { |indexable| indexable.full_path.start_with?(File.join(dir, "ignore")) })
147
+
148
+ # After switching the workspace path, all indexables will be found in one of these places:
149
+ # - The new workspace path
150
+ # - The Ruby LSP's own code (because Bundler is requiring the dependency from source)
151
+ # - Bundled gems
152
+ # - Default gems
153
+ assert(
154
+ indexables.all? do |i|
155
+ i.full_path.start_with?(dir) ||
156
+ i.full_path.start_with?(File.join(Dir.pwd, "lib")) ||
157
+ i.full_path.start_with?(Bundler.bundle_path.to_s) ||
158
+ i.full_path.start_with?(RbConfig::CONFIG["rubylibdir"])
159
+ end,
160
+ )
161
+ end
162
+ end
129
163
  end
130
164
  end
@@ -7,15 +7,19 @@ module RubyLsp
7
7
  enums do
8
8
  Ruby = new("ruby")
9
9
  ERB = new("erb")
10
+ RBS = new("rbs")
10
11
  end
11
12
  end
12
13
 
13
14
  extend T::Sig
14
15
  extend T::Helpers
16
+ extend T::Generic
17
+
18
+ ParseResultType = type_member
15
19
 
16
20
  abstract!
17
21
 
18
- sig { returns(Prism::ParseResult) }
22
+ sig { returns(ParseResultType) }
19
23
  attr_reader :parse_result
20
24
 
21
25
  sig { returns(String) }
@@ -38,10 +42,10 @@ module RubyLsp
38
42
  @version = T.let(version, Integer)
39
43
  @uri = T.let(uri, URI::Generic)
40
44
  @needs_parsing = T.let(true, T::Boolean)
41
- @parse_result = T.let(parse, Prism::ParseResult)
45
+ @parse_result = T.let(parse, ParseResultType)
42
46
  end
43
47
 
44
- sig { params(other: Document).returns(T::Boolean) }
48
+ sig { params(other: Document[T.untyped]).returns(T::Boolean) }
45
49
  def ==(other)
46
50
  self.class == other.class && uri == other.uri && @source == other.source
47
51
  end
@@ -54,7 +58,7 @@ module RubyLsp
54
58
  type_parameters(:T)
55
59
  .params(
56
60
  request_name: String,
57
- block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
61
+ block: T.proc.params(document: Document[ParseResultType]).returns(T.type_parameter(:T)),
58
62
  ).returns(T.type_parameter(:T))
59
63
  end
60
64
  def cache_fetch(request_name, &block)
@@ -93,7 +97,7 @@ module RubyLsp
93
97
  @cache.clear
94
98
  end
95
99
 
96
- sig { abstract.returns(Prism::ParseResult) }
100
+ sig { abstract.returns(ParseResultType) }
97
101
  def parse; end
98
102
 
99
103
  sig { abstract.returns(T::Boolean) }
@@ -104,115 +108,6 @@ module RubyLsp
104
108
  Scanner.new(@source, @encoding)
105
109
  end
106
110
 
107
- sig do
108
- params(
109
- position: T::Hash[Symbol, T.untyped],
110
- node_types: T::Array[T.class_of(Prism::Node)],
111
- ).returns(NodeContext)
112
- end
113
- def locate_node(position, node_types: [])
114
- locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
115
- end
116
-
117
- sig do
118
- params(
119
- node: Prism::Node,
120
- char_position: Integer,
121
- node_types: T::Array[T.class_of(Prism::Node)],
122
- ).returns(NodeContext)
123
- end
124
- def locate(node, char_position, node_types: [])
125
- queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
126
- closest = node
127
- parent = T.let(nil, T.nilable(Prism::Node))
128
- nesting_nodes = T.let(
129
- [],
130
- T::Array[T.any(
131
- Prism::ClassNode,
132
- Prism::ModuleNode,
133
- Prism::SingletonClassNode,
134
- Prism::DefNode,
135
- Prism::BlockNode,
136
- Prism::LambdaNode,
137
- Prism::ProgramNode,
138
- )],
139
- )
140
-
141
- nesting_nodes << node if node.is_a?(Prism::ProgramNode)
142
- call_node = T.let(nil, T.nilable(Prism::CallNode))
143
-
144
- until queue.empty?
145
- candidate = queue.shift
146
-
147
- # Skip nil child nodes
148
- next if candidate.nil?
149
-
150
- # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
151
- # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
152
- # sibling
153
- T.unsafe(queue).unshift(*candidate.child_nodes)
154
-
155
- # Skip if the current node doesn't cover the desired position
156
- loc = candidate.location
157
- next unless (loc.start_offset...loc.end_offset).cover?(char_position)
158
-
159
- # If the node's start character is already past the position, then we should've found the closest node
160
- # already
161
- break if char_position < loc.start_offset
162
-
163
- # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
164
- # need to pop the stack
165
- previous_level = nesting_nodes.last
166
- nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
167
-
168
- # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
169
- # target when it is a constant
170
- case candidate
171
- when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
172
- Prism::LambdaNode
173
- nesting_nodes << candidate
174
- end
175
-
176
- if candidate.is_a?(Prism::CallNode)
177
- arg_loc = candidate.arguments&.location
178
- blk_loc = candidate.block&.location
179
- if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
180
- (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
181
- call_node = candidate
182
- end
183
- end
184
-
185
- # If there are node types to filter by, and the current node is not one of those types, then skip it
186
- next if node_types.any? && node_types.none? { |type| candidate.class == type }
187
-
188
- # If the current node is narrower than or equal to the previous closest node, then it is more precise
189
- closest_loc = closest.location
190
- if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
191
- parent = closest
192
- closest = candidate
193
- end
194
- end
195
-
196
- # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
197
- # is, when targeting Bar in the following example:
198
- #
199
- # ```ruby
200
- # class Foo::Bar; end
201
- # ```
202
- # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
203
- # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
204
- if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
205
- last_level = nesting_nodes.last
206
-
207
- if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
208
- last_level.constant_path == closest
209
- nesting_nodes.pop
210
- end
211
- end
212
-
213
- NodeContext.new(closest, parent, nesting_nodes, call_node)
214
- end
215
-
216
111
  class Scanner
217
112
  extend T::Sig
218
113
 
@@ -4,15 +4,19 @@
4
4
  module RubyLsp
5
5
  class ERBDocument < Document
6
6
  extend T::Sig
7
+ extend T::Generic
7
8
 
8
- sig { override.returns(Prism::ParseResult) }
9
+ ParseResultType = type_member { { fixed: Prism::ParseResult } }
10
+
11
+ sig { override.returns(ParseResultType) }
9
12
  def parse
10
13
  return @parse_result unless @needs_parsing
11
14
 
12
15
  @needs_parsing = false
13
16
  scanner = ERBScanner.new(@source)
14
17
  scanner.scan
15
- @parse_result = Prism.parse(scanner.ruby)
18
+ # assigning empty scopes to turn Prism into eval mode
19
+ @parse_result = Prism.parse(scanner.ruby, scopes: [[]])
16
20
  end
17
21
 
18
22
  sig { override.returns(T::Boolean) }
@@ -25,6 +29,16 @@ module RubyLsp
25
29
  LanguageId::ERB
26
30
  end
27
31
 
32
+ sig do
33
+ params(
34
+ position: T::Hash[Symbol, T.untyped],
35
+ node_types: T::Array[T.class_of(Prism::Node)],
36
+ ).returns(NodeContext)
37
+ end
38
+ def locate_node(position, node_types: [])
39
+ RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
40
+ end
41
+
28
42
  class ERBScanner
29
43
  extend T::Sig
30
44
 
@@ -93,7 +93,7 @@ module RubyLsp
93
93
  @test_library = detect_test_library(direct_dependencies)
94
94
  notifications << Notification.window_log_message("Detected test library: #{@test_library}")
95
95
 
96
- @has_type_checker = detect_typechecker(direct_dependencies)
96
+ @has_type_checker = detect_typechecker(all_dependencies)
97
97
  if @has_type_checker
98
98
  notifications << Notification.window_log_message(
99
99
  "Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
@@ -36,6 +36,7 @@ require "ruby_lsp/node_context"
36
36
  require "ruby_lsp/document"
37
37
  require "ruby_lsp/ruby_document"
38
38
  require "ruby_lsp/erb_document"
39
+ require "ruby_lsp/rbs_document"
39
40
  require "ruby_lsp/store"
40
41
  require "ruby_lsp/addon"
41
42
  require "ruby_lsp/requests/support/rubocop_runner"
@@ -208,10 +208,13 @@ module RubyLsp
208
208
  def handle_method_definition(message, receiver_type, inherited_only: false)
209
209
  methods = if receiver_type
210
210
  @index.resolve_method(message, receiver_type.name, inherited_only: inherited_only)
211
- else
212
- # If the method doesn't have a receiver, then we provide a few candidates to jump to
213
- # But we don't want to provide too many candidates, as it can be overwhelming
214
- @index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
211
+ end
212
+
213
+ # If the method doesn't have a receiver, or the guessed receiver doesn't have any matched candidates,
214
+ # then we provide a few candidates to jump to
215
+ # But we don't want to provide too many candidates, as it can be overwhelming
216
+ if receiver_type.nil? || (receiver_type.is_a?(TypeInferrer::GuessedType) && methods.nil?)
217
+ methods = @index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
215
218
  end
216
219
 
217
220
  return unless methods
@@ -250,7 +253,7 @@ module RubyLsp
250
253
  when :require_relative
251
254
  required_file = "#{node.content}.rb"
252
255
  path = @uri.to_standardized_path
253
- current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
256
+ current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : @global_state.workspace_path
254
257
  candidate = File.expand_path(File.join(current_folder, required_file))
255
258
 
256
259
  @response_builder << Interface::Location.new(
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class RBSDocument < Document
6
+ extend T::Sig
7
+ extend T::Generic
8
+
9
+ ParseResultType = type_member { { fixed: T::Array[RBS::AST::Declarations::Base] } }
10
+
11
+ sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
12
+ def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
13
+ @syntax_error = T.let(false, T::Boolean)
14
+ super
15
+ end
16
+
17
+ sig { override.returns(ParseResultType) }
18
+ def parse
19
+ return @parse_result unless @needs_parsing
20
+
21
+ @needs_parsing = false
22
+
23
+ _, _, declarations = RBS::Parser.parse_signature(@source)
24
+ @syntax_error = false
25
+ @parse_result = declarations
26
+ rescue RBS::ParsingError
27
+ @syntax_error = true
28
+ @parse_result
29
+ end
30
+
31
+ sig { override.returns(T::Boolean) }
32
+ def syntax_error?
33
+ @syntax_error
34
+ end
35
+
36
+ sig { override.returns(LanguageId) }
37
+ def language_id
38
+ LanguageId::RBS
39
+ end
40
+ end
41
+ end
@@ -112,7 +112,7 @@ module RubyLsp
112
112
  extracted_source = T.must(@document.source[start_index...end_index])
113
113
 
114
114
  # Find the closest statements node, so that we place the refactor in a valid position
115
- node_context = @document
115
+ node_context = RubyDocument
116
116
  .locate(@document.parse_result.value, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
117
117
 
118
118
  closest_statements = node_context.node
@@ -206,28 +206,44 @@ module RubyLsp
206
206
  extracted_source = T.must(@document.source[start_index...end_index])
207
207
 
208
208
  # Find the closest method declaration node, so that we place the refactor in a valid position
209
- node_context = @document.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
210
- closest_def = T.cast(node_context.node, Prism::DefNode)
211
- return Error::InvalidTargetRange if closest_def.nil?
209
+ node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
210
+ closest_node = node_context.node
211
+ return Error::InvalidTargetRange unless closest_node
212
212
 
213
- end_keyword_loc = closest_def.end_keyword_loc
214
- return Error::InvalidTargetRange if end_keyword_loc.nil?
213
+ target_range = if closest_node.is_a?(Prism::DefNode)
214
+ end_keyword_loc = closest_node.end_keyword_loc
215
+ return Error::InvalidTargetRange unless end_keyword_loc
215
216
 
216
- end_line = end_keyword_loc.end_line - 1
217
- character = end_keyword_loc.end_column
218
- indentation = " " * end_keyword_loc.start_column
219
- target_range = {
220
- start: { line: end_line, character: character },
221
- end: { line: end_line, character: character },
222
- }
217
+ end_line = end_keyword_loc.end_line - 1
218
+ character = end_keyword_loc.end_column
219
+ indentation = " " * end_keyword_loc.start_column
223
220
 
224
- new_method_source = <<~RUBY.chomp
221
+ new_method_source = <<~RUBY.chomp
225
222
 
226
223
 
227
- #{indentation}def #{NEW_METHOD_NAME}
228
- #{indentation} #{extracted_source}
229
- #{indentation}end
230
- RUBY
224
+ #{indentation}def #{NEW_METHOD_NAME}
225
+ #{indentation} #{extracted_source}
226
+ #{indentation}end
227
+ RUBY
228
+
229
+ {
230
+ start: { line: end_line, character: character },
231
+ end: { line: end_line, character: character },
232
+ }
233
+ else
234
+ new_method_source = <<~RUBY
235
+ #{indentation}def #{NEW_METHOD_NAME}
236
+ #{indentation} #{extracted_source.gsub("\n", "\n ")}
237
+ #{indentation}end
238
+
239
+ RUBY
240
+
241
+ line = [0, source_range.dig(:start, :line) - 1].max
242
+ {
243
+ start: { line: line, character: source_range.dig(:start, :character) },
244
+ end: { line: line, character: source_range.dig(:start, :character) },
245
+ }
246
+ end
231
247
 
232
248
  Interface::CodeAction.new(
233
249
  title: CodeActions::EXTRACT_TO_METHOD_TITLE,
@@ -37,7 +37,7 @@ module RubyLsp
37
37
 
38
38
  sig do
39
39
  params(
40
- document: Document,
40
+ document: T.any(RubyDocument, ERBDocument),
41
41
  range: T::Hash[Symbol, T.untyped],
42
42
  context: T::Hash[Symbol, T.untyped],
43
43
  ).void
@@ -46,7 +46,7 @@ module RubyLsp
46
46
 
47
47
  sig do
48
48
  params(
49
- document: Document,
49
+ document: T.any(RubyDocument, ERBDocument),
50
50
  global_state: GlobalState,
51
51
  params: T::Hash[Symbol, T.untyped],
52
52
  sorbet_level: RubyDocument::SorbetLevel,
@@ -60,7 +60,7 @@ module RubyLsp
60
60
  # Completion always receives the position immediately after the character that was just typed. Here we adjust it
61
61
  # back by 1, so that we find the right node
62
62
  char_position = document.create_scanner.find_char_position(params[:position]) - 1
63
- node_context = document.locate(
63
+ node_context = RubyDocument.locate(
64
64
  document.parse_result.value,
65
65
  char_position,
66
66
  node_types: [
@@ -38,7 +38,7 @@ module RubyLsp
38
38
 
39
39
  sig do
40
40
  params(
41
- document: Document,
41
+ document: T.any(RubyDocument, ERBDocument),
42
42
  global_state: GlobalState,
43
43
  position: T::Hash[Symbol, T.untyped],
44
44
  dispatcher: Prism::Dispatcher,
@@ -32,7 +32,7 @@ module RubyLsp
32
32
  end
33
33
  end
34
34
 
35
- sig { params(global_state: GlobalState, document: Document).void }
35
+ sig { params(global_state: GlobalState, document: RubyDocument).void }
36
36
  def initialize(global_state, document)
37
37
  super()
38
38
  @active_linters = T.let(global_state.active_linters, T::Array[Support::Formatter])