ruby-lsp-rails 0.2.10 → 0.3.1

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: 41272ca8c9212dc08dc09beeef481c282656946e182e0c62c72585ecc6cfe658
4
- data.tar.gz: 0dc3c80ab252123291ab7b77a02b22fff1f27dd5bc07eb49ea51dfdbff4310f2
3
+ metadata.gz: 75f1c942c624b415b9ac1207d42c3cd0d32fe5403ef49626c3be6f18f8cb3682
4
+ data.tar.gz: 7a6740caeae1fd86c55f917bd3ae59c1b1fc4d0080875ee37875e04ad3dc8c55
5
5
  SHA512:
6
- metadata.gz: fb753c4c3a4282faeefbd254840be186969179e6eaff83320a79e6fd5d2149e4d35acd68f7c38d8bd436e07b233f5e3cebf553c53317576333b83c4b81582dc0
7
- data.tar.gz: 881b3d77f931f890ed68a12b478b7762ea491165e70c4cd61af078eccdeb139c28d9f9eed83406b7fa01927e6b0ea3a07993e0d73ae4c0eede146c6fb124a4aa
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
 
@@ -52,16 +39,15 @@ See the [documentation](https://shopify.github.io/ruby-lsp-rails) for more in-de
52
39
 
53
40
  ## How It Works
54
41
 
55
- This gem consists of two components that enable enhanced Rails functionality in the editor:
56
-
57
- 1. A Rack app that automatically exposes APIs when Rails server is running
58
- 1. A Ruby LSP addon that connects to the exposed APIs to fetch runtime information from the Rails server
42
+ When Ruby LSP Rails starts, it spawns a `rails runner` instance which runs
43
+ [`server.rb`](https://github.com/Shopify/ruby-lsp-rails/blob/main/lib/ruby_lsp/ruby_lsp_rails/server.rb).
44
+ The addon communicates with this process over a pipe (i.e. `stdin` and `stdout`) to fetch runtime information about the application.
59
45
 
60
- This is why the Rails server needs to be running for some features to work.
46
+ When extension is stopped (e.g. by quitting the editor), the server instance is shut down.
61
47
 
62
48
  > [!NOTE]
63
- > There is no need to restart the Ruby LSP every time the Rails server is booted.
64
- > If the server is shut down, the extra features will temporarily disappear and reappear once the server is running again.
49
+ > Prior to v0.3, `ruby-lsp-rails` used a different approach which involved mounting a Rack application within the Rails app.
50
+ > That approach was brittle and susceptible to the application's configuration, such as routing and middleware.
65
51
 
66
52
  ## Contributing
67
53
 
@@ -1,9 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "sorbet-runtime"
5
- require "pathname"
6
-
7
4
  require "ruby_lsp_rails/version"
8
5
  require "ruby_lsp_rails/railtie"
9
6
 
@@ -3,27 +3,36 @@
3
3
 
4
4
  require "ruby_lsp/addon"
5
5
 
6
- require_relative "rails_client"
6
+ require_relative "support/active_support_test_case_helper"
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 { returns(RailsClient) }
16
- def client
17
- @client ||= T.let(RailsClient.new, T.nilable(RailsClient))
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
27
  def activate(message_queue)
22
- client.check_if_server_is_running!
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 }
23
30
  end
24
31
 
25
32
  sig { override.void }
26
- def deactivate; end
33
+ def deactivate
34
+ @client.shutdown
35
+ end
27
36
 
28
37
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
29
38
  sig do
@@ -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
- message_value = node.message
60
- return unless message_value == "test"
60
+ content = extract_test_case_name(node)
61
61
 
62
- arguments = node.arguments&.arguments
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
@@ -22,7 +22,7 @@ module RubyLsp
22
22
 
23
23
  sig do
24
24
  params(
25
- client: RailsClient,
25
+ client: RunnerClient,
26
26
  response_builder: ResponseBuilders::Hover,
27
27
  nesting: T::Array[String],
28
28
  index: RubyIndexer::Index,
@@ -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 { params(request: T.untyped, params: T.untyped).returns(T.untyped) }
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: T.untyped, params: T.untyped).void }
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
- raw_response = @stdout.read(T.must(headers)[/Content-Length: (\d+)/i, 1].to_i)
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 { params(model_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
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
- schema_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(const.connection.pool.db_config)
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
- next if response == VOID
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
@@ -2,34 +2,17 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "rails"
5
- require "ruby_lsp_rails/rack_app"
6
5
 
7
6
  module RubyLsp
8
7
  module Rails
9
8
  class Railtie < ::Rails::Railtie
10
9
  config.ruby_lsp_rails = ActiveSupport::OrderedOptions.new
11
- config.ruby_lsp_rails.server = true
12
10
 
13
11
  initializer "ruby_lsp_rails.setup" do |_app|
14
- config.after_initialize do |app|
15
- # If we start the app with `bin/rails console` then `Rails::Server` is not defined.
16
- if defined?(::Rails::Server) && config.ruby_lsp_rails.server
17
- app.routes.prepend do
18
- T.bind(self, ActionDispatch::Routing::Mapper)
19
- mount(RackApp.new => RackApp::BASE_PATH)
20
- end
21
-
22
- ssl_enable, host, port = ::Rails::Server::Options.new.parse!(ARGV).values_at(:SSLEnable, :Host, :Port)
23
- app_uri = "#{ssl_enable ? "https" : "http"}://#{host}:#{port}"
24
- app_uri_path = ::Rails.root.join("tmp", "app_uri.txt")
25
- app_uri_path.write(app_uri)
26
-
27
- at_exit do
28
- # The app_uri.txt file should only exist when the server is running. The addon uses its presence to
29
- # report if the server is running or not. If the server is not running, some of the addon features
30
- # will not be available.
31
- File.delete(app_uri_path) if File.exist?(app_uri_path)
32
- end
12
+ config.after_initialize do |_app|
13
+ unless config.ruby_lsp_rails.server.nil?
14
+ ActiveSupport::Deprecation.new.warn("The `ruby_lsp_rails.server` configuration option is no longer " \
15
+ "needed and will be removed in a future release.")
33
16
  end
34
17
  end
35
18
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.2.10"
6
+ VERSION = "0.3.1"
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.2.10
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-13 00:00:00.000000000 Z
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.0
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.0
71
+ version: 0.14.2
72
72
  - - "<"
73
73
  - !ruby/object:Gem::Version
74
74
  version: 0.15.0
@@ -99,12 +99,12 @@ 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
- - lib/ruby_lsp/ruby_lsp_rails/rails_client.rb
104
104
  - lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
105
105
  - lib/ruby_lsp/ruby_lsp_rails/server.rb
106
+ - lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb
106
107
  - lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
107
- - lib/ruby_lsp_rails/rack_app.rb
108
108
  - lib/ruby_lsp_rails/railtie.rb
109
109
  - lib/ruby_lsp_rails/version.rb
110
110
  - lib/tasks/ruby_lsp_rails_tasks.rake
@@ -1,77 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "net/http"
5
-
6
- module RubyLsp
7
- module Rails
8
- class RailsClient
9
- class ServerAddressUnknown < StandardError; end
10
-
11
- extend T::Sig
12
-
13
- SERVER_NOT_RUNNING_MESSAGE = "Rails server is not running. " \
14
- "To get Rails features in the editor, boot the Rails server"
15
-
16
- sig { returns(Pathname) }
17
- attr_reader :root
18
-
19
- sig { void }
20
- def initialize
21
- project_root = T.let(Bundler.with_unbundled_env { Bundler.default_gemfile }.dirname, Pathname)
22
- dummy_path = project_root.join("test", "dummy")
23
-
24
- @root = T.let(dummy_path.exist? ? dummy_path : project_root, Pathname)
25
- app_uri_path = @root.join("tmp", "app_uri.txt")
26
-
27
- if app_uri_path.exist?
28
- url = URI(app_uri_path.read.chomp)
29
-
30
- @ssl = T.let(url.scheme == "https", T::Boolean)
31
- @address = T.let(
32
- [url.host, url.path].reject { |component| component.nil? || component.empty? }.join("/"),
33
- T.nilable(String),
34
- )
35
- @port = T.let(T.must(url.port).to_i, Integer)
36
- end
37
- end
38
-
39
- sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
40
- def model(name)
41
- response = request("models/#{name}")
42
- return unless response.code == "200"
43
-
44
- JSON.parse(response.body.chomp, symbolize_names: true)
45
- rescue Errno::ECONNREFUSED,
46
- Errno::EADDRNOTAVAIL,
47
- Errno::ECONNRESET,
48
- Net::ReadTimeout,
49
- Net::OpenTimeout,
50
- ServerAddressUnknown
51
- nil
52
- end
53
-
54
- sig { void }
55
- def check_if_server_is_running!
56
- request("activate", 0.2)
57
- rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Errno::ECONNRESET, ServerAddressUnknown
58
- warn(SERVER_NOT_RUNNING_MESSAGE)
59
- rescue Net::ReadTimeout, Net::OpenTimeout
60
- # If the server is running, but the initial request is taking too long, we don't want to block the
61
- # initialization of the Ruby LSP
62
- end
63
-
64
- private
65
-
66
- sig { params(path: String, timeout: T.nilable(Float)).returns(Net::HTTPResponse) }
67
- def request(path, timeout = nil)
68
- raise ServerAddressUnknown unless @address
69
-
70
- http = Net::HTTP.new(@address, @port)
71
- http.use_ssl = @ssl
72
- http.read_timeout = timeout if timeout
73
- http.get("/ruby_lsp_rails/#{path}")
74
- end
75
- end
76
- end
77
- end
@@ -1,58 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module RubyLsp
5
- module Rails
6
- class RackApp
7
- extend T::Sig
8
-
9
- BASE_PATH = "/ruby_lsp_rails/"
10
-
11
- sig { params(env: T::Hash[T.untyped, T.untyped]).returns(T::Array[T.untyped]) }
12
- def call(env)
13
- request = ActionDispatch::Request.new(env)
14
- path = request.path
15
-
16
- route, argument = path.delete_prefix(BASE_PATH).split("/")
17
-
18
- case route
19
- when "activate"
20
- [200, { "Content-Type" => "application/json" }, []]
21
- when "models"
22
- resolve_database_info_from_model(argument)
23
- else
24
- not_found
25
- end
26
- end
27
-
28
- private
29
-
30
- sig { params(model_name: String).returns(T::Array[T.untyped]) }
31
- def resolve_database_info_from_model(model_name)
32
- const = ActiveSupport::Inflector.safe_constantize(model_name)
33
-
34
- if const && const < ActiveRecord::Base && !const.abstract_class?
35
- begin
36
- schema_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(const.connection.pool.db_config)
37
- rescue => e
38
- warn("Could not locate schema: #{e.message}")
39
- end
40
-
41
- body = JSON.dump({
42
- columns: const.columns.map { |column| [column.name, column.type] },
43
- schema_file: schema_file,
44
- })
45
-
46
- [200, { "Content-Type" => "application/json" }, [body]]
47
- else
48
- not_found
49
- end
50
- end
51
-
52
- sig { returns(T::Array[T.untyped]) }
53
- def not_found
54
- [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
55
- end
56
- end
57
- end
58
- end