ruby-lsp-mongoid 0.1.0 → 0.1.2

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: a0ab653d0a88cf1ad6cc028fee44f4e74e90528d9435a62c8ba600371e2b25b8
4
- data.tar.gz: 7f913b8a4028be0151cdb591d98049bb6173835e85a925b6bc1517bc6e8c2a4e
3
+ metadata.gz: 459274f7c2be280ff2e284869fcd8dc7e424dcce61799d881a27f7bc250edf75
4
+ data.tar.gz: 07ee1f8583968c5b01bc395df23e3b598967e9ae389b10100d4681eb5c592f9d
5
5
  SHA512:
6
- metadata.gz: bf1bbab9edc8c6632cdfbb31b26a58505bb885b7ff4b7b6aff6965df7d1d78cae117d8a44bf31bfd4cd248ef72fbeb5de3ab07bf49d461972a56409a1fd1d321
7
- data.tar.gz: cd83a370ef45d4c2e2d343966ca69e5429ea7893150c24d9a824837300490aedc4e6f23ce33ecb3f7d7f0379d6bc1bc11f995249008a1c8bca621fe581156431
6
+ metadata.gz: c946f9ee440c08d5620f074272c1f34376624b4cfb7251bf26d4e7342a76792bd625d86b78eda2015da57eccdaf4b38be838e97e82a39ead6e8d73c8fe890750
7
+ data.tar.gz: 2343c3b4a06ecd64abf51015cac50ce6f45380148a796777860474e025e446526892a2f2a486235a0f5e38906258b9c2b6b07044d12500b38468ad2cfc8adcde
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2025-12-15
4
+
5
+ ### Added
6
+
7
+ - Auto-index core instance methods when including `Mongoid::Document` or `ApplicationDocument`:
8
+ - ID accessors: `_id`, `_id=`, `id`, `id=` (always present in Mongoid documents)
9
+ - Persistence methods: `save`, `save!`, `update`, `update!`, `destroy`, `delete`, `upsert`, `reload`
10
+ - State methods: `new_record?`, `persisted?`, `valid?`, `changed?`
11
+ - Attribute methods: `attributes`, `attributes=`, `assign_attributes`, `read_attribute`, `write_attribute`, `changes`, `errors`
12
+ - Identity methods: `to_key`, `to_param`, `model_name`, `inspect`
13
+ - Auto-index core class methods when including `Mongoid::Document` or `ApplicationDocument`:
14
+ - Query methods: `all`, `where`, `find`, `find_by`, `find_by!`, `first`, `last`, `count`, `exists?`, `distinct`
15
+ - Creation methods: `create`, `create!`, `new`, `build`
16
+ - Modification methods: `update_all`, `delete_all`, `destroy_all`
17
+ - Database methods: `collection`, `database`
18
+ - Support for `ApplicationDocument` pattern (common Rails pattern where `ApplicationDocument` includes `Mongoid::Document`)
19
+
20
+ ### Changed
21
+
22
+ - ID accessors (`_id`, `id`) are now indexed when including `Mongoid::Document`/`ApplicationDocument` instead of at the first DSL call
23
+ - Simplified internal implementation by removing `ensure_id_field_indexed` mechanism
24
+
25
+ ## [0.1.1] - 2025-12-08
26
+
27
+ ### Added
28
+
29
+ - Auto-index `_id` and `id` accessor methods for all Mongoid models that use any DSL (field, associations, scope)
30
+ - Each class using Mongoid DSL now automatically gets `_id`, `_id=`, `id`, and `id=` methods indexed at the first DSL location
31
+
3
32
  ## [0.1.0] - 2025-12-02
4
33
 
5
34
  - Initial release
data/TODO.md CHANGED
@@ -14,3 +14,4 @@ The add-on will recognize methods generated by the following Mongoid DSLs:
14
14
  ### Others
15
15
  - [ ] Callbacks - Lifecycle callback methods
16
16
  - [ ] Validations - Validation-related methods
17
+ - [ ] Enhance index only when class includes Mongoid::Document
@@ -1,19 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../ruby_lsp_mongoid/version"
4
+ require_relative "signature_resolver"
4
5
  require_relative "indexing_enhancement"
5
6
  require_relative "hover"
6
7
 
7
8
  module RubyLsp
8
9
  module Mongoid
9
10
  class Addon < ::RubyLsp::Addon
11
+ include SignatureResolver
12
+
10
13
  def activate(global_state, outgoing_queue)
11
14
  @global_state = global_state
12
15
  @outgoing_queue = outgoing_queue
13
16
  @outgoing_queue << Notification.window_log_message("Activating Ruby LSP Mongoid add-on v#{VERSION}")
17
+
18
+ # Start background thread to update signatures after indexing completes
19
+ @signature_update_thread = Thread.new { wait_for_indexing_and_update_signatures }
14
20
  end
15
21
 
16
- def deactivate; end
22
+ def deactivate
23
+ @signature_update_thread&.kill
24
+ end
17
25
 
18
26
  def name
19
27
  "Ruby LSP Mongoid"
@@ -28,6 +36,91 @@ module RubyLsp
28
36
 
29
37
  Hover.new(response_builder, node_context, @global_state.index, dispatcher)
30
38
  end
39
+
40
+ private
41
+
42
+ def wait_for_indexing_and_update_signatures
43
+ return unless @global_state
44
+
45
+ index = @global_state.index
46
+
47
+ # Wait for initial indexing to complete
48
+ sleep(0.1) until index.instance_variable_get(:@initial_indexing_completed)
49
+
50
+ update_mongoid_signatures(index)
51
+ rescue StandardError => e
52
+ @outgoing_queue << Notification.window_log_message(
53
+ "Ruby LSP Mongoid: Error updating signatures: #{e.message}",
54
+ )
55
+ end
56
+
57
+ def update_mongoid_signatures(index)
58
+ updated_count = 0
59
+
60
+ # Update instance method signatures
61
+ CORE_INSTANCE_METHODS.each do |method_name|
62
+ mongoid_signatures = resolve_instance_method_signature(index, method_name)
63
+ next unless mongoid_signatures
64
+
65
+ updated_count += update_method_signatures_for_mongoid_models(index, method_name, mongoid_signatures)
66
+ end
67
+
68
+ # Update class method signatures
69
+ CORE_CLASS_METHODS.each do |method_name|
70
+ mongoid_signatures = resolve_class_method_signature(index, method_name)
71
+ next unless mongoid_signatures
72
+
73
+ updated_count += update_singleton_method_signatures_for_mongoid_models(
74
+ index,
75
+ method_name,
76
+ mongoid_signatures,
77
+ )
78
+ end
79
+
80
+ if updated_count > 0
81
+ @outgoing_queue << Notification.window_log_message(
82
+ "Ruby LSP Mongoid: Updated #{updated_count} method signatures from Mongoid modules",
83
+ )
84
+ end
85
+ end
86
+
87
+ def update_method_signatures_for_mongoid_models(index, method_name, new_signatures)
88
+ updated = 0
89
+
90
+ # Find all classes that have this method registered by our addon
91
+ # We look for methods that were registered with empty signatures
92
+ index.instance_variable_get(:@entries).each do |_name, entries|
93
+ entries.each do |entry|
94
+ next unless entry.is_a?(RubyIndexer::Entry::Method)
95
+ next unless entry.name == method_name
96
+ next unless entry.signatures.empty? || entry.signatures.first.parameters.empty?
97
+
98
+ # Update the signatures
99
+ entry.instance_variable_set(:@signatures, new_signatures)
100
+ updated += 1
101
+ end
102
+ end
103
+
104
+ updated
105
+ end
106
+
107
+ def update_singleton_method_signatures_for_mongoid_models(index, method_name, new_signatures)
108
+ updated = 0
109
+
110
+ index.instance_variable_get(:@entries).each do |_name, entries|
111
+ entries.each do |entry|
112
+ next unless entry.is_a?(RubyIndexer::Entry::Method)
113
+ next unless entry.name == method_name
114
+ next unless entry.owner.is_a?(RubyIndexer::Entry::SingletonClass)
115
+ next unless entry.signatures.empty? || entry.signatures.first.parameters.empty?
116
+
117
+ entry.instance_variable_set(:@signatures, new_signatures)
118
+ updated += 1
119
+ end
120
+ end
121
+
122
+ updated
123
+ end
31
124
  end
32
125
  end
33
126
  end
@@ -43,10 +43,21 @@ module RubyLsp
43
43
  return unless entries&.any?
44
44
 
45
45
  entry = entries.first
46
+
47
+ # Build hover content with signature and options
48
+ content_parts = []
49
+
50
+ # Add method signature
51
+ signature = format_signature(name.to_s, entry)
52
+ content_parts << signature if signature
53
+
54
+ # Add field options from comments
46
55
  comments = entry.comments
47
- return if comments.nil? || comments.empty?
56
+ content_parts << comments if comments && !comments.empty?
48
57
 
49
- @response_builder.push(comments, category: :documentation)
58
+ return if content_parts.empty?
59
+
60
+ @response_builder.push(content_parts.join("\n\n"), category: :documentation)
50
61
  end
51
62
 
52
63
  def handle_association(node)
@@ -128,6 +139,37 @@ module RubyLsp
128
139
 
129
140
  @response_builder.push(content, category: :documentation)
130
141
  end
142
+
143
+ def format_signature(method_name, entry)
144
+ return nil unless entry.respond_to?(:signatures) && entry.signatures.any?
145
+
146
+ sig = entry.signatures.first
147
+ return nil if sig.parameters.empty?
148
+
149
+ params = sig.parameters.map { |param| format_parameter(param) }.join(", ")
150
+ "```ruby\ndef #{method_name}(#{params})\n```"
151
+ end
152
+
153
+ def format_parameter(param)
154
+ case param
155
+ when RubyIndexer::Entry::RequiredParameter
156
+ param.name.to_s
157
+ when RubyIndexer::Entry::OptionalParameter
158
+ "#{param.name} = nil"
159
+ when RubyIndexer::Entry::KeywordParameter
160
+ "#{param.name}:"
161
+ when RubyIndexer::Entry::OptionalKeywordParameter
162
+ "#{param.name}: nil"
163
+ when RubyIndexer::Entry::RestParameter
164
+ "*#{param.name}"
165
+ when RubyIndexer::Entry::KeywordRestParameter
166
+ "**#{param.name}"
167
+ when RubyIndexer::Entry::BlockParameter
168
+ "&#{param.name}"
169
+ else
170
+ param.name.to_s
171
+ end
172
+ end
131
173
  end
132
174
  end
133
175
  end
@@ -3,11 +3,17 @@
3
3
  module RubyLsp
4
4
  module Mongoid
5
5
  class IndexingEnhancement < RubyIndexer::Enhancement
6
+ def initialize(listener)
7
+ super
8
+ end
9
+
6
10
  def on_call_node_enter(call_node)
7
11
  owner = @listener.current_owner
8
12
  return unless owner
9
13
 
10
14
  case call_node.name
15
+ when :include
16
+ handle_include(call_node)
11
17
  when :field
12
18
  handle_field(call_node)
13
19
  when :embeds_many, :embedded_in
@@ -25,6 +31,58 @@ module RubyLsp
25
31
 
26
32
  private
27
33
 
34
+ # Core instance methods automatically added by Mongoid::Document
35
+ CORE_INSTANCE_METHODS = %w[
36
+ save save! update update! destroy delete upsert reload
37
+ new_record? persisted? valid? changed?
38
+ attributes attributes= assign_attributes read_attribute write_attribute changes errors
39
+ to_key to_param model_name inspect
40
+ ].freeze
41
+
42
+ # Class methods automatically added by Mongoid::Document
43
+ CORE_CLASS_METHODS = %w[
44
+ all where find find_by find_by! first last count exists? distinct
45
+ create create! new build
46
+ update_all delete_all destroy_all
47
+ collection database
48
+ ].freeze
49
+
50
+ def handle_include(call_node)
51
+ arguments = call_node.arguments&.arguments
52
+ return unless arguments
53
+
54
+ # Check if including Mongoid::Document or ApplicationDocument
55
+ first_arg = arguments.first
56
+ module_name = case first_arg
57
+ when Prism::ConstantReadNode
58
+ first_arg.name.to_s
59
+ when Prism::ConstantPathNode
60
+ first_arg.full_name
61
+ end
62
+
63
+ # Support both Mongoid::Document and ApplicationDocument (common Rails pattern)
64
+ return unless module_name == "Mongoid::Document" || module_name == "ApplicationDocument"
65
+
66
+ owner = @listener.current_owner
67
+ return unless owner
68
+
69
+ loc = call_node.location
70
+
71
+ # Add _id and id accessor methods (always present in Mongoid documents)
72
+ add_accessor_methods("_id", loc)
73
+ add_accessor_methods("id", loc)
74
+
75
+ # Add core instance methods
76
+ CORE_INSTANCE_METHODS.each do |method_name|
77
+ add_core_method(method_name, loc)
78
+ end
79
+
80
+ # Add core class methods
81
+ CORE_CLASS_METHODS.each do |method_name|
82
+ add_singleton_method(method_name, loc, owner)
83
+ end
84
+ end
85
+
28
86
  def handle_field(call_node)
29
87
  name = extract_name(call_node)
30
88
  return unless name
@@ -43,7 +101,9 @@ module RubyLsp
43
101
  name = extract_name(call_node)
44
102
  return unless name
45
103
 
46
- add_accessor_methods(name, call_node.location)
104
+ loc = call_node.location
105
+
106
+ add_accessor_methods(name, loc)
47
107
  end
48
108
 
49
109
  def handle_many_association(call_node)
@@ -75,7 +135,18 @@ module RubyLsp
75
135
  owner = @listener.current_owner
76
136
  return unless owner
77
137
 
78
- add_singleton_method(name.to_s, call_node.location, owner)
138
+ loc = call_node.location
139
+
140
+ # Extract lambda parameters if present
141
+ lambda_node = extract_lambda_node(call_node)
142
+ signatures = if lambda_node
143
+ params = extract_lambda_parameters(lambda_node)
144
+ [RubyIndexer::Entry::Signature.new(params)]
145
+ else
146
+ [RubyIndexer::Entry::Signature.new([])]
147
+ end
148
+
149
+ add_singleton_method_with_signatures(name.to_s, loc, owner, signatures)
79
150
  end
80
151
 
81
152
  def extract_name(call_node)
@@ -160,21 +231,34 @@ module RubyLsp
160
231
  @listener.add_method("#{name}=", location, writer_signatures, comments: comments)
161
232
  end
162
233
 
234
+ def add_core_method(name, location)
235
+ signatures = [RubyIndexer::Entry::Signature.new([])]
236
+ @listener.add_method(name.to_s, location, signatures)
237
+ end
238
+
163
239
  def add_builder_methods(name, location)
164
- builder_signatures = [RubyIndexer::Entry::Signature.new([])]
240
+ builder_signatures = [
241
+ RubyIndexer::Entry::Signature.new([
242
+ RubyIndexer::Entry::OptionalParameter.new(name: :attributes),
243
+ ]),
244
+ ]
165
245
  @listener.add_method("build_#{name}", location, builder_signatures)
166
246
  @listener.add_method("create_#{name}", location, builder_signatures)
167
247
  @listener.add_method("create_#{name}!", location, builder_signatures)
168
248
  end
169
249
 
170
250
  def add_singleton_method(name, node_location, owner)
251
+ signatures = [RubyIndexer::Entry::Signature.new([])]
252
+ add_singleton_method_with_signatures(name, node_location, owner, signatures)
253
+ end
254
+
255
+ def add_singleton_method_with_signatures(name, node_location, owner, signatures)
171
256
  index = @listener.instance_variable_get(:@index)
172
257
  code_units_cache = @listener.instance_variable_get(:@code_units_cache)
173
258
  uri = @listener.instance_variable_get(:@uri)
174
259
 
175
260
  location = RubyIndexer::Location.from_prism_location(node_location, code_units_cache)
176
261
  singleton = index.existing_or_new_singleton_class(owner.name)
177
- signatures = [RubyIndexer::Entry::Signature.new([])]
178
262
 
179
263
  index.add(RubyIndexer::Entry::Method.new(
180
264
  name,
@@ -188,6 +272,69 @@ module RubyLsp
188
272
  ))
189
273
  end
190
274
 
275
+ def extract_lambda_node(call_node)
276
+ arguments = call_node.arguments&.arguments
277
+ return unless arguments
278
+
279
+ arguments.find { |arg| arg.is_a?(Prism::LambdaNode) }
280
+ end
281
+
282
+ def extract_lambda_parameters(lambda_node)
283
+ return [] unless lambda_node.is_a?(Prism::LambdaNode)
284
+
285
+ params_node = lambda_node.parameters
286
+ return [] unless params_node
287
+
288
+ # Lambda parameters can be either BlockParametersNode or NumberedParametersNode
289
+ case params_node
290
+ when Prism::BlockParametersNode
291
+ extract_parameters_from_block_params(params_node)
292
+ else
293
+ []
294
+ end
295
+ end
296
+
297
+ def extract_parameters_from_block_params(block_params_node)
298
+ params = []
299
+
300
+ # BlockParametersNode has a `parameters` method that returns ParametersNode
301
+ inner_params = block_params_node.parameters
302
+ return params unless inner_params
303
+
304
+ # Required parameters
305
+ inner_params.requireds&.each do |param|
306
+ next unless param.respond_to?(:name)
307
+
308
+ params << RubyIndexer::Entry::RequiredParameter.new(name: param.name)
309
+ end
310
+
311
+ # Optional parameters
312
+ inner_params.optionals&.each do |param|
313
+ next unless param.respond_to?(:name)
314
+
315
+ params << RubyIndexer::Entry::OptionalParameter.new(name: param.name)
316
+ end
317
+
318
+ # Rest parameter
319
+ if inner_params.rest && inner_params.rest.respond_to?(:name)
320
+ name = inner_params.rest.name || :args
321
+ params << RubyIndexer::Entry::RestParameter.new(name: name)
322
+ end
323
+
324
+ # Keyword parameters
325
+ inner_params.keywords&.each do |param|
326
+ next unless param.respond_to?(:name)
327
+
328
+ if param.respond_to?(:value) && param.value
329
+ params << RubyIndexer::Entry::OptionalKeywordParameter.new(name: param.name)
330
+ else
331
+ params << RubyIndexer::Entry::KeywordParameter.new(name: param.name)
332
+ end
333
+ end
334
+
335
+ params
336
+ end
337
+
191
338
  def singularize(name)
192
339
  name_str = name.to_s
193
340
  if name_str.end_with?("ies")
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module Mongoid
5
+ # Resolves method signatures from Mongoid modules in the Ruby LSP index.
6
+ # Used to update method signatures after initial indexing is complete.
7
+ module SignatureResolver
8
+ # Mongoid modules that provide instance methods
9
+ INSTANCE_METHOD_SOURCES = [
10
+ "Mongoid::Persistable::Savable",
11
+ "Mongoid::Persistable::Updatable",
12
+ "Mongoid::Persistable::Deletable",
13
+ "Mongoid::Persistable::Destroyable",
14
+ "Mongoid::Persistable::Upsertable",
15
+ "Mongoid::Attributes",
16
+ "Mongoid::Reloadable",
17
+ "Mongoid::Stateful",
18
+ "Mongoid::Changeable",
19
+ "Mongoid::Inspectable",
20
+ ].freeze
21
+
22
+ # Mongoid modules that provide class methods
23
+ CLASS_METHOD_SOURCES = [
24
+ "Mongoid::Findable",
25
+ "Mongoid::Criteria",
26
+ "Mongoid::Persistable::Creatable::ClassMethods",
27
+ "Mongoid::Clients::Sessions::ClassMethods",
28
+ ].freeze
29
+
30
+ # Core instance methods to look up signatures for
31
+ CORE_INSTANCE_METHODS = %w[
32
+ save save! update update! destroy delete upsert reload
33
+ new_record? persisted? valid? changed?
34
+ attributes attributes= assign_attributes read_attribute write_attribute
35
+ changes errors to_key to_param model_name inspect
36
+ ].freeze
37
+
38
+ # Core class methods to look up signatures for
39
+ CORE_CLASS_METHODS = %w[
40
+ all where find find_by find_by! first last count exists? distinct
41
+ create create! new build update_all delete_all destroy_all
42
+ collection database
43
+ ].freeze
44
+
45
+ # Resolve instance method signature from Mongoid modules
46
+ # @param index [RubyIndexer::Index] Ruby LSP index
47
+ # @param method_name [String] Method name to look up
48
+ # @return [Array<RubyIndexer::Entry::Signature>, nil] Signatures or nil if not found
49
+ def resolve_instance_method_signature(index, method_name)
50
+ INSTANCE_METHOD_SOURCES.each do |module_name|
51
+ entries = index.resolve_method(method_name, module_name)
52
+ next unless entries&.any?
53
+
54
+ entry = entries.first
55
+ return entry.signatures if entry.respond_to?(:signatures) && entry.signatures.any?
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ # Resolve class method signature from Mongoid modules
62
+ # @param index [RubyIndexer::Index] Ruby LSP index
63
+ # @param method_name [String] Method name to look up
64
+ # @return [Array<RubyIndexer::Entry::Signature>, nil] Signatures or nil if not found
65
+ def resolve_class_method_signature(index, method_name)
66
+ CLASS_METHOD_SOURCES.each do |module_name|
67
+ entries = index.resolve_method(method_name, module_name)
68
+ next unless entries&.any?
69
+
70
+ entry = entries.first
71
+ return entry.signatures if entry.respond_to?(:signatures) && entry.signatures.any?
72
+ end
73
+
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end
@@ -4,4 +4,5 @@ require "ruby_lsp/addon"
4
4
  require "ruby_lsp/internal"
5
5
 
6
6
  require_relative "../ruby_lsp_mongoid/version"
7
+ require_relative "ruby_lsp_mongoid/signature_resolver"
7
8
  require_relative "ruby_lsp_mongoid/addon"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLsp
4
4
  module Mongoid
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia
@@ -41,6 +41,7 @@ files:
41
41
  - lib/ruby_lsp/ruby_lsp_mongoid/addon.rb
42
42
  - lib/ruby_lsp/ruby_lsp_mongoid/hover.rb
43
43
  - lib/ruby_lsp/ruby_lsp_mongoid/indexing_enhancement.rb
44
+ - lib/ruby_lsp/ruby_lsp_mongoid/signature_resolver.rb
44
45
  - lib/ruby_lsp_mongoid/version.rb
45
46
  homepage: https://github.com/riseshia/ruby-lsp-mongoid
46
47
  licenses: