ruby-lsp-extras 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/ruby_lsp/ruby_lsp_extras/addon.rb +83 -0
- data/lib/ruby_lsp/ruby_lsp_extras/definition.rb +287 -0
- data/lib/ruby_lsp/ruby_lsp_extras/server_addon.rb +152 -0
- data/lib/ruby_lsp/ruby_lsp_extras/version.rb +22 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/addon.rb +99 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/completion.rb +67 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/definition.rb +63 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/hover.rb +61 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/partial_index.rb +89 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/partial_resolver.rb +82 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/util.rb +89 -0
- data/lib/ruby_lsp/ruby_lsp_rails_partial/version.rb +7 -0
- data/lib/ruby_lsp/ruby_lsp_rails_routes/addon.rb +73 -0
- data/lib/ruby_lsp/ruby_lsp_rails_routes/definition.rb +42 -0
- data/lib/ruby_lsp/ruby_lsp_rails_routes/server_addon.rb +63 -0
- data/lib/ruby_lsp/ruby_lsp_rails_routes/util.rb +34 -0
- data/lib/ruby_lsp/ruby_lsp_rails_routes/version.rb +7 -0
- metadata +75 -4
- data/lib/ruby-lsp-extras.rb +0 -6
- data/lib/ruby_lsp_extras/version.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8efd636d699d2b97731469135f307359822327a3f6277ebc04f58868b531c93
|
|
4
|
+
data.tar.gz: 2d05242a2ed15aa74f96bbc0cce194126a3dac0b28515877042b47bb0ef0c757
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99f0c383f3371b462454512643c1b6677d58a643587f13aa76e3d85b51459116a41acfb3b0630da384411089a8d6cc8438e73e8e8bd85cb760fec7bdb6ce60cf
|
|
7
|
+
data.tar.gz: 6822f14538bddac7bbf72b0fa0563e0049853176bd16ee9debdbafba736e2b57fb91d1bea84fead5c3f944f854de55b4a33288c758d7408773e457bba5068201
|
data/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby LSP add-on (auto-discovered by Ruby LSP; not loaded by Rails).
|
|
4
|
+
#
|
|
5
|
+
# Adds go-to-definition targets Ruby LSP/ruby-lsp-rails don't provide:
|
|
6
|
+
# - Route helpers -> the routed controller action, alongside the route file location ruby-lsp-rails already provides.
|
|
7
|
+
# e.g. `companies_path` -> `CompaniesController#create`
|
|
8
|
+
# - Route declarations in `config/routes.rb`/`config/routes/*.rb` -> the controller action (single-action lines) or
|
|
9
|
+
# controller class (multi-action lines). e.g. `resources :posts` -> `PostsController`
|
|
10
|
+
# - View helpers (`app/helpers/`) called from controllers or views, when the method isn't on the current class.
|
|
11
|
+
# e.g. `my_post_helper_methd` / `helpers.my_post_helper_methd` -> `PostsHelper#my_post_helper_methd`
|
|
12
|
+
# - Column attribute methods in models -> the column's line in `db/schema.rb`. e.g. `title` -> `t.string "title"`
|
|
13
|
+
#
|
|
14
|
+
# Relies on ruby-lsp-rails internals (`rails_runner_client`, server add-ons), so it's pinned to 0.4.x.
|
|
15
|
+
#
|
|
16
|
+
# NOTE: ActiveSupport isn't loaded in the Ruby LSP process -- stick to plain Ruby (no `presence`/`blank?`/`camelize`)
|
|
17
|
+
|
|
18
|
+
require_relative 'definition'
|
|
19
|
+
require_relative 'version'
|
|
20
|
+
|
|
21
|
+
module RubyLsp
|
|
22
|
+
module Extras
|
|
23
|
+
RAILS_ADDON_NAME = 'Ruby LSP Rails'
|
|
24
|
+
RAILS_ADDON_VERSIONS = ['>= 0.4.0', '< 0.5.0'].freeze
|
|
25
|
+
# Must match `ServerAddon#name` (server_addon.rb runs in the Rails process, so it can't share this constant)
|
|
26
|
+
SERVER_ADDON_NAME = 'Extras'
|
|
27
|
+
|
|
28
|
+
# :nodoc:
|
|
29
|
+
class Addon < ::RubyLsp::Addon
|
|
30
|
+
# @see super
|
|
31
|
+
def activate(global_state, outgoing_queue)
|
|
32
|
+
@global_state = global_state
|
|
33
|
+
@outgoing_queue = outgoing_queue
|
|
34
|
+
@client = nil
|
|
35
|
+
|
|
36
|
+
# `rails_runner_client` blocks until the Rails process finishes booting
|
|
37
|
+
@boot_thread = Thread.new { register_server_addon }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @see super
|
|
41
|
+
def deactivate
|
|
42
|
+
@boot_thread&.kill
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @see super
|
|
46
|
+
def name
|
|
47
|
+
'Ruby LSP Extras'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @see super
|
|
51
|
+
def version
|
|
52
|
+
VERSION.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @see super
|
|
56
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
57
|
+
return if @global_state.nil?
|
|
58
|
+
|
|
59
|
+
Definition.new(@client, response_builder, uri, node_context, @global_state.index, dispatcher: dispatcher)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# @return [void]
|
|
65
|
+
def register_server_addon
|
|
66
|
+
rails_addon = ::RubyLsp::Addon.get(RAILS_ADDON_NAME, *RAILS_ADDON_VERSIONS)
|
|
67
|
+
client = rails_addon.rails_runner_client
|
|
68
|
+
client.register_server_addon(File.expand_path('server_addon.rb', __dir__))
|
|
69
|
+
@client = client
|
|
70
|
+
rescue ::RubyLsp::Addon::AddonNotFoundError, ::RubyLsp::Addon::IncompatibleApiError => e
|
|
71
|
+
log("#{name} add-on disabled: #{e.message}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param message [String]
|
|
75
|
+
#
|
|
76
|
+
# @return [void]
|
|
77
|
+
#
|
|
78
|
+
def log(message)
|
|
79
|
+
@outgoing_queue << Notification.window_log_message(message)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module Extras
|
|
5
|
+
# Adds definitions for route helpers/declarations, view helpers, and model columns
|
|
6
|
+
class Definition
|
|
7
|
+
ROUTE_HELPER = /_(?:path|url)\z/
|
|
8
|
+
# Receivers that proxy to the view context, e.g. `helpers.my_helper` in a controller
|
|
9
|
+
HELPER_PROXIES = %w[helpers view_context].freeze
|
|
10
|
+
HELPERS_DIR = '/app/helpers/'
|
|
11
|
+
# ActiveRecord attribute methods -> column name, e.g. `title=`, `title?`, `title_was`, `saved_change_to_title?`
|
|
12
|
+
ATTRIBUTE_METHOD = /
|
|
13
|
+
\A
|
|
14
|
+
(?:saved_change_to_|will_save_change_to_)?
|
|
15
|
+
(?<column>\w+?)
|
|
16
|
+
(?:=|\?|_was|_changed\?|_before_last_save|_in_database)?
|
|
17
|
+
\z
|
|
18
|
+
/x
|
|
19
|
+
|
|
20
|
+
include ::RubyLsp::Requests::Support::Common
|
|
21
|
+
|
|
22
|
+
# @param client [RubyLsp::Rails::RunnerClient, nil] `nil` until the Rails server finishes booting
|
|
23
|
+
# @param response_builder [RubyLsp::ResponseBuilders::CollectionResponseBuilder]
|
|
24
|
+
# @param uri [URI::Generic] the document the request came from
|
|
25
|
+
# @param node_context [RubyLsp::NodeContext]
|
|
26
|
+
# @param index [RubyIndexer::Index]
|
|
27
|
+
# @param dispatcher [Prism::Dispatcher]
|
|
28
|
+
#
|
|
29
|
+
def initialize(client, response_builder, uri, node_context, index, dispatcher:)
|
|
30
|
+
@client = client
|
|
31
|
+
@response_builder = response_builder
|
|
32
|
+
@path = uri.to_s
|
|
33
|
+
@nesting = node_context.nesting
|
|
34
|
+
@index = index
|
|
35
|
+
|
|
36
|
+
dispatcher.register(self, :on_call_node_enter, :on_symbol_node_enter, :on_string_node_enter)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# e.g. `:posts` in `resources :posts`, or `:download_attachment` in `get '...', action: :download_attachment`
|
|
40
|
+
#
|
|
41
|
+
# @param node [Prism::SymbolNode]
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
#
|
|
45
|
+
def on_symbol_node_enter(node)
|
|
46
|
+
handle_route_declaration(node)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# e.g. `'static_pages#test_service'` in `get 'test_service', to: 'static_pages#test_service'`
|
|
50
|
+
#
|
|
51
|
+
# @param node [Prism::StringNode]
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
def on_string_node_enter(node)
|
|
56
|
+
handle_route_declaration(node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param node [Prism::CallNode]
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
62
|
+
#
|
|
63
|
+
def on_call_node_enter(node)
|
|
64
|
+
message = node.message
|
|
65
|
+
return if message.nil?
|
|
66
|
+
|
|
67
|
+
if self_receiver?(node) && message.match?(ROUTE_HELPER)
|
|
68
|
+
handle_route(message)
|
|
69
|
+
elsif self_receiver?(node) && in_model?
|
|
70
|
+
handle_column(message)
|
|
71
|
+
elsif self_receiver?(node) || proxy_receiver?(node)
|
|
72
|
+
handle_helper(node, message)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# @param message [String] e.g. `companies_path`
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
#
|
|
82
|
+
def handle_route(message)
|
|
83
|
+
return if @client.nil?
|
|
84
|
+
|
|
85
|
+
result = @client.delegate_request(
|
|
86
|
+
server_addon_name: SERVER_ADDON_NAME,
|
|
87
|
+
request_name: 'controller_action',
|
|
88
|
+
name: message,
|
|
89
|
+
)
|
|
90
|
+
return if result.nil?
|
|
91
|
+
|
|
92
|
+
add_route_locations(result[:controller], result[:action])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# In a routes file, jumps to whatever the routes declared on this line point at: the action for single-action
|
|
96
|
+
# lines (`get 'x', to: 'foo#bar'`), the controller class for multi-action lines (`resources :posts`)
|
|
97
|
+
#
|
|
98
|
+
# @param node [Prism::SymbolNode, Prism::StringNode]
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
#
|
|
102
|
+
def handle_route_declaration(node)
|
|
103
|
+
return if @client.nil? || !in_routes_file?
|
|
104
|
+
|
|
105
|
+
result = @client.delegate_request(
|
|
106
|
+
server_addon_name: SERVER_ADDON_NAME,
|
|
107
|
+
request_name: 'routes_at',
|
|
108
|
+
file: URI(@path).path,
|
|
109
|
+
line: node.location.start_line,
|
|
110
|
+
)
|
|
111
|
+
return if result.nil?
|
|
112
|
+
|
|
113
|
+
result[:routes].each do |route|
|
|
114
|
+
if route[:action].nil?
|
|
115
|
+
add_locations(@index[route[:controller]])
|
|
116
|
+
else
|
|
117
|
+
add_route_locations(route[:controller], route[:action])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# In a model, a column's attribute method (`title`, `self.title = ...`, `title_changed?`) -> its line in the
|
|
123
|
+
# schema file. Skipped when the method really exists (e.g. an overridden attribute), so we don't duplicate
|
|
124
|
+
# Ruby LSP.
|
|
125
|
+
#
|
|
126
|
+
# @param message [String]
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
#
|
|
130
|
+
def handle_column(message)
|
|
131
|
+
return if @client.nil? || resolves_on_current_class?(message)
|
|
132
|
+
|
|
133
|
+
column = message[ATTRIBUTE_METHOD, :column]
|
|
134
|
+
return if column.nil?
|
|
135
|
+
|
|
136
|
+
result = @client.delegate_request(
|
|
137
|
+
server_addon_name: SERVER_ADDON_NAME,
|
|
138
|
+
request_name: 'column_location',
|
|
139
|
+
model: @nesting.join('::'),
|
|
140
|
+
column: column,
|
|
141
|
+
)
|
|
142
|
+
return if result.nil?
|
|
143
|
+
|
|
144
|
+
path, line = result[:location].split(/:(?=\d+\z)/)
|
|
145
|
+
@response_builder << Interface::Location.new(
|
|
146
|
+
uri: URI::Generic.from_path(path: path).to_s,
|
|
147
|
+
range: Interface::Range.new(
|
|
148
|
+
start: Interface::Position.new(line: line.to_i - 1, character: 0),
|
|
149
|
+
end: Interface::Position.new(line: line.to_i - 1, character: 0),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @return [Boolean]
|
|
155
|
+
def in_model?
|
|
156
|
+
@path.include?('/app/models/') && !@nesting.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @return [Boolean] `config/routes.rb` or a `draw`n file under `config/routes/`
|
|
160
|
+
def in_routes_file?
|
|
161
|
+
@path.match?(%r{/config/routes(?:\.rb\z|/.+\.rb\z)})
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Prefers the action method; falls back to the controller class (e.g. an implicit, view-only action)
|
|
165
|
+
#
|
|
166
|
+
# @param controller [String] e.g. `Api::V1::CompaniesController`
|
|
167
|
+
# @param action [String]
|
|
168
|
+
#
|
|
169
|
+
# @return [void]
|
|
170
|
+
#
|
|
171
|
+
def add_route_locations(controller, action)
|
|
172
|
+
entries = @index.resolve_method(action, controller)
|
|
173
|
+
entries = @index[controller] if entries.nil? || entries.empty?
|
|
174
|
+
add_locations(entries)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Only from controllers/views. For a plain call in a controller, only when the method doesn't exist on the
|
|
178
|
+
# controller (so we don't duplicate what Ruby LSP already found)
|
|
179
|
+
#
|
|
180
|
+
# @param node [Prism::CallNode] `foo`, `self.foo`, `helpers.foo`, or `view_context.foo`
|
|
181
|
+
# @param message [String]
|
|
182
|
+
#
|
|
183
|
+
# @return [void]
|
|
184
|
+
#
|
|
185
|
+
def handle_helper(node, message)
|
|
186
|
+
return if !in_controller? && !in_view?
|
|
187
|
+
return if in_controller? && !proxy_receiver?(node) && resolves_on_current_class?(message)
|
|
188
|
+
|
|
189
|
+
entries = (@index[message] || []).select { |entry| helper_method?(entry) }
|
|
190
|
+
add_locations(sort_helpers(entries))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @param node [Prism::CallNode]
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean] `helpers.foo` or `view_context.foo`
|
|
196
|
+
#
|
|
197
|
+
def proxy_receiver?(node)
|
|
198
|
+
receiver = node.receiver
|
|
199
|
+
receiver.is_a?(Prism::CallNode) && self_receiver?(receiver) && HELPER_PROXIES.include?(receiver.message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @return [Boolean]
|
|
203
|
+
def resolves_on_current_class?(message)
|
|
204
|
+
return false if @nesting.empty?
|
|
205
|
+
|
|
206
|
+
entries = @index.resolve_method(message, @nesting.join('::'))
|
|
207
|
+
!entries.nil? && !entries.empty?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @return [Boolean]
|
|
211
|
+
def helper_method?(entry)
|
|
212
|
+
entry.is_a?(RubyIndexer::Entry::Member) && entry.uri.to_s.include?(HELPERS_DIR)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The helper named after the current controller/view folder (e.g. `PostsHelper`) first, then alphabetical
|
|
216
|
+
#
|
|
217
|
+
# @param entries [Array<RubyIndexer::Entry::Member>]
|
|
218
|
+
#
|
|
219
|
+
# @return [Array<RubyIndexer::Entry::Member>]
|
|
220
|
+
#
|
|
221
|
+
def sort_helpers(entries)
|
|
222
|
+
matching = matching_helper_name
|
|
223
|
+
|
|
224
|
+
entries.sort_by { |entry| [(entry.owner&.name == matching) ? 0 : 1, entry.owner&.name.to_s] }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# @return [String, nil] e.g. `PostsHelper` for `PostsController` or `app/views/posts/index.html.erb`
|
|
228
|
+
def matching_helper_name
|
|
229
|
+
if in_controller?
|
|
230
|
+
@nesting.join('::').sub(/Controller\z/, 'Helper')
|
|
231
|
+
elsif in_view?
|
|
232
|
+
folder = @path[%r{/app/views/(.+)/[^/]+\z}, 1]
|
|
233
|
+
return if folder.nil?
|
|
234
|
+
|
|
235
|
+
"#{folder.split('/').map { |part| part.split('_').map(&:capitalize).join }.join('::')}Helper"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @return [Boolean]
|
|
240
|
+
def in_controller?
|
|
241
|
+
@path.include?('/app/controllers/') && !@nesting.empty? && @nesting.last.end_with?('Controller')
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# @return [Boolean]
|
|
245
|
+
def in_view?
|
|
246
|
+
@path.include?('/app/views/') && @path.end_with?('.erb')
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Skips locations already in the response -- Ruby LSP's own listener runs first and, for receivers it can't
|
|
250
|
+
# type (`helpers.foo`, ERB), already matches methods by name
|
|
251
|
+
#
|
|
252
|
+
# @param entries [Array<RubyIndexer::Entry>, nil]
|
|
253
|
+
#
|
|
254
|
+
# @return [void]
|
|
255
|
+
#
|
|
256
|
+
def add_locations(entries)
|
|
257
|
+
return if entries.nil?
|
|
258
|
+
|
|
259
|
+
entries.each do |entry|
|
|
260
|
+
uri = entry.uri.to_s
|
|
261
|
+
range = range_from_location(entry.location)
|
|
262
|
+
next if already_added?(uri, range.start.line)
|
|
263
|
+
|
|
264
|
+
@response_builder << Interface::Location.new(uri: uri, range: range)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# @param uri [String]
|
|
269
|
+
# @param line [Integer] 0-based
|
|
270
|
+
#
|
|
271
|
+
# @return [Boolean]
|
|
272
|
+
#
|
|
273
|
+
def already_added?(uri, line)
|
|
274
|
+
@response_builder.response.any? do |item|
|
|
275
|
+
item_uri, item_range =
|
|
276
|
+
if item.is_a?(Interface::LocationLink)
|
|
277
|
+
[item.target_uri, item.target_range]
|
|
278
|
+
else
|
|
279
|
+
[item.uri, item.range]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
item_uri == uri && item_range.start.line == line
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module Extras
|
|
5
|
+
# Runs inside the Rails process booted by ruby-lsp-rails. Resolves routes to the controller class and action they
|
|
6
|
+
# route to (by route helper name, e.g. `companies_path`, or by where the route is declared in a routes file), and
|
|
7
|
+
# model columns to their line in the schema file.
|
|
8
|
+
class ServerAddon < ::RubyLsp::Rails::ServerAddon
|
|
9
|
+
# Must match `RubyLsp::Extras::SERVER_ADDON_NAME` in addon.rb
|
|
10
|
+
#
|
|
11
|
+
# @return [String]
|
|
12
|
+
#
|
|
13
|
+
def name
|
|
14
|
+
'Extras'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param request [String]
|
|
18
|
+
# @param params [Hash]
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
#
|
|
22
|
+
def execute(request, params)
|
|
23
|
+
case request
|
|
24
|
+
when 'controller_action'
|
|
25
|
+
with_request_error_handling(request) do
|
|
26
|
+
send_result(controller_action(params[:name]))
|
|
27
|
+
end
|
|
28
|
+
when 'routes_at'
|
|
29
|
+
with_request_error_handling(request) do
|
|
30
|
+
send_result(routes_at(params[:file], params[:line]))
|
|
31
|
+
end
|
|
32
|
+
when 'column_location'
|
|
33
|
+
with_request_error_handling(request) do
|
|
34
|
+
send_result(column_location(params[:model], params[:column]))
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
send_result(nil)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @param helper_name [String] e.g. `companies_path` or `edit_company_url`
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash, nil]
|
|
46
|
+
#
|
|
47
|
+
def controller_action(helper_name)
|
|
48
|
+
match_data = helper_name.to_s.match(/\A(.+)_(?:path|url)\z/)
|
|
49
|
+
return if match_data.nil?
|
|
50
|
+
|
|
51
|
+
# In Rails 8, `named_routes` is not populated by default
|
|
52
|
+
if ::Rails.application.respond_to?(:reload_routes_unless_loaded)
|
|
53
|
+
::Rails.application.reload_routes_unless_loaded
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
route = ::Rails.application.routes.named_routes.get(match_data[1])
|
|
57
|
+
controller = route&.defaults&.dig(:controller)
|
|
58
|
+
return if controller.blank?
|
|
59
|
+
|
|
60
|
+
{ controller: "#{controller}_controller".camelize, action: route.defaults[:action].to_s }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Routes declared on `line` of `file`, grouped by controller. `action` is only set when the line declares a
|
|
64
|
+
# single action (e.g. `get 'x', to: 'foo#bar'`); multi-action lines (e.g. `resources :posts`) leave it `nil`.
|
|
65
|
+
#
|
|
66
|
+
# Relies on `ActionDispatch::Routing::Mapper.route_source_locations` (on in development since Rails 7.1)
|
|
67
|
+
#
|
|
68
|
+
# @param file [String] absolute path, e.g. `/Easy/config/routes.rb`
|
|
69
|
+
# @param line [Integer] 1-based
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] `{ routes: [{ controller: 'PostsController', action: nil }] }`
|
|
72
|
+
#
|
|
73
|
+
def routes_at(file, line)
|
|
74
|
+
# `source_location` is absolute here, but may be relative to Rails.root depending on Rails version
|
|
75
|
+
location = "#{file}:#{line}"
|
|
76
|
+
|
|
77
|
+
routes = ::Rails.application.routes.routes.select do |route|
|
|
78
|
+
next false unless route.respond_to?(:source_location)
|
|
79
|
+
next false unless route.source_location
|
|
80
|
+
next false unless route.defaults[:controller]
|
|
81
|
+
|
|
82
|
+
::Rails.root.join(route.source_location).to_s == location
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
grouped = routes.group_by { |route| route.defaults[:controller] }.map do |controller, controller_routes|
|
|
86
|
+
actions = controller_routes.map { |route| route.defaults[:action].to_s }
|
|
87
|
+
actions.uniq!
|
|
88
|
+
|
|
89
|
+
{ controller: "#{controller}_controller".camelize, action: (actions.one? ? actions.first : nil) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
{ routes: grouped }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Where `column` of `model`'s table is declared in the schema file (`db/schema.rb`). The primary key (and any
|
|
96
|
+
# column without its own `t.<type>` line) points at the `create_table` line.
|
|
97
|
+
#
|
|
98
|
+
# @param model [String] e.g. `Company`
|
|
99
|
+
# @param column [String] e.g. `name`
|
|
100
|
+
#
|
|
101
|
+
# @return [Hash, nil] `{ location: '/Easy/db/schema.rb:123' }`
|
|
102
|
+
#
|
|
103
|
+
def column_location(model, column)
|
|
104
|
+
const = ActiveSupport::Inflector.safe_constantize(model)
|
|
105
|
+
return unless const.is_a?(Class)
|
|
106
|
+
return unless const < ActiveRecord::Base
|
|
107
|
+
|
|
108
|
+
return if const.abstract_class?
|
|
109
|
+
|
|
110
|
+
return unless const.table_exists?
|
|
111
|
+
return unless const.column_names.include?(column)
|
|
112
|
+
|
|
113
|
+
db_config = const.connection.pool.db_config
|
|
114
|
+
schema_file =
|
|
115
|
+
if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
|
|
116
|
+
ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config)
|
|
117
|
+
else
|
|
118
|
+
# Rails 6.1
|
|
119
|
+
ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.name)
|
|
120
|
+
end
|
|
121
|
+
return if schema_file.nil?
|
|
122
|
+
|
|
123
|
+
return unless File.exist?(schema_file)
|
|
124
|
+
|
|
125
|
+
line = schema_line(File.readlines(schema_file), const.table_name, column)
|
|
126
|
+
return if line.nil?
|
|
127
|
+
|
|
128
|
+
{ location: "#{schema_file}:#{line}" }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param lines [Array<String>] schema.rb lines
|
|
132
|
+
# @param table [String]
|
|
133
|
+
# @param column [String]
|
|
134
|
+
#
|
|
135
|
+
# @return [Integer, nil] 1-based line
|
|
136
|
+
#
|
|
137
|
+
def schema_line(lines, table, column)
|
|
138
|
+
table_index = lines.index { |l| l.match?(/^\s*create_table\s+"#{Regexp.escape(table)}"/) }
|
|
139
|
+
return if table_index.nil?
|
|
140
|
+
|
|
141
|
+
column_pattern = /^\s*t\.\w+\s+"#{Regexp.escape(column)}"/
|
|
142
|
+
lines[(table_index + 1)..].each_with_index do |l, offset|
|
|
143
|
+
break if l.match?(/^\s*end\s*$/)
|
|
144
|
+
|
|
145
|
+
return table_index + offset + 2 if l.match?(column_pattern)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
table_index + 1
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module Extras
|
|
5
|
+
module VERSION
|
|
6
|
+
MAJOR = 0
|
|
7
|
+
MINOR = 2
|
|
8
|
+
PATCH = 0
|
|
9
|
+
TINY = PATCH
|
|
10
|
+
|
|
11
|
+
# Set PRE to nil unless it's a pre-release (beta, rc, etc.)
|
|
12
|
+
PRE = nil
|
|
13
|
+
|
|
14
|
+
STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.').freeze
|
|
15
|
+
|
|
16
|
+
# :nodoc:
|
|
17
|
+
def self.to_s
|
|
18
|
+
STRING
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'completion'
|
|
4
|
+
require_relative 'definition'
|
|
5
|
+
require_relative 'hover'
|
|
6
|
+
require_relative 'partial_index'
|
|
7
|
+
require_relative 'partial_resolver'
|
|
8
|
+
require_relative 'util'
|
|
9
|
+
require_relative 'version'
|
|
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
|
+
# :nodoc:
|
|
22
|
+
def activate(global_state, outgoing_queue)
|
|
23
|
+
views_path = File.join(global_state.workspace_path, 'app', 'views')
|
|
24
|
+
@index = PartialIndex.new(views_path)
|
|
25
|
+
@resolver = PartialResolver.new(@index, views_path)
|
|
26
|
+
|
|
27
|
+
register_file_watcher(global_state, outgoing_queue)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# :nodoc:
|
|
31
|
+
def deactivate
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# :nodoc:
|
|
35
|
+
def name
|
|
36
|
+
'Rails Partial'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# :nodoc:
|
|
40
|
+
def version
|
|
41
|
+
VERSION
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# :nodoc:
|
|
45
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
46
|
+
return unless @resolver
|
|
47
|
+
|
|
48
|
+
Definition.new(response_builder, uri, node_context, @resolver, dispatcher)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# :nodoc:
|
|
52
|
+
def create_completion_listener(response_builder, node_context, dispatcher, uri)
|
|
53
|
+
return unless @resolver
|
|
54
|
+
|
|
55
|
+
Completion.new(response_builder, node_context, @resolver, uri, dispatcher)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# :nodoc:
|
|
59
|
+
def create_hover_listener(response_builder, node_context, dispatcher)
|
|
60
|
+
return unless @resolver
|
|
61
|
+
|
|
62
|
+
Hover.new(response_builder, node_context, @resolver, dispatcher)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Watches partial changes under app/views and updates the index incrementally.
|
|
66
|
+
def workspace_did_change_watched_files(changes)
|
|
67
|
+
return unless @index
|
|
68
|
+
|
|
69
|
+
changes.each do |change|
|
|
70
|
+
path = Util.uri_to_path(change[:uri])
|
|
71
|
+
next unless path
|
|
72
|
+
|
|
73
|
+
case change[:type]
|
|
74
|
+
when Constant::FileChangeType::CREATED, Constant::FileChangeType::CHANGED
|
|
75
|
+
@index.add(path)
|
|
76
|
+
when Constant::FileChangeType::DELETED
|
|
77
|
+
@index.remove(path)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# :nodoc:
|
|
85
|
+
def register_file_watcher(global_state, outgoing_queue)
|
|
86
|
+
return unless global_state.supports_watching_files
|
|
87
|
+
|
|
88
|
+
outgoing_queue << Request.register_watched_files(
|
|
89
|
+
1,
|
|
90
|
+
Interface::RelativePattern.new(
|
|
91
|
+
base_uri: global_state.workspace_uri.to_s,
|
|
92
|
+
pattern: 'app/views/**/_*',
|
|
93
|
+
),
|
|
94
|
+
registration_id: WATCHER_REGISTRATION_ID,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|