ruby-lsp 0.17.13 → 0.17.15

Sign up to get free protection for your applications and to get access to all the features.
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])