ruby-lsp 0.17.14 → 0.17.16

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