ruby-lsp 0.17.15 → 0.17.17

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: 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