ruby-lsp-rails 0.3.5 → 0.3.7
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/README.md +4 -3
- data/Rakefile +1 -1
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +36 -6
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +13 -5
- data/lib/ruby_lsp/ruby_lsp_rails/definition.rb +53 -5
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +44 -3
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +6 -5
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +34 -7
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +75 -31
- data/lib/ruby_lsp/ruby_lsp_rails/support/associations.rb +20 -0
- data/lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb +33 -0
- data/lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb +10 -3
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +9 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6bb6fcc96e3d0fd9357e7f8700acbae09e0d792499ed0d8b92fd577a97949108
|
4
|
+
data.tar.gz: cbb971feed21416f187195d0b23295801dbbc5c875c5d64961f2591e253f375a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc8c684b4900726219d73c2855a1afe159e0ba02f8a953de86356e1cfb9667fe11dff4cbaf3446d5dd471689151f34072f1a099f82033c959516db30dcf81ce7
|
7
|
+
data.tar.gz: 72b8a8ba741fbccd174dc46bb86ef683634630866f209f0968eb6d228992f4986f46bac7f0de249a936649eef80a799c5505f9c8a3514f76d44e77a511b70325
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q)
|
2
|
+
|
1
3
|
# Ruby LSP Rails
|
2
4
|
|
3
5
|
Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for extra Rails editor features, such as:
|
@@ -6,6 +8,7 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
|
|
6
8
|
* Run or debug a test by clicking on the code lens which appears above the test class, or an individual test.
|
7
9
|
* Navigate to associations, validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
|
8
10
|
* Jump to the definition of callbacks using your editor's "Go to Definition" feature.
|
11
|
+
* Jump to the declaration of a route.
|
9
12
|
|
10
13
|
## Installation
|
11
14
|
|
@@ -36,9 +39,7 @@ When extension is stopped (e.g. by quitting the editor), the server instance is
|
|
36
39
|
|
37
40
|
## Contributing
|
38
41
|
|
39
|
-
|
40
|
-
intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
|
41
|
-
[Contributor Covenant](https://github.com/Shopify/ruby-lsp-rails/blob/main/CODE_OF_CONDUCT.md) code of conduct.
|
42
|
+
See [CONTRIBUTING.md](https://github.com/Shopify/ruby-lsp-rails/blob/main/CONTRIBUTING.md)
|
42
43
|
|
43
44
|
## License
|
44
45
|
|
data/Rakefile
CHANGED
@@ -3,8 +3,11 @@
|
|
3
3
|
|
4
4
|
require "ruby_lsp/addon"
|
5
5
|
|
6
|
+
require_relative "../../ruby_lsp_rails/version"
|
6
7
|
require_relative "support/active_support_test_case_helper"
|
8
|
+
require_relative "support/associations"
|
7
9
|
require_relative "support/callbacks"
|
10
|
+
require_relative "support/location_builder"
|
8
11
|
require_relative "runner_client"
|
9
12
|
require_relative "hover"
|
10
13
|
require_relative "code_lens"
|
@@ -31,6 +34,7 @@ module RubyLsp
|
|
31
34
|
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
|
32
35
|
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
33
36
|
Thread.new { @client = RunnerClient.create_client }
|
37
|
+
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
|
34
38
|
end
|
35
39
|
|
36
40
|
sig { override.void }
|
@@ -55,12 +59,12 @@ module RubyLsp
|
|
55
59
|
sig do
|
56
60
|
override.params(
|
57
61
|
response_builder: ResponseBuilders::Hover,
|
58
|
-
|
62
|
+
node_context: NodeContext,
|
59
63
|
dispatcher: Prism::Dispatcher,
|
60
64
|
).void
|
61
65
|
end
|
62
|
-
def create_hover_listener(response_builder,
|
63
|
-
Hover.new(@client, response_builder,
|
66
|
+
def create_hover_listener(response_builder, node_context, dispatcher)
|
67
|
+
Hover.new(@client, response_builder, node_context, T.must(@global_state), dispatcher)
|
64
68
|
end
|
65
69
|
|
66
70
|
sig do
|
@@ -77,13 +81,13 @@ module RubyLsp
|
|
77
81
|
override.params(
|
78
82
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
|
79
83
|
uri: URI::Generic,
|
80
|
-
|
84
|
+
node_context: NodeContext,
|
81
85
|
dispatcher: Prism::Dispatcher,
|
82
86
|
).void
|
83
87
|
end
|
84
|
-
def create_definition_listener(response_builder, uri,
|
88
|
+
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
85
89
|
index = T.must(@global_state).index
|
86
|
-
Definition.new(response_builder,
|
90
|
+
Definition.new(@client, response_builder, node_context, index, dispatcher)
|
87
91
|
end
|
88
92
|
|
89
93
|
sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
|
@@ -95,6 +99,32 @@ module RubyLsp
|
|
95
99
|
end
|
96
100
|
end
|
97
101
|
|
102
|
+
sig { params(global_state: GlobalState, message_queue: Thread::Queue).void }
|
103
|
+
def register_additional_file_watchers(global_state:, message_queue:)
|
104
|
+
return unless global_state.supports_watching_files
|
105
|
+
|
106
|
+
message_queue << Request.new(
|
107
|
+
id: "ruby-lsp-rails-file-watcher",
|
108
|
+
method: "client/registerCapability",
|
109
|
+
params: Interface::RegistrationParams.new(
|
110
|
+
registrations: [
|
111
|
+
Interface::Registration.new(
|
112
|
+
id: "workspace/didChangeWatchedFilesRails",
|
113
|
+
method: "workspace/didChangeWatchedFiles",
|
114
|
+
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
|
115
|
+
watchers: [
|
116
|
+
Interface::FileSystemWatcher.new(
|
117
|
+
glob_pattern: "**/*structure.sql",
|
118
|
+
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
|
119
|
+
),
|
120
|
+
],
|
121
|
+
),
|
122
|
+
),
|
123
|
+
],
|
124
|
+
),
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
98
128
|
sig { override.returns(String) }
|
99
129
|
def name
|
100
130
|
"Ruby LSP Rails"
|
@@ -6,6 +6,7 @@ module RubyLsp
|
|
6
6
|
# 
|
7
7
|
#
|
8
8
|
# This feature adds several CodeLens features for Rails applications using Active Support test cases:
|
9
|
+
#
|
9
10
|
# - Run tests in the VS Terminal
|
10
11
|
# - Run tests in the VS Code Test Explorer
|
11
12
|
# - Debug tests
|
@@ -42,8 +43,6 @@ module RubyLsp
|
|
42
43
|
include Requests::Support::Common
|
43
44
|
include ActiveSupportTestCaseHelper
|
44
45
|
|
45
|
-
BASE_COMMAND = "bin/rails test"
|
46
|
-
|
47
46
|
sig do
|
48
47
|
params(
|
49
48
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
|
@@ -67,7 +66,7 @@ module RubyLsp
|
|
67
66
|
return unless content
|
68
67
|
|
69
68
|
line_number = node.location.start_line
|
70
|
-
command = "#{
|
69
|
+
command = "#{test_command} #{@path}:#{line_number}"
|
71
70
|
add_test_code_lens(node, name: content, command: command, kind: :example)
|
72
71
|
end
|
73
72
|
|
@@ -77,7 +76,7 @@ module RubyLsp
|
|
77
76
|
method_name = node.name.to_s
|
78
77
|
if method_name.start_with?("test_")
|
79
78
|
line_number = node.location.start_line
|
80
|
-
command = "#{
|
79
|
+
command = "#{test_command} #{@path}:#{line_number}"
|
81
80
|
add_test_code_lens(node, name: method_name, command: command, kind: :example)
|
82
81
|
end
|
83
82
|
end
|
@@ -86,7 +85,7 @@ module RubyLsp
|
|
86
85
|
def on_class_node_enter(node)
|
87
86
|
class_name = node.constant_path.slice
|
88
87
|
if class_name.end_with?("Test")
|
89
|
-
command = "#{
|
88
|
+
command = "#{test_command} #{@path}"
|
90
89
|
add_test_code_lens(node, name: class_name, command: command, kind: :group)
|
91
90
|
@group_id_stack.push(@group_id)
|
92
91
|
@group_id += 1
|
@@ -103,6 +102,15 @@ module RubyLsp
|
|
103
102
|
|
104
103
|
private
|
105
104
|
|
105
|
+
sig { returns(String) }
|
106
|
+
def test_command
|
107
|
+
if Gem.win_platform?
|
108
|
+
"ruby bin/rails test"
|
109
|
+
else
|
110
|
+
"bin/rails test"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
106
114
|
sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
|
107
115
|
def add_test_code_lens(node, name:, command:, kind:)
|
108
116
|
return unless @path
|
@@ -10,28 +10,40 @@ module RubyLsp
|
|
10
10
|
# definition of the symbol under the cursor.
|
11
11
|
#
|
12
12
|
# Currently supported targets:
|
13
|
+
#
|
13
14
|
# - Callbacks
|
15
|
+
# - Named routes (e.g. `users_path`)
|
14
16
|
#
|
15
17
|
# # Example
|
16
18
|
#
|
17
19
|
# ```ruby
|
18
20
|
# before_action :foo # <- Go to definition on this symbol will jump to the method if it is defined in the same class
|
19
21
|
# ```
|
22
|
+
#
|
23
|
+
# Notes for named routes:
|
24
|
+
#
|
25
|
+
# - It is available only in Rails 7.1 or newer.
|
26
|
+
# - Route may be defined across multiple files, e.g. using `draw`, rather than in `routes.rb`.
|
27
|
+
# - Routes won't be found if not defined for the Rails development environment.
|
28
|
+
# - If using `constraints`, the route can only be found if the constraints are met.
|
29
|
+
# - Changes to routes won't be picked up until the server is restarted.
|
20
30
|
class Definition
|
21
31
|
extend T::Sig
|
22
32
|
include Requests::Support::Common
|
23
33
|
|
24
34
|
sig do
|
25
35
|
params(
|
36
|
+
client: RunnerClient,
|
26
37
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
|
27
|
-
|
38
|
+
node_context: NodeContext,
|
28
39
|
index: RubyIndexer::Index,
|
29
40
|
dispatcher: Prism::Dispatcher,
|
30
41
|
).void
|
31
42
|
end
|
32
|
-
def initialize(response_builder,
|
43
|
+
def initialize(client, response_builder, node_context, index, dispatcher)
|
44
|
+
@client = client
|
33
45
|
@response_builder = response_builder
|
34
|
-
@nesting = nesting
|
46
|
+
@nesting = T.let(node_context.nesting, T::Array[String])
|
35
47
|
@index = index
|
36
48
|
|
37
49
|
dispatcher.register(self, :on_call_node_enter)
|
@@ -43,8 +55,21 @@ module RubyLsp
|
|
43
55
|
|
44
56
|
message = node.message
|
45
57
|
|
46
|
-
return unless message
|
58
|
+
return unless message
|
59
|
+
|
60
|
+
if Support::Associations::ALL.include?(message)
|
61
|
+
handle_association(node)
|
62
|
+
elsif Support::Callbacks::ALL.include?(message)
|
63
|
+
handle_callback(node)
|
64
|
+
elsif message.end_with?("_path") || message.end_with?("_url")
|
65
|
+
handle_route(node)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
47
70
|
|
71
|
+
sig { params(node: Prism::CallNode).void }
|
72
|
+
def handle_callback(node)
|
48
73
|
arguments = node.arguments&.arguments
|
49
74
|
return unless arguments&.any?
|
50
75
|
|
@@ -62,7 +87,30 @@ module RubyLsp
|
|
62
87
|
end
|
63
88
|
end
|
64
89
|
|
65
|
-
|
90
|
+
sig { params(node: Prism::CallNode).void }
|
91
|
+
def handle_association(node)
|
92
|
+
first_argument = node.arguments&.arguments&.first
|
93
|
+
return unless first_argument.is_a?(Prism::SymbolNode)
|
94
|
+
|
95
|
+
association_name = first_argument.unescaped
|
96
|
+
|
97
|
+
result = @client.association_target_location(
|
98
|
+
model_name: @nesting.join("::"),
|
99
|
+
association_name: association_name,
|
100
|
+
)
|
101
|
+
|
102
|
+
return unless result
|
103
|
+
|
104
|
+
@response_builder << Support::LocationBuilder.line_location_from_s(result.fetch(:location))
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { params(node: Prism::CallNode).void }
|
108
|
+
def handle_route(node)
|
109
|
+
result = @client.route_location(T.must(node.message))
|
110
|
+
return unless result
|
111
|
+
|
112
|
+
@response_builder << Support::LocationBuilder.line_location_from_s(result.fetch(:location))
|
113
|
+
end
|
66
114
|
|
67
115
|
sig { params(name: String).void }
|
68
116
|
def collect_definitions(name)
|
@@ -21,12 +21,22 @@ module RubyLsp
|
|
21
21
|
end
|
22
22
|
def initialize(response_builder, dispatcher)
|
23
23
|
@response_builder = response_builder
|
24
|
-
|
25
|
-
|
24
|
+
@namespace_stack = T.let([], T::Array[String])
|
25
|
+
|
26
|
+
dispatcher.register(
|
27
|
+
self,
|
28
|
+
:on_call_node_enter,
|
29
|
+
:on_class_node_enter,
|
30
|
+
:on_class_node_leave,
|
31
|
+
:on_module_node_enter,
|
32
|
+
:on_module_node_leave,
|
33
|
+
)
|
26
34
|
end
|
27
35
|
|
28
36
|
sig { params(node: Prism::CallNode).void }
|
29
37
|
def on_call_node_enter(node)
|
38
|
+
return if @namespace_stack.empty?
|
39
|
+
|
30
40
|
content = extract_test_case_name(node)
|
31
41
|
|
32
42
|
if content
|
@@ -44,15 +54,46 @@ module RubyLsp
|
|
44
54
|
case message
|
45
55
|
when *Support::Callbacks::ALL, "validate"
|
46
56
|
handle_all_arg_types(node, T.must(message))
|
47
|
-
when "validates", "validates!", "validates_each", "belongs_to", "has_one", "has_many",
|
57
|
+
when "validates", "validates!", "validates_each", "belongs_to", "has_one", "has_many",
|
58
|
+
"has_and_belongs_to_many", "attr_readonly", "scope"
|
48
59
|
handle_symbol_and_string_arg_types(node, T.must(message))
|
49
60
|
when "validates_with"
|
50
61
|
handle_class_arg_types(node, T.must(message))
|
51
62
|
end
|
52
63
|
end
|
53
64
|
|
65
|
+
sig { params(node: Prism::ClassNode).void }
|
66
|
+
def on_class_node_enter(node)
|
67
|
+
add_to_namespace_stack(node)
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { params(node: Prism::ClassNode).void }
|
71
|
+
def on_class_node_leave(node)
|
72
|
+
remove_from_namespace_stack(node)
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { params(node: Prism::ModuleNode).void }
|
76
|
+
def on_module_node_enter(node)
|
77
|
+
add_to_namespace_stack(node)
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { params(node: Prism::ModuleNode).void }
|
81
|
+
def on_module_node_leave(node)
|
82
|
+
remove_from_namespace_stack(node)
|
83
|
+
end
|
84
|
+
|
54
85
|
private
|
55
86
|
|
87
|
+
sig { params(node: T.any(Prism::ClassNode, Prism::ModuleNode)).void }
|
88
|
+
def add_to_namespace_stack(node)
|
89
|
+
@namespace_stack << node.constant_path.slice
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { params(node: T.any(Prism::ClassNode, Prism::ModuleNode)).void }
|
93
|
+
def remove_from_namespace_stack(node)
|
94
|
+
@namespace_stack.delete(node.constant_path.slice)
|
95
|
+
end
|
96
|
+
|
56
97
|
sig { params(node: Prism::CallNode, message: String).void }
|
57
98
|
def handle_all_arg_types(node, message)
|
58
99
|
block = node.block
|
@@ -24,15 +24,15 @@ module RubyLsp
|
|
24
24
|
params(
|
25
25
|
client: RunnerClient,
|
26
26
|
response_builder: ResponseBuilders::Hover,
|
27
|
-
|
27
|
+
node_context: NodeContext,
|
28
28
|
global_state: GlobalState,
|
29
29
|
dispatcher: Prism::Dispatcher,
|
30
30
|
).void
|
31
31
|
end
|
32
|
-
def initialize(client, response_builder,
|
32
|
+
def initialize(client, response_builder, node_context, global_state, dispatcher)
|
33
33
|
@client = client
|
34
34
|
@response_builder = response_builder
|
35
|
-
@nesting = nesting
|
35
|
+
@nesting = T.let(node_context.nesting, T::Array[String])
|
36
36
|
@index = T.let(global_state.index, RubyIndexer::Index)
|
37
37
|
dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter, :on_call_node_enter)
|
38
38
|
end
|
@@ -77,13 +77,14 @@ module RubyLsp
|
|
77
77
|
schema_file = model[:schema_file]
|
78
78
|
|
79
79
|
@response_builder.push(
|
80
|
-
"[Schema](#{URI::Generic.
|
80
|
+
"[Schema](#{URI::Generic.from_path(path: schema_file)})",
|
81
81
|
category: :links,
|
82
82
|
) if schema_file
|
83
83
|
|
84
84
|
@response_builder.push(
|
85
85
|
model[:columns].map do |name, type|
|
86
|
-
"
|
86
|
+
primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
|
87
|
+
"**#{name}**: #{type}#{primary_key_suffix}\n"
|
87
88
|
end.join("\n"),
|
88
89
|
category: :documentation,
|
89
90
|
)
|
@@ -46,14 +46,15 @@ module RubyLsp
|
|
46
46
|
Process.setsid
|
47
47
|
rescue Errno::EPERM
|
48
48
|
# If we can't set the session ID, continue
|
49
|
+
rescue NotImplementedError
|
50
|
+
# setpgrp() may be unimplemented on some platform
|
51
|
+
# https://github.com/Shopify/ruby-lsp-rails/issues/348
|
52
|
+
end
|
53
|
+
|
54
|
+
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
|
55
|
+
Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
|
49
56
|
end
|
50
57
|
|
51
|
-
stdin, stdout, stderr, wait_thread = Open3.popen3(
|
52
|
-
"bin/rails",
|
53
|
-
"runner",
|
54
|
-
"#{__dir__}/server.rb",
|
55
|
-
"start",
|
56
|
-
)
|
57
58
|
@stdin = T.let(stdin, IO)
|
58
59
|
@stdout = T.let(stdout, IO)
|
59
60
|
@stderr = T.let(stderr, IO)
|
@@ -79,7 +80,9 @@ module RubyLsp
|
|
79
80
|
if @wait_thread.alive?
|
80
81
|
$stderr.puts("Ruby LSP Rails is force killing the server")
|
81
82
|
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
|
82
|
-
|
83
|
+
|
84
|
+
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
85
|
+
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
|
83
86
|
end
|
84
87
|
end
|
85
88
|
end
|
@@ -95,6 +98,30 @@ module RubyLsp
|
|
95
98
|
nil
|
96
99
|
end
|
97
100
|
|
101
|
+
sig do
|
102
|
+
params(
|
103
|
+
model_name: String,
|
104
|
+
association_name: String,
|
105
|
+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
106
|
+
end
|
107
|
+
def association_target_location(model_name:, association_name:)
|
108
|
+
make_request(
|
109
|
+
"association_target_location",
|
110
|
+
model_name: model_name,
|
111
|
+
association_name: association_name,
|
112
|
+
)
|
113
|
+
rescue => e
|
114
|
+
$stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
|
115
|
+
end
|
116
|
+
|
117
|
+
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
118
|
+
def route_location(name)
|
119
|
+
make_request("route_location", name: name)
|
120
|
+
rescue IncompleteMessageError
|
121
|
+
$stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
98
125
|
sig { void }
|
99
126
|
def trigger_reload
|
100
127
|
$stderr.puts("Reloading Rails application")
|
@@ -1,24 +1,8 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: false
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "sorbet-runtime"
|
5
4
|
require "json"
|
6
5
|
|
7
|
-
begin
|
8
|
-
T::Configuration.default_checked_level = :never
|
9
|
-
# Suppresses call validation errors
|
10
|
-
T::Configuration.call_validation_error_handler = ->(*) {}
|
11
|
-
# Suppresses errors caused by T.cast, T.let, T.must, etc.
|
12
|
-
T::Configuration.inline_type_error_handler = ->(*) {}
|
13
|
-
# Suppresses errors caused by incorrect parameter ordering
|
14
|
-
T::Configuration.sig_validation_error_handler = ->(*) {}
|
15
|
-
rescue
|
16
|
-
# Need this rescue so that if another gem has
|
17
|
-
# already set the checked level by the time we
|
18
|
-
# get to it, we don't fail outright.
|
19
|
-
nil
|
20
|
-
end
|
21
|
-
|
22
6
|
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
|
23
7
|
# client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
|
24
8
|
|
@@ -27,17 +11,19 @@ module RubyLsp
|
|
27
11
|
class Server
|
28
12
|
VOID = Object.new
|
29
13
|
|
30
|
-
extend T::Sig
|
31
|
-
|
32
|
-
sig { void }
|
33
14
|
def initialize
|
34
15
|
$stdin.sync = true
|
35
16
|
$stdout.sync = true
|
36
|
-
|
17
|
+
$stdin.binmode
|
18
|
+
$stdout.binmode
|
19
|
+
@running = true
|
37
20
|
end
|
38
21
|
|
39
|
-
sig { void }
|
40
22
|
def start
|
23
|
+
# Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
|
24
|
+
routes_reloader = ::Rails.application.routes_reloader
|
25
|
+
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
|
26
|
+
|
41
27
|
initialize_result = { result: { message: "ok" } }.to_json
|
42
28
|
$stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
|
43
29
|
|
@@ -54,22 +40,20 @@ module RubyLsp
|
|
54
40
|
end
|
55
41
|
end
|
56
42
|
|
57
|
-
sig do
|
58
|
-
params(
|
59
|
-
request: String,
|
60
|
-
params: T.nilable(T::Hash[Symbol, T.untyped]),
|
61
|
-
).returns(T.any(Object, T::Hash[Symbol, T.untyped]))
|
62
|
-
end
|
63
43
|
def execute(request, params)
|
64
44
|
case request
|
65
45
|
when "shutdown"
|
66
46
|
@running = false
|
67
47
|
VOID
|
68
48
|
when "model"
|
69
|
-
resolve_database_info_from_model(
|
49
|
+
resolve_database_info_from_model(params.fetch(:name))
|
50
|
+
when "association_target_location"
|
51
|
+
resolve_association_target(params)
|
70
52
|
when "reload"
|
71
53
|
::Rails.application.reloader.reload!
|
72
54
|
VOID
|
55
|
+
when "route_location"
|
56
|
+
route_location(params.fetch(:name))
|
73
57
|
else
|
74
58
|
VOID
|
75
59
|
end
|
@@ -79,10 +63,37 @@ module RubyLsp
|
|
79
63
|
|
80
64
|
private
|
81
65
|
|
82
|
-
|
66
|
+
# Older versions of Rails don't support `route_source_locations`.
|
67
|
+
# We also check that it's enabled.
|
68
|
+
if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
|
69
|
+
ActionDispatch::Routing::Mapper.route_source_locations
|
70
|
+
def route_location(name)
|
71
|
+
match_data = name.match(/^(.+)(_path|_url)$/)
|
72
|
+
return { result: nil } unless match_data
|
73
|
+
|
74
|
+
key = match_data[1]
|
75
|
+
|
76
|
+
# A token could match the _path or _url pattern, but not be an actual route.
|
77
|
+
route = ::Rails.application.routes.named_routes.get(key)
|
78
|
+
return { result: nil } unless route&.source_location
|
79
|
+
|
80
|
+
{
|
81
|
+
result: {
|
82
|
+
location: ::Rails.root.join(route.source_location).to_s,
|
83
|
+
},
|
84
|
+
}
|
85
|
+
rescue => e
|
86
|
+
{ error: e.full_message(highlight: false) }
|
87
|
+
end
|
88
|
+
else
|
89
|
+
def route_location(name)
|
90
|
+
{ result: nil }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
83
94
|
def resolve_database_info_from_model(model_name)
|
84
95
|
const = ActiveSupport::Inflector.safe_constantize(model_name)
|
85
|
-
unless
|
96
|
+
unless active_record_model?(const)
|
86
97
|
return {
|
87
98
|
result: nil,
|
88
99
|
}
|
@@ -91,6 +102,7 @@ module RubyLsp
|
|
91
102
|
info = {
|
92
103
|
result: {
|
93
104
|
columns: const.columns.map { |column| [column.name, column.type] },
|
105
|
+
primary_keys: Array(const.primary_key),
|
94
106
|
},
|
95
107
|
}
|
96
108
|
|
@@ -103,6 +115,38 @@ module RubyLsp
|
|
103
115
|
rescue => e
|
104
116
|
{ error: e.full_message(highlight: false) }
|
105
117
|
end
|
118
|
+
|
119
|
+
def resolve_association_target(params)
|
120
|
+
const = ActiveSupport::Inflector.safe_constantize(params[:model_name])
|
121
|
+
unless active_record_model?(const)
|
122
|
+
return {
|
123
|
+
result: nil,
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
association_klass = const.reflect_on_association(params[:association_name].intern).klass
|
128
|
+
|
129
|
+
source_location = Object.const_source_location(association_klass.to_s)
|
130
|
+
|
131
|
+
{
|
132
|
+
result: {
|
133
|
+
location: source_location.first + ":" + source_location.second.to_s,
|
134
|
+
},
|
135
|
+
}
|
136
|
+
rescue NameError
|
137
|
+
{
|
138
|
+
result: nil,
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def active_record_model?(const)
|
143
|
+
!!(
|
144
|
+
const &&
|
145
|
+
defined?(ActiveRecord) &&
|
146
|
+
ActiveRecord::Base > const && # We do this 'backwards' in case the class overwrites `<`
|
147
|
+
!const.abstract_class?
|
148
|
+
)
|
149
|
+
end
|
106
150
|
end
|
107
151
|
end
|
108
152
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
module Support
|
7
|
+
module Associations
|
8
|
+
ALL = T.let(
|
9
|
+
[
|
10
|
+
"belongs_to",
|
11
|
+
"has_many",
|
12
|
+
"has_one",
|
13
|
+
"has_and_belongs_to_many",
|
14
|
+
].freeze,
|
15
|
+
T::Array[String],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
module Support
|
7
|
+
class LocationBuilder
|
8
|
+
class << self
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(location_string: String).returns(Interface::Location) }
|
12
|
+
def line_location_from_s(location_string)
|
13
|
+
*file_parts, line = location_string.split(":")
|
14
|
+
|
15
|
+
raise ArgumentError, "Invalid location string given" unless file_parts
|
16
|
+
|
17
|
+
# On Windows, file paths will look something like `C:/path/to/file.rb:123`. Only the last colon is the line
|
18
|
+
# number and all other parts compose the file path
|
19
|
+
file_path = file_parts.join(":")
|
20
|
+
|
21
|
+
Interface::Location.new(
|
22
|
+
uri: URI::Generic.from_path(path: file_path).to_s,
|
23
|
+
range: Interface::Range.new(
|
24
|
+
start: Interface::Position.new(line: Integer(line) - 1, character: 0),
|
25
|
+
end: Interface::Position.new(line: Integer(line) - 1, character: 0),
|
26
|
+
),
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -66,18 +66,25 @@ module RubyLsp
|
|
66
66
|
private def build_search_index
|
67
67
|
return unless RAILTIES_VERSION
|
68
68
|
|
69
|
-
$stderr.puts("Fetching Rails
|
69
|
+
$stderr.puts("Fetching search index for Rails documentation")
|
70
70
|
|
71
|
-
response = Net::HTTP.get_response(
|
71
|
+
response = Net::HTTP.get_response(
|
72
|
+
URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"),
|
73
|
+
{ "User-Agent" => "ruby-lsp-rails/#{RubyLsp::Rails::VERSION}" },
|
74
|
+
)
|
72
75
|
|
73
76
|
body = case response
|
74
77
|
when Net::HTTPSuccess
|
78
|
+
$stderr.puts("Finished fetching search index for Rails documentation")
|
75
79
|
response.body
|
76
80
|
when Net::HTTPRedirection
|
77
81
|
# If the version's doc is not found, e.g. Rails main, it'll be redirected
|
78
82
|
# In this case, we just fetch the latest doc
|
79
83
|
response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
|
80
|
-
|
84
|
+
if response.is_a?(Net::HTTPSuccess)
|
85
|
+
$stderr.puts("Finished fetching search index for Rails documentation")
|
86
|
+
response.body
|
87
|
+
end
|
81
88
|
else
|
82
89
|
$stderr.puts("Response failed: #{response.inspect}")
|
83
90
|
nil
|
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.
|
4
|
+
version: 0.3.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-lsp
|
@@ -16,34 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.17.0
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.
|
22
|
+
version: 0.18.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 0.16.0
|
30
|
-
- - "<"
|
31
28
|
- !ruby/object:Gem::Version
|
32
29
|
version: 0.17.0
|
33
|
-
-
|
34
|
-
name: sorbet-runtime
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - ">="
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: 0.5.9897
|
40
|
-
type: :runtime
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - ">="
|
30
|
+
- - "<"
|
45
31
|
- !ruby/object:Gem::Version
|
46
|
-
version: 0.
|
32
|
+
version: 0.18.0
|
47
33
|
description: A Ruby LSP addon that adds extra editor functionality for Rails applications
|
48
34
|
email:
|
49
35
|
- ruby@shopify.com
|
@@ -63,7 +49,9 @@ files:
|
|
63
49
|
- lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
|
64
50
|
- lib/ruby_lsp/ruby_lsp_rails/server.rb
|
65
51
|
- lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb
|
52
|
+
- lib/ruby_lsp/ruby_lsp_rails/support/associations.rb
|
66
53
|
- lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb
|
54
|
+
- lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb
|
67
55
|
- lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
|
68
56
|
- lib/ruby_lsp_rails/railtie.rb
|
69
57
|
- lib/ruby_lsp_rails/version.rb
|
@@ -91,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
79
|
- !ruby/object:Gem::Version
|
92
80
|
version: '0'
|
93
81
|
requirements: []
|
94
|
-
rubygems_version: 3.5.
|
82
|
+
rubygems_version: 3.5.10
|
95
83
|
signing_key:
|
96
84
|
specification_version: 4
|
97
85
|
summary: A Ruby LSP addon for Rails
|