ruby-lsp 0.17.15 → 0.17.17

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: 43d715172510bba67801670dbfc574eed9851b9852aaea3f6e210acd74734af4
4
- data.tar.gz: b35b3a55c3d65987390e4b125a090ff8f6da902bfa7ab9c08eb9eb8c72d5157f
3
+ metadata.gz: e4b4c9204834ddf87c84009003c5eef5872211eaef6ed64022a93a2749e1849e
4
+ data.tar.gz: 0d770cf2f67f90fb974a9fca08d9fb4dea1a73d668984ff96e5cc5785d7c5766
5
5
  SHA512:
6
- metadata.gz: ef8a2ca24755eef8c5ed1146f1f6c40ec725988e9dd6a51efb326583e5606a221f733625f08d4dad5bb6091675c39fb18a485ff8c0bed1dc0b2e221a4390c585
7
- data.tar.gz: 16101462585105a9a201f6341908f0b238f77f4acaa6dac723c95e35fcddc7c7b68d937bead5beecc14ae6bcac276b2f813523304f7eaded3132d3aa07eb943b
6
+ metadata.gz: 3534227d67761a6738c5e2ae4a06a08964a0836c4a30c9d306917efdd93aae1285d6ed03992e88486a44e5d3f7e7271997e56bbb12ab908599687c6a1e622cb4
7
+ data.tar.gz: 5345bcf362b6206a0be392eebe5db2175fe9d06b1a38f113fe11d979e5fdcc48851cb47e5f016ae49726ffc73432e81ef801522588cee79ebb36fe545e02d965
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.15
1
+ 0.17.17
data/exe/ruby-lsp CHANGED
@@ -98,16 +98,17 @@ if options[:debug]
98
98
  end
99
99
 
100
100
  if options[:time_index]
101
- require "benchmark"
102
-
103
101
  index = RubyIndexer::Index.new
104
102
 
105
- result = Benchmark.realtime { index.index_all }
103
+ time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
+ index.index_all
105
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_start
106
+
106
107
  entries = index.instance_variable_get(:@entries)
107
108
  entries_by_entry_type = entries.values.flatten.group_by(&:class)
108
109
 
109
110
  puts <<~MSG
110
- Ruby LSP v#{RubyLsp::VERSION}: Indexing took #{result.round(5)} seconds and generated:
111
+ Ruby LSP v#{RubyLsp::VERSION}: Indexing took #{elapsed_time.round(5)} seconds and generated:
111
112
  - #{entries_by_entry_type.sort_by { |k, _| k.to_s }.map { |k, v| "#{k.name.split("::").last}: #{v.size}" }.join("\n- ")}
112
113
  MSG
113
114
  return
@@ -232,7 +232,7 @@ module RubyIndexer
232
232
  next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)
233
233
 
234
234
  if entry.is_a?(Entry::UnresolvedMethodAlias)
235
- resolved_alias = resolve_method_alias(entry, receiver_name)
235
+ resolved_alias = resolve_method_alias(entry, receiver_name, [])
236
236
  hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
237
237
  else
238
238
  hash[entry_name] = [entry, ancestor_index]
@@ -394,16 +394,18 @@ module RubyIndexer
394
394
  real_parts.join("::")
395
395
  end
396
396
 
397
- # Attempts to find methods for a resolved fully qualified receiver name.
397
+ # Attempts to find methods for a resolved fully qualified receiver name. Do not provide the `seen_names` parameter
398
+ # as it is used only internally to prevent infinite loops when resolving circular aliases
398
399
  # Returns `nil` if the method does not exist on that receiver
399
400
  sig do
400
401
  params(
401
402
  method_name: String,
402
403
  receiver_name: String,
404
+ seen_names: T::Array[String],
403
405
  inherited_only: T::Boolean,
404
406
  ).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)]))
405
407
  end
406
- def resolve_method(method_name, receiver_name, inherited_only: false)
408
+ def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false)
407
409
  method_entries = self[method_name]
408
410
  return unless method_entries
409
411
 
@@ -418,7 +420,7 @@ module RubyIndexer
418
420
  when Entry::UnresolvedMethodAlias
419
421
  # Resolve aliases lazily as we find them
420
422
  if entry.owner&.name == ancestor
421
- resolved_alias = resolve_method_alias(entry, receiver_name)
423
+ resolved_alias = resolve_method_alias(entry, receiver_name, seen_names)
422
424
  resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
423
425
  end
424
426
  end
@@ -614,6 +616,17 @@ module RubyIndexer
614
616
  singleton
615
617
  end
616
618
 
619
+ sig do
620
+ type_parameters(:T).params(
621
+ path: String,
622
+ type: T::Class[T.all(T.type_parameter(:T), Entry)],
623
+ ).returns(T.nilable(T::Array[T.type_parameter(:T)]))
624
+ end
625
+ def entries_for(path, type)
626
+ entries = @files_to_entries[path]
627
+ entries&.grep(type)
628
+ end
629
+
617
630
  private
618
631
 
619
632
  # Runs the registered included hooks
@@ -919,16 +932,21 @@ module RubyIndexer
919
932
  params(
920
933
  entry: Entry::UnresolvedMethodAlias,
921
934
  receiver_name: String,
935
+ seen_names: T::Array[String],
922
936
  ).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias))
923
937
  end
924
- def resolve_method_alias(entry, receiver_name)
925
- return entry if entry.new_name == entry.old_name
938
+ def resolve_method_alias(entry, receiver_name, seen_names)
939
+ new_name = entry.new_name
940
+ return entry if new_name == entry.old_name
941
+ return entry if seen_names.include?(new_name)
942
+
943
+ seen_names << new_name
926
944
 
927
- target_method_entries = resolve_method(entry.old_name, receiver_name)
945
+ target_method_entries = resolve_method(entry.old_name, receiver_name, seen_names)
928
946
  return entry unless target_method_entries
929
947
 
930
948
  resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry)
931
- original_entries = T.must(@entries[entry.new_name])
949
+ original_entries = T.must(@entries[new_name])
932
950
  original_entries.delete(entry)
933
951
  original_entries << resolved_alias
934
952
  resolved_alias
@@ -1822,5 +1822,46 @@ module RubyIndexer
1822
1822
  @index.linearized_ancestors_of("Foo::Child::<Class:Child>"),
1823
1823
  )
1824
1824
  end
1825
+
1826
+ def test_resolving_circular_method_aliases_on_class_reopen
1827
+ index(<<~RUBY)
1828
+ class Foo
1829
+ alias bar ==
1830
+ def ==(other) = true
1831
+ end
1832
+
1833
+ class Foo
1834
+ alias == bar
1835
+ end
1836
+ RUBY
1837
+
1838
+ method = @index.resolve_method("==", "Foo").first
1839
+ assert_kind_of(Entry::Method, method)
1840
+ assert_equal("==", method.name)
1841
+
1842
+ candidates = @index.method_completion_candidates("=", "Foo")
1843
+ assert_equal(["==", "==="], candidates.map(&:name))
1844
+ end
1845
+
1846
+ def test_entries_for
1847
+ index(<<~RUBY)
1848
+ class Foo; end
1849
+
1850
+ module Bar
1851
+ def my_def; end
1852
+ def self.my_singleton_def; end
1853
+ end
1854
+ RUBY
1855
+
1856
+ entries = @index.entries_for("/fake/path/foo.rb", Entry)
1857
+ assert_equal(["Foo", "Bar", "my_def", "Bar::<Class:Bar>", "my_singleton_def"], entries.map(&:name))
1858
+
1859
+ entries = @index.entries_for("/fake/path/foo.rb", RubyIndexer::Entry::Namespace)
1860
+ assert_equal(["Foo", "Bar", "Bar::<Class:Bar>"], entries.map(&:name))
1861
+ end
1862
+
1863
+ def test_entries_for_returns_nil_if_no_matches
1864
+ assert_nil(@index.entries_for("non_existing_file.rb", Entry::Namespace))
1865
+ end
1825
1866
  end
1826
1867
  end
@@ -30,6 +30,8 @@ module RubyLsp
30
30
  # Addon instances that have declared a handler to accept file watcher events
31
31
  @file_watcher_addons = T.let([], T::Array[Addon])
32
32
 
33
+ AddonNotFoundError = Class.new(StandardError)
34
+
33
35
  class << self
34
36
  extend T::Sig
35
37
 
@@ -78,11 +80,10 @@ module RubyLsp
78
80
  errors
79
81
  end
80
82
 
81
- # Intended for use by tests for addons
82
83
  sig { params(addon_name: String).returns(Addon) }
83
84
  def get(addon_name)
84
85
  addon = addons.find { |addon| addon.name == addon_name }
85
- raise "Could not find addon '#{addon_name}'" unless addon
86
+ raise AddonNotFoundError, "Could not find addon '#{addon_name}'" unless addon
86
87
 
87
88
  addon
88
89
  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
 
@@ -40,6 +40,12 @@ module RubyLsp
40
40
  @supports_watching_files = T.let(false, T::Boolean)
41
41
  @experimental_features = T.let(false, T::Boolean)
42
42
  @type_inferrer = T.let(TypeInferrer.new(@index, @experimental_features), TypeInferrer)
43
+ @addon_settings = T.let({}, T::Hash[String, T.untyped])
44
+ end
45
+
46
+ sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
47
+ def settings_for_addon(addon_name)
48
+ @addon_settings[addon_name]
43
49
  end
44
50
 
45
51
  sig { params(identifier: String, instance: Requests::Support::Formatter).void }
@@ -119,6 +125,12 @@ module RubyLsp
119
125
  @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
120
126
  @type_inferrer.experimental_features = @experimental_features
121
127
 
128
+ addon_settings = options.dig(:initializationOptions, :addonSettings)
129
+ if addon_settings
130
+ addon_settings.transform_keys!(&:to_s)
131
+ @addon_settings.merge!(addon_settings)
132
+ end
133
+
122
134
  notifications
123
135
  end
124
136
 
@@ -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
@@ -242,7 +242,7 @@ module RubyLsp
242
242
  body = statements.body
243
243
  return if body.empty?
244
244
 
245
- add_lines_range(node.location.start_line, T.must(body.last).location.end_line)
245
+ add_lines_range(node.location.start_line, body.last.location.end_line)
246
246
  end
247
247
 
248
248
  sig { params(node: Prism::Node).void }
@@ -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)
@@ -52,13 +50,6 @@ module RubyLsp
52
50
  :on_optional_parameter_node_enter,
53
51
  :on_required_parameter_node_enter,
54
52
  :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
53
  :on_local_variable_and_write_node_enter,
63
54
  :on_local_variable_operator_write_node_enter,
64
55
  :on_local_variable_or_write_node_enter,
@@ -74,7 +65,6 @@ module RubyLsp
74
65
  sig { params(node: Prism::CallNode).void }
75
66
  def on_call_node_enter(node)
76
67
  return if @inside_implicit_node
77
- return unless visible?(node, @range)
78
68
 
79
69
  message = node.message
80
70
  return unless message
@@ -85,8 +75,14 @@ module RubyLsp
85
75
  return if message == "=~"
86
76
  return if special_method?(message)
87
77
 
88
- type = Requests::Support::Sorbet.annotation?(node) ? :type : :method
89
- @response_builder.add_token(T.must(node.message_loc), type)
78
+ if Requests::Support::Sorbet.annotation?(node)
79
+ @response_builder.add_token(T.must(node.message_loc), :type)
80
+ elsif !node.receiver && !node.opening_loc
81
+ # If the node has a receiver, then the syntax is not ambiguous and semantic highlighting is not necessary to
82
+ # determine that the token is a method call. The only ambiguous case is method calls with implicit self
83
+ # receiver and no parenthesis, which may be confused with local variables
84
+ @response_builder.add_token(T.must(node.message_loc), :method)
85
+ end
90
86
  end
91
87
 
92
88
  sig { params(node: Prism::MatchWriteNode).void }
@@ -104,55 +100,9 @@ module RubyLsp
104
100
  @inside_regex_capture = true if node.call.message == "=~"
105
101
  end
106
102
 
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
103
  sig { params(node: Prism::DefNode).void }
151
104
  def on_def_node_enter(node)
152
105
  @current_scope = ParameterScope.new(@current_scope)
153
- return unless visible?(node, @range)
154
-
155
- @response_builder.add_token(node.name_loc, :method, [:declaration])
156
106
  end
157
107
 
158
108
  sig { params(node: Prism::DefNode).void }
@@ -184,77 +134,49 @@ module RubyLsp
184
134
  sig { params(node: Prism::RequiredKeywordParameterNode).void }
185
135
  def on_required_keyword_parameter_node_enter(node)
186
136
  @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
137
  end
192
138
 
193
139
  sig { params(node: Prism::OptionalKeywordParameterNode).void }
194
140
  def on_optional_keyword_parameter_node_enter(node)
195
141
  @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
142
  end
201
143
 
202
144
  sig { params(node: Prism::KeywordRestParameterNode).void }
203
145
  def on_keyword_rest_parameter_node_enter(node)
204
146
  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
147
+ @current_scope << name.to_sym if name
211
148
  end
212
149
 
213
150
  sig { params(node: Prism::OptionalParameterNode).void }
214
151
  def on_optional_parameter_node_enter(node)
215
152
  @current_scope << node.name
216
- return unless visible?(node, @range)
217
-
218
- @response_builder.add_token(node.name_loc, :parameter)
219
153
  end
220
154
 
221
155
  sig { params(node: Prism::RequiredParameterNode).void }
222
156
  def on_required_parameter_node_enter(node)
223
157
  @current_scope << node.name
224
- return unless visible?(node, @range)
225
-
226
- @response_builder.add_token(node.location, :parameter)
227
158
  end
228
159
 
229
160
  sig { params(node: Prism::RestParameterNode).void }
230
161
  def on_rest_parameter_node_enter(node)
231
162
  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
163
+ @current_scope << name.to_sym if name
238
164
  end
239
165
 
240
166
  sig { params(node: Prism::SelfNode).void }
241
167
  def on_self_node_enter(node)
242
- return unless visible?(node, @range)
243
-
244
168
  @response_builder.add_token(node.location, :variable, [:default_library])
245
169
  end
246
170
 
247
171
  sig { params(node: Prism::LocalVariableWriteNode).void }
248
172
  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))
173
+ type = @current_scope.type_for(node.name)
174
+ @response_builder.add_token(node.name_loc, type) if type == :parameter
252
175
  end
253
176
 
254
177
  sig { params(node: Prism::LocalVariableReadNode).void }
255
178
  def on_local_variable_read_node_enter(node)
256
179
  return if @inside_implicit_node
257
- return unless visible?(node, @range)
258
180
 
259
181
  # Numbered parameters
260
182
  if /_\d+/.match?(node.name)
@@ -267,23 +189,20 @@ module RubyLsp
267
189
 
268
190
  sig { params(node: Prism::LocalVariableAndWriteNode).void }
269
191
  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))
192
+ type = @current_scope.type_for(node.name)
193
+ @response_builder.add_token(node.name_loc, type) if type == :parameter
273
194
  end
274
195
 
275
196
  sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
276
197
  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))
198
+ type = @current_scope.type_for(node.name)
199
+ @response_builder.add_token(node.name_loc, type) if type == :parameter
280
200
  end
281
201
 
282
202
  sig { params(node: Prism::LocalVariableOrWriteNode).void }
283
203
  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))
204
+ type = @current_scope.type_for(node.name)
205
+ @response_builder.add_token(node.name_loc, type) if type == :parameter
287
206
  end
288
207
 
289
208
  sig { params(node: Prism::LocalVariableTargetNode).void }
@@ -294,15 +213,11 @@ module RubyLsp
294
213
  # prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
295
214
  return if @inside_regex_capture
296
215
 
297
- return unless visible?(node, @range)
298
-
299
216
  @response_builder.add_token(node.location, @current_scope.type_for(node.name))
300
217
  end
301
218
 
302
219
  sig { params(node: Prism::ClassNode).void }
303
220
  def on_class_node_enter(node)
304
- return unless visible?(node, @range)
305
-
306
221
  constant_path = node.constant_path
307
222
 
308
223
  if constant_path.is_a?(Prism::ConstantReadNode)
@@ -342,8 +257,6 @@ module RubyLsp
342
257
 
343
258
  sig { params(node: Prism::ModuleNode).void }
344
259
  def on_module_node_enter(node)
345
- return unless visible?(node, @range)
346
-
347
260
  constant_path = node.constant_path
348
261
 
349
262
  if constant_path.is_a?(Prism::ConstantReadNode)
@@ -365,8 +278,6 @@ module RubyLsp
365
278
 
366
279
  sig { params(node: Prism::ImplicitNode).void }
367
280
  def on_implicit_node_enter(node)
368
- return unless visible?(node, @range)
369
-
370
281
  @inside_implicit_node = true
371
282
  end
372
283
 
@@ -375,14 +286,6 @@ module RubyLsp
375
286
  @inside_implicit_node = false
376
287
  end
377
288
 
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
289
  private
387
290
 
388
291
  # 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)
@@ -58,14 +58,16 @@ module RubyLsp
58
58
  formatted_text = @active_formatter.run_formatting(@uri, @document)
59
59
  return unless formatted_text
60
60
 
61
+ lines = @document.source.lines
61
62
  size = @document.source.size
63
+
62
64
  return if formatted_text.size == size && formatted_text == @document.source
63
65
 
64
66
  [
65
67
  Interface::TextEdit.new(
66
68
  range: Interface::Range.new(
67
69
  start: Interface::Position.new(line: 0, character: 0),
68
- end: Interface::Position.new(line: size, character: size),
70
+ end: Interface::Position.new(line: lines.size, character: 0),
69
71
  ),
70
72
  new_text: formatted_text,
71
73
  ),
@@ -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 }
@@ -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.15
4
+ version: 0.17.17
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-20 00:00:00.000000000 Z
11
+ date: 2024-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -28,22 +28,16 @@ dependencies:
28
28
  name: prism
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 0.29.0
34
- - - "<"
31
+ - - "~>"
35
32
  - !ruby/object:Gem::Version
36
- version: '0.31'
33
+ version: '1.0'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: 0.29.0
44
- - - "<"
38
+ - - "~>"
45
39
  - !ruby/object:Gem::Version
46
- version: '0.31'
40
+ version: '1.0'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: rbs
49
43
  requirement: !ruby/object:Gem::Requirement