ruby-lsp-view_component 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f750624290fc729f716c2bb2c20b13d56baa7fad7878014e795342cbdb61f148
4
- data.tar.gz: 3b28ec82ce7c86cddf56b35595f472161b526cb0eb57ee2fb11204edb4355d24
3
+ metadata.gz: b6a116e228f6b21375f00e88b1d1f00f11013c9f132e381edc5a08461efd74f5
4
+ data.tar.gz: 523da12a2ef3e17ba2b72c61218cb87c97af8e1c3440a0a7612e39aa14ff2a47
5
5
  SHA512:
6
- metadata.gz: 4738e8c1e9240ac1444aebdbf7f5ddb93df0d0374604811c234b36f5a14334b51d8638d813460e5f1bc66fe088e1233faaa38febaabbde17649f105079f39f77
7
- data.tar.gz: 2e47b3801dde3a15a8757f3e767282a25bf0038d794a81dc5ab3b246b5b77cc2a8d870d07b7af9b01902ecf4f69f628385a1a95c8ab6e74ac0da9c29e3ce6030
6
+ metadata.gz: 642cef99fc71c8d9536c83f9f6bd17f88a5c83d36e1dc08f73b768ac7b38c61df40b8216cf35f5e6b43c9d3740f81d5d189f7cb4470d8ca21e00f132a3438e3b
7
+ data.tar.gz: 0a52380b179d23b4f6cd41de1873533990085aa6538761ca90752dab8605aa6b948c46eb35d71ec36016740fa242954f34bc49fa28e343bc80bcb5e75490f5ae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0]
4
+
5
+ - `Jump to template` also shows on components that need a template and have none. It opens the missing `.html.erb` next to the class, and VS Code offers to create it.
6
+
3
7
  ## [0.1.0]
4
8
 
5
9
  - `Jump to template` CodeLens on ViewComponent classes. It opens the component template, or a picker when the component has variants or sub-templates.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ruby-lsp-view_component
2
2
 
3
- A [Ruby LSP](https://github.com/Shopify/ruby-lsp) add-on for [ViewComponent](https://viewcomponent.org). It adds a `Jump to template` CodeLens to each component class that opens the component's template.
3
+ A [Ruby LSP](https://github.com/Shopify/ruby-lsp) add-on for [ViewComponent](https://viewcomponent.org). It adds a `Jump to template` CodeLens to each component class that opens the component's template, or lets you create it when it is missing.
4
4
 
5
5
  ## Features
6
6
 
@@ -14,18 +14,31 @@ When the component has more than one template, the click opens a picker with the
14
14
 
15
15
  ![Pick a template](.github/assets/pick_template.gif)
16
16
 
17
- For `app/components/bar_component.rb`, the lens lists:
17
+ For `app/components/foo_component.rb` with all of these templates, the lens lists:
18
18
 
19
19
  | File | What it is |
20
20
  |---|---|
21
- | `bar_component.html.erb` | [Main template](https://viewcomponent.org/guide/templates.html#sibling-file) |
22
- | `bar_component.html+phone.erb` | Variant of the main template |
23
- | `bar_component/bar_component.html.erb` | [Main template in a subdirectory](https://viewcomponent.org/guide/templates.html#subdirectory) |
24
- | `bar_component/list.html.erb` | Sub-template rendered with `call_list`, from [view_component_subtemplates](https://github.com/bukhr/view_component_subtemplates) |
21
+ | `foo_component.html.erb` | [Main template](https://viewcomponent.org/guide/templates.html#sibling-file) |
22
+ | `foo_component.html+phone.erb` | Variant of the main template |
23
+ | `foo_component/foo_component.html.erb` | [Main template in a subdirectory](https://viewcomponent.org/guide/templates.html#subdirectory) |
24
+ | `foo_component/list.html.erb` | Sub-template rendered with `call_list`, from [view_component_subtemplates](https://github.com/bukhr/view_component_subtemplates) |
25
25
 
26
- ERB, Haml and Slim templates are supported. Templates of nested components in the subdirectory, such as `bar_component/item_component.html.erb` next to `bar_component/item_component.rb`, belong to that component and are left out.
26
+ ERB, Haml and Slim templates are supported. Templates of nested components in the subdirectory, such as `foo_component/item_component.html.erb` next to `foo_component/item_component.rb`, belong to that component and are left out.
27
27
 
28
- Components without a template file of their own get no lens. That covers [inline](https://viewcomponent.org/guide/templates.html#inline) and [`call`](https://viewcomponent.org/guide/templates.html#call) components, and components that [inherit](https://viewcomponent.org/guide/templates.html#inherited) their template from a parent class.
28
+ ### Missing template
29
+
30
+ When a component needs a template and has none, the lens opens `foo_component.html.erb` next to the class. VS Code reports that the file does not exist and offers a **Create File** button. Once the file exists, the same lens opens it.
31
+
32
+ ![Create a missing template](.github/assets/create_template.gif)
33
+
34
+ A component counts as needing a template only when it renders through a template file. There is no lens for components that:
35
+
36
+ - define [`call`](https://viewcomponent.org/guide/templates.html#call), themselves or in a parent class
37
+ - declare an [inline template](https://viewcomponent.org/guide/templates.html#inline) with `erb_template`, `haml_template` or `slim_template`
38
+ - [inherit](https://viewcomponent.org/guide/templates.html#inherited) a template from a parent class
39
+ - are marked `abstract!`
40
+
41
+ A base class that is never rendered on its own still gets the lens. Mark it `abstract!` to hide it.
29
42
 
30
43
  ## Installation
31
44
 
@@ -11,7 +11,9 @@ module RubyLsp
11
11
  class Addon < ::RubyLsp::Addon
12
12
  # @override
13
13
  #: (GlobalState, Thread::Queue) -> void
14
- def activate(global_state, outgoing_queue); end
14
+ def activate(global_state, outgoing_queue)
15
+ @index = global_state.index #: RubyIndexer::Index?
16
+ end
15
17
 
16
18
  # @override
17
19
  #: -> void
@@ -32,7 +34,8 @@ module RubyLsp
32
34
  # @override
33
35
  #: (ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens], URI::Generic, Prism::Dispatcher) -> void
34
36
  def create_code_lens_listener(response_builder, uri, dispatcher)
35
- CodeLens.new(response_builder, uri, dispatcher)
37
+ index = @index #: as !nil
38
+ CodeLens.new(response_builder, uri, dispatcher, index)
36
39
  end
37
40
  end
38
41
  end
@@ -6,37 +6,78 @@ module RubyLsp
6
6
  include Requests::Support::Common
7
7
 
8
8
  TEMPLATE_EXTENSIONS = "{erb,haml,slim}"
9
+ NO_TEMPLATE_CALLS = [:abstract!, :erb_template, :haml_template, :slim_template].freeze
9
10
 
10
- #: (ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens], URI::Generic, Prism::Dispatcher) -> void
11
- def initialize(response_builder, uri, dispatcher)
11
+ #: (ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens], URI::Generic, Prism::Dispatcher, RubyIndexer::Index) -> void
12
+ def initialize(response_builder, uri, dispatcher, index)
12
13
  @response_builder = response_builder
14
+ @index = index
13
15
  @path = uri.to_standardized_path #: String?
14
- @seen_class = false
15
- dispatcher.register(self, :on_class_node_enter) if @path&.end_with?(".rb")
16
+ @nesting = [] #: Array[String]
17
+ @class_node = nil #: Prism::ClassNode?
18
+ return unless @path&.end_with?(".rb")
19
+
20
+ dispatcher.register(self, :on_module_node_enter, :on_module_node_leave, :on_class_node_enter, :on_class_node_leave)
21
+ end
22
+
23
+ #: (Prism::ModuleNode) -> void
24
+ def on_module_node_enter(node)
25
+ @nesting << node.constant_path.slice
26
+ end
27
+
28
+ #: (Prism::ModuleNode) -> void
29
+ def on_module_node_leave(node)
30
+ @nesting.pop
16
31
  end
17
32
 
18
33
  #: (Prism::ClassNode) -> void
19
34
  def on_class_node_enter(node)
20
- return if @seen_class
35
+ @class_node ||= node
36
+ end
21
37
 
22
- @seen_class = true
23
- uris = templates.map { |path| URI::Generic.from_path(path: path).to_s }
24
- return if uris.empty?
38
+ #: (Prism::ClassNode) -> void
39
+ def on_class_node_leave(node)
40
+ return unless node.equal?(@class_node)
41
+
42
+ path = @path #: as !nil
43
+ paths = templates(path)
44
+ paths = ["#{path.delete_suffix(".rb")}.html.erb"] if paths.empty? && needs_template?(node)
45
+ return if paths.empty?
25
46
 
26
47
  @response_builder << create_code_lens(
27
48
  node,
28
49
  title: "Jump to template",
29
50
  command_name: "rubyLsp.openFile",
30
- arguments: [uris],
51
+ arguments: [paths.map { |path| URI::Generic.from_path(path: path).to_s }],
31
52
  data: { type: "file" },
32
53
  )
33
54
  end
34
55
 
35
56
  private
36
57
 
37
- #: -> Array[String]
38
- def templates
39
- path = @path #: as !nil
58
+ #: (Prism::ClassNode) -> bool
59
+ def needs_template?(node)
60
+ name = [*@nesting, node.constant_path.slice].join("::").delete_prefix("::")
61
+ ancestors = @index.linearized_ancestors_of(name)
62
+ base = ancestors.index("ViewComponent::Base")
63
+ return false unless base
64
+ return false if @index.resolve_method("call", name)
65
+ return false if (ancestors.take(base) - [name]).any? { |ancestor| inherited_template?(ancestor) }
66
+
67
+ Array(node.body&.child_nodes).none? do |child|
68
+ child.is_a?(Prism::CallNode) && child.receiver.nil? && NO_TEMPLATE_CALLS.include?(child.name)
69
+ end
70
+ rescue RubyIndexer::Index::NonExistingNamespaceError
71
+ false
72
+ end
73
+
74
+ #: (String) -> bool
75
+ def inherited_template?(ancestor)
76
+ @index[ancestor]&.any? { |entry| entry.file_path && templates(entry.file_path).any? } || false
77
+ end
78
+
79
+ #: (String) -> Array[String]
80
+ def templates(path)
40
81
  dir = File.dirname(path)
41
82
  name = File.basename(path, ".rb")
42
83
  Dir.glob(["#{name}.*#{TEMPLATE_EXTENSIONS}", "#{name}/*.#{TEMPLATE_EXTENSIONS}"], base: dir)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLsp
4
4
  module RubyLspViewComponent
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Retamal