ruby-lsp 0.17.4 → 0.17.13

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +26 -1
  5. data/exe/ruby-lsp-check +1 -1
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +74 -43
  7. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +26 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +147 -29
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +383 -79
  10. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +195 -61
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -8
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +71 -3
  13. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  14. data/lib/ruby_indexer/test/constant_test.rb +17 -17
  15. data/lib/ruby_indexer/test/enhancements_test.rb +197 -0
  16. data/lib/ruby_indexer/test/index_test.rb +367 -17
  17. data/lib/ruby_indexer/test/method_test.rb +58 -25
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +297 -0
  19. data/lib/ruby_indexer/test/test_case.rb +1 -5
  20. data/lib/ruby_lsp/addon.rb +22 -5
  21. data/lib/ruby_lsp/base_server.rb +8 -3
  22. data/lib/ruby_lsp/document.rb +27 -46
  23. data/lib/ruby_lsp/erb_document.rb +125 -0
  24. data/lib/ruby_lsp/global_state.rb +47 -19
  25. data/lib/ruby_lsp/internal.rb +2 -0
  26. data/lib/ruby_lsp/listeners/completion.rb +161 -57
  27. data/lib/ruby_lsp/listeners/definition.rb +91 -27
  28. data/lib/ruby_lsp/listeners/document_highlight.rb +5 -1
  29. data/lib/ruby_lsp/listeners/hover.rb +61 -19
  30. data/lib/ruby_lsp/listeners/signature_help.rb +13 -6
  31. data/lib/ruby_lsp/node_context.rb +65 -5
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +107 -9
  33. data/lib/ruby_lsp/requests/code_actions.rb +11 -2
  34. data/lib/ruby_lsp/requests/completion.rb +4 -4
  35. data/lib/ruby_lsp/requests/completion_resolve.rb +14 -9
  36. data/lib/ruby_lsp/requests/definition.rb +18 -8
  37. data/lib/ruby_lsp/requests/diagnostics.rb +6 -5
  38. data/lib/ruby_lsp/requests/document_symbol.rb +2 -7
  39. data/lib/ruby_lsp/requests/folding_ranges.rb +6 -2
  40. data/lib/ruby_lsp/requests/formatting.rb +15 -0
  41. data/lib/ruby_lsp/requests/hover.rb +5 -5
  42. data/lib/ruby_lsp/requests/on_type_formatting.rb +6 -4
  43. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  44. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
  45. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  46. data/lib/ruby_lsp/requests/support/common.rb +11 -2
  47. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +2 -6
  48. data/lib/ruby_lsp/ruby_document.rb +74 -0
  49. data/lib/ruby_lsp/server.rb +129 -54
  50. data/lib/ruby_lsp/store.rb +33 -9
  51. data/lib/ruby_lsp/test_helper.rb +3 -1
  52. data/lib/ruby_lsp/type_inferrer.rb +61 -25
  53. data/lib/ruby_lsp/utils.rb +13 -0
  54. metadata +9 -8
  55. data/exe/ruby-lsp-doctor +0 -23
@@ -3,6 +3,13 @@
3
3
 
4
4
  module RubyLsp
5
5
  class Document
6
+ class LanguageId < T::Enum
7
+ enums do
8
+ Ruby = new("ruby")
9
+ ERB = new("erb")
10
+ end
11
+ end
12
+
6
13
  extend T::Sig
7
14
  extend T::Helpers
8
15
 
@@ -34,21 +41,14 @@ module RubyLsp
34
41
  @parse_result = T.let(parse, Prism::ParseResult)
35
42
  end
36
43
 
37
- sig { returns(Prism::ProgramNode) }
38
- def tree
39
- @parse_result.value
40
- end
41
-
42
- sig { returns(T::Array[Prism::Comment]) }
43
- def comments
44
- @parse_result.comments
45
- end
46
-
47
44
  sig { params(other: Document).returns(T::Boolean) }
48
45
  def ==(other)
49
- @source == other.source
46
+ self.class == other.class && uri == other.uri && @source == other.source
50
47
  end
51
48
 
49
+ sig { abstract.returns(LanguageId) }
50
+ def language_id; end
51
+
52
52
  # TODO: remove this method once all nonpositional requests have been migrated to the listener pattern
53
53
  sig do
54
54
  type_parameters(:T)
@@ -96,10 +96,8 @@ module RubyLsp
96
96
  sig { abstract.returns(Prism::ParseResult) }
97
97
  def parse; end
98
98
 
99
- sig { returns(T::Boolean) }
100
- def syntax_error?
101
- @parse_result.failure?
102
- end
99
+ sig { abstract.returns(T::Boolean) }
100
+ def syntax_error?; end
103
101
 
104
102
  sig { returns(Scanner) }
105
103
  def create_scanner
@@ -129,8 +127,18 @@ module RubyLsp
129
127
  parent = T.let(nil, T.nilable(Prism::Node))
130
128
  nesting_nodes = T.let(
131
129
  [],
132
- T::Array[T.any(Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode)],
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
+ )],
133
139
  )
140
+
141
+ nesting_nodes << node if node.is_a?(Prism::ProgramNode)
134
142
  call_node = T.let(nil, T.nilable(Prism::CallNode))
135
143
 
136
144
  until queue.empty?
@@ -160,11 +168,8 @@ module RubyLsp
160
168
  # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
161
169
  # target when it is a constant
162
170
  case candidate
163
- when Prism::ClassNode, Prism::ModuleNode
164
- nesting_nodes << candidate
165
- when Prism::SingletonClassNode
166
- nesting_nodes << candidate
167
- when Prism::DefNode
171
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
172
+ Prism::LambdaNode
168
173
  nesting_nodes << candidate
169
174
  end
170
175
 
@@ -205,31 +210,7 @@ module RubyLsp
205
210
  end
206
211
  end
207
212
 
208
- nesting = []
209
- surrounding_method = T.let(nil, T.nilable(String))
210
-
211
- nesting_nodes.each do |node|
212
- case node
213
- when Prism::ClassNode, Prism::ModuleNode
214
- nesting << node.constant_path.slice
215
- when Prism::SingletonClassNode
216
- nesting << "<Class:#{nesting.last}>"
217
- when Prism::DefNode
218
- surrounding_method = node.name.to_s
219
- next unless node.receiver.is_a?(Prism::SelfNode)
220
-
221
- nesting << "<Class:#{nesting.last}>"
222
- end
223
- end
224
-
225
- NodeContext.new(closest, parent, nesting, call_node, surrounding_method)
226
- end
227
-
228
- sig { returns(T::Boolean) }
229
- def sorbet_sigil_is_true_or_higher
230
- parse_result.magic_comments.any? do |comment|
231
- comment.key == "typed" && ["true", "strict", "strong"].include?(comment.value)
232
- end
213
+ NodeContext.new(closest, parent, nesting_nodes, call_node)
233
214
  end
234
215
 
235
216
  class Scanner
@@ -0,0 +1,125 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class ERBDocument < Document
6
+ extend T::Sig
7
+
8
+ sig { override.returns(Prism::ParseResult) }
9
+ def parse
10
+ return @parse_result unless @needs_parsing
11
+
12
+ @needs_parsing = false
13
+ scanner = ERBScanner.new(@source)
14
+ scanner.scan
15
+ @parse_result = Prism.parse(scanner.ruby)
16
+ end
17
+
18
+ sig { override.returns(T::Boolean) }
19
+ def syntax_error?
20
+ @parse_result.failure?
21
+ end
22
+
23
+ sig { override.returns(LanguageId) }
24
+ def language_id
25
+ LanguageId::ERB
26
+ end
27
+
28
+ class ERBScanner
29
+ extend T::Sig
30
+
31
+ sig { returns(String) }
32
+ attr_reader :ruby, :html
33
+
34
+ sig { params(source: String).void }
35
+ def initialize(source)
36
+ @source = source
37
+ @html = T.let(+"", String)
38
+ @ruby = T.let(+"", String)
39
+ @current_pos = T.let(0, Integer)
40
+ @inside_ruby = T.let(false, T::Boolean)
41
+ end
42
+
43
+ sig { void }
44
+ def scan
45
+ while @current_pos < @source.length
46
+ scan_char
47
+ @current_pos += 1
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ sig { void }
54
+ def scan_char
55
+ char = @source[@current_pos]
56
+
57
+ case char
58
+ when "<"
59
+ if next_char == "%"
60
+ @inside_ruby = true
61
+ @current_pos += 1
62
+ push_char(" ")
63
+
64
+ if next_char == "=" && @source[@current_pos + 2] == "="
65
+ @current_pos += 2
66
+ push_char(" ")
67
+ elsif next_char == "=" || next_char == "-"
68
+ @current_pos += 1
69
+ push_char(" ")
70
+ end
71
+ else
72
+ push_char(T.must(char))
73
+ end
74
+ when "-"
75
+ if @inside_ruby && next_char == "%" &&
76
+ @source[@current_pos + 2] == ">"
77
+ @current_pos += 2
78
+ push_char(" ")
79
+ @inside_ruby = false
80
+ else
81
+ push_char(T.must(char))
82
+ end
83
+ when "%"
84
+ if @inside_ruby && next_char == ">"
85
+ @inside_ruby = false
86
+ @current_pos += 1
87
+ push_char(" ")
88
+ else
89
+ push_char(T.must(char))
90
+ end
91
+ when "\r"
92
+ @ruby << char
93
+ @html << char
94
+
95
+ if next_char == "\n"
96
+ @ruby << next_char
97
+ @html << next_char
98
+ @current_pos += 1
99
+ end
100
+ when "\n"
101
+ @ruby << char
102
+ @html << char
103
+ else
104
+ push_char(T.must(char))
105
+ end
106
+ end
107
+
108
+ sig { params(char: String).void }
109
+ def push_char(char)
110
+ if @inside_ruby
111
+ @ruby << char
112
+ @html << " " * char.length
113
+ else
114
+ @ruby << " " * char.length
115
+ @html << char
116
+ end
117
+ end
118
+
119
+ sig { returns(String) }
120
+ def next_char
121
+ @source[@current_pos + 1] || ""
122
+ end
123
+ end
124
+ end
125
+ end
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  attr_reader :encoding
22
22
 
23
23
  sig { returns(T::Boolean) }
24
- attr_reader :supports_watching_files
24
+ attr_reader :supports_watching_files, :experimental_features
25
25
 
26
26
  sig { returns(TypeInferrer) }
27
27
  attr_reader :type_inferrer
@@ -36,9 +36,10 @@ module RubyLsp
36
36
  @test_library = T.let("minitest", String)
37
37
  @has_type_checker = T.let(true, T::Boolean)
38
38
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
39
- @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
40
39
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
41
40
  @supports_watching_files = T.let(false, T::Boolean)
41
+ @experimental_features = T.let(false, T::Boolean)
42
+ @type_inferrer = T.let(TypeInferrer.new(@index, @experimental_features), TypeInferrer)
42
43
  end
43
44
 
44
45
  sig { params(identifier: String, instance: Requests::Support::Formatter).void }
@@ -56,21 +57,48 @@ module RubyLsp
56
57
  @linters.filter_map { |name| @supported_formatters[name] }
57
58
  end
58
59
 
59
- sig { params(options: T::Hash[Symbol, T.untyped]).void }
60
+ # Applies the options provided by the editor and returns an array of notifications to send back to the client
61
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T::Array[Notification]) }
60
62
  def apply_options(options)
63
+ notifications = []
61
64
  direct_dependencies = gather_direct_dependencies
62
65
  all_dependencies = gather_direct_and_indirect_dependencies
63
66
  workspace_uri = options.dig(:workspaceFolders, 0, :uri)
64
67
  @workspace_uri = URI(workspace_uri) if workspace_uri
65
68
 
66
69
  specified_formatter = options.dig(:initializationOptions, :formatter)
67
- @formatter = specified_formatter if specified_formatter
68
- @formatter = detect_formatter(direct_dependencies, all_dependencies) if @formatter == "auto"
70
+
71
+ if specified_formatter
72
+ @formatter = specified_formatter
73
+
74
+ if specified_formatter != "auto"
75
+ notifications << Notification.window_log_message("Using formatter specified by user: #{@formatter}")
76
+ end
77
+ end
78
+
79
+ if @formatter == "auto"
80
+ @formatter = detect_formatter(direct_dependencies, all_dependencies)
81
+ notifications << Notification.window_log_message("Auto detected formatter: #{@formatter}")
82
+ end
69
83
 
70
84
  specified_linters = options.dig(:initializationOptions, :linters)
71
- @linters = specified_linters || detect_linters(direct_dependencies)
85
+ @linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)
86
+
87
+ notifications << if specified_linters
88
+ Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
89
+ else
90
+ Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
91
+ end
92
+
72
93
  @test_library = detect_test_library(direct_dependencies)
94
+ notifications << Notification.window_log_message("Detected test library: #{@test_library}")
95
+
73
96
  @has_type_checker = detect_typechecker(direct_dependencies)
97
+ if @has_type_checker
98
+ notifications << Notification.window_log_message(
99
+ "Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
100
+ )
101
+ end
74
102
 
75
103
  encodings = options.dig(:capabilities, :general, :positionEncodings)
76
104
  @encoding = if !encodings || encodings.empty?
@@ -87,6 +115,11 @@ module RubyLsp
87
115
  if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
88
116
  @supports_watching_files = true
89
117
  end
118
+
119
+ @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
120
+ @type_inferrer.experimental_features = @experimental_features
121
+
122
+ notifications
90
123
  end
91
124
 
92
125
  sig { returns(String) }
@@ -124,10 +157,14 @@ module RubyLsp
124
157
 
125
158
  # Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
126
159
  # single linter. To have multiple linters running, the user must configure them manually
127
- sig { params(dependencies: T::Array[String]).returns(T::Array[String]) }
128
- def detect_linters(dependencies)
160
+ sig { params(dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(T::Array[String]) }
161
+ def detect_linters(dependencies, all_dependencies)
129
162
  linters = []
130
- linters << "rubocop" if dependencies.any?(/^rubocop/)
163
+
164
+ if dependencies.any?(/^rubocop/) || (all_dependencies.include?("rubocop") && dot_rubocop_yml_present)
165
+ linters << "rubocop"
166
+ end
167
+
131
168
  linters
132
169
  end
133
170
 
@@ -155,16 +192,7 @@ module RubyLsp
155
192
  def detect_typechecker(dependencies)
156
193
  return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
157
194
 
158
- # We can't read the env from within `Bundle.with_original_env` so we need to set it here.
159
- ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
160
- Bundler.with_original_env do
161
- sorbet_static_detected = dependencies.any?(/^sorbet-static/)
162
- # Don't show message while running tests, since it's noisy
163
- if sorbet_static_detected && !ruby_lsp_env_is_test
164
- $stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
165
- end
166
- sorbet_static_detected
167
- end
195
+ dependencies.any?(/^sorbet-static/)
168
196
  rescue Bundler::GemfileNotFound
169
197
  false
170
198
  end
@@ -15,6 +15,7 @@ Bundler.ui.level = :silent
15
15
  require "uri"
16
16
  require "cgi"
17
17
  require "set"
18
+ require "strscan"
18
19
  require "prism"
19
20
  require "prism/visitor"
20
21
  require "language_server-protocol"
@@ -34,6 +35,7 @@ require "ruby_lsp/response_builders"
34
35
  require "ruby_lsp/node_context"
35
36
  require "ruby_lsp/document"
36
37
  require "ruby_lsp/ruby_document"
38
+ require "ruby_lsp/erb_document"
37
39
  require "ruby_lsp/store"
38
40
  require "ruby_lsp/addon"
39
41
  require "ruby_lsp/requests/support/rubocop_runner"