ruby-lsp-rails 0.3.8 → 0.3.10

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: accc5f9539ddceaa7fde3dfdb64fcc74f697e9d17178c45e9271df463cad4f18
4
- data.tar.gz: d594ee73741270b0d6837680a5bacc6e912c1ca856aeaa5806e7d1ad9e96aeb8
3
+ metadata.gz: f90b7d238d437e9ad3c29cad137e14e3094340243802f3d0f5cb7ddf68e582e1
4
+ data.tar.gz: cb64d09552cf7a0862ceeb8fb27c0c2b40ba596a267bec6aebdaf3ab2db139a4
5
5
  SHA512:
6
- metadata.gz: 955347d73c343311571e62d7c0bb656a3cc9176f8e6ff87ab010287345d754b49f1889c436c716ce0a40c2aa45938f094124cd18315f787c854db7aed2172ee1
7
- data.tar.gz: 65731a4414f5b68884ea6003097b5b1e67caaaaf2a954b0d25de0ed937cc7600d1b1e4e8d2c25a28b25f526c72fc05e6f9a1996874f95584c4450a4212ef9269
6
+ metadata.gz: 8721f1e2bb8a2fc0acff6a3caab8b216c0225b9fdd69eb883944b10d0159619d35c501b7d3761b9fb086e0662e082dedc28c759cca3481d65ea97b3dae157e23
7
+ data.tar.gz: 7c329b8da4692954f7382069f8f046672675192536c79ee3c58f0239ee87b82c5aa7083b1e786dc3827a713b9e08232d7c8e92d8513bb2f89a37e97957116070
data/README.md CHANGED
@@ -9,6 +9,8 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
9
9
  * Navigate to associations, validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
10
10
  * Jump to the definition of callbacks using your editor's "Go to Definition" feature.
11
11
  * Jump to the declaration of a route.
12
+ * Code Lens allowing fast-forwarding or rewinding of migrations.
13
+ * Code Lens showing the path that a route action corresponds to.
12
14
 
13
15
  ## Installation
14
16
 
@@ -53,7 +53,7 @@ module RubyLsp
53
53
  def create_code_lens_listener(response_builder, uri, dispatcher)
54
54
  return unless T.must(@global_state).test_library == "rails"
55
55
 
56
- CodeLens.new(response_builder, uri, dispatcher)
56
+ CodeLens.new(@client, response_builder, uri, dispatcher)
57
57
  end
58
58
 
59
59
  sig do
@@ -5,11 +5,22 @@ module RubyLsp
5
5
  module Rails
6
6
  # ![CodeLens demo](../../code_lens.gif)
7
7
  #
8
- # This feature adds several CodeLens features for Rails applications using Active Support test cases:
8
+ # This feature adds Code Lens features for Rails applications.
9
+ #
10
+ # For Active Support test cases:
9
11
  #
10
12
  # - Run tests in the VS Terminal
11
13
  # - Run tests in the VS Code Test Explorer
12
14
  # - Debug tests
15
+ # - Run migrations in the VS Terminal
16
+ #
17
+ # For Rails controllers:
18
+ #
19
+ # - See the path corresponding to an action
20
+ # - Click on the action's Code Lens to jump to its declaration in the routes.
21
+ #
22
+ # Note: This depends on a support for the `rubyLsp.openFile` command.
23
+ # For the VS Code extension this is built-in, but for other editors this may require some custom configuration.
13
24
  #
14
25
  # The
15
26
  # [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
@@ -32,12 +43,34 @@ module RubyLsp
32
43
  # # ...
33
44
  # end
34
45
  # end
46
+ # ```
47
+ #
48
+ # # Example:
49
+ # ```ruby
50
+ # Run
51
+ # class AddFirstNameToUsers < ActiveRecord::Migration[7.1]
52
+ # # ...
53
+ # end
35
54
  # ````
36
55
  #
37
56
  # The code lenses will be displayed above the class and above each test method.
38
57
  #
39
58
  # Note: When using the Test Explorer view, if your code contains a statement to pause execution (e.g. `debugger`) it
40
59
  # will cause the test runner to hang.
60
+ #
61
+ # For the following code, assuming the routing contains `resources :users`, a Code Lens will be seen above each
62
+ # action.
63
+ #
64
+ # ```ruby
65
+ # class UsersController < ApplicationController
66
+ # GET /users(.:format)
67
+ # def index # <- Will show code lens above for the path
68
+ # end
69
+ # end
70
+ # ```
71
+ #
72
+ # Note: Complex routing configurations may not be supported.
73
+ #
41
74
  class CodeLens
42
75
  extend T::Sig
43
76
  include Requests::Support::Common
@@ -45,16 +78,19 @@ module RubyLsp
45
78
 
46
79
  sig do
47
80
  params(
81
+ client: RunnerClient,
48
82
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
49
83
  uri: URI::Generic,
50
84
  dispatcher: Prism::Dispatcher,
51
85
  ).void
52
86
  end
53
- def initialize(response_builder, uri, dispatcher)
87
+ def initialize(client, response_builder, uri, dispatcher)
88
+ @client = client
54
89
  @response_builder = response_builder
55
90
  @path = T.let(uri.to_standardized_path, T.nilable(String))
56
91
  @group_id = T.let(1, Integer)
57
92
  @group_id_stack = T.let([], T::Array[Integer])
93
+ @constant_name_stack = T.let([], T::Array[[String, T.nilable(String)]])
58
94
 
59
95
  dispatcher.register(self, :on_call_node_enter, :on_class_node_enter, :on_def_node_enter, :on_class_node_leave)
60
96
  end
@@ -74,41 +110,121 @@ module RubyLsp
74
110
  sig { params(node: Prism::DefNode).void }
75
111
  def on_def_node_enter(node)
76
112
  method_name = node.name.to_s
113
+
77
114
  if method_name.start_with?("test_")
78
115
  line_number = node.location.start_line
79
116
  command = "#{test_command} #{@path}:#{line_number}"
80
117
  add_test_code_lens(node, name: method_name, command: command, kind: :example)
81
118
  end
119
+
120
+ if controller?
121
+ add_route_code_lens_to_action(node)
122
+ end
82
123
  end
83
124
 
84
125
  sig { params(node: Prism::ClassNode).void }
85
126
  def on_class_node_enter(node)
86
127
  class_name = node.constant_path.slice
128
+ superclass_name = node.superclass&.slice
129
+
87
130
  if class_name.end_with?("Test")
88
131
  command = "#{test_command} #{@path}"
89
132
  add_test_code_lens(node, name: class_name, command: command, kind: :group)
90
133
  @group_id_stack.push(@group_id)
91
134
  @group_id += 1
92
135
  end
136
+
137
+ if superclass_name&.start_with?("ActiveRecord::Migration")
138
+ command = "#{migrate_command} VERSION=#{migration_version}"
139
+ add_migrate_code_lens(node, name: class_name, command: command)
140
+ end
141
+
142
+ # We need to use a stack because someone could define a nested class
143
+ # inside a controller. When we exit that nested class declaration, we are
144
+ # back in a controller context. This part is used in other places in the LSP
145
+ @constant_name_stack << [class_name, superclass_name]
93
146
  end
94
147
 
95
148
  sig { params(node: Prism::ClassNode).void }
96
149
  def on_class_node_leave(node)
97
150
  class_name = node.constant_path.slice
151
+
98
152
  if class_name.end_with?("Test")
99
153
  @group_id_stack.pop
100
154
  end
155
+
156
+ @constant_name_stack.pop
101
157
  end
102
158
 
103
159
  private
104
160
 
161
+ sig { returns(T.nilable(T::Boolean)) }
162
+ def controller?
163
+ class_name, superclass_name = @constant_name_stack.last
164
+ return false unless class_name && superclass_name
165
+
166
+ class_name.end_with?("Controller") && superclass_name.end_with?("Controller")
167
+ end
168
+
169
+ sig { params(node: Prism::DefNode).void }
170
+ def add_route_code_lens_to_action(node)
171
+ class_name, _ = T.must(@constant_name_stack.last)
172
+ route = @client.route(
173
+ controller: class_name,
174
+ action: node.name.to_s,
175
+ )
176
+
177
+ return unless route
178
+
179
+ path = route[:path]
180
+ verb = route[:verb]
181
+ source_location = route[:source_location]
182
+
183
+ arguments = [
184
+ source_location,
185
+ {
186
+ start_line: node.location.start_line - 1,
187
+ start_column: node.location.start_column,
188
+ end_line: node.location.end_line - 1,
189
+ end_column: node.location.end_column,
190
+ },
191
+ ]
192
+
193
+ @response_builder << create_code_lens(
194
+ node,
195
+ title: [verb, path].join(" "),
196
+ command_name: "rubyLsp.openFile",
197
+ arguments: arguments,
198
+ data: { type: "file" },
199
+ )
200
+ end
201
+
105
202
  sig { returns(String) }
106
203
  def test_command
107
- if Gem.win_platform?
108
- "ruby bin/rails test"
109
- else
110
- "bin/rails test"
111
- end
204
+ "#{RbConfig.ruby} bin/rails test"
205
+ end
206
+
207
+ sig { returns(String) }
208
+ def migrate_command
209
+ "#{RbConfig.ruby} bin/rails db:migrate"
210
+ end
211
+
212
+ sig { returns(T.nilable(String)) }
213
+ def migration_version
214
+ File.basename(T.must(@path)).split("_").first
215
+ end
216
+
217
+ sig { params(node: Prism::Node, name: String, command: String).void }
218
+ def add_migrate_code_lens(node, name:, command:)
219
+ return unless @path
220
+
221
+ @response_builder << create_code_lens(
222
+ node,
223
+ title: "Run",
224
+ command_name: "rubyLsp.runTask",
225
+ arguments: [command],
226
+ data: { type: "migrate" },
227
+ )
112
228
  end
113
229
 
114
230
  sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
@@ -122,6 +122,14 @@ module RubyLsp
122
122
  nil
123
123
  end
124
124
 
125
+ sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
126
+ def route(controller:, action:)
127
+ make_request("route_info", controller: controller, action: action)
128
+ rescue IncompleteMessageError
129
+ $stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
130
+ nil
131
+ end
132
+
125
133
  sig { void }
126
134
  def trigger_reload
127
135
  $stderr.puts("Reloading Rails application")
@@ -164,6 +172,8 @@ module RubyLsp
164
172
  json = message.to_json
165
173
 
166
174
  @stdin.write("Content-Length: #{json.length}\r\n\r\n", json)
175
+ rescue Errno::EPIPE
176
+ # The server connection died
167
177
  end
168
178
 
169
179
  alias_method :send_notification, :send_message
@@ -185,6 +195,9 @@ module RubyLsp
185
195
  end
186
196
 
187
197
  response.fetch(:result)
198
+ rescue Errno::EPIPE
199
+ # The server connection died
200
+ nil
188
201
  end
189
202
  end
190
203
 
@@ -54,6 +54,8 @@ module RubyLsp
54
54
  VOID
55
55
  when "route_location"
56
56
  route_location(params.fetch(:name))
57
+ when "route_info"
58
+ resolve_route_info(params)
57
59
  else
58
60
  VOID
59
61
  end
@@ -63,6 +65,32 @@ module RubyLsp
63
65
 
64
66
  private
65
67
 
68
+ def resolve_route_info(requirements)
69
+ if requirements[:controller]
70
+ requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
71
+ end
72
+
73
+ # In Rails 7.2 we can use `from_requirements, otherwise we fall back to a private API
74
+ route = if ::Rails.application.routes.respond_to?(:from_requirements)
75
+ ::Rails.application.routes.from_requirements(requirements)
76
+ else
77
+ ::Rails.application.routes.routes.find { |route| route.requirements == requirements }
78
+ end
79
+
80
+ if route&.source_location
81
+ file, _, line = route.source_location.rpartition(":")
82
+ body = {
83
+ source_location: [::Rails.root.join(file).to_s, line],
84
+ verb: route.verb,
85
+ path: route.path.spec.to_s,
86
+ }
87
+
88
+ { result: body }
89
+ else
90
+ { result: nil }
91
+ end
92
+ end
93
+
66
94
  # Older versions of Rails don't support `route_source_locations`.
67
95
  # We also check that it's enabled.
68
96
  if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.8"
6
+ VERSION = "0.3.10"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-02 00:00:00.000000000 Z
11
+ date: 2024-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-lsp