ruby-lsp-mongoid 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/AGENTS.md +87 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +156 -0
- data/Rakefile +8 -0
- data/TODO.md +16 -0
- data/lib/ruby_lsp/ruby_lsp_mongoid/addon.rb +33 -0
- data/lib/ruby_lsp/ruby_lsp_mongoid/hover.rb +133 -0
- data/lib/ruby_lsp/ruby_lsp_mongoid/indexing_enhancement.rb +203 -0
- data/lib/ruby_lsp/ruby_lsp_mongoid.rb +7 -0
- data/lib/ruby_lsp_mongoid/version.rb +7 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a0ab653d0a88cf1ad6cc028fee44f4e74e90528d9435a62c8ba600371e2b25b8
|
|
4
|
+
data.tar.gz: 7f913b8a4028be0151cdb591d98049bb6173835e85a925b6bc1517bc6e8c2a4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bf1bbab9edc8c6632cdfbb31b26a58505bb885b7ff4b7b6aff6965df7d1d78cae117d8a44bf31bfd4cd248ef72fbeb5de3ab07bf49d461972a56409a1fd1d321
|
|
7
|
+
data.tar.gz: cd83a370ef45d4c2e2d343966ca69e5429ea7893150c24d9a824837300490aedc4e6f23ce33ecb3f7d7f0379d6bc1bc11f995249008a1c8bca621fe581156431
|
data/.rspec
ADDED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Ruby LSP Mongoid - Agent Guidelines
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This is a Ruby LSP add-on that provides index enhancements for Mongoid DSL-generated methods. Similar to ruby-lsp-rails, it helps Ruby LSP understand dynamically generated methods from Mongoid models.
|
|
6
|
+
|
|
7
|
+
## TDD Development Workflow
|
|
8
|
+
|
|
9
|
+
This project follows strict Test-Driven Development (TDD) practices based on Kent Beck's principles.
|
|
10
|
+
|
|
11
|
+
### TDD Cycle: Red → Green → Refactor
|
|
12
|
+
|
|
13
|
+
1. **Red:** Write a failing test first
|
|
14
|
+
2. **Green:** Write minimal code to make the test pass
|
|
15
|
+
3. **Refactor:** Clean up code while keeping tests green
|
|
16
|
+
4. **Commit:** Only commit when all tests pass
|
|
17
|
+
|
|
18
|
+
### Code Quality Standards
|
|
19
|
+
|
|
20
|
+
- Eliminate duplication ruthlessly
|
|
21
|
+
- Express intent clearly through naming
|
|
22
|
+
- Keep methods small and focused
|
|
23
|
+
- Use the simplest solution that works
|
|
24
|
+
- Separate structural changes from behavioral changes
|
|
25
|
+
|
|
26
|
+
## Important Conventions
|
|
27
|
+
|
|
28
|
+
1. **Language:** **ALL code-related content MUST be written in English:**
|
|
29
|
+
- Commit messages (both title and body)
|
|
30
|
+
- Pull request titles and descriptions
|
|
31
|
+
- Code comments
|
|
32
|
+
- Variable names, function names, class names
|
|
33
|
+
- Documentation and README updates
|
|
34
|
+
- Test descriptions
|
|
35
|
+
- Error messages and log output
|
|
36
|
+
- **Exception:** You may communicate with the user in Korean for clarifications and discussions, but all artifacts (commits, PRs, code) must be in English
|
|
37
|
+
|
|
38
|
+
2. **Frozen String Literals:** All Ruby files use `# frozen_string_literal: true`
|
|
39
|
+
|
|
40
|
+
3. **Testing:** Uses RSpec for testing
|
|
41
|
+
|
|
42
|
+
4. **Naming:**
|
|
43
|
+
- Module: `RubyLsp::Mongoid` (add-on namespace)
|
|
44
|
+
- Gem: `ruby-lsp-mongoid`
|
|
45
|
+
- Files follow Ruby conventions (snake_case)
|
|
46
|
+
|
|
47
|
+
## Before Making Changes
|
|
48
|
+
|
|
49
|
+
**Pre-Implementation Checklist:**
|
|
50
|
+
|
|
51
|
+
1. **Read relevant files in parallel** - Use multiple Read tool calls together
|
|
52
|
+
2. **Always run tests first:**
|
|
53
|
+
```bash
|
|
54
|
+
bundle exec rspec
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Pre-Commit Checklist:**
|
|
58
|
+
|
|
59
|
+
1. **Run all tests** - Ensure nothing breaks
|
|
60
|
+
```bash
|
|
61
|
+
bundle exec rspec
|
|
62
|
+
```
|
|
63
|
+
2. **Check for untracked files** - Add relevant new files
|
|
64
|
+
```bash
|
|
65
|
+
git status
|
|
66
|
+
```
|
|
67
|
+
3. **Make ONE atomic commit** - Group all related changes together (code + new files)
|
|
68
|
+
|
|
69
|
+
## Commit Strategy
|
|
70
|
+
|
|
71
|
+
### Atomic Commits
|
|
72
|
+
|
|
73
|
+
**Always group related changes into single commits:**
|
|
74
|
+
|
|
75
|
+
✅ **Good** - Single commit:
|
|
76
|
+
```
|
|
77
|
+
"Add field DSL indexing support"
|
|
78
|
+
- Implement field declaration parsing
|
|
79
|
+
- Register generated accessor methods
|
|
80
|
+
- Add test cases
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
❌ **Bad** - Multiple commits:
|
|
84
|
+
```
|
|
85
|
+
"Add field DSL parsing"
|
|
86
|
+
"Add tests"
|
|
87
|
+
```
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Ruby LSP Mongoid
|
|
2
|
+
|
|
3
|
+
A [Ruby LSP](https://github.com/Shopify/ruby-lsp) add-on that provides enhanced editor features for [Mongoid](https://www.mongodb.com/docs/mongoid/current/) applications.
|
|
4
|
+
|
|
5
|
+
Similar to [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails), this add-on enhances Ruby LSP with Mongoid-specific functionality by providing index enhancements for methods generated by Mongoid DSLs.
|
|
6
|
+
|
|
7
|
+
> **Note:** This project is under active development. See [TODO.md](TODO.md) for planned features.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this gem to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
group :development do
|
|
15
|
+
gem "ruby-lsp-mongoid"
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Ruby LSP will automatically detect and load the add-on.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Ruby 3.3+
|
|
30
|
+
- [Ruby LSP](https://github.com/Shopify/ruby-lsp)
|
|
31
|
+
- [Mongoid](https://github.com/mongodb/mongoid)
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
Ruby LSP Mongoid implements the [Ruby LSP add-on interface](https://shopify.github.io/ruby-lsp/add-ons.html) to provide index enhancements. It statically analyzes Mongoid model files to understand which DSL methods are used and registers the generated methods with Ruby LSP's index.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
### Go-to-Definition & Autocomplete for Mongoid DSL Methods
|
|
40
|
+
|
|
41
|
+
Ruby LSP Mongoid indexes methods generated by Mongoid DSLs. Once indexed, Ruby LSP's built-in features automatically work:
|
|
42
|
+
|
|
43
|
+
- **Go-to-Definition**: Jump to the DSL declaration from method calls
|
|
44
|
+
- **Autocomplete**: See generated methods in completion suggestions (e.g., type `user.` to see `posts`, `profile`, etc.)
|
|
45
|
+
|
|
46
|
+
#### Field Accessors
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class User
|
|
50
|
+
include Mongoid::Document
|
|
51
|
+
|
|
52
|
+
field :name, type: String
|
|
53
|
+
field :email, type: String
|
|
54
|
+
field :age, type: Integer
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
user = User.new
|
|
58
|
+
user.name # Go-to-definition works! → jumps to `field :name`
|
|
59
|
+
user.name= # Writer method also indexed
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Associations
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class User
|
|
66
|
+
include Mongoid::Document
|
|
67
|
+
|
|
68
|
+
has_many :posts
|
|
69
|
+
has_one :profile
|
|
70
|
+
belongs_to :organization
|
|
71
|
+
has_and_belongs_to_many :roles
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
user.posts # → jumps to `has_many :posts`
|
|
75
|
+
user.post_ids # has_many also generates `_ids` accessor
|
|
76
|
+
user.profile # → jumps to `has_one :profile`
|
|
77
|
+
user.build_profile # builder methods indexed
|
|
78
|
+
user.create_profile
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Embedded Documents
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class Post
|
|
85
|
+
include Mongoid::Document
|
|
86
|
+
|
|
87
|
+
embeds_many :comments
|
|
88
|
+
embeds_one :metadata
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Comment
|
|
92
|
+
include Mongoid::Document
|
|
93
|
+
|
|
94
|
+
embedded_in :post
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
post.comments # → jumps to `embeds_many :comments`
|
|
98
|
+
post.metadata # → jumps to `embeds_one :metadata`
|
|
99
|
+
comment.post # → jumps to `embedded_in :post`
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Scopes
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class Post
|
|
106
|
+
include Mongoid::Document
|
|
107
|
+
|
|
108
|
+
scope :published, -> { where(published: true) }
|
|
109
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Post.published # → jumps to `scope :published`
|
|
113
|
+
Post.recent # → jumps to `scope :recent`
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Hover Information
|
|
117
|
+
|
|
118
|
+
When hovering over Mongoid DSL declarations, you'll see additional information:
|
|
119
|
+
|
|
120
|
+
#### Field Options
|
|
121
|
+
|
|
122
|
+
Hovering over a field symbol shows its type, alias, and default value:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
field :active, type: Boolean, default: true
|
|
126
|
+
# ^^^^^^
|
|
127
|
+
# Hover shows: "type: Boolean, default: true"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Association Target Class
|
|
131
|
+
|
|
132
|
+
Hovering over an association symbol shows the target class with a clickable link:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
has_many :posts
|
|
136
|
+
# ^^^^^^
|
|
137
|
+
# Hover shows: "has_many: [Post](file:///path/to/post.rb#L1)"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This works for all association and embedded document DSLs:
|
|
141
|
+
- `has_many`, `has_one`, `belongs_to`, `has_and_belongs_to_many`
|
|
142
|
+
- `embeds_many`, `embeds_one`, `embedded_in`
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
147
|
+
|
|
148
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/riseshia/ruby-lsp-mongoid.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/TODO.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Planned Features
|
|
4
|
+
|
|
5
|
+
This add-on will provide Ruby LSP with information about methods dynamically generated by Mongoid DSLs, enabling:
|
|
6
|
+
|
|
7
|
+
- [ ] **Go to Definition** - Navigate to the source of dynamically generated methods
|
|
8
|
+
- [ ] **Hover** - Documentation for generated methods
|
|
9
|
+
|
|
10
|
+
## Supported Mongoid DSLs
|
|
11
|
+
|
|
12
|
+
The add-on will recognize methods generated by the following Mongoid DSLs:
|
|
13
|
+
|
|
14
|
+
### Others
|
|
15
|
+
- [ ] Callbacks - Lifecycle callback methods
|
|
16
|
+
- [ ] Validations - Validation-related methods
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../ruby_lsp_mongoid/version"
|
|
4
|
+
require_relative "indexing_enhancement"
|
|
5
|
+
require_relative "hover"
|
|
6
|
+
|
|
7
|
+
module RubyLsp
|
|
8
|
+
module Mongoid
|
|
9
|
+
class Addon < ::RubyLsp::Addon
|
|
10
|
+
def activate(global_state, outgoing_queue)
|
|
11
|
+
@global_state = global_state
|
|
12
|
+
@outgoing_queue = outgoing_queue
|
|
13
|
+
@outgoing_queue << Notification.window_log_message("Activating Ruby LSP Mongoid add-on v#{VERSION}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def deactivate; end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
"Ruby LSP Mongoid"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def version
|
|
23
|
+
VERSION
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_hover_listener(response_builder, node_context, dispatcher)
|
|
27
|
+
return unless @global_state
|
|
28
|
+
|
|
29
|
+
Hover.new(response_builder, node_context, @global_state.index, dispatcher)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module Mongoid
|
|
5
|
+
class Hover
|
|
6
|
+
include RubyLsp::Requests::Support::Common
|
|
7
|
+
|
|
8
|
+
FIELD_DSL = :field
|
|
9
|
+
ASSOCIATION_DSLS = %i[
|
|
10
|
+
embeds_many embeds_one embedded_in
|
|
11
|
+
has_many has_one belongs_to has_and_belongs_to_many
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(response_builder, node_context, index, dispatcher)
|
|
15
|
+
@response_builder = response_builder
|
|
16
|
+
@node_context = node_context
|
|
17
|
+
@index = index
|
|
18
|
+
|
|
19
|
+
dispatcher.register(self, :on_symbol_node_enter)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_symbol_node_enter(node)
|
|
23
|
+
call_node = @node_context.call_node
|
|
24
|
+
return unless call_node.is_a?(Prism::CallNode)
|
|
25
|
+
|
|
26
|
+
if call_node.name == FIELD_DSL
|
|
27
|
+
handle_field(call_node)
|
|
28
|
+
elsif ASSOCIATION_DSLS.include?(call_node.name)
|
|
29
|
+
handle_association(call_node)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def handle_field(node)
|
|
36
|
+
owner = @node_context.nesting.last
|
|
37
|
+
return unless owner
|
|
38
|
+
|
|
39
|
+
name = extract_name(node)
|
|
40
|
+
return unless name
|
|
41
|
+
|
|
42
|
+
entries = @index.resolve_method(name.to_s, owner)
|
|
43
|
+
return unless entries&.any?
|
|
44
|
+
|
|
45
|
+
entry = entries.first
|
|
46
|
+
comments = entry.comments
|
|
47
|
+
return if comments.nil? || comments.empty?
|
|
48
|
+
|
|
49
|
+
@response_builder.push(comments, category: :documentation)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle_association(node)
|
|
53
|
+
association_type = node.name
|
|
54
|
+
association_name = extract_name(node)
|
|
55
|
+
return unless association_name
|
|
56
|
+
|
|
57
|
+
class_name = extract_class_name_option(node) || classify(association_name)
|
|
58
|
+
generate_association_hover(class_name, association_type)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_name(call_node)
|
|
62
|
+
arguments = call_node.arguments&.arguments
|
|
63
|
+
return unless arguments
|
|
64
|
+
|
|
65
|
+
name_arg = arguments.first
|
|
66
|
+
|
|
67
|
+
case name_arg
|
|
68
|
+
when Prism::SymbolNode
|
|
69
|
+
name_arg.value
|
|
70
|
+
when Prism::StringNode
|
|
71
|
+
name_arg.content
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_class_name_option(call_node)
|
|
76
|
+
arguments = call_node.arguments&.arguments
|
|
77
|
+
return unless arguments
|
|
78
|
+
|
|
79
|
+
keyword_hash = arguments.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
80
|
+
return unless keyword_hash
|
|
81
|
+
|
|
82
|
+
element = keyword_hash.elements.find do |el|
|
|
83
|
+
el.is_a?(Prism::AssocNode) &&
|
|
84
|
+
el.key.is_a?(Prism::SymbolNode) &&
|
|
85
|
+
el.key.value == "class_name"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
return unless element
|
|
89
|
+
|
|
90
|
+
case element.value
|
|
91
|
+
when Prism::SymbolNode
|
|
92
|
+
element.value.value
|
|
93
|
+
when Prism::StringNode
|
|
94
|
+
element.value.content
|
|
95
|
+
when Prism::ConstantReadNode
|
|
96
|
+
element.value.name.to_s
|
|
97
|
+
when Prism::ConstantPathNode
|
|
98
|
+
element.value.full_name
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def classify(name)
|
|
103
|
+
singular = singularize(name)
|
|
104
|
+
singular.to_s.split("_").map(&:capitalize).join
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def singularize(name)
|
|
108
|
+
name_str = name.to_s
|
|
109
|
+
if name_str.end_with?("ies")
|
|
110
|
+
name_str[0..-4] + "y"
|
|
111
|
+
elsif name_str.end_with?("s")
|
|
112
|
+
name_str[0..-2]
|
|
113
|
+
else
|
|
114
|
+
name_str
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def generate_association_hover(class_name, association_type)
|
|
119
|
+
entries = @index[class_name]
|
|
120
|
+
|
|
121
|
+
if entries&.any?
|
|
122
|
+
entry = entries.first
|
|
123
|
+
line = entry.location.start_line
|
|
124
|
+
content = "#{association_type}: [#{class_name}](#{entry.uri}#L#{line})"
|
|
125
|
+
else
|
|
126
|
+
content = "#{association_type}: #{class_name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@response_builder.push(content, category: :documentation)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module Mongoid
|
|
5
|
+
class IndexingEnhancement < RubyIndexer::Enhancement
|
|
6
|
+
def on_call_node_enter(call_node)
|
|
7
|
+
owner = @listener.current_owner
|
|
8
|
+
return unless owner
|
|
9
|
+
|
|
10
|
+
case call_node.name
|
|
11
|
+
when :field
|
|
12
|
+
handle_field(call_node)
|
|
13
|
+
when :embeds_many, :embedded_in
|
|
14
|
+
handle_accessor_dsl(call_node)
|
|
15
|
+
when :has_many, :has_and_belongs_to_many
|
|
16
|
+
handle_many_association(call_node)
|
|
17
|
+
when :has_one, :belongs_to, :embeds_one
|
|
18
|
+
handle_singular_association(call_node)
|
|
19
|
+
when :scope
|
|
20
|
+
handle_scope(call_node)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def on_call_node_leave(call_node); end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def handle_field(call_node)
|
|
29
|
+
name = extract_name(call_node)
|
|
30
|
+
return unless name
|
|
31
|
+
|
|
32
|
+
loc = call_node.location
|
|
33
|
+
comment = build_field_options_comment(call_node)
|
|
34
|
+
|
|
35
|
+
add_accessor_methods(name, loc, comments: comment)
|
|
36
|
+
|
|
37
|
+
# Handle as: option for field alias
|
|
38
|
+
alias_name = extract_option_value(call_node, "as")
|
|
39
|
+
add_accessor_methods(alias_name, loc, comments: comment) if alias_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle_accessor_dsl(call_node)
|
|
43
|
+
name = extract_name(call_node)
|
|
44
|
+
return unless name
|
|
45
|
+
|
|
46
|
+
add_accessor_methods(name, call_node.location)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def handle_many_association(call_node)
|
|
50
|
+
name = extract_name(call_node)
|
|
51
|
+
return unless name
|
|
52
|
+
|
|
53
|
+
loc = call_node.location
|
|
54
|
+
|
|
55
|
+
add_accessor_methods(name, loc)
|
|
56
|
+
|
|
57
|
+
singular_name = singularize(name)
|
|
58
|
+
add_accessor_methods("#{singular_name}_ids", loc)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_singular_association(call_node)
|
|
62
|
+
name = extract_name(call_node)
|
|
63
|
+
return unless name
|
|
64
|
+
|
|
65
|
+
loc = call_node.location
|
|
66
|
+
|
|
67
|
+
add_accessor_methods(name, loc)
|
|
68
|
+
add_builder_methods(name, loc)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def handle_scope(call_node)
|
|
72
|
+
name = extract_name(call_node)
|
|
73
|
+
return unless name
|
|
74
|
+
|
|
75
|
+
owner = @listener.current_owner
|
|
76
|
+
return unless owner
|
|
77
|
+
|
|
78
|
+
add_singleton_method(name.to_s, call_node.location, owner)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_name(call_node)
|
|
82
|
+
arguments = call_node.arguments&.arguments
|
|
83
|
+
return unless arguments
|
|
84
|
+
|
|
85
|
+
name_arg = arguments.first
|
|
86
|
+
|
|
87
|
+
case name_arg
|
|
88
|
+
when Prism::SymbolNode
|
|
89
|
+
name_arg.value
|
|
90
|
+
when Prism::StringNode
|
|
91
|
+
name_arg.content
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_option_value(call_node, key)
|
|
96
|
+
arguments = call_node.arguments&.arguments
|
|
97
|
+
return unless arguments
|
|
98
|
+
|
|
99
|
+
keyword_hash = arguments.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
100
|
+
return unless keyword_hash
|
|
101
|
+
|
|
102
|
+
element = keyword_hash.elements.find do |el|
|
|
103
|
+
el.is_a?(Prism::AssocNode) &&
|
|
104
|
+
el.key.is_a?(Prism::SymbolNode) &&
|
|
105
|
+
el.key.value == key
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return unless element
|
|
109
|
+
|
|
110
|
+
case element.value
|
|
111
|
+
when Prism::SymbolNode
|
|
112
|
+
element.value.value
|
|
113
|
+
when Prism::StringNode
|
|
114
|
+
element.value.content
|
|
115
|
+
when Prism::ConstantReadNode
|
|
116
|
+
element.value.name.to_s
|
|
117
|
+
when Prism::ConstantPathNode
|
|
118
|
+
element.value.full_name
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def extract_option_source(call_node, key)
|
|
123
|
+
arguments = call_node.arguments&.arguments
|
|
124
|
+
return unless arguments
|
|
125
|
+
|
|
126
|
+
keyword_hash = arguments.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
127
|
+
return unless keyword_hash
|
|
128
|
+
|
|
129
|
+
element = keyword_hash.elements.find do |el|
|
|
130
|
+
el.is_a?(Prism::AssocNode) &&
|
|
131
|
+
el.key.is_a?(Prism::SymbolNode) &&
|
|
132
|
+
el.key.value == key
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
element&.value&.slice
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_field_options_comment(call_node)
|
|
139
|
+
options = []
|
|
140
|
+
|
|
141
|
+
type_value = extract_option_value(call_node, "type")
|
|
142
|
+
options << "type: #{type_value}" if type_value
|
|
143
|
+
|
|
144
|
+
as_value = extract_option_value(call_node, "as")
|
|
145
|
+
options << "as: #{as_value}" if as_value
|
|
146
|
+
|
|
147
|
+
default_source = extract_option_source(call_node, "default")
|
|
148
|
+
options << "default: #{default_source}" if default_source
|
|
149
|
+
|
|
150
|
+
options.any? ? options.join(", ") : nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def add_accessor_methods(name, location, comments: nil)
|
|
154
|
+
reader_signatures = [RubyIndexer::Entry::Signature.new([])]
|
|
155
|
+
@listener.add_method(name.to_s, location, reader_signatures, comments: comments)
|
|
156
|
+
|
|
157
|
+
writer_signatures = [
|
|
158
|
+
RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: :value)]),
|
|
159
|
+
]
|
|
160
|
+
@listener.add_method("#{name}=", location, writer_signatures, comments: comments)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def add_builder_methods(name, location)
|
|
164
|
+
builder_signatures = [RubyIndexer::Entry::Signature.new([])]
|
|
165
|
+
@listener.add_method("build_#{name}", location, builder_signatures)
|
|
166
|
+
@listener.add_method("create_#{name}", location, builder_signatures)
|
|
167
|
+
@listener.add_method("create_#{name}!", location, builder_signatures)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def add_singleton_method(name, node_location, owner)
|
|
171
|
+
index = @listener.instance_variable_get(:@index)
|
|
172
|
+
code_units_cache = @listener.instance_variable_get(:@code_units_cache)
|
|
173
|
+
uri = @listener.instance_variable_get(:@uri)
|
|
174
|
+
|
|
175
|
+
location = RubyIndexer::Location.from_prism_location(node_location, code_units_cache)
|
|
176
|
+
singleton = index.existing_or_new_singleton_class(owner.name)
|
|
177
|
+
signatures = [RubyIndexer::Entry::Signature.new([])]
|
|
178
|
+
|
|
179
|
+
index.add(RubyIndexer::Entry::Method.new(
|
|
180
|
+
name,
|
|
181
|
+
uri,
|
|
182
|
+
location,
|
|
183
|
+
location,
|
|
184
|
+
"",
|
|
185
|
+
signatures,
|
|
186
|
+
:public,
|
|
187
|
+
singleton,
|
|
188
|
+
))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def singularize(name)
|
|
192
|
+
name_str = name.to_s
|
|
193
|
+
if name_str.end_with?("ies")
|
|
194
|
+
name_str[0..-4] + "y"
|
|
195
|
+
elsif name_str.end_with?("s")
|
|
196
|
+
name_str[0..-2]
|
|
197
|
+
else
|
|
198
|
+
name_str
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-lsp-mongoid
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shia
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby-lsp
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.22'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.22'
|
|
26
|
+
description: A Ruby LSP Addon for Mongoid.
|
|
27
|
+
email:
|
|
28
|
+
- rise.shia@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- ".rspec"
|
|
34
|
+
- AGENTS.md
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- Rakefile
|
|
39
|
+
- TODO.md
|
|
40
|
+
- lib/ruby_lsp/ruby_lsp_mongoid.rb
|
|
41
|
+
- lib/ruby_lsp/ruby_lsp_mongoid/addon.rb
|
|
42
|
+
- lib/ruby_lsp/ruby_lsp_mongoid/hover.rb
|
|
43
|
+
- lib/ruby_lsp/ruby_lsp_mongoid/indexing_enhancement.rb
|
|
44
|
+
- lib/ruby_lsp_mongoid/version.rb
|
|
45
|
+
homepage: https://github.com/riseshia/ruby-lsp-mongoid
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
homepage_uri: https://github.com/riseshia/ruby-lsp-mongoid
|
|
50
|
+
source_code_uri: https://github.com/riseshia/ruby-lsp-mongoid
|
|
51
|
+
changelog_uri: https://github.com/riseshia/ruby-lsp-mongoid/blob/main/CHANGELOG.md
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 3.3.0
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 3.6.9
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: A Ruby LSP Addon for Mongoid
|
|
69
|
+
test_files: []
|