ruby-lsp 0.18.3 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/core_ext/uri.rb +9 -4
  4. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +66 -8
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +56 -32
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +3 -2
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +32 -8
  9. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +262 -0
  10. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +11 -0
  12. data/lib/ruby_indexer/test/constant_test.rb +8 -0
  13. data/lib/ruby_indexer/test/enhancements_test.rb +2 -0
  14. data/lib/ruby_indexer/test/instance_variables_test.rb +12 -0
  15. data/lib/ruby_indexer/test/method_test.rb +10 -0
  16. data/lib/ruby_indexer/test/rbs_indexer_test.rb +8 -0
  17. data/lib/ruby_indexer/test/reference_finder_test.rb +86 -0
  18. data/lib/ruby_lsp/addon.rb +79 -17
  19. data/lib/ruby_lsp/base_server.rb +6 -0
  20. data/lib/ruby_lsp/erb_document.rb +3 -2
  21. data/lib/ruby_lsp/global_state.rb +8 -0
  22. data/lib/ruby_lsp/internal.rb +3 -1
  23. data/lib/ruby_lsp/listeners/completion.rb +1 -1
  24. data/lib/ruby_lsp/listeners/hover.rb +57 -0
  25. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +24 -21
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
  27. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  28. data/lib/ruby_lsp/requests/rename.rb +189 -0
  29. data/lib/ruby_lsp/requests/support/common.rb +2 -2
  30. data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
  31. data/lib/ruby_lsp/scope.rb +47 -0
  32. data/lib/ruby_lsp/server.rb +64 -32
  33. data/lib/ruby_lsp/static_docs.rb +15 -0
  34. data/lib/ruby_lsp/store.rb +12 -0
  35. data/lib/ruby_lsp/test_helper.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +6 -1
  37. data/lib/ruby_lsp/utils.rb +3 -6
  38. data/static_docs/yield.md +81 -0
  39. metadata +19 -8
  40. data/lib/ruby_lsp/parameter_scope.rb +0 -33
@@ -0,0 +1,262 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class ReferenceFinder
6
+ extend T::Sig
7
+
8
+ class Reference
9
+ extend T::Sig
10
+
11
+ sig { returns(String) }
12
+ attr_reader :name
13
+
14
+ sig { returns(Prism::Location) }
15
+ attr_reader :location
16
+
17
+ sig { params(name: String, location: Prism::Location).void }
18
+ def initialize(name, location)
19
+ @name = name
20
+ @location = location
21
+ end
22
+ end
23
+
24
+ sig { returns(T::Array[Reference]) }
25
+ attr_reader :references
26
+
27
+ sig do
28
+ params(
29
+ fully_qualified_name: String,
30
+ index: RubyIndexer::Index,
31
+ dispatcher: Prism::Dispatcher,
32
+ ).void
33
+ end
34
+ def initialize(fully_qualified_name, index, dispatcher)
35
+ @fully_qualified_name = fully_qualified_name
36
+ @index = index
37
+ @stack = T.let([], T::Array[String])
38
+ @references = T.let([], T::Array[Reference])
39
+
40
+ dispatcher.register(
41
+ self,
42
+ :on_class_node_enter,
43
+ :on_class_node_leave,
44
+ :on_module_node_enter,
45
+ :on_module_node_leave,
46
+ :on_singleton_class_node_enter,
47
+ :on_singleton_class_node_leave,
48
+ :on_def_node_enter,
49
+ :on_def_node_leave,
50
+ :on_multi_write_node_enter,
51
+ :on_constant_path_write_node_enter,
52
+ :on_constant_path_or_write_node_enter,
53
+ :on_constant_path_operator_write_node_enter,
54
+ :on_constant_path_and_write_node_enter,
55
+ :on_constant_or_write_node_enter,
56
+ :on_constant_path_node_enter,
57
+ :on_constant_read_node_enter,
58
+ :on_constant_write_node_enter,
59
+ :on_constant_or_write_node_enter,
60
+ :on_constant_and_write_node_enter,
61
+ :on_constant_operator_write_node_enter,
62
+ )
63
+ end
64
+
65
+ sig { params(node: Prism::ClassNode).void }
66
+ def on_class_node_enter(node)
67
+ constant_path = node.constant_path
68
+ name = constant_path.slice
69
+ nesting = actual_nesting(name)
70
+
71
+ if nesting.join("::") == @fully_qualified_name
72
+ @references << Reference.new(name, constant_path.location)
73
+ end
74
+
75
+ @stack << name
76
+ end
77
+
78
+ sig { params(node: Prism::ClassNode).void }
79
+ def on_class_node_leave(node)
80
+ @stack.pop
81
+ end
82
+
83
+ sig { params(node: Prism::ModuleNode).void }
84
+ def on_module_node_enter(node)
85
+ constant_path = node.constant_path
86
+ name = constant_path.slice
87
+ nesting = actual_nesting(name)
88
+
89
+ if nesting.join("::") == @fully_qualified_name
90
+ @references << Reference.new(name, constant_path.location)
91
+ end
92
+
93
+ @stack << name
94
+ end
95
+
96
+ sig { params(node: Prism::ModuleNode).void }
97
+ def on_module_node_leave(node)
98
+ @stack.pop
99
+ end
100
+
101
+ sig { params(node: Prism::SingletonClassNode).void }
102
+ def on_singleton_class_node_enter(node)
103
+ expression = node.expression
104
+ return unless expression.is_a?(Prism::SelfNode)
105
+
106
+ @stack << "<Class:#{@stack.last}>"
107
+ end
108
+
109
+ sig { params(node: Prism::SingletonClassNode).void }
110
+ def on_singleton_class_node_leave(node)
111
+ @stack.pop
112
+ end
113
+
114
+ sig { params(node: Prism::ConstantPathNode).void }
115
+ def on_constant_path_node_enter(node)
116
+ name = constant_name(node)
117
+ return unless name
118
+
119
+ collect_constant_references(name, node.location)
120
+ end
121
+
122
+ sig { params(node: Prism::ConstantReadNode).void }
123
+ def on_constant_read_node_enter(node)
124
+ name = constant_name(node)
125
+ return unless name
126
+
127
+ collect_constant_references(name, node.location)
128
+ end
129
+
130
+ sig { params(node: Prism::MultiWriteNode).void }
131
+ def on_multi_write_node_enter(node)
132
+ [*node.lefts, *node.rest, *node.rights].each do |target|
133
+ case target
134
+ when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
135
+ collect_constant_references(target.name.to_s, target.location)
136
+ end
137
+ end
138
+ end
139
+
140
+ sig { params(node: Prism::ConstantPathWriteNode).void }
141
+ def on_constant_path_write_node_enter(node)
142
+ target = node.target
143
+ return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
144
+
145
+ name = constant_name(target)
146
+ return unless name
147
+
148
+ collect_constant_references(name, target.location)
149
+ end
150
+
151
+ sig { params(node: Prism::ConstantPathOrWriteNode).void }
152
+ def on_constant_path_or_write_node_enter(node)
153
+ target = node.target
154
+ return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
155
+
156
+ name = constant_name(target)
157
+ return unless name
158
+
159
+ collect_constant_references(name, target.location)
160
+ end
161
+
162
+ sig { params(node: Prism::ConstantPathOperatorWriteNode).void }
163
+ def on_constant_path_operator_write_node_enter(node)
164
+ target = node.target
165
+ return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
166
+
167
+ name = constant_name(target)
168
+ return unless name
169
+
170
+ collect_constant_references(name, target.location)
171
+ end
172
+
173
+ sig { params(node: Prism::ConstantPathAndWriteNode).void }
174
+ def on_constant_path_and_write_node_enter(node)
175
+ target = node.target
176
+ return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
177
+
178
+ name = constant_name(target)
179
+ return unless name
180
+
181
+ collect_constant_references(name, target.location)
182
+ end
183
+
184
+ sig { params(node: Prism::ConstantWriteNode).void }
185
+ def on_constant_write_node_enter(node)
186
+ collect_constant_references(node.name.to_s, node.name_loc)
187
+ end
188
+
189
+ sig { params(node: Prism::ConstantOrWriteNode).void }
190
+ def on_constant_or_write_node_enter(node)
191
+ collect_constant_references(node.name.to_s, node.name_loc)
192
+ end
193
+
194
+ sig { params(node: Prism::ConstantAndWriteNode).void }
195
+ def on_constant_and_write_node_enter(node)
196
+ collect_constant_references(node.name.to_s, node.name_loc)
197
+ end
198
+
199
+ sig { params(node: Prism::ConstantOperatorWriteNode).void }
200
+ def on_constant_operator_write_node_enter(node)
201
+ collect_constant_references(node.name.to_s, node.name_loc)
202
+ end
203
+
204
+ sig { params(node: Prism::DefNode).void }
205
+ def on_def_node_enter(node)
206
+ if node.receiver.is_a?(Prism::SelfNode)
207
+ @stack << "<Class:#{@stack.last}>"
208
+ end
209
+ end
210
+
211
+ sig { params(node: Prism::DefNode).void }
212
+ def on_def_node_leave(node)
213
+ if node.receiver.is_a?(Prism::SelfNode)
214
+ @stack.pop
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ sig { params(name: String).returns(T::Array[String]) }
221
+ def actual_nesting(name)
222
+ nesting = @stack + [name]
223
+ corrected_nesting = []
224
+
225
+ nesting.reverse_each do |name|
226
+ corrected_nesting.prepend(name.delete_prefix("::"))
227
+
228
+ break if name.start_with?("::")
229
+ end
230
+
231
+ corrected_nesting
232
+ end
233
+
234
+ sig { params(name: String, location: Prism::Location).void }
235
+ def collect_constant_references(name, location)
236
+ entries = @index.resolve(name, @stack)
237
+ return unless entries
238
+
239
+ entries.each do |entry|
240
+ next unless entry.name == @fully_qualified_name
241
+
242
+ @references << Reference.new(name, location)
243
+ end
244
+ end
245
+
246
+ sig do
247
+ params(
248
+ node: T.any(
249
+ Prism::ConstantPathNode,
250
+ Prism::ConstantReadNode,
251
+ Prism::ConstantPathTargetNode,
252
+ ),
253
+ ).returns(T.nilable(String))
254
+ end
255
+ def constant_name(node)
256
+ node.full_name
257
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
258
+ Prism::ConstantPathNode::MissingNodesInConstantPathError
259
+ nil
260
+ end
261
+ end
262
+ end
@@ -6,6 +6,7 @@ require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/indexable_path"
8
8
  require "ruby_indexer/lib/ruby_indexer/declaration_listener"
9
+ require "ruby_indexer/lib/ruby_indexer/reference_finder"
9
10
  require "ruby_indexer/lib/ruby_indexer/enhancement"
10
11
  require "ruby_indexer/lib/ruby_indexer/index"
11
12
  require "ruby_indexer/lib/ruby_indexer/entry"
@@ -159,6 +159,17 @@ module RubyIndexer
159
159
  assert_entry("Foo::Bar", Entry::Module, "/fake/path/foo.rb:4-2:5-5")
160
160
  end
161
161
 
162
+ def test_nested_modules_and_classes_with_multibyte_characters
163
+ index(<<~RUBY)
164
+ module A動物
165
+ class Bねこ; end
166
+ end
167
+ RUBY
168
+
169
+ assert_entry("A動物", Entry::Module, "/fake/path/foo.rb:0-0:2-3")
170
+ assert_entry("A動物::Bねこ", Entry::Class, "/fake/path/foo.rb:1-2:1-16")
171
+ end
172
+
162
173
  def test_nested_modules_and_classes
163
174
  index(<<~RUBY)
164
175
  module Foo
@@ -21,6 +21,14 @@ module RubyIndexer
21
21
  assert_entry("BAR", Entry::Constant, "/fake/path/foo.rb:6-0:6-7")
22
22
  end
23
23
 
24
+ def test_constant_with_multibyte_characters
25
+ index(<<~RUBY)
26
+ CONST_💎 = "Ruby"
27
+ RUBY
28
+
29
+ assert_entry("CONST_💎", Entry::Constant, "/fake/path/foo.rb:0-0:0-16")
30
+ end
31
+
24
32
  def test_constant_or_writes
25
33
  index(<<~RUBY)
26
34
  FOO ||= 1
@@ -39,6 +39,7 @@ module RubyIndexer
39
39
  location,
40
40
  location,
41
41
  nil,
42
+ index.configuration.encoding,
42
43
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
44
  Entry::Visibility::PUBLIC,
44
45
  owner,
@@ -121,6 +122,7 @@ module RubyIndexer
121
122
  location,
122
123
  location,
123
124
  nil,
125
+ index.configuration.encoding,
124
126
  [],
125
127
  Entry::Visibility::PUBLIC,
126
128
  owner,
@@ -25,6 +25,18 @@ module RubyIndexer
25
25
  assert_equal("Foo::Bar", owner.name)
26
26
  end
27
27
 
28
+ def test_instance_variable_with_multibyte_characters
29
+ index(<<~RUBY)
30
+ class Foo
31
+ def initialize
32
+ @あ = 1
33
+ end
34
+ end
35
+ RUBY
36
+
37
+ assert_entry("@あ", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")
38
+ end
39
+
28
40
  def test_instance_variable_and_write
29
41
  index(<<~RUBY)
30
42
  module Foo
@@ -27,6 +27,16 @@ module RubyIndexer
27
27
  assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
28
28
  end
29
29
 
30
+ def test_method_with_multibyte_characters
31
+ index(<<~RUBY)
32
+ class Foo
33
+ def こんにちは; end
34
+ end
35
+ RUBY
36
+
37
+ assert_entry("こんにちは", Entry::Method, "/fake/path/foo.rb:1-2:1-16")
38
+ end
39
+
30
40
  def test_singleton_method_using_self_receiver
31
41
  index(<<~RUBY)
32
42
  class Foo
@@ -348,6 +348,14 @@ module RubyIndexer
348
348
  assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
349
349
  end
350
350
 
351
+ def test_indexing_untyped_functions
352
+ entries = @index.resolve_method("call", "Method")
353
+
354
+ parameters = entries.first.signatures.first.parameters
355
+ assert_equal(1, parameters.length)
356
+ assert_instance_of(Entry::ForwardingParameter, parameters.first)
357
+ end
358
+
351
359
  private
352
360
 
353
361
  def parse_rbs_methods(rbs, method_name)
@@ -0,0 +1,86 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class ReferenceFinderTest < Minitest::Test
8
+ def test_finds_constant_references
9
+ refs = find_references("Foo::Bar", <<~RUBY)
10
+ module Foo
11
+ class Bar
12
+ end
13
+
14
+ Bar
15
+ end
16
+
17
+ Foo::Bar
18
+ RUBY
19
+
20
+ assert_equal("Bar", refs[0].name)
21
+ assert_equal(2, refs[0].location.start_line)
22
+
23
+ assert_equal("Bar", refs[1].name)
24
+ assert_equal(5, refs[1].location.start_line)
25
+
26
+ assert_equal("Foo::Bar", refs[2].name)
27
+ assert_equal(8, refs[2].location.start_line)
28
+ end
29
+
30
+ def test_finds_constant_references_inside_singleton_contexts
31
+ refs = find_references("Foo::<Class:Foo>::Bar", <<~RUBY)
32
+ class Foo
33
+ class << self
34
+ class Bar
35
+ end
36
+
37
+ Bar
38
+ end
39
+ end
40
+ RUBY
41
+
42
+ assert_equal("Bar", refs[0].name)
43
+ assert_equal(3, refs[0].location.start_line)
44
+
45
+ assert_equal("Bar", refs[1].name)
46
+ assert_equal(6, refs[1].location.start_line)
47
+ end
48
+
49
+ def test_finds_top_level_constant_references
50
+ refs = find_references("Bar", <<~RUBY)
51
+ class Bar
52
+ end
53
+
54
+ class Foo
55
+ ::Bar
56
+
57
+ class << self
58
+ ::Bar
59
+ end
60
+ end
61
+ RUBY
62
+
63
+ assert_equal("Bar", refs[0].name)
64
+ assert_equal(1, refs[0].location.start_line)
65
+
66
+ assert_equal("::Bar", refs[1].name)
67
+ assert_equal(5, refs[1].location.start_line)
68
+
69
+ assert_equal("::Bar", refs[2].name)
70
+ assert_equal(8, refs[2].location.start_line)
71
+ end
72
+
73
+ private
74
+
75
+ def find_references(fully_qualified_name, source)
76
+ file_path = "/fake.rb"
77
+ index = Index.new
78
+ index.index_single(IndexablePath.new(nil, file_path), source)
79
+ parse_result = Prism.parse(source)
80
+ dispatcher = Prism::Dispatcher.new
81
+ finder = ReferenceFinder.new(fully_qualified_name, index, dispatcher)
82
+ dispatcher.visit(parse_result.value)
83
+ finder.references.uniq(&:location)
84
+ end
85
+ end
86
+ end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
- # To register an addon, inherit from this class and implement both `name` and `activate`
5
+ # To register an add-on, inherit from this class and implement both `name` and `activate`
6
6
  #
7
7
  # # Example
8
8
  #
@@ -14,7 +14,7 @@ module RubyLsp
14
14
  # end
15
15
  #
16
16
  # def name
17
- # "My addon name"
17
+ # "My add-on name"
18
18
  # end
19
19
  # end
20
20
  # end
@@ -27,11 +27,13 @@ module RubyLsp
27
27
 
28
28
  @addons = T.let([], T::Array[Addon])
29
29
  @addon_classes = T.let([], T::Array[T.class_of(Addon)])
30
- # Addon instances that have declared a handler to accept file watcher events
30
+ # Add-on instances that have declared a handler to accept file watcher events
31
31
  @file_watcher_addons = T.let([], T::Array[Addon])
32
32
 
33
33
  AddonNotFoundError = Class.new(StandardError)
34
34
 
35
+ class IncompatibleApiError < StandardError; end
36
+
35
37
  class << self
36
38
  extend T::Sig
37
39
 
@@ -51,15 +53,30 @@ module RubyLsp
51
53
  super
52
54
  end
53
55
 
54
- # Discovers and loads all addons. Returns a list of errors when trying to require addons
56
+ # Discovers and loads all add-ons. Returns a list of errors when trying to require add-ons
55
57
  sig do
56
- params(global_state: GlobalState, outgoing_queue: Thread::Queue).returns(T::Array[StandardError])
58
+ params(
59
+ global_state: GlobalState,
60
+ outgoing_queue: Thread::Queue,
61
+ include_project_addons: T::Boolean,
62
+ ).returns(T::Array[StandardError])
57
63
  end
58
- def load_addons(global_state, outgoing_queue)
59
- # Require all addons entry points, which should be placed under
60
- # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb`
61
- errors = Gem.find_files("ruby_lsp/**/addon.rb").filter_map do |addon|
62
- require File.expand_path(addon)
64
+ def load_addons(global_state, outgoing_queue, include_project_addons: true)
65
+ # Require all add-ons entry points, which should be placed under
66
+ # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb` or in the workspace under
67
+ # `your_project/ruby_lsp/project_name/addon.rb`
68
+ addon_files = Gem.find_files("ruby_lsp/**/addon.rb")
69
+
70
+ if include_project_addons
71
+ addon_files.concat(Dir.glob(File.join(global_state.workspace_path, "**", "ruby_lsp/**/addon.rb")))
72
+ end
73
+
74
+ errors = addon_files.filter_map do |addon_path|
75
+ # Avoid requiring this file twice. This may happen if you're working on the Ruby LSP itself and at the same
76
+ # time have `ruby-lsp` installed as a vendored gem
77
+ next if File.basename(File.dirname(addon_path)) == "ruby_lsp"
78
+
79
+ require File.expand_path(addon_path)
63
80
  nil
64
81
  rescue => e
65
82
  e
@@ -69,7 +86,7 @@ module RubyLsp
69
86
  self.addons = addon_classes.map(&:new)
70
87
  self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) }
71
88
 
72
- # Activate each one of the discovered addons. If any problems occur in the addons, we don't want to
89
+ # Activate each one of the discovered add-ons. If any problems occur in the add-ons, we don't want to
73
90
  # fail to boot the server
74
91
  addons.each do |addon|
75
92
  addon.activate(global_state, outgoing_queue)
@@ -80,13 +97,53 @@ module RubyLsp
80
97
  errors
81
98
  end
82
99
 
83
- sig { params(addon_name: String).returns(Addon) }
84
- def get(addon_name)
100
+ # Get a reference to another add-on object by name and version. If an add-on exports an API that can be used by
101
+ # other add-ons, this is the way to get access to that API.
102
+ #
103
+ # Important: if the add-on is not found, AddonNotFoundError will be raised. If the add-on is found, but its
104
+ # current version does not satisfy the given version constraint, then IncompatibleApiError will be raised. It is
105
+ # the responsibility of the add-ons using this API to handle these errors appropriately.
106
+ sig { params(addon_name: String, version_constraints: String).returns(Addon) }
107
+ def get(addon_name, *version_constraints)
108
+ if version_constraints.empty?
109
+ raise IncompatibleApiError, "Must specify version constraints when accessing other add-ons"
110
+ end
111
+
85
112
  addon = addons.find { |addon| addon.name == addon_name }
86
- raise AddonNotFoundError, "Could not find addon '#{addon_name}'" unless addon
113
+ raise AddonNotFoundError, "Could not find add-on '#{addon_name}'" unless addon
114
+
115
+ version_object = Gem::Version.new(addon.version)
116
+
117
+ unless version_constraints.all? { |constraint| Gem::Requirement.new(constraint).satisfied_by?(version_object) }
118
+ raise IncompatibleApiError,
119
+ "Constraints #{version_constraints.inspect} is incompatible with #{addon_name} version #{addon.version}"
120
+ end
87
121
 
88
122
  addon
89
123
  end
124
+
125
+ # Depend on a specific version of the Ruby LSP. This method should only be used if the add-on is distributed in a
126
+ # gem that does not have a runtime dependency on the ruby-lsp gem. This method should be invoked at the top of the
127
+ # `addon.rb` file before defining any classes or requiring any files. For example:
128
+ #
129
+ # ```ruby
130
+ # RubyLsp::Addon.depend_on_ruby_lsp!(">= 0.18.0")
131
+ #
132
+ # module MyGem
133
+ # class MyAddon < RubyLsp::Addon
134
+ # # ...
135
+ # end
136
+ # end
137
+ # ```
138
+ sig { params(version_constraints: String).void }
139
+ def depend_on_ruby_lsp!(*version_constraints)
140
+ version_object = Gem::Version.new(RubyLsp::VERSION)
141
+
142
+ unless version_constraints.all? { |constraint| Gem::Requirement.new(constraint).satisfied_by?(version_object) }
143
+ raise IncompatibleApiError,
144
+ "Add-on is not compatible with this version of the Ruby LSP. Skipping its activation"
145
+ end
146
+ end
90
147
  end
91
148
 
92
149
  sig { void }
@@ -118,20 +175,25 @@ module RubyLsp
118
175
  @errors.map(&:full_message).join("\n\n")
119
176
  end
120
177
 
121
- # Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
178
+ # Each add-on should implement `MyAddon#activate` and use to perform any sort of initialization, such as
122
179
  # reading information into memory or even spawning a separate process
123
180
  sig { abstract.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
124
181
  def activate(global_state, outgoing_queue); end
125
182
 
126
- # Each addon should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
183
+ # Each add-on should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
127
184
  # child process
128
185
  sig { abstract.void }
129
186
  def deactivate; end
130
187
 
131
- # Addons should override the `name` method to return the addon name
188
+ # Add-ons should override the `name` method to return the add-on name
132
189
  sig { abstract.returns(String) }
133
190
  def name; end
134
191
 
192
+ # Add-ons should override the `version` method to return a semantic version string representing the add-on's
193
+ # version. This is used for compatibility checks
194
+ sig { abstract.returns(String) }
195
+ def version; end
196
+
135
197
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
136
198
  sig do
137
199
  overridable.params(
@@ -130,6 +130,12 @@ module RubyLsp
130
130
  sig { abstract.void }
131
131
  def shutdown; end
132
132
 
133
+ sig { params(id: Integer, message: String, type: Integer).void }
134
+ def fail_request_and_notify(id, message, type: Constant::MessageType::INFO)
135
+ send_message(Error.new(id: id, code: Constant::ErrorCodes::REQUEST_FAILED, message: message))
136
+ send_message(Notification.window_show_message(message, type: type))
137
+ end
138
+
133
139
  sig { returns(Thread) }
134
140
  def new_worker
135
141
  Thread.new do
@@ -27,8 +27,9 @@ module RubyLsp
27
27
  scanner = ERBScanner.new(@source)
28
28
  scanner.scan
29
29
  @host_language_source = scanner.host_language
30
- # assigning empty scopes to turn Prism into eval mode
31
- @parse_result = Prism.parse(scanner.ruby, scopes: [[]])
30
+ # Use partial script to avoid syntax errors in ERB files where keywords may be used without the full context in
31
+ # which they will be evaluated
32
+ @parse_result = Prism.parse(scanner.ruby, partial_script: true)
32
33
  true
33
34
  end
34
35
 
@@ -23,6 +23,9 @@ module RubyLsp
23
23
  sig { returns(T::Boolean) }
24
24
  attr_reader :supports_watching_files, :experimental_features, :supports_request_delegation
25
25
 
26
+ sig { returns(T::Array[String]) }
27
+ attr_reader :supported_resource_operations
28
+
26
29
  sig { returns(TypeInferrer) }
27
30
  attr_reader :type_inferrer
28
31
 
@@ -42,6 +45,7 @@ module RubyLsp
42
45
  @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
43
46
  @addon_settings = T.let({}, T::Hash[String, T.untyped])
44
47
  @supports_request_delegation = T.let(false, T::Boolean)
48
+ @supported_resource_operations = T.let([], T::Array[String])
45
49
  end
46
50
 
47
51
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -117,6 +121,7 @@ module RubyLsp
117
121
  else
118
122
  Encoding::UTF_32
119
123
  end
124
+ @index.configuration.encoding = @encoding
120
125
 
121
126
  file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
122
127
  if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
@@ -132,6 +137,9 @@ module RubyLsp
132
137
  end
133
138
 
134
139
  @supports_request_delegation = options.dig(:capabilities, :experimental, :requestDelegation) || false
140
+ supported_resource_operations = options.dig(:capabilities, :workspace, :workspaceEdit, :resourceOperations)
141
+ @supported_resource_operations = supported_resource_operations if supported_resource_operations
142
+
135
143
  notifications
136
144
  end
137
145