ruby-lsp-rails-partial 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/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/addon.rb +90 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/completion.rb +65 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/definition.rb +61 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/hover.rb +57 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/partial_index.rb +85 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/partial_resolver.rb +82 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/util.rb +73 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/version.rb +7 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b2262132a35328b7bb8515a4cef737de185fc42aa2daf334685655721f9e1357
|
|
4
|
+
data.tar.gz: 6cef6d521642ca0e9a9e1b01343c6dd820c9c5eebab3b2297a40d7dd4fee961a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ec7ab69646adadc23d3d9eccc5768257d364994c8f5a542b06680f4dd7a344a39d3bf8b93ead06b05b5a1cb12fc3db05d998b64d6a673fbb47d049f82af93481
|
|
7
|
+
data.tar.gz: f0396414c88f72616c91926db9205051951492742c79c54bf25b883e656657cf7930c4d0bad3d6843d35fa7af8abf4a048e6aec089a77073e9237eacde23b8eb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release.
|
|
15
|
+
- Go to definition from a Rails `render` partial name string to the matching partial file.
|
|
16
|
+
- Completion for partial names while typing inside the string.
|
|
17
|
+
- Hover showing the list of resolved partial files as Markdown links.
|
|
18
|
+
|
|
19
|
+
[Unreleased]: https://github.com/aki77/ruby-lsp-rails-partial/compare/v0.1.0...HEAD
|
|
20
|
+
[0.1.0]: https://github.com/aki77/ruby-lsp-rails-partial/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aki77
|
|
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,45 @@
|
|
|
1
|
+
# ruby-lsp-rails-partial
|
|
2
|
+
|
|
3
|
+
[](https://github.com/aki77/ruby-lsp-rails-partial/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A [Ruby LSP](https://github.com/Shopify/ruby-lsp) add-on that understands the partial name string
|
|
6
|
+
of Rails `render` calls and provides:
|
|
7
|
+
|
|
8
|
+
- **Go to definition** — jump from `render 'admin/areas/form'` (or `render partial: '...'`) to the
|
|
9
|
+
matching partial file. When multiple formats match, all candidates are offered.
|
|
10
|
+
- **Completion** — complete partial names while typing inside the string. Partials in the same
|
|
11
|
+
directory are offered as bare names; others use the path form.
|
|
12
|
+
- **Hover** — show the list of resolved partial files as Markdown links.
|
|
13
|
+
|
|
14
|
+
Bare names (`render 'form'`) are resolved relative to the current view's directory, or to the
|
|
15
|
+
controller name when called from a controller. Locale and variant qualifiers
|
|
16
|
+
(`_form.en.html.erb`, `_form.html+phone.erb`) are treated as the same partial.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add the gem to your application's `Gemfile`:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
group :development do
|
|
24
|
+
gem 'ruby-lsp-rails-partial', require: false
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then run `bundle install` and restart the Ruby LSP server in your editor. The add-on is discovered
|
|
29
|
+
automatically by Ruby LSP via the `lib/ruby_lsp/**/addon.rb` convention.
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
The view root is fixed to `app/views`. `prepend_view_path` / `append_view_path` and independent
|
|
34
|
+
engines are not supported.
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bundle install
|
|
40
|
+
bundle exec rake test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
require_relative "util"
|
|
5
|
+
require_relative "partial_index"
|
|
6
|
+
require_relative "partial_resolver"
|
|
7
|
+
require_relative "definition"
|
|
8
|
+
require_relative "completion"
|
|
9
|
+
require_relative "hover"
|
|
10
|
+
|
|
11
|
+
module RubyLsp
|
|
12
|
+
module RailsPartial
|
|
13
|
+
# Ruby LSP add-on that provides go-to-definition and completion for the partial name string of
|
|
14
|
+
# a render call.
|
|
15
|
+
#
|
|
16
|
+
# The view root is fixed to app/views (this add-on does not support prepend/append_view_path or
|
|
17
|
+
# independent engines).
|
|
18
|
+
class Addon < ::RubyLsp::Addon
|
|
19
|
+
WATCHER_REGISTRATION_ID = "rails-partial-watcher"
|
|
20
|
+
|
|
21
|
+
def activate(global_state, outgoing_queue)
|
|
22
|
+
views_path = File.join(global_state.workspace_path, "app", "views")
|
|
23
|
+
@index = PartialIndex.new(views_path)
|
|
24
|
+
@resolver = PartialResolver.new(@index, views_path)
|
|
25
|
+
|
|
26
|
+
register_file_watcher(global_state, outgoing_queue)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def deactivate; end
|
|
30
|
+
|
|
31
|
+
def name
|
|
32
|
+
"Rails Partial"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def version
|
|
36
|
+
VERSION
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
40
|
+
return unless @resolver
|
|
41
|
+
|
|
42
|
+
Definition.new(response_builder, uri, node_context, @resolver, dispatcher)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_completion_listener(response_builder, node_context, dispatcher, uri)
|
|
46
|
+
return unless @resolver
|
|
47
|
+
|
|
48
|
+
Completion.new(response_builder, node_context, @resolver, uri, dispatcher)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_hover_listener(response_builder, node_context, dispatcher)
|
|
52
|
+
return unless @resolver
|
|
53
|
+
|
|
54
|
+
Hover.new(response_builder, node_context, @resolver, dispatcher)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Watches partial changes under app/views and updates the index incrementally.
|
|
58
|
+
def workspace_did_change_watched_files(changes)
|
|
59
|
+
return unless @index
|
|
60
|
+
|
|
61
|
+
changes.each do |change|
|
|
62
|
+
path = Util.uri_to_path(change[:uri])
|
|
63
|
+
next unless path
|
|
64
|
+
|
|
65
|
+
case change[:type]
|
|
66
|
+
when Constant::FileChangeType::CREATED, Constant::FileChangeType::CHANGED
|
|
67
|
+
@index.add(path)
|
|
68
|
+
when Constant::FileChangeType::DELETED
|
|
69
|
+
@index.remove(path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def register_file_watcher(global_state, outgoing_queue)
|
|
77
|
+
return unless global_state.supports_watching_files
|
|
78
|
+
|
|
79
|
+
outgoing_queue << Request.register_watched_files(
|
|
80
|
+
1,
|
|
81
|
+
Interface::RelativePattern.new(
|
|
82
|
+
base_uri: global_state.workspace_uri.to_s,
|
|
83
|
+
pattern: "app/views/**/_*"
|
|
84
|
+
),
|
|
85
|
+
registration_id: WATCHER_REGISTRATION_ID
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RailsPartial
|
|
5
|
+
# Completes partial names while typing the partial name string of a render call.
|
|
6
|
+
# Candidates are always offered in path form (e.g. "admin/areas/form").
|
|
7
|
+
#
|
|
8
|
+
# NOTE: A completion request dispatches only the single node directly under the cursor, and the
|
|
9
|
+
# target node types do not include StringNode. When the cursor is inside the string, what gets
|
|
10
|
+
# dispatched is the enclosing CallNode (render), so this is handled in `on_call_node_enter`
|
|
11
|
+
# rather than `on_string_node_enter`.
|
|
12
|
+
class Completion
|
|
13
|
+
include Requests::Support::Common
|
|
14
|
+
|
|
15
|
+
# uri is part of the fixed listener signature but unused: candidates do not depend on the
|
|
16
|
+
# current file's directory (always path form).
|
|
17
|
+
def initialize(response_builder, node_context, resolver, _uri, dispatcher)
|
|
18
|
+
@response_builder = response_builder
|
|
19
|
+
@node_context = node_context
|
|
20
|
+
@resolver = resolver
|
|
21
|
+
|
|
22
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_call_node_enter(node)
|
|
26
|
+
return unless Util.render_call?(node)
|
|
27
|
+
|
|
28
|
+
string_node = Util.partial_string_argument(node)
|
|
29
|
+
return unless string_node
|
|
30
|
+
|
|
31
|
+
edit_range = content_range(string_node)
|
|
32
|
+
prefix = string_node.unescaped
|
|
33
|
+
|
|
34
|
+
@resolver.completion_candidates.each do |key, path|
|
|
35
|
+
# label, new_text, and the implicit filter_text (defaults to label) are all the path-form
|
|
36
|
+
# key, so the item survives the client-side filter regardless of how the user typed the
|
|
37
|
+
# reference. Mirrors ruby-lsp's require completion (build_completion).
|
|
38
|
+
next unless key.start_with?(prefix)
|
|
39
|
+
|
|
40
|
+
@response_builder << Interface::CompletionItem.new(
|
|
41
|
+
label: key,
|
|
42
|
+
kind: Constant::CompletionItemKind::FILE,
|
|
43
|
+
text_edit: Interface::TextEdit.new(range: edit_range, new_text: key),
|
|
44
|
+
detail: relative_detail(path)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Range pointing at the inside of the string node's quotes. Falls back to the whole node if
|
|
52
|
+
# content_loc is absent.
|
|
53
|
+
def content_range(node)
|
|
54
|
+
location = node.content_loc || node.location
|
|
55
|
+
range_from_location(location)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def relative_detail(path)
|
|
59
|
+
return unless path
|
|
60
|
+
|
|
61
|
+
Util.relative_to_views(path)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RailsPartial
|
|
5
|
+
# When the cursor is on the partial name string of a render call, provides go-to-definition
|
|
6
|
+
# to the matching partial file. When multiple formats match, returns all Locations and lets
|
|
7
|
+
# ruby-lsp present the candidate picker.
|
|
8
|
+
class Definition
|
|
9
|
+
include Requests::Support::Common
|
|
10
|
+
|
|
11
|
+
def initialize(response_builder, uri, node_context, resolver, dispatcher)
|
|
12
|
+
@response_builder = response_builder
|
|
13
|
+
@uri = uri
|
|
14
|
+
@node_context = node_context
|
|
15
|
+
@resolver = resolver
|
|
16
|
+
|
|
17
|
+
dispatcher.register(self, :on_string_node_enter)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_string_node_enter(node)
|
|
21
|
+
return unless render_partial_argument?(node)
|
|
22
|
+
|
|
23
|
+
reference = node.unescaped
|
|
24
|
+
return if reference.empty?
|
|
25
|
+
|
|
26
|
+
# Highlight range of the origin. Pointing at the whole partial name (inside the quotes)
|
|
27
|
+
# makes the entire string a link even for names containing `/` like `admin/areas/form`.
|
|
28
|
+
origin_range = range_from_location(node.content_loc || node.location)
|
|
29
|
+
|
|
30
|
+
# The partial body is not parsed, so a leading zero-width range is reused as the target for
|
|
31
|
+
# all candidates. target_selection_range must be contained within target_range, so both are
|
|
32
|
+
# made identical.
|
|
33
|
+
target_range = Interface::Range.new(
|
|
34
|
+
start: Interface::Position.new(line: 0, character: 0),
|
|
35
|
+
end: Interface::Position.new(line: 0, character: 0)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@resolver.resolve(reference, @uri.to_s).each do |path|
|
|
39
|
+
@response_builder << Interface::LocationLink.new(
|
|
40
|
+
target_uri: URI::Generic.from_path(path:).to_s,
|
|
41
|
+
target_range:,
|
|
42
|
+
target_selection_range: target_range,
|
|
43
|
+
origin_selection_range: origin_range
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Whether this string node is the render call's partial argument (the first positional
|
|
51
|
+
# argument or the value of `partial:`). Decided by whether the StringNode under the cursor
|
|
52
|
+
# is identical to that render call's partial argument node.
|
|
53
|
+
def render_partial_argument?(node)
|
|
54
|
+
call_node = @node_context.call_node
|
|
55
|
+
return false unless call_node && Util.render_call?(call_node)
|
|
56
|
+
|
|
57
|
+
Util.partial_string_argument(call_node).equal?(node)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RailsPartial
|
|
5
|
+
# When the cursor is on the partial name string of a render call, shows the list of resolved
|
|
6
|
+
# file paths in a tooltip (Markdown).
|
|
7
|
+
#
|
|
8
|
+
# NOTE: A hover request does not pass a uri to the listener (unlike definition/completion).
|
|
9
|
+
# A bare name `render 'form'` needs a uri to infer the current directory, so it is out of
|
|
10
|
+
# scope; only slash-qualified names like `render 'admin/areas/form'` are resolved.
|
|
11
|
+
class Hover
|
|
12
|
+
def initialize(response_builder, node_context, resolver, dispatcher)
|
|
13
|
+
@response_builder = response_builder
|
|
14
|
+
@node_context = node_context
|
|
15
|
+
@resolver = resolver
|
|
16
|
+
|
|
17
|
+
dispatcher.register(self, :on_string_node_enter)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_string_node_enter(node)
|
|
21
|
+
return unless render_partial_argument?(node)
|
|
22
|
+
|
|
23
|
+
reference = node.unescaped
|
|
24
|
+
return if reference.empty?
|
|
25
|
+
|
|
26
|
+
paths = @resolver.resolve_without_context(reference)
|
|
27
|
+
return if paths.empty?
|
|
28
|
+
|
|
29
|
+
@response_builder.push("**Rails partial: `#{reference}`**", category: :title)
|
|
30
|
+
@response_builder.push(links_markdown(paths), category: :links)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Renders each resolved file as a Markdown bullet link with its view-root-relative path.
|
|
36
|
+
def links_markdown(paths)
|
|
37
|
+
paths.sort.map { |path| link_line(path) }.join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def link_line(path)
|
|
41
|
+
uri = URI::Generic.from_path(path:).to_s
|
|
42
|
+
"- [#{Util.relative_to_views(path)}](#{uri})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether this string node is the render call's partial argument.
|
|
46
|
+
# This duplicates Definition's logic, but it is intentionally re-stated here so that the
|
|
47
|
+
# behavior can be followed within a single file (explicit duplication over implicit
|
|
48
|
+
# abstraction).
|
|
49
|
+
def render_partial_argument?(node)
|
|
50
|
+
call_node = @node_context.call_node
|
|
51
|
+
return false unless call_node && Util.render_call?(call_node)
|
|
52
|
+
|
|
53
|
+
Util.partial_string_argument(call_node).equal?(node)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RailsPartial
|
|
5
|
+
# In-memory index of partials (`_*.*`) under the view root.
|
|
6
|
+
#
|
|
7
|
+
# ## Key normalization
|
|
8
|
+
# The path relative to the view root is normalized to "directory + basename without the
|
|
9
|
+
# leading underscore". All qualifiers that follow the partial name (format, handler, locale,
|
|
10
|
+
# variant) are dropped.
|
|
11
|
+
# app/views/admin/areas/_form.html.erb -> "admin/areas/form"
|
|
12
|
+
# app/views/admin/areas/_form.en.html.erb -> "admin/areas/form" (locale variant shares the key)
|
|
13
|
+
# app/views/admin/areas/_form.html+phone.erb -> "admin/areas/form" (variant shares the key)
|
|
14
|
+
# Since multiple formats can map to the same key, the value is always an array of file paths.
|
|
15
|
+
#
|
|
16
|
+
# The index is updated incrementally via file-watching events rather than re-globbing on every
|
|
17
|
+
# request (performance requirement).
|
|
18
|
+
class PartialIndex
|
|
19
|
+
def initialize(views_path)
|
|
20
|
+
@views_path = views_path
|
|
21
|
+
# normalized key => Array[absolute path]
|
|
22
|
+
@entries = Hash.new { |hash, key| hash[key] = [] }
|
|
23
|
+
build
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the array of partial file paths for the normalized key, or an empty array if none.
|
|
27
|
+
def lookup(key)
|
|
28
|
+
@entries.fetch(key, [])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def all_entries
|
|
32
|
+
@entries
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Incremental update from a file-watching event. Paths that are not partials are ignored.
|
|
36
|
+
def add(absolute_path)
|
|
37
|
+
key = key_for(absolute_path)
|
|
38
|
+
return unless key
|
|
39
|
+
|
|
40
|
+
paths = @entries[key]
|
|
41
|
+
paths << absolute_path unless paths.include?(absolute_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remove(absolute_path)
|
|
45
|
+
key = key_for(absolute_path)
|
|
46
|
+
return unless key
|
|
47
|
+
|
|
48
|
+
paths = @entries[key]
|
|
49
|
+
paths.delete(absolute_path)
|
|
50
|
+
@entries.delete(key) if paths.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build
|
|
56
|
+
Dir.glob(File.join(@views_path, "**", "_*.*")).each { |path| add(path) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Converts an absolute path to a normalized key. Returns nil unless it is a partial
|
|
60
|
+
# (basename starts with `_`).
|
|
61
|
+
def key_for(absolute_path)
|
|
62
|
+
relative = relative_from_views(absolute_path)
|
|
63
|
+
return unless relative
|
|
64
|
+
|
|
65
|
+
dir = File.dirname(relative)
|
|
66
|
+
basename = File.basename(relative)
|
|
67
|
+
return unless basename.start_with?("_")
|
|
68
|
+
|
|
69
|
+
# Strip the leading `_` and drop everything after the first `.`
|
|
70
|
+
# (format/handler/locale/variant).
|
|
71
|
+
name = basename[1..].split(".").first
|
|
72
|
+
return unless name
|
|
73
|
+
|
|
74
|
+
dir == "." ? name : "#{dir}/#{name}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def relative_from_views(absolute_path)
|
|
78
|
+
prefix = "#{@views_path}/"
|
|
79
|
+
return unless absolute_path.start_with?(prefix)
|
|
80
|
+
|
|
81
|
+
absolute_path[prefix.length..]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module RailsPartial
|
|
5
|
+
# Resolves the partial reference string of a `render` call to a PartialIndex key.
|
|
6
|
+
# Because resolution depends on the reference form and on the context of "which file the
|
|
7
|
+
# render is called from", this class is responsible only for bridging to the index's
|
|
8
|
+
# normalized key.
|
|
9
|
+
class PartialResolver
|
|
10
|
+
def initialize(index, views_path)
|
|
11
|
+
@index = index
|
|
12
|
+
@views_path = views_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Resolves a partial reference string ("foo" / "dir/foo") in the context of the current file
|
|
16
|
+
# and returns the array of absolute paths of the matching partial files.
|
|
17
|
+
def resolve(reference, current_uri)
|
|
18
|
+
key = key_for(reference, current_uri)
|
|
19
|
+
return [] unless key
|
|
20
|
+
|
|
21
|
+
@index.lookup(key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# For contexts without a uri (hover). Only resolves references that contain a slash.
|
|
25
|
+
# Bare names are out of scope (they need a uri for directory inference) and return [].
|
|
26
|
+
def resolve_without_context(reference)
|
|
27
|
+
return [] unless reference.include?("/")
|
|
28
|
+
|
|
29
|
+
@index.lookup(reference)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns completion candidates as an array of [key (path form), representative path].
|
|
33
|
+
# The key is always the normalized path form so that the display label, filter text, and
|
|
34
|
+
# inserted text all match, which keeps the item past the client-side filter regardless of
|
|
35
|
+
# how the user typed the reference.
|
|
36
|
+
def completion_candidates
|
|
37
|
+
@index.all_entries.map { |key, paths| [key, paths.first] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Converts a reference string to a normalized key.
|
|
43
|
+
# A slash means root-relative; otherwise it is relative to the current file's directory.
|
|
44
|
+
def key_for(reference, current_uri)
|
|
45
|
+
return if reference.empty?
|
|
46
|
+
|
|
47
|
+
if reference.include?("/")
|
|
48
|
+
reference
|
|
49
|
+
else
|
|
50
|
+
dir = current_dir(current_uri)
|
|
51
|
+
dir ? "#{dir}/#{reference}" : reference
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Derives the view-root-relative directory from the current file's URI.
|
|
56
|
+
# A view (under the view root) uses its own directory; a controller derives it from the
|
|
57
|
+
# controller name.
|
|
58
|
+
def current_dir(current_uri)
|
|
59
|
+
path = Util.uri_to_path(current_uri)
|
|
60
|
+
return unless path
|
|
61
|
+
|
|
62
|
+
if path.start_with?("#{@views_path}/")
|
|
63
|
+
relative = path[(@views_path.length + 1)..]
|
|
64
|
+
dir = File.dirname(relative)
|
|
65
|
+
dir == "." ? nil : dir
|
|
66
|
+
elsif path.include?("/app/controllers/")
|
|
67
|
+
controller_dir_from(path)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Derives the view prefix from a controller file path.
|
|
72
|
+
# app/controllers/admin/areas_controller.rb -> "admin/areas"
|
|
73
|
+
def controller_dir_from(path)
|
|
74
|
+
relative = path.split("/app/controllers/", 2).last
|
|
75
|
+
return unless relative
|
|
76
|
+
|
|
77
|
+
dir = relative.delete_suffix("_controller.rb")
|
|
78
|
+
dir.empty? ? nil : dir
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module RailsPartial
|
|
7
|
+
# AST inspection / path conversion helpers. Shared by the Definition / Completion / Hover
|
|
8
|
+
# listeners and by Addon / PartialResolver.
|
|
9
|
+
module Util
|
|
10
|
+
RENDER_METHODS = %i[render render_to_string].freeze
|
|
11
|
+
VIEWS_MARKER = "/app/views/"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Converts a file:// URI to a filesystem path. Non-file:// URIs are returned as-is.
|
|
15
|
+
def uri_to_path(uri)
|
|
16
|
+
return uri unless uri.start_with?("file://")
|
|
17
|
+
|
|
18
|
+
URI.parse(uri).path
|
|
19
|
+
rescue URI::InvalidURIError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the path relative to the app/views root. Falls back to the basename if the path
|
|
24
|
+
# is not under views.
|
|
25
|
+
def relative_to_views(path)
|
|
26
|
+
index = path.index(VIEWS_MARKER)
|
|
27
|
+
index ? path[(index + VIEWS_MARKER.length)..] : File.basename(path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether call_node is a render call with no receiver (or self).
|
|
31
|
+
def render_call?(call_node)
|
|
32
|
+
return false unless RENDER_METHODS.include?(call_node.message&.to_sym)
|
|
33
|
+
|
|
34
|
+
receiver = call_node.receiver
|
|
35
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the string node of the render call's partial argument.
|
|
39
|
+
# The target is (1) the first positional argument, or (2) the value of the `partial:`
|
|
40
|
+
# keyword. Returns nil if neither is present.
|
|
41
|
+
def partial_string_argument(call_node)
|
|
42
|
+
arguments = call_node.arguments&.arguments
|
|
43
|
+
return unless arguments&.any?
|
|
44
|
+
|
|
45
|
+
first = arguments.first
|
|
46
|
+
return first if first.is_a?(Prism::StringNode)
|
|
47
|
+
|
|
48
|
+
partial_keyword_value(arguments)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Returns the string node for the `partial:` keyword.
|
|
54
|
+
def partial_keyword_value(arguments)
|
|
55
|
+
keyword_hash = arguments.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
|
|
56
|
+
return unless keyword_hash
|
|
57
|
+
|
|
58
|
+
assoc =
|
|
59
|
+
keyword_hash.elements.find do |element|
|
|
60
|
+
element.is_a?(Prism::AssocNode) &&
|
|
61
|
+
partial_key?(element.key) &&
|
|
62
|
+
element.value.is_a?(Prism::StringNode)
|
|
63
|
+
end
|
|
64
|
+
assoc&.value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def partial_key?(key)
|
|
68
|
+
key.is_a?(Prism::SymbolNode) && key.unescaped == "partial"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-lsp-rails-partial
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- aki77
|
|
8
|
+
bindir: bin
|
|
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.26.0
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.27.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 0.26.0
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.27.0
|
|
32
|
+
description: Provides go-to-definition, completion, and hover for the partial name
|
|
33
|
+
string of Rails render calls.
|
|
34
|
+
email:
|
|
35
|
+
- 163168+aki77@users.noreply.github.com
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/addon.rb
|
|
44
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/completion.rb
|
|
45
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/definition.rb
|
|
46
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/hover.rb
|
|
47
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/partial_index.rb
|
|
48
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/partial_resolver.rb
|
|
49
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/util.rb
|
|
50
|
+
- lib/ruby_lsp/ruby_lsp_rails_partial/version.rb
|
|
51
|
+
homepage: https://github.com/aki77/ruby-lsp-rails-partial
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
source_code_uri: https://github.com/aki77/ruby-lsp-rails-partial
|
|
56
|
+
changelog_uri: https://github.com/aki77/ruby-lsp-rails-partial/blob/main/CHANGELOG.md
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '3.4'
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 4.0.10
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Ruby LSP add-on for Rails partials
|
|
75
|
+
test_files: []
|