ruby-lsp-rails 0.3.8 → 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: accc5f9539ddceaa7fde3dfdb64fcc74f697e9d17178c45e9271df463cad4f18
4
- data.tar.gz: d594ee73741270b0d6837680a5bacc6e912c1ca856aeaa5806e7d1ad9e96aeb8
3
+ metadata.gz: 2499b5ff0e291d166c0fe823ccd3c34d3c60606500a7f9db6a152508cb6f1346
4
+ data.tar.gz: 7ef48a79261a23f531f0b86ca18f7cec27c9a75490b4b111a495badcdf267fa5
5
5
  SHA512:
6
- metadata.gz: 955347d73c343311571e62d7c0bb656a3cc9176f8e6ff87ab010287345d754b49f1889c436c716ce0a40c2aa45938f094124cd18315f787c854db7aed2172ee1
7
- data.tar.gz: 65731a4414f5b68884ea6003097b5b1e67caaaaf2a954b0d25de0ed937cc7600d1b1e4e8d2c25a28b25f526c72fc05e6f9a1996874f95584c4450a4212ef9269
6
+ metadata.gz: ee15545d1a6e8dd9221b06f403af4219e59a0b823b527d92edce63539ca11b7fa01faeea1a7c2d8f91933686c8b92884a961698075d7c8826ba24e27619cd509
7
+ data.tar.gz: dbee9b4f8dc8658254ec9db2d2e585a0fa6a78545a8e296d3b3f4d5881a53cb642475cf92c0915ddfaeeede2306d925957bb75e0d0c52029d3bca4ba39195061
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,119 @@ 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 = T.must(@constant_name_stack.last)
164
+ class_name.end_with?("Controller") && superclass_name&.end_with?("Controller")
165
+ end
166
+
167
+ sig { params(node: Prism::DefNode).void }
168
+ def add_route_code_lens_to_action(node)
169
+ class_name, _ = T.must(@constant_name_stack.last)
170
+ route = @client.route(
171
+ controller: class_name,
172
+ action: node.name.to_s,
173
+ )
174
+
175
+ return unless route
176
+
177
+ path = route[:path]
178
+ verb = route[:verb]
179
+ source_location = route[:source_location]
180
+
181
+ arguments = [
182
+ source_location,
183
+ {
184
+ start_line: node.location.start_line - 1,
185
+ start_column: node.location.start_column,
186
+ end_line: node.location.end_line - 1,
187
+ end_column: node.location.end_column,
188
+ },
189
+ ]
190
+
191
+ @response_builder << create_code_lens(
192
+ node,
193
+ title: [verb, path].join(" "),
194
+ command_name: "rubyLsp.openFile",
195
+ arguments: arguments,
196
+ data: { type: "file" },
197
+ )
198
+ end
199
+
105
200
  sig { returns(String) }
106
201
  def test_command
107
- if Gem.win_platform?
108
- "ruby bin/rails test"
109
- else
110
- "bin/rails test"
111
- end
202
+ "#{RbConfig.ruby} bin/rails test"
203
+ end
204
+
205
+ sig { returns(String) }
206
+ def migrate_command
207
+ "#{RbConfig.ruby} bin/rails db:migrate"
208
+ end
209
+
210
+ sig { returns(T.nilable(String)) }
211
+ def migration_version
212
+ File.basename(T.must(@path)).split("_").first
213
+ end
214
+
215
+ sig { params(node: Prism::Node, name: String, command: String).void }
216
+ def add_migrate_code_lens(node, name:, command:)
217
+ return unless @path
218
+
219
+ @response_builder << create_code_lens(
220
+ node,
221
+ title: "Run",
222
+ command_name: "rubyLsp.runTask",
223
+ arguments: [command],
224
+ data: { type: "migrate" },
225
+ )
112
226
  end
113
227
 
114
228
  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.9"
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.9
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-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-lsp