ruby-lsp-rails 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecc8c1b4cd4569bdb9c816f842a90f0dfb1122931adcd9c96d1e2eb8a3799471
4
- data.tar.gz: 3c1bcc4f69a361ae6a26b635331f6475413fed38bac9a88c7ed45ffea02129eb
3
+ metadata.gz: 7b68f8a0b2e4d7d4fa32aa2712865dea5c4734c86aa6a490c2a7ace718e3405a
4
+ data.tar.gz: a146d2ff4091073a9517147dffd3f5326337b947563ed39848d3a823662b5fdc
5
5
  SHA512:
6
- metadata.gz: eff545775dfa463c99b301a94ab450070f683d3f70a3c974683ac1b311a53d10da57acbd051a6fd0e4c8b005629c1d18bd8d61e33c94548e2fcf2b22b70df606
7
- data.tar.gz: 927c991dc2d499be19c626b4a64cfcc11064f2bbadad41abb71ebea13e1e33592810c7fe6135393d7a1c2140d760e8518eee085d3a2d596942af5e031ff8f272
6
+ metadata.gz: 8902056649daff38b9c6e73231ecc0fb55485d4789d52b9dbf92afe4983104082a959b242b1a9f898d3697344663a678da952b0c0a4f76db026ef07c4744fe52
7
+ data.tar.gz: 7550880a0a82da99734d38178c5aa47a1909b1787f2070bbf960f2b70ea177a3dd330484428b220de10d3d511a93b7f87a736dfbaf5c47bd12c50f547da15858
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
- `[server.rb](https://github.com/Shopify/ruby-lsp-rails/blob/main/lib/ruby_lsp/ruby_lsp_rails/server.rb)`.
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 { returns(RunnerClient) }
16
- def client
17
- @client ||= T.let(RunnerClient.new, T.nilable(RunnerClient))
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); end
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
- 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
@@ -4,17 +4,39 @@
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.setpgrp
35
+ Process.setsid
36
+ rescue Errno::EPERM
37
+ # If we can't set the session ID, continue
38
+ end
39
+
18
40
  stdin, stdout, stderr, wait_thread = Open3.popen3(
19
41
  "bin/rails",
20
42
  "runner",
@@ -27,17 +49,37 @@ module RubyLsp
27
49
  @wait_thread = T.let(wait_thread, Process::Waiter)
28
50
  @stdin.binmode # for Windows compatibility
29
51
  @stdout.binmode # for Windows compatibility
52
+
53
+ warn("Ruby LSP Rails booting server")
54
+ read_response
55
+ warn("Finished booting Ruby LSP Rails server")
56
+
57
+ unless ENV["RAILS_ENV"] == "test"
58
+ at_exit do
59
+ if @wait_thread.alive?
60
+ warn("Ruby LSP Rails is force killing the server")
61
+ sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
62
+ Process.kill(T.must(Signal.list["TERM"]), @wait_thread.pid)
63
+ end
64
+ end
65
+ end
66
+ rescue Errno::EPIPE, IncompleteMessageError
67
+ raise InitializationError, @stderr.read
30
68
  end
31
69
 
32
70
  sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
33
71
  def model(name)
34
72
  make_request("model", name: name)
73
+ rescue IncompleteMessageError
74
+ warn("Ruby LSP Rails failed to get model information: #{@stderr.read}")
75
+ nil
35
76
  end
36
77
 
37
78
  sig { void }
38
79
  def shutdown
39
- send_notification("shutdown")
40
- Thread.pass while @wait_thread.alive?
80
+ warn("Ruby LSP Rails shutting down server")
81
+ send_message("shutdown")
82
+ sleep(0.5) # give the server a bit of time to shutdown
41
83
  [@stdin, @stdout, @stderr].each(&:close)
42
84
  end
43
85
 
@@ -48,13 +90,18 @@ module RubyLsp
48
90
 
49
91
  private
50
92
 
51
- sig { params(request: T.untyped, params: T.untyped).returns(T.untyped) }
93
+ sig do
94
+ params(
95
+ request: String,
96
+ params: T.nilable(T::Hash[Symbol, T.untyped]),
97
+ ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
98
+ end
52
99
  def make_request(request, params = nil)
53
100
  send_message(request, params)
54
101
  read_response
55
102
  end
56
103
 
57
- sig { params(request: T.untyped, params: T.untyped).void }
104
+ sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
58
105
  def send_message(request, params = nil)
59
106
  message = { method: request }
60
107
  message[:params] = params if params
@@ -68,8 +115,9 @@ module RubyLsp
68
115
  sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
69
116
  def read_response
70
117
  headers = @stdout.gets("\r\n\r\n")
71
- raw_response = @stdout.read(T.must(headers)[/Content-Length: (\d+)/i, 1].to_i)
118
+ raise IncompleteMessageError unless headers
72
119
 
120
+ raw_response = @stdout.read(headers[/Content-Length: (\d+)/i, 1].to_i)
73
121
  response = JSON.parse(T.must(raw_response), symbolize_names: true)
74
122
 
75
123
  if response[:error]
@@ -80,5 +128,35 @@ module RubyLsp
80
128
  response.fetch(:result)
81
129
  end
82
130
  end
131
+
132
+ class NullClient < RunnerClient
133
+ extend T::Sig
134
+
135
+ sig { void }
136
+ def initialize # rubocop:disable Lint/MissingSuper
137
+ end
138
+
139
+ sig { override.void }
140
+ def shutdown
141
+ # no-op
142
+ end
143
+
144
+ sig { override.returns(T::Boolean) }
145
+ def stopped?
146
+ true
147
+ end
148
+
149
+ private
150
+
151
+ sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
152
+ def send_message(request, params = nil)
153
+ # no-op
154
+ end
155
+
156
+ sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
157
+ def read_response
158
+ # no-op
159
+ end
160
+ end
83
161
  end
84
162
  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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.0"
6
+ VERSION = "0.3.2"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
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-15 00:00:00.000000000 Z
11
+ date: 2024-02-29 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,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