ruby-lsp 0.17.14 → 0.17.16

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: 17c576dbc88c8c4012eb777b15df7df630a827d7692f6bceec06b68178b25357
4
- data.tar.gz: f023f22a818616b1c8c6fb1de77d0637e7bdae37b60b46e941b297aaa95729a9
3
+ metadata.gz: fc4416ea8876d8207b6c1955cd47af5f3312a0f0dc3644563bfd35138a903535
4
+ data.tar.gz: 5961dd8ca49539c20049509c0afed8b2442c3d16ec12d1fb84c9e45dbd027760
5
5
  SHA512:
6
- metadata.gz: e0848d63fef1677ccf276182cdea5a8304e4a1e04a7ec4936f8f083c27bc62f575fe3f0a207f75805cf3c8eb106e43d5d1afe2df366028980cb5eee6ed97c14b
7
- data.tar.gz: d57869c1ee7dcec65becd9ee4fc225c91e946e697c3aab8a42d2453c1a85bcc91cd8e0a6364d810f8109c2b97102f101d24bf8986d2e4b3905b56368e260b0d3
6
+ metadata.gz: 4b77547221c1750871ff6e1a3635b0ae6f945b86862f484d20eb7587a1feb9830e1d1d7770f16c2e7efeb70e6a953529041f8acc8fe3d7c30940687d20563f96
7
+ data.tar.gz: 1093a1a6ac45e8627f26a6c9dbb0017079d2804904fe9a4303e3698d4131c6eeedf2bbafac93e6475f676860624b9c53d482c52cfe8380e67ed05ce525e99957
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.14
1
+ 0.17.16
@@ -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
@@ -17,6 +17,11 @@ module RubyLsp
17
17
 
18
18
  ParseResultType = type_member
19
19
 
20
+ # This maximum number of characters for providing expensive features, like semantic highlighting and diagnostics.
21
+ # This is the same number used by the TypeScript extension in VS Code
22
+ MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES = 100_000
23
+ EMPTY_CACHE = T.let(Object.new.freeze, Object)
24
+
20
25
  abstract!
21
26
 
22
27
  sig { returns(ParseResultType) }
@@ -34,9 +39,13 @@ module RubyLsp
34
39
  sig { returns(Encoding) }
35
40
  attr_reader :encoding
36
41
 
42
+ sig { returns(T.any(Interface::SemanticTokens, Object)) }
43
+ attr_accessor :semantic_tokens
44
+
37
45
  sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
38
46
  def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
39
- @cache = T.let({}, T::Hash[String, T.untyped])
47
+ @cache = T.let(Hash.new(EMPTY_CACHE), T::Hash[String, T.untyped])
48
+ @semantic_tokens = T.let(EMPTY_CACHE, T.any(Interface::SemanticTokens, Object))
40
49
  @encoding = T.let(encoding, Encoding)
41
50
  @source = T.let(source, String)
42
51
  @version = T.let(version, Integer)
@@ -63,7 +72,7 @@ module RubyLsp
63
72
  end
64
73
  def cache_fetch(request_name, &block)
65
74
  cached = @cache[request_name]
66
- return cached if cached
75
+ return cached if cached != EMPTY_CACHE
67
76
 
68
77
  result = block.call(self)
69
78
  @cache[request_name] = result
@@ -108,6 +117,11 @@ module RubyLsp
108
117
  Scanner.new(@source, @encoding)
109
118
  end
110
119
 
120
+ sig { returns(T::Boolean) }
121
+ def past_expensive_limit?
122
+ @source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
123
+ end
124
+
111
125
  class Scanner
112
126
  extend T::Sig
113
127
 
@@ -366,7 +366,7 @@ module RubyLsp
366
366
 
367
367
  return unless range
368
368
 
369
- guessed_type = type.name
369
+ guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
370
370
 
371
371
  @index.method_completion_candidates(method_name, type.name).each do |entry|
372
372
  entry_name = entry.name
@@ -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(
@@ -69,6 +69,17 @@ module RubyLsp
69
69
  tooltip: tooltip,
70
70
  )
71
71
  end
72
+
73
+ private
74
+
75
+ sig { params(node: T.nilable(Prism::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
76
+ def visible?(node, range)
77
+ return true if range.nil?
78
+ return false if node.nil?
79
+
80
+ loc = node.location
81
+ range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
82
+ end
72
83
  end
73
84
  end
74
85
  end
@@ -14,7 +14,7 @@ module RubyLsp
14
14
  Kernel.methods(false),
15
15
  Bundler::Dsl.instance_methods(false),
16
16
  Module.private_instance_methods(false),
17
- ].flatten.map(&:to_s),
17
+ ].flatten.map(&:to_s).freeze,
18
18
  T::Array[String],
19
19
  )
20
20
 
@@ -22,12 +22,10 @@ module RubyLsp
22
22
  params(
23
23
  dispatcher: Prism::Dispatcher,
24
24
  response_builder: ResponseBuilders::SemanticHighlighting,
25
- range: T.nilable(T::Range[Integer]),
26
25
  ).void
27
26
  end
28
- def initialize(dispatcher, response_builder, range: nil)
27
+ def initialize(dispatcher, response_builder)
29
28
  @response_builder = response_builder
30
- @range = range
31
29
  @special_methods = T.let(nil, T.nilable(T::Array[String]))
32
30
  @current_scope = T.let(ParameterScope.new, ParameterScope)
33
31
  @inside_regex_capture = T.let(false, T::Boolean)
@@ -43,7 +41,6 @@ module RubyLsp
43
41
  :on_block_node_leave,
44
42
  :on_self_node_enter,
45
43
  :on_module_node_enter,
46
- :on_local_variable_write_node_enter,
47
44
  :on_local_variable_read_node_enter,
48
45
  :on_block_parameter_node_enter,
49
46
  :on_required_keyword_parameter_node_enter,
@@ -52,16 +49,6 @@ module RubyLsp
52
49
  :on_optional_parameter_node_enter,
53
50
  :on_required_parameter_node_enter,
54
51
  :on_rest_parameter_node_enter,
55
- :on_constant_read_node_enter,
56
- :on_constant_write_node_enter,
57
- :on_constant_and_write_node_enter,
58
- :on_constant_operator_write_node_enter,
59
- :on_constant_or_write_node_enter,
60
- :on_constant_target_node_enter,
61
- :on_constant_path_node_enter,
62
- :on_local_variable_and_write_node_enter,
63
- :on_local_variable_operator_write_node_enter,
64
- :on_local_variable_or_write_node_enter,
65
52
  :on_local_variable_target_node_enter,
66
53
  :on_block_local_variable_node_enter,
67
54
  :on_match_write_node_enter,
@@ -74,7 +61,6 @@ module RubyLsp
74
61
  sig { params(node: Prism::CallNode).void }
75
62
  def on_call_node_enter(node)
76
63
  return if @inside_implicit_node
77
- return unless visible?(node, @range)
78
64
 
79
65
  message = node.message
80
66
  return unless message
@@ -85,8 +71,14 @@ module RubyLsp
85
71
  return if message == "=~"
86
72
  return if special_method?(message)
87
73
 
88
- type = Requests::Support::Sorbet.annotation?(node) ? :type : :method
89
- @response_builder.add_token(T.must(node.message_loc), type)
74
+ if Requests::Support::Sorbet.annotation?(node)
75
+ @response_builder.add_token(T.must(node.message_loc), :type)
76
+ elsif !node.receiver && !node.opening_loc
77
+ # If the node has a receiver, then the syntax is not ambiguous and semantic highlighting is not necessary to
78
+ # determine that the token is a method call. The only ambiguous case is method calls with implicit self
79
+ # receiver and no parenthesis, which may be confused with local variables
80
+ @response_builder.add_token(T.must(node.message_loc), :method)
81
+ end
90
82
  end
91
83
 
92
84
  sig { params(node: Prism::MatchWriteNode).void }
@@ -104,55 +96,9 @@ module RubyLsp
104
96
  @inside_regex_capture = true if node.call.message == "=~"
105
97
  end
106
98
 
107
- sig { params(node: Prism::ConstantReadNode).void }
108
- def on_constant_read_node_enter(node)
109
- return if @inside_implicit_node
110
- return unless visible?(node, @range)
111
-
112
- @response_builder.add_token(node.location, :namespace)
113
- end
114
-
115
- sig { params(node: Prism::ConstantWriteNode).void }
116
- def on_constant_write_node_enter(node)
117
- return unless visible?(node, @range)
118
-
119
- @response_builder.add_token(node.name_loc, :namespace)
120
- end
121
-
122
- sig { params(node: Prism::ConstantAndWriteNode).void }
123
- def on_constant_and_write_node_enter(node)
124
- return unless visible?(node, @range)
125
-
126
- @response_builder.add_token(node.name_loc, :namespace)
127
- end
128
-
129
- sig { params(node: Prism::ConstantOperatorWriteNode).void }
130
- def on_constant_operator_write_node_enter(node)
131
- return unless visible?(node, @range)
132
-
133
- @response_builder.add_token(node.name_loc, :namespace)
134
- end
135
-
136
- sig { params(node: Prism::ConstantOrWriteNode).void }
137
- def on_constant_or_write_node_enter(node)
138
- return unless visible?(node, @range)
139
-
140
- @response_builder.add_token(node.name_loc, :namespace)
141
- end
142
-
143
- sig { params(node: Prism::ConstantTargetNode).void }
144
- def on_constant_target_node_enter(node)
145
- return unless visible?(node, @range)
146
-
147
- @response_builder.add_token(node.location, :namespace)
148
- end
149
-
150
99
  sig { params(node: Prism::DefNode).void }
151
100
  def on_def_node_enter(node)
152
101
  @current_scope = ParameterScope.new(@current_scope)
153
- return unless visible?(node, @range)
154
-
155
- @response_builder.add_token(node.name_loc, :method, [:declaration])
156
102
  end
157
103
 
158
104
  sig { params(node: Prism::DefNode).void }
@@ -184,77 +130,43 @@ module RubyLsp
184
130
  sig { params(node: Prism::RequiredKeywordParameterNode).void }
185
131
  def on_required_keyword_parameter_node_enter(node)
186
132
  @current_scope << node.name
187
- return unless visible?(node, @range)
188
-
189
- location = node.name_loc
190
- @response_builder.add_token(location.copy(length: location.length - 1), :parameter)
191
133
  end
192
134
 
193
135
  sig { params(node: Prism::OptionalKeywordParameterNode).void }
194
136
  def on_optional_keyword_parameter_node_enter(node)
195
137
  @current_scope << node.name
196
- return unless visible?(node, @range)
197
-
198
- location = node.name_loc
199
- @response_builder.add_token(location.copy(length: location.length - 1), :parameter)
200
138
  end
201
139
 
202
140
  sig { params(node: Prism::KeywordRestParameterNode).void }
203
141
  def on_keyword_rest_parameter_node_enter(node)
204
142
  name = node.name
205
-
206
- if name
207
- @current_scope << name.to_sym
208
-
209
- @response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
210
- end
143
+ @current_scope << name.to_sym if name
211
144
  end
212
145
 
213
146
  sig { params(node: Prism::OptionalParameterNode).void }
214
147
  def on_optional_parameter_node_enter(node)
215
148
  @current_scope << node.name
216
- return unless visible?(node, @range)
217
-
218
- @response_builder.add_token(node.name_loc, :parameter)
219
149
  end
220
150
 
221
151
  sig { params(node: Prism::RequiredParameterNode).void }
222
152
  def on_required_parameter_node_enter(node)
223
153
  @current_scope << node.name
224
- return unless visible?(node, @range)
225
-
226
- @response_builder.add_token(node.location, :parameter)
227
154
  end
228
155
 
229
156
  sig { params(node: Prism::RestParameterNode).void }
230
157
  def on_rest_parameter_node_enter(node)
231
158
  name = node.name
232
-
233
- if name
234
- @current_scope << name.to_sym
235
-
236
- @response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
237
- end
159
+ @current_scope << name.to_sym if name
238
160
  end
239
161
 
240
162
  sig { params(node: Prism::SelfNode).void }
241
163
  def on_self_node_enter(node)
242
- return unless visible?(node, @range)
243
-
244
164
  @response_builder.add_token(node.location, :variable, [:default_library])
245
165
  end
246
166
 
247
- sig { params(node: Prism::LocalVariableWriteNode).void }
248
- def on_local_variable_write_node_enter(node)
249
- return unless visible?(node, @range)
250
-
251
- @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
252
- end
253
-
254
167
  sig { params(node: Prism::LocalVariableReadNode).void }
255
168
  def on_local_variable_read_node_enter(node)
256
169
  return if @inside_implicit_node
257
- return unless visible?(node, @range)
258
170
 
259
171
  # Numbered parameters
260
172
  if /_\d+/.match?(node.name)
@@ -265,27 +177,6 @@ module RubyLsp
265
177
  @response_builder.add_token(node.location, @current_scope.type_for(node.name))
266
178
  end
267
179
 
268
- sig { params(node: Prism::LocalVariableAndWriteNode).void }
269
- def on_local_variable_and_write_node_enter(node)
270
- return unless visible?(node, @range)
271
-
272
- @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
273
- end
274
-
275
- sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
276
- def on_local_variable_operator_write_node_enter(node)
277
- return unless visible?(node, @range)
278
-
279
- @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
280
- end
281
-
282
- sig { params(node: Prism::LocalVariableOrWriteNode).void }
283
- def on_local_variable_or_write_node_enter(node)
284
- return unless visible?(node, @range)
285
-
286
- @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
287
- end
288
-
289
180
  sig { params(node: Prism::LocalVariableTargetNode).void }
290
181
  def on_local_variable_target_node_enter(node)
291
182
  # If we're inside a regex capture, Prism will add LocalVariableTarget nodes for each captured variable.
@@ -294,15 +185,11 @@ module RubyLsp
294
185
  # prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
295
186
  return if @inside_regex_capture
296
187
 
297
- return unless visible?(node, @range)
298
-
299
188
  @response_builder.add_token(node.location, @current_scope.type_for(node.name))
300
189
  end
301
190
 
302
191
  sig { params(node: Prism::ClassNode).void }
303
192
  def on_class_node_enter(node)
304
- return unless visible?(node, @range)
305
-
306
193
  constant_path = node.constant_path
307
194
 
308
195
  if constant_path.is_a?(Prism::ConstantReadNode)
@@ -342,8 +229,6 @@ module RubyLsp
342
229
 
343
230
  sig { params(node: Prism::ModuleNode).void }
344
231
  def on_module_node_enter(node)
345
- return unless visible?(node, @range)
346
-
347
232
  constant_path = node.constant_path
348
233
 
349
234
  if constant_path.is_a?(Prism::ConstantReadNode)
@@ -365,8 +250,6 @@ module RubyLsp
365
250
 
366
251
  sig { params(node: Prism::ImplicitNode).void }
367
252
  def on_implicit_node_enter(node)
368
- return unless visible?(node, @range)
369
-
370
253
  @inside_implicit_node = true
371
254
  end
372
255
 
@@ -375,14 +258,6 @@ module RubyLsp
375
258
  @inside_implicit_node = false
376
259
  end
377
260
 
378
- sig { params(node: Prism::ConstantPathNode).void }
379
- def on_constant_path_node_enter(node)
380
- return if @inside_implicit_node
381
- return unless visible?(node, @range)
382
-
383
- @response_builder.add_token(node.name_loc, :namespace)
384
- end
385
-
386
261
  private
387
262
 
388
263
  # Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
@@ -46,7 +46,9 @@ module RubyLsp
46
46
  diagnostics.concat(syntax_error_diagnostics, syntax_warning_diagnostics)
47
47
 
48
48
  # Running RuboCop is slow, so to avoid excessive runs we only do so if the file is syntactically valid
49
- return diagnostics if @document.syntax_error? || @active_linters.empty?
49
+ if @document.syntax_error? || @active_linters.empty? || @document.past_expensive_limit?
50
+ return diagnostics
51
+ end
50
52
 
51
53
  @active_linters.each do |linter|
52
54
  linter_diagnostics = linter.run_diagnostic(@uri, @document)
@@ -19,6 +19,16 @@ module RubyLsp
19
19
  # some_invocation # --> semantic highlighting: method invocation
20
20
  # var # --> semantic highlighting: local variable
21
21
  # end
22
+ #
23
+ # # Strategy
24
+ #
25
+ # To maximize editor performance, the Ruby LSP will return the minimum number of semantic tokens, since applying
26
+ # them is an expensive operation for the client. This means that the server only returns tokens for ambiguous pieces
27
+ # of syntax, such as method invocations with no receivers or parenthesis (which can be confused with local
28
+ # variables).
29
+ #
30
+ # Offloading as much handling as possible to Text Mate grammars or equivalent will guarantee responsiveness in the
31
+ # editor and allow for a much smoother experience.
22
32
  # ```
23
33
  class SemanticHighlighting < Request
24
34
  extend T::Sig
@@ -35,28 +45,125 @@ module RubyLsp
35
45
  token_modifiers: ResponseBuilders::SemanticHighlighting::TOKEN_MODIFIERS.keys,
36
46
  ),
37
47
  range: true,
38
- full: { delta: false },
48
+ full: { delta: true },
49
+ )
50
+ end
51
+
52
+ # The compute_delta method receives the current semantic tokens and the previous semantic tokens and then tries
53
+ # to compute the smallest possible semantic token edit that will turn previous into current
54
+ sig do
55
+ params(
56
+ current_tokens: T::Array[Integer],
57
+ previous_tokens: T::Array[Integer],
58
+ result_id: String,
59
+ ).returns(Interface::SemanticTokensDelta)
60
+ end
61
+ def compute_delta(current_tokens, previous_tokens, result_id)
62
+ # Find the index of the first token that is different between the two sets of tokens
63
+ first_different_position = current_tokens.zip(previous_tokens).find_index { |new, old| new != old }
64
+
65
+ # When deleting a token from the end, the first_different_position will be nil, but since we're removing at
66
+ # the end, then we have to initialize it to the length of the current tokens after the deletion
67
+ if !first_different_position && current_tokens.length < previous_tokens.length
68
+ first_different_position = current_tokens.length
69
+ end
70
+
71
+ unless first_different_position
72
+ return Interface::SemanticTokensDelta.new(result_id: result_id, edits: [])
73
+ end
74
+
75
+ # Filter the tokens based on the first different position. This must happen at this stage, before we try to
76
+ # find the next position from the end or else we risk confusing sets of token that may have different lengths,
77
+ # but end with the exact same token
78
+ old_tokens = T.must(previous_tokens[first_different_position...])
79
+ new_tokens = T.must(current_tokens[first_different_position...])
80
+
81
+ # Then search from the end to find the first token that doesn't match. Since the user is normally editing the
82
+ # middle of the file, this will minimize the number of edits since the end of the token array has not changed
83
+ first_different_token_from_end = new_tokens.reverse.zip(old_tokens.reverse).find_index do |new, old|
84
+ new != old
85
+ end || 0
86
+
87
+ # Filter the old and new tokens to only the section that will be replaced/inserted/deleted
88
+ old_tokens = T.must(old_tokens[...old_tokens.length - first_different_token_from_end])
89
+ new_tokens = T.must(new_tokens[...new_tokens.length - first_different_token_from_end])
90
+
91
+ # And we send back a single edit, replacing an entire section for the new tokens
92
+ Interface::SemanticTokensDelta.new(
93
+ result_id: result_id,
94
+ edits: [{ start: first_different_position, deleteCount: old_tokens.length, data: new_tokens }],
39
95
  )
40
96
  end
97
+
98
+ sig { returns(Integer) }
99
+ def next_result_id
100
+ @mutex.synchronize do
101
+ @result_id += 1
102
+ end
103
+ end
41
104
  end
42
105
 
43
- sig { params(global_state: GlobalState, dispatcher: Prism::Dispatcher, range: T.nilable(T::Range[Integer])).void }
44
- def initialize(global_state, dispatcher, range: nil)
106
+ @result_id = T.let(0, Integer)
107
+ @mutex = T.let(Mutex.new, Mutex)
108
+
109
+ sig do
110
+ params(
111
+ global_state: GlobalState,
112
+ dispatcher: Prism::Dispatcher,
113
+ document: T.any(RubyDocument, ERBDocument),
114
+ previous_result_id: T.nilable(String),
115
+ range: T.nilable(T::Range[Integer]),
116
+ ).void
117
+ end
118
+ def initialize(global_state, dispatcher, document, previous_result_id, range: nil)
45
119
  super()
120
+
121
+ @document = document
122
+ @previous_result_id = previous_result_id
123
+ @range = range
124
+ @result_id = T.let(SemanticHighlighting.next_result_id.to_s, String)
46
125
  @response_builder = T.let(
47
126
  ResponseBuilders::SemanticHighlighting.new(global_state.encoding),
48
127
  ResponseBuilders::SemanticHighlighting,
49
128
  )
50
- Listeners::SemanticHighlighting.new(dispatcher, @response_builder, range: range)
129
+ Listeners::SemanticHighlighting.new(dispatcher, @response_builder)
51
130
 
52
131
  Addon.addons.each do |addon|
53
132
  addon.create_semantic_highlighting_listener(@response_builder, dispatcher)
54
133
  end
55
134
  end
56
135
 
57
- sig { override.returns(Interface::SemanticTokens) }
136
+ sig { override.returns(T.any(Interface::SemanticTokens, Interface::SemanticTokensDelta)) }
58
137
  def perform
59
- @response_builder.response
138
+ previous_tokens = @document.semantic_tokens
139
+ tokens = @response_builder.response
140
+ encoded_tokens = ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens)
141
+ full_response = Interface::SemanticTokens.new(result_id: @result_id, data: encoded_tokens)
142
+ @document.semantic_tokens = full_response
143
+
144
+ if @range
145
+ tokens_within_range = tokens.select { |token| @range.cover?(token.start_line - 1) }
146
+
147
+ return Interface::SemanticTokens.new(
148
+ result_id: @result_id,
149
+ data: ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens_within_range),
150
+ )
151
+ end
152
+
153
+ # Semantic tokens full delta
154
+ if @previous_result_id
155
+ response = if previous_tokens.is_a?(Interface::SemanticTokens) &&
156
+ previous_tokens.result_id == @previous_result_id
157
+ Requests::SemanticHighlighting.compute_delta(encoded_tokens, previous_tokens.data, @result_id)
158
+ else
159
+ full_response
160
+ end
161
+
162
+ return response
163
+ end
164
+
165
+ # Semantic tokens full
166
+ full_response
60
167
  end
61
168
  end
62
169
  end
@@ -37,15 +37,6 @@ module RubyLsp
37
37
  )
38
38
  end
39
39
 
40
- sig { params(node: T.nilable(Prism::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
41
- def visible?(node, range)
42
- return true if range.nil?
43
- return false if node.nil?
44
-
45
- loc = node.location
46
- range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
47
- end
48
-
49
40
  sig do
50
41
  params(
51
42
  node: Prism::Node,
@@ -91,9 +91,9 @@ module RubyLsp
91
91
  @stack.last
92
92
  end
93
93
 
94
- sig { override.returns(Interface::SemanticTokens) }
94
+ sig { override.returns(T::Array[SemanticToken]) }
95
95
  def response
96
- SemanticTokenEncoder.new.encode(@stack)
96
+ @stack
97
97
  end
98
98
 
99
99
  class SemanticToken
@@ -162,7 +162,7 @@ module RubyLsp
162
162
  sig do
163
163
  params(
164
164
  tokens: T::Array[SemanticToken],
165
- ).returns(Interface::SemanticTokens)
165
+ ).returns(T::Array[Integer])
166
166
  end
167
167
  def encode(tokens)
168
168
  sorted_tokens = tokens.sort_by.with_index do |token, index|
@@ -176,7 +176,7 @@ module RubyLsp
176
176
  compute_delta(token)
177
177
  end
178
178
 
179
- Interface::SemanticTokens.new(data: delta)
179
+ delta
180
180
  end
181
181
 
182
182
  # The delta array is computed according to the LSP specification:
@@ -23,6 +23,7 @@ module RubyLsp
23
23
  run_initialize(message)
24
24
  when "initialized"
25
25
  send_log_message("Finished initializing Ruby LSP!") unless @test_mode
26
+
26
27
  run_initialized
27
28
  when "textDocument/didOpen"
28
29
  text_document_did_open(message)
@@ -40,6 +41,8 @@ module RubyLsp
40
41
  text_document_code_lens(message)
41
42
  when "textDocument/semanticTokens/full"
42
43
  text_document_semantic_tokens_full(message)
44
+ when "textDocument/semanticTokens/full/delta"
45
+ text_document_semantic_tokens_delta(message)
43
46
  when "textDocument/foldingRange"
44
47
  text_document_folding_range(message)
45
48
  when "textDocument/semanticTokens/range"
@@ -304,13 +307,26 @@ module RubyLsp
304
307
  Document::LanguageId::Ruby
305
308
  end
306
309
 
307
- @store.set(
310
+ document = @store.set(
308
311
  uri: text_document[:uri],
309
312
  source: text_document[:text],
310
313
  version: text_document[:version],
311
314
  encoding: @global_state.encoding,
312
315
  language_id: language_id,
313
316
  )
317
+
318
+ if document.past_expensive_limit?
319
+ send_message(
320
+ Notification.new(
321
+ method: "window/showMessage",
322
+ params: Interface::ShowMessageParams.new(
323
+ type: Constant::MessageType::WARNING,
324
+ message: "This file is too long. For performance reasons, semantic highlighting and " \
325
+ "diagnostics will be disabled",
326
+ ),
327
+ ),
328
+ )
329
+ end
314
330
  end
315
331
  end
316
332
 
@@ -378,7 +394,7 @@ module RubyLsp
378
394
 
379
395
  # If the response has already been cached by another request, return it
380
396
  cached_response = document.cache_get(message[:method])
381
- if cached_response
397
+ if cached_response != Document::EMPTY_CACHE
382
398
  send_message(Result.new(id: message[:id], response: cached_response))
383
399
  return
384
400
  end
@@ -391,8 +407,6 @@ module RubyLsp
391
407
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
392
408
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
393
409
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
394
-
395
- semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher)
396
410
  dispatcher.dispatch(parse_result.value)
397
411
 
398
412
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
@@ -401,19 +415,61 @@ module RubyLsp
401
415
  document.cache_set("textDocument/documentSymbol", document_symbol.perform)
402
416
  document.cache_set("textDocument/documentLink", document_link.perform)
403
417
  document.cache_set("textDocument/codeLens", code_lens.perform)
404
- document.cache_set(
405
- "textDocument/semanticTokens/full",
406
- semantic_highlighting.perform,
407
- )
418
+
408
419
  send_message(Result.new(id: message[:id], response: document.cache_get(message[:method])))
409
420
  end
410
421
 
411
422
  alias_method :text_document_document_symbol, :run_combined_requests
412
423
  alias_method :text_document_document_link, :run_combined_requests
413
424
  alias_method :text_document_code_lens, :run_combined_requests
414
- alias_method :text_document_semantic_tokens_full, :run_combined_requests
415
425
  alias_method :text_document_folding_range, :run_combined_requests
416
426
 
427
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
428
+ def text_document_semantic_tokens_full(message)
429
+ document = @store.get(message.dig(:params, :textDocument, :uri))
430
+
431
+ if document.past_expensive_limit?
432
+ send_empty_response(message[:id])
433
+ return
434
+ end
435
+
436
+ unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
437
+ send_empty_response(message[:id])
438
+ return
439
+ end
440
+
441
+ dispatcher = Prism::Dispatcher.new
442
+ semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher, document, nil)
443
+ dispatcher.visit(document.parse_result.value)
444
+
445
+ send_message(Result.new(id: message[:id], response: semantic_highlighting.perform))
446
+ end
447
+
448
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
449
+ def text_document_semantic_tokens_delta(message)
450
+ document = @store.get(message.dig(:params, :textDocument, :uri))
451
+
452
+ if document.past_expensive_limit?
453
+ send_empty_response(message[:id])
454
+ return
455
+ end
456
+
457
+ unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
458
+ send_empty_response(message[:id])
459
+ return
460
+ end
461
+
462
+ dispatcher = Prism::Dispatcher.new
463
+ request = Requests::SemanticHighlighting.new(
464
+ @global_state,
465
+ dispatcher,
466
+ document,
467
+ message.dig(:params, :previousResultId),
468
+ )
469
+ dispatcher.visit(document.parse_result.value)
470
+ send_message(Result.new(id: message[:id], response: request.perform))
471
+ end
472
+
417
473
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
418
474
  def text_document_semantic_tokens_range(message)
419
475
  params = message[:params]
@@ -421,20 +477,26 @@ module RubyLsp
421
477
  uri = params.dig(:textDocument, :uri)
422
478
  document = @store.get(uri)
423
479
 
424
- unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
480
+ if document.past_expensive_limit?
425
481
  send_empty_response(message[:id])
426
482
  return
427
483
  end
428
484
 
429
- start_line = range.dig(:start, :line)
430
- end_line = range.dig(:end, :line)
485
+ unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
486
+ send_empty_response(message[:id])
487
+ return
488
+ end
431
489
 
432
490
  dispatcher = Prism::Dispatcher.new
433
- request = Requests::SemanticHighlighting.new(@global_state, dispatcher, range: start_line..end_line)
491
+ request = Requests::SemanticHighlighting.new(
492
+ @global_state,
493
+ dispatcher,
494
+ document,
495
+ nil,
496
+ range: range.dig(:start, :line)..range.dig(:end, :line),
497
+ )
434
498
  dispatcher.visit(document.parse_result.value)
435
-
436
- response = request.perform
437
- send_message(Result.new(id: message[:id], response: response))
499
+ send_message(Result.new(id: message[:id], response: request.perform))
438
500
  end
439
501
 
440
502
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
@@ -879,7 +941,7 @@ module RubyLsp
879
941
  # Indexing produces a high number of short lived object allocations. That might lead to some fragmentation and
880
942
  # an unnecessarily expanded heap. Compacting ensures that the heap is as small as possible and that future
881
943
  # allocations and garbage collections are faster
882
- GC.compact
944
+ GC.compact unless @test_mode
883
945
 
884
946
  # Always end the progress notification even if indexing failed or else it never goes away and the user has no
885
947
  # way of dismissing it
@@ -1000,10 +1062,10 @@ module RubyLsp
1000
1062
 
1001
1063
  return unless indexing_options
1002
1064
 
1065
+ configuration = @global_state.index.configuration
1066
+ configuration.workspace_path = @global_state.workspace_path
1003
1067
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1004
- @global_state.index.configuration.apply_config(
1005
- indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase },
1006
- )
1068
+ configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1007
1069
  end
1008
1070
  end
1009
1071
  end
@@ -43,7 +43,7 @@ module RubyLsp
43
43
  @gemfile_name = T.let(@gemfile&.basename&.to_s || "Gemfile", String)
44
44
 
45
45
  # Custom bundle paths
46
- @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(Dir.pwd), Pathname)
46
+ @custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(@project_path), Pathname)
47
47
  @custom_gemfile = T.let(@custom_dir + @gemfile_name, Pathname)
48
48
  @custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
49
49
  @lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
@@ -183,14 +183,14 @@ module RubyLsp
183
183
  # `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
184
184
  # want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
185
185
  path = Bundler.settings["path"]
186
- expanded_path = File.expand_path(path, Dir.pwd) if path
186
+ expanded_path = File.expand_path(path, @project_path) if path
187
187
 
188
188
  # Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
189
189
  env = {}
190
190
  env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
191
191
  env["BUNDLE_PATH"] = expanded_path if expanded_path
192
192
 
193
- local_config_path = File.join(Dir.pwd, ".bundle")
193
+ local_config_path = File.join(@project_path, ".bundle")
194
194
  env["BUNDLE_APP_CONFIG"] = local_config_path if Dir.exist?(local_config_path)
195
195
 
196
196
  # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
@@ -286,7 +286,7 @@ module RubyLsp
286
286
  sig { returns(T::Boolean) }
287
287
  def rails_app?
288
288
  config = Pathname.new("config/application.rb").expand_path
289
- application_contents = config.read if config.exist?
289
+ application_contents = config.read(external_encoding: Encoding::UTF_8) if config.exist?
290
290
  return false unless application_contents
291
291
 
292
292
  /class .* < (::)?Rails::Application/.match?(application_contents)
@@ -66,7 +66,7 @@ module RubyLsp
66
66
  version: Integer,
67
67
  language_id: Document::LanguageId,
68
68
  encoding: Encoding,
69
- ).void
69
+ ).returns(Document[T.untyped])
70
70
  end
71
71
  def set(uri:, source:, version:, language_id:, encoding: Encoding::UTF_8)
72
72
  @state[uri.to_s] = case language_id
@@ -20,8 +20,8 @@ module RubyLsp
20
20
  def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typechecker: false, load_addons: true,
21
21
  &block)
22
22
  server = RubyLsp::Server.new(test_mode: true)
23
- server.global_state.stubs(:has_type_checker).returns(false) if stub_no_typechecker
24
23
  server.global_state.apply_options({ initializationOptions: { experimentalFeaturesEnabled: true } })
24
+ server.global_state.instance_variable_set(:@has_type_checker, false) if stub_no_typechecker
25
25
  language_id = uri.to_s.end_with?(".erb") ? "erb" : "ruby"
26
26
 
27
27
  if source
@@ -36,9 +36,47 @@ module RubyLsp
36
36
  def infer_receiver_for_call_node(node, node_context)
37
37
  receiver = node.receiver
38
38
 
39
+ # For receivers inside parenthesis, such as ranges like (0...2), we need to unwrap the parenthesis to get the
40
+ # actual node
41
+ if receiver.is_a?(Prism::ParenthesesNode)
42
+ statements = receiver.body
43
+
44
+ if statements.is_a?(Prism::StatementsNode)
45
+ body = statements.body
46
+
47
+ if body.length == 1
48
+ receiver = body.first
49
+ end
50
+ end
51
+ end
52
+
39
53
  case receiver
40
54
  when Prism::SelfNode, nil
41
55
  self_receiver_handling(node_context)
56
+ when Prism::StringNode
57
+ Type.new("String")
58
+ when Prism::SymbolNode
59
+ Type.new("Symbol")
60
+ when Prism::ArrayNode
61
+ Type.new("Array")
62
+ when Prism::HashNode
63
+ Type.new("Hash")
64
+ when Prism::IntegerNode
65
+ Type.new("Integer")
66
+ when Prism::FloatNode
67
+ Type.new("Float")
68
+ when Prism::RegularExpressionNode
69
+ Type.new("Regexp")
70
+ when Prism::NilNode
71
+ Type.new("NilClass")
72
+ when Prism::TrueNode
73
+ Type.new("TrueClass")
74
+ when Prism::FalseNode
75
+ Type.new("FalseClass")
76
+ when Prism::RangeNode
77
+ Type.new("Range")
78
+ when Prism::LambdaNode
79
+ Type.new("Proc")
42
80
  when Prism::ConstantPathNode, Prism::ConstantReadNode
43
81
  # When the receiver is a constant reference, we have to try to resolve it to figure out the right
44
82
  # receiver. But since the invocation is directly on the constant, that's the singleton context of that
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.17.14
4
+ version: 0.17.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-19 00:00:00.000000000 Z
11
+ date: 2024-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol