ruby-lsp-rails 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -14
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +25 -6
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +3 -19
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +42 -0
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +73 -7
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +59 -39
- data/lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb +38 -0
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75f1c942c624b415b9ac1207d42c3cd0d32fe5403ef49626c3be6f18f8cb3682
|
4
|
+
data.tar.gz: 7a6740caeae1fd86c55f917bd3ae59c1b1fc4d0080875ee37875e04ad3dc8c55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a86197ecdea2653c87dfb3d79b092681504391fd76e1a0849b754aca21b60fdcc8855f6c4d8fab4ee8e9a1816a1e39d896311bf30f558fb8a9b195ac6de3711
|
7
|
+
data.tar.gz: f295e7756887b52128eafd56a1cbb70017c48f0128c1f82d521b81098e50c894ae1c32f97c66dddd3752a62a5a9171f35dd7368ef6444a13acb467a368cd5cee
|
data/README.md
CHANGED
@@ -15,19 +15,6 @@ group :development do
|
|
15
15
|
gem "ruby-lsp-rails"
|
16
16
|
end
|
17
17
|
```
|
18
|
-
Some features rely on server introspection, and use a Rack server which is automatically mounted by using a Railtie.
|
19
|
-
|
20
|
-
For applications with specialized routing requirements, such as custom sharding, this may not be compatible. It can
|
21
|
-
be disabled with:
|
22
|
-
|
23
|
-
```ruby
|
24
|
-
# config/environments/development.rb
|
25
|
-
Rails.application.configure do
|
26
|
-
# ...
|
27
|
-
config.ruby_lsp_rails.server = false
|
28
|
-
# ...
|
29
|
-
end
|
30
|
-
```
|
31
18
|
|
32
19
|
## Usage
|
33
20
|
|
@@ -53,7 +40,7 @@ See the [documentation](https://shopify.github.io/ruby-lsp-rails) for more in-de
|
|
53
40
|
## How It Works
|
54
41
|
|
55
42
|
When Ruby LSP Rails starts, it spawns a `rails runner` instance which runs
|
56
|
-
`
|
43
|
+
[`server.rb`](https://github.com/Shopify/ruby-lsp-rails/blob/main/lib/ruby_lsp/ruby_lsp_rails/server.rb).
|
57
44
|
The addon communicates with this process over a pipe (i.e. `stdin` and `stdout`) to fetch runtime information about the application.
|
58
45
|
|
59
46
|
When extension is stopped (e.g. by quitting the editor), the server instance is shut down.
|
@@ -3,26 +3,35 @@
|
|
3
3
|
|
4
4
|
require "ruby_lsp/addon"
|
5
5
|
|
6
|
+
require_relative "support/active_support_test_case_helper"
|
6
7
|
require_relative "runner_client"
|
7
8
|
require_relative "hover"
|
8
9
|
require_relative "code_lens"
|
10
|
+
require_relative "document_symbol"
|
9
11
|
|
10
12
|
module RubyLsp
|
11
13
|
module Rails
|
12
14
|
class Addon < ::RubyLsp::Addon
|
13
15
|
extend T::Sig
|
14
16
|
|
15
|
-
sig {
|
16
|
-
def
|
17
|
-
|
17
|
+
sig { void }
|
18
|
+
def initialize
|
19
|
+
super
|
20
|
+
|
21
|
+
# We first initialize the client as a NullClient, so that we can start the server in a background thread. Until
|
22
|
+
# the real client is initialized, features that depend on it will not be blocked by using the NullClient
|
23
|
+
@client = T.let(NullClient.new, RunnerClient)
|
18
24
|
end
|
19
25
|
|
20
26
|
sig { override.params(message_queue: Thread::Queue).void }
|
21
|
-
def activate(message_queue)
|
27
|
+
def activate(message_queue)
|
28
|
+
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
29
|
+
Thread.new { @client = RunnerClient.create_client }
|
30
|
+
end
|
22
31
|
|
23
32
|
sig { override.void }
|
24
33
|
def deactivate
|
25
|
-
client.shutdown
|
34
|
+
@client.shutdown
|
26
35
|
end
|
27
36
|
|
28
37
|
# Creates a new CodeLens listener. This method is invoked on every CodeLens request
|
@@ -46,7 +55,17 @@ module RubyLsp
|
|
46
55
|
).void
|
47
56
|
end
|
48
57
|
def create_hover_listener(response_builder, nesting, index, dispatcher)
|
49
|
-
Hover.new(client, response_builder, nesting, index, dispatcher)
|
58
|
+
Hover.new(@client, response_builder, nesting, index, dispatcher)
|
59
|
+
end
|
60
|
+
|
61
|
+
sig do
|
62
|
+
override.params(
|
63
|
+
response_builder: ResponseBuilders::DocumentSymbol,
|
64
|
+
dispatcher: Prism::Dispatcher,
|
65
|
+
).returns(Object)
|
66
|
+
end
|
67
|
+
def create_document_symbol_listener(response_builder, dispatcher)
|
68
|
+
DocumentSymbol.new(response_builder, dispatcher)
|
50
69
|
end
|
51
70
|
|
52
71
|
sig { override.returns(String) }
|
@@ -35,6 +35,7 @@ module RubyLsp
|
|
35
35
|
class CodeLens
|
36
36
|
extend T::Sig
|
37
37
|
include Requests::Support::Common
|
38
|
+
include ActiveSupportTestCaseHelper
|
38
39
|
|
39
40
|
BASE_COMMAND = "bin/rails test"
|
40
41
|
|
@@ -56,26 +57,9 @@ module RubyLsp
|
|
56
57
|
|
57
58
|
sig { params(node: Prism::CallNode).void }
|
58
59
|
def on_call_node_enter(node)
|
59
|
-
|
60
|
-
return unless message_value == "test"
|
60
|
+
content = extract_test_case_name(node)
|
61
61
|
|
62
|
-
|
63
|
-
return unless arguments&.any?
|
64
|
-
|
65
|
-
first_argument = arguments.first
|
66
|
-
|
67
|
-
content = case first_argument
|
68
|
-
when Prism::InterpolatedStringNode
|
69
|
-
parts = first_argument.parts
|
70
|
-
|
71
|
-
if parts.all? { |part| part.is_a?(Prism::StringNode) }
|
72
|
-
T.cast(parts, T::Array[Prism::StringNode]).map(&:content).join
|
73
|
-
end
|
74
|
-
when Prism::StringNode
|
75
|
-
first_argument.content
|
76
|
-
end
|
77
|
-
|
78
|
-
return unless content && !content.empty?
|
62
|
+
return unless content
|
79
63
|
|
80
64
|
line_number = node.location.start_line
|
81
65
|
command = "#{BASE_COMMAND} #{@path}:#{line_number}"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
# ![Document Symbol demo](../../document_symbol.gif)
|
7
|
+
#
|
8
|
+
# The [document symbol](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol)
|
9
|
+
# request allows users to navigate between ActiveSupport test cases with VS Code's "Go to Symbol" feature.
|
10
|
+
class DocumentSymbol
|
11
|
+
extend T::Sig
|
12
|
+
include Requests::Support::Common
|
13
|
+
include ActiveSupportTestCaseHelper
|
14
|
+
|
15
|
+
sig do
|
16
|
+
params(
|
17
|
+
response_builder: ResponseBuilders::DocumentSymbol,
|
18
|
+
dispatcher: Prism::Dispatcher,
|
19
|
+
).void
|
20
|
+
end
|
21
|
+
def initialize(response_builder, dispatcher)
|
22
|
+
@response_builder = response_builder
|
23
|
+
|
24
|
+
dispatcher.register(self, :on_call_node_enter)
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { params(node: Prism::CallNode).void }
|
28
|
+
def on_call_node_enter(node)
|
29
|
+
content = extract_test_case_name(node)
|
30
|
+
|
31
|
+
return unless content
|
32
|
+
|
33
|
+
@response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
|
34
|
+
name: content,
|
35
|
+
kind: LanguageServer::Protocol::Constant::SymbolKind::METHOD,
|
36
|
+
selection_range: range_from_node(node),
|
37
|
+
range: range_from_node(node),
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -4,17 +4,38 @@
|
|
4
4
|
require "json"
|
5
5
|
require "open3"
|
6
6
|
|
7
|
-
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe
|
8
|
-
# from the client, so it will become full and eventually hang or crash.
|
9
|
-
# Instead, return a response with an `error` key.
|
10
|
-
|
11
7
|
module RubyLsp
|
12
8
|
module Rails
|
13
9
|
class RunnerClient
|
10
|
+
class << self
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(RunnerClient) }
|
14
|
+
def create_client
|
15
|
+
new
|
16
|
+
rescue Errno::ENOENT, StandardError => e # rubocop:disable Lint/ShadowedException
|
17
|
+
warn("Ruby LSP Rails failed to initialize server: #{e.message}\n#{e.backtrace&.join("\n")}")
|
18
|
+
warn("Server dependent features will not be available")
|
19
|
+
NullClient.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class InitializationError < StandardError; end
|
24
|
+
class IncompleteMessageError < StandardError; end
|
25
|
+
|
14
26
|
extend T::Sig
|
15
27
|
|
16
28
|
sig { void }
|
17
29
|
def initialize
|
30
|
+
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
31
|
+
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
|
32
|
+
# set its own session ID
|
33
|
+
begin
|
34
|
+
Process.setsid
|
35
|
+
rescue Errno::EPERM
|
36
|
+
# If we can't set the session ID, continue
|
37
|
+
end
|
38
|
+
|
18
39
|
stdin, stdout, stderr, wait_thread = Open3.popen3(
|
19
40
|
"bin/rails",
|
20
41
|
"runner",
|
@@ -27,11 +48,20 @@ module RubyLsp
|
|
27
48
|
@wait_thread = T.let(wait_thread, Process::Waiter)
|
28
49
|
@stdin.binmode # for Windows compatibility
|
29
50
|
@stdout.binmode # for Windows compatibility
|
51
|
+
|
52
|
+
warn("Ruby LSP Rails booting server")
|
53
|
+
read_response
|
54
|
+
warn("Finished booting Ruby LSP Rails server")
|
55
|
+
rescue Errno::EPIPE, IncompleteMessageError
|
56
|
+
raise InitializationError, @stderr.read
|
30
57
|
end
|
31
58
|
|
32
59
|
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
33
60
|
def model(name)
|
34
61
|
make_request("model", name: name)
|
62
|
+
rescue IncompleteMessageError
|
63
|
+
warn("Ruby LSP Rails failed to get model information: #{@stderr.read}")
|
64
|
+
nil
|
35
65
|
end
|
36
66
|
|
37
67
|
sig { void }
|
@@ -48,13 +78,18 @@ module RubyLsp
|
|
48
78
|
|
49
79
|
private
|
50
80
|
|
51
|
-
sig
|
81
|
+
sig do
|
82
|
+
params(
|
83
|
+
request: String,
|
84
|
+
params: T.nilable(T::Hash[Symbol, T.untyped]),
|
85
|
+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
86
|
+
end
|
52
87
|
def make_request(request, params = nil)
|
53
88
|
send_message(request, params)
|
54
89
|
read_response
|
55
90
|
end
|
56
91
|
|
57
|
-
sig { params(request:
|
92
|
+
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
58
93
|
def send_message(request, params = nil)
|
59
94
|
message = { method: request }
|
60
95
|
message[:params] = params if params
|
@@ -68,8 +103,9 @@ module RubyLsp
|
|
68
103
|
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
69
104
|
def read_response
|
70
105
|
headers = @stdout.gets("\r\n\r\n")
|
71
|
-
|
106
|
+
raise IncompleteMessageError unless headers
|
72
107
|
|
108
|
+
raw_response = @stdout.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
73
109
|
response = JSON.parse(T.must(raw_response), symbolize_names: true)
|
74
110
|
|
75
111
|
if response[:error]
|
@@ -80,5 +116,35 @@ module RubyLsp
|
|
80
116
|
response.fetch(:result)
|
81
117
|
end
|
82
118
|
end
|
119
|
+
|
120
|
+
class NullClient < RunnerClient
|
121
|
+
extend T::Sig
|
122
|
+
|
123
|
+
sig { void }
|
124
|
+
def initialize # rubocop:disable Lint/MissingSuper
|
125
|
+
end
|
126
|
+
|
127
|
+
sig { override.void }
|
128
|
+
def shutdown
|
129
|
+
# no-op
|
130
|
+
end
|
131
|
+
|
132
|
+
sig { override.returns(T::Boolean) }
|
133
|
+
def stopped?
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
140
|
+
def send_message(request, params = nil)
|
141
|
+
# no-op
|
142
|
+
end
|
143
|
+
|
144
|
+
sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
145
|
+
def read_response
|
146
|
+
# no-op
|
147
|
+
end
|
148
|
+
end
|
83
149
|
end
|
84
150
|
end
|
@@ -19,6 +19,9 @@ rescue
|
|
19
19
|
nil
|
20
20
|
end
|
21
21
|
|
22
|
+
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
|
23
|
+
# client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
|
24
|
+
|
22
25
|
module RubyLsp
|
23
26
|
module Rails
|
24
27
|
class Server
|
@@ -26,59 +29,76 @@ module RubyLsp
|
|
26
29
|
|
27
30
|
extend T::Sig
|
28
31
|
|
29
|
-
sig {
|
32
|
+
sig { void }
|
33
|
+
def initialize
|
34
|
+
$stdin.sync = true
|
35
|
+
$stdout.sync = true
|
36
|
+
@running = T.let(true, T::Boolean)
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { void }
|
40
|
+
def start
|
41
|
+
initialize_result = { result: { message: "ok" } }.to_json
|
42
|
+
$stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
|
43
|
+
|
44
|
+
while @running
|
45
|
+
headers = $stdin.gets("\r\n\r\n")
|
46
|
+
json = $stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
47
|
+
|
48
|
+
request = JSON.parse(json, symbolize_names: true)
|
49
|
+
response = execute(request.fetch(:method), request[:params])
|
50
|
+
next if response == VOID
|
51
|
+
|
52
|
+
json_response = response.to_json
|
53
|
+
$stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
sig do
|
58
|
+
params(
|
59
|
+
request: String,
|
60
|
+
params: T::Hash[Symbol, T.untyped],
|
61
|
+
).returns(T.any(Object, T::Hash[Symbol, T.untyped]))
|
62
|
+
end
|
63
|
+
def execute(request, params = {})
|
64
|
+
case request
|
65
|
+
when "shutdown"
|
66
|
+
@running = false
|
67
|
+
VOID
|
68
|
+
when "model"
|
69
|
+
resolve_database_info_from_model(params.fetch(:name))
|
70
|
+
else
|
71
|
+
VOID
|
72
|
+
end
|
73
|
+
rescue => e
|
74
|
+
{ error: e.full_message(highlight: false) }
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
sig { params(model_name: String).returns(T::Hash[Symbol, T.untyped]) }
|
30
80
|
def resolve_database_info_from_model(model_name)
|
31
81
|
const = ActiveSupport::Inflector.safe_constantize(model_name)
|
32
|
-
unless const && const < ActiveRecord::Base && !const.abstract_class?
|
82
|
+
unless const && defined?(ActiveRecord) && const < ActiveRecord::Base && !const.abstract_class?
|
33
83
|
return {
|
34
84
|
result: nil,
|
35
85
|
}
|
36
86
|
end
|
37
87
|
|
38
|
-
|
39
|
-
|
40
|
-
{
|
88
|
+
info = {
|
41
89
|
result: {
|
42
90
|
columns: const.columns.map { |column| [column.name, column.type] },
|
43
|
-
schema_file: ::Rails.root + schema_file,
|
44
91
|
},
|
45
92
|
}
|
46
|
-
rescue => e
|
47
|
-
{
|
48
|
-
error: e.message,
|
49
|
-
}
|
50
|
-
end
|
51
|
-
|
52
|
-
sig { void }
|
53
|
-
def start
|
54
|
-
$stdin.sync = true
|
55
|
-
$stdout.sync = true
|
56
|
-
|
57
|
-
running = T.let(true, T::Boolean)
|
58
|
-
|
59
|
-
while running
|
60
|
-
headers = $stdin.gets("\r\n\r\n")
|
61
|
-
request = $stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
62
|
-
|
63
|
-
json = JSON.parse(request, symbolize_names: true)
|
64
|
-
request_method = json.fetch(:method)
|
65
|
-
params = json[:params]
|
66
|
-
|
67
|
-
response = case request_method
|
68
|
-
when "shutdown"
|
69
|
-
running = false
|
70
|
-
VOID
|
71
|
-
when "model"
|
72
|
-
resolve_database_info_from_model(params.fetch(:name))
|
73
|
-
else
|
74
|
-
VOID
|
75
|
-
end
|
76
93
|
|
77
|
-
|
94
|
+
if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
|
95
|
+
info[:result][:schema_file] =
|
96
|
+
ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(const.connection.pool.db_config)
|
78
97
|
|
79
|
-
json_response = response.to_json
|
80
|
-
$stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
|
81
98
|
end
|
99
|
+
info
|
100
|
+
rescue => e
|
101
|
+
{ error: e.full_message(highlight: false) }
|
82
102
|
end
|
83
103
|
end
|
84
104
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
module ActiveSupportTestCaseHelper
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(node: Prism::CallNode).returns(T.nilable(String)) }
|
10
|
+
def extract_test_case_name(node)
|
11
|
+
message_value = node.message
|
12
|
+
return unless message_value == "test" || message_value == "it"
|
13
|
+
|
14
|
+
return unless node.arguments
|
15
|
+
|
16
|
+
arguments = node.arguments&.arguments
|
17
|
+
return unless arguments&.any?
|
18
|
+
|
19
|
+
first_argument = arguments.first
|
20
|
+
|
21
|
+
content = case first_argument
|
22
|
+
when Prism::InterpolatedStringNode
|
23
|
+
parts = first_argument.parts
|
24
|
+
|
25
|
+
if parts.all? { |part| part.is_a?(Prism::StringNode) }
|
26
|
+
T.cast(parts, T::Array[Prism::StringNode]).map(&:content).join
|
27
|
+
end
|
28
|
+
when Prism::StringNode
|
29
|
+
first_argument.content
|
30
|
+
end
|
31
|
+
|
32
|
+
if content && !content.empty?
|
33
|
+
content
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -58,7 +58,7 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.14.
|
61
|
+
version: 0.14.2
|
62
62
|
- - "<"
|
63
63
|
- !ruby/object:Gem::Version
|
64
64
|
version: 0.15.0
|
@@ -68,7 +68,7 @@ dependencies:
|
|
68
68
|
requirements:
|
69
69
|
- - ">="
|
70
70
|
- !ruby/object:Gem::Version
|
71
|
-
version: 0.14.
|
71
|
+
version: 0.14.2
|
72
72
|
- - "<"
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: 0.15.0
|
@@ -99,9 +99,11 @@ files:
|
|
99
99
|
- lib/ruby-lsp-rails.rb
|
100
100
|
- lib/ruby_lsp/ruby_lsp_rails/addon.rb
|
101
101
|
- lib/ruby_lsp/ruby_lsp_rails/code_lens.rb
|
102
|
+
- lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb
|
102
103
|
- lib/ruby_lsp/ruby_lsp_rails/hover.rb
|
103
104
|
- lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
|
104
105
|
- lib/ruby_lsp/ruby_lsp_rails/server.rb
|
106
|
+
- lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb
|
105
107
|
- lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
|
106
108
|
- lib/ruby_lsp_rails/railtie.rb
|
107
109
|
- lib/ruby_lsp_rails/version.rb
|