ruby-lsp 0.18.4 → 0.19.0

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.
@@ -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
@@ -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
@@ -32,6 +32,8 @@ module RubyLsp
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
 
@@ -53,13 +55,28 @@ module RubyLsp
53
55
 
54
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)
64
+ def load_addons(global_state, outgoing_queue, include_project_addons: true)
59
65
  # Require all add-ons 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)
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
@@ -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
113
  raise AddonNotFoundError, "Could not find add-on '#{addon_name}'" unless addon
87
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
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 }
@@ -132,6 +189,11 @@ module RubyLsp
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(
@@ -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
 
@@ -26,6 +26,7 @@ require "ruby_lsp/base_server"
26
26
  require "ruby_indexer/ruby_indexer"
27
27
  require "core_ext/uri"
28
28
  require "ruby_lsp/utils"
29
+ require "ruby_lsp/static_docs"
29
30
  require "ruby_lsp/scope"
30
31
  require "ruby_lsp/global_state"
31
32
  require "ruby_lsp/server"
@@ -80,3 +81,4 @@ require "ruby_lsp/requests/show_syntax_tree"
80
81
  require "ruby_lsp/requests/signature_help"
81
82
  require "ruby_lsp/requests/type_hierarchy_supertypes"
82
83
  require "ruby_lsp/requests/workspace_symbol"
84
+ require "ruby_lsp/requests/rename"
@@ -438,7 +438,7 @@ module RubyLsp
438
438
  text_edit: Interface::TextEdit.new(range: range, new_text: keyword),
439
439
  kind: Constant::CompletionItemKind::KEYWORD,
440
440
  data: {
441
- skip_resolve: true,
441
+ keyword: true,
442
442
  },
443
443
  )
444
444
  end
@@ -24,6 +24,7 @@ module RubyLsp
24
24
  Prism::InterpolatedStringNode,
25
25
  Prism::SuperNode,
26
26
  Prism::ForwardingSuperNode,
27
+ Prism::YieldNode,
27
28
  ],
28
29
  T::Array[T.class_of(Prism::Node)],
29
30
  )
@@ -71,6 +72,7 @@ module RubyLsp
71
72
  :on_forwarding_super_node_enter,
72
73
  :on_string_node_enter,
73
74
  :on_interpolated_string_node_enter,
75
+ :on_yield_node_enter,
74
76
  )
75
77
  end
76
78
 
@@ -166,6 +168,11 @@ module RubyLsp
166
168
  handle_super_node_hover
167
169
  end
168
170
 
171
+ sig { params(node: Prism::YieldNode).void }
172
+ def on_yield_node_enter(node)
173
+ handle_keyword_documentation(node.keyword)
174
+ end
175
+
169
176
  private
170
177
 
171
178
  sig { params(node: T.any(Prism::InterpolatedStringNode, Prism::StringNode)).void }
@@ -193,6 +200,18 @@ module RubyLsp
193
200
  end
194
201
  end
195
202
 
203
+ sig { params(keyword: String).void }
204
+ def handle_keyword_documentation(keyword)
205
+ content = KEYWORD_DOCS[keyword]
206
+ return unless content
207
+
208
+ doc_path = File.join(STATIC_DOCS_PATH, "#{keyword}.md")
209
+
210
+ @response_builder.push("```ruby\n#{keyword}\n```", category: :title)
211
+ @response_builder.push("[Read more](#{doc_path})", category: :links)
212
+ @response_builder.push(content, category: :documentation)
213
+ end
214
+
196
215
  sig { void }
197
216
  def handle_super_node_hover
198
217
  # Sorbet can handle super hover on typed true or higher