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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecc8c1b4cd4569bdb9c816f842a90f0dfb1122931adcd9c96d1e2eb8a3799471
4
- data.tar.gz: 3c1bcc4f69a361ae6a26b635331f6475413fed38bac9a88c7ed45ffea02129eb
3
+ metadata.gz: 75f1c942c624b415b9ac1207d42c3cd0d32fe5403ef49626c3be6f18f8cb3682
4
+ data.tar.gz: 7a6740caeae1fd86c55f917bd3ae59c1b1fc4d0080875ee37875e04ad3dc8c55
5
5
  SHA512:
6
- metadata.gz: eff545775dfa463c99b301a94ab450070f683d3f70a3c974683ac1b311a53d10da57acbd051a6fd0e4c8b005629c1d18bd8d61e33c94548e2fcf2b22b70df606
7
- data.tar.gz: 927c991dc2d499be19c626b4a64cfcc11064f2bbadad41abb71ebea13e1e33592810c7fe6135393d7a1c2140d760e8518eee085d3a2d596942af5e031ff8f272
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
- `[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,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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.0"
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.3.0
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-15 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,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