ruby-lsp-rails 0.3.5 → 0.3.6

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: 175aa77410d6d479169ab7c5c212d12d56ec2c6624d7766bd8e1575da706d7a9
4
- data.tar.gz: 76799c42df0346b1628479e61c86914a7f52b96c656ddf39975bed904e13e459
3
+ metadata.gz: 65c6258c3267f6d1d080e056f4561d61cf8f9bb93059dec1c0b70086fa0ce540
4
+ data.tar.gz: 536aac52752c432fc3c18d76c66151c099533849abc8968aafe832524e25303b
5
5
  SHA512:
6
- metadata.gz: 59a4059ffdeb381dd3b8c4858e91b64a624df5e424807540b99aa6182f908f944cc6ea0ddf677776eb2d4680f8bc7c7944883fea2eb346da8252125eedd2c788
7
- data.tar.gz: 8bdc777be1f0663cc6d7e2a450b42ceef091f5e75769cf74570ce536bc914de2c2b559cbdcd05989b55d3fc6a58a843f637b2caaf1ec165a97b4c3d0b2c7db11
6
+ metadata.gz: 3b527faec645912d5c49aef3d4d2bb094a789410283ddcebcc9be168f703807831a5a5f2561a88dcecf5c4e3d894c0d2ccf9fe617688f09462e88ab7ae1a2ee8
7
+ data.tar.gz: b080811bd5697ae68573df2fd6d922dec47e2e16b5264cd86b707f73cab9689fffa858851da2ebed7a819930fbc148234db4de5c4a15dc2e4fffbec3e4fdfbcb
data/README.md CHANGED
@@ -6,6 +6,7 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
6
6
  * Run or debug a test by clicking on the code lens which appears above the test class, or an individual test.
7
7
  * Navigate to associations, validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
8
8
  * Jump to the definition of callbacks using your editor's "Go to Definition" feature.
9
+ * Jump to the declaration of a route.
9
10
 
10
11
  ## Installation
11
12
 
@@ -36,9 +37,7 @@ When extension is stopped (e.g. by quitting the editor), the server instance is
36
37
 
37
38
  ## Contributing
38
39
 
39
- Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ruby-lsp-rails. This project is
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.
40
+ See [CONTRIBUTING.md](https://github.com/Shopify/ruby-lsp-rails/blob/main/CONTRIBUTING.md)
42
41
 
43
42
  ## License
44
43
 
@@ -3,6 +3,7 @@
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"
7
8
  require_relative "support/callbacks"
8
9
  require_relative "runner_client"
@@ -31,6 +32,7 @@ module RubyLsp
31
32
  $stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
32
33
  # Start booting the real client in a background thread. Until this completes, the client will be a NullClient
33
34
  Thread.new { @client = RunnerClient.create_client }
35
+ register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
34
36
  end
35
37
 
36
38
  sig { override.void }
@@ -83,7 +85,7 @@ module RubyLsp
83
85
  end
84
86
  def create_definition_listener(response_builder, uri, nesting, dispatcher)
85
87
  index = T.must(@global_state).index
86
- Definition.new(response_builder, nesting, index, dispatcher)
88
+ Definition.new(@client, response_builder, nesting, index, dispatcher)
87
89
  end
88
90
 
89
91
  sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
@@ -95,6 +97,32 @@ module RubyLsp
95
97
  end
96
98
  end
97
99
 
100
+ sig { params(global_state: GlobalState, message_queue: Thread::Queue).void }
101
+ def register_additional_file_watchers(global_state:, message_queue:)
102
+ return unless global_state.supports_watching_files
103
+
104
+ message_queue << Request.new(
105
+ id: "ruby-lsp-rails-file-watcher",
106
+ method: "client/registerCapability",
107
+ params: Interface::RegistrationParams.new(
108
+ registrations: [
109
+ Interface::Registration.new(
110
+ id: "workspace/didChangeWatchedFilesRails",
111
+ method: "workspace/didChangeWatchedFiles",
112
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
113
+ watchers: [
114
+ Interface::FileSystemWatcher.new(
115
+ glob_pattern: "**/*structure.sql",
116
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
117
+ ),
118
+ ],
119
+ ),
120
+ ),
121
+ ],
122
+ ),
123
+ )
124
+ end
125
+
98
126
  sig { override.returns(String) }
99
127
  def name
100
128
  "Ruby LSP Rails"
@@ -42,8 +42,6 @@ module RubyLsp
42
42
  include Requests::Support::Common
43
43
  include ActiveSupportTestCaseHelper
44
44
 
45
- BASE_COMMAND = "bin/rails test"
46
-
47
45
  sig do
48
46
  params(
49
47
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
@@ -67,7 +65,7 @@ module RubyLsp
67
65
  return unless content
68
66
 
69
67
  line_number = node.location.start_line
70
- command = "#{BASE_COMMAND} #{@path}:#{line_number}"
68
+ command = "#{test_command} #{@path}:#{line_number}"
71
69
  add_test_code_lens(node, name: content, command: command, kind: :example)
72
70
  end
73
71
 
@@ -77,7 +75,7 @@ module RubyLsp
77
75
  method_name = node.name.to_s
78
76
  if method_name.start_with?("test_")
79
77
  line_number = node.location.start_line
80
- command = "#{BASE_COMMAND} #{@path}:#{line_number}"
78
+ command = "#{test_command} #{@path}:#{line_number}"
81
79
  add_test_code_lens(node, name: method_name, command: command, kind: :example)
82
80
  end
83
81
  end
@@ -86,7 +84,7 @@ module RubyLsp
86
84
  def on_class_node_enter(node)
87
85
  class_name = node.constant_path.slice
88
86
  if class_name.end_with?("Test")
89
- command = "#{BASE_COMMAND} #{@path}"
87
+ command = "#{test_command} #{@path}"
90
88
  add_test_code_lens(node, name: class_name, command: command, kind: :group)
91
89
  @group_id_stack.push(@group_id)
92
90
  @group_id += 1
@@ -103,6 +101,15 @@ module RubyLsp
103
101
 
104
102
  private
105
103
 
104
+ sig { returns(String) }
105
+ def test_command
106
+ if Gem.win_platform?
107
+ "ruby bin/rails test"
108
+ else
109
+ "bin/rails test"
110
+ end
111
+ end
112
+
106
113
  sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
107
114
  def add_test_code_lens(node, name:, command:, kind:)
108
115
  return unless @path
@@ -11,25 +11,35 @@ module RubyLsp
11
11
  #
12
12
  # Currently supported targets:
13
13
  # - Callbacks
14
+ # - Named routes (e.g. `users_path`)
14
15
  #
15
16
  # # Example
16
17
  #
17
18
  # ```ruby
18
19
  # before_action :foo # <- Go to definition on this symbol will jump to the method if it is defined in the same class
19
20
  # ```
21
+ #
22
+ # Notes for named routes:
23
+ # - It is available only in Rails 7.1 or newer.
24
+ # - Route may be defined across multiple files, e.g. using `draw`, rather than in `routes.rb`.
25
+ # - Routes won't be found if not defined for the Rails development environment.
26
+ # - If using `constraints`, the route can only be found if the constraints are met.
27
+ # - Changes to routes won't be picked up until the server is restarted.
20
28
  class Definition
21
29
  extend T::Sig
22
30
  include Requests::Support::Common
23
31
 
24
32
  sig do
25
33
  params(
34
+ client: RunnerClient,
26
35
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
27
36
  nesting: T::Array[String],
28
37
  index: RubyIndexer::Index,
29
38
  dispatcher: Prism::Dispatcher,
30
39
  ).void
31
40
  end
32
- def initialize(response_builder, nesting, index, dispatcher)
41
+ def initialize(client, response_builder, nesting, index, dispatcher)
42
+ @client = client
33
43
  @response_builder = response_builder
34
44
  @nesting = nesting
35
45
  @index = index
@@ -43,8 +53,19 @@ module RubyLsp
43
53
 
44
54
  message = node.message
45
55
 
46
- return unless message && Support::Callbacks::ALL.include?(message)
56
+ return unless message
57
+
58
+ if Support::Callbacks::ALL.include?(message)
59
+ handle_callback(node)
60
+ elsif message.end_with?("_path") || message.end_with?("_url")
61
+ handle_route(node)
62
+ end
63
+ end
47
64
 
65
+ private
66
+
67
+ sig { params(node: Prism::CallNode).void }
68
+ def handle_callback(node)
48
69
  arguments = node.arguments&.arguments
49
70
  return unless arguments&.any?
50
71
 
@@ -62,7 +83,25 @@ module RubyLsp
62
83
  end
63
84
  end
64
85
 
65
- private
86
+ sig { params(node: Prism::CallNode).void }
87
+ def handle_route(node)
88
+ result = @client.route_location(T.must(node.message))
89
+ return unless result
90
+
91
+ *file_parts, line = result.fetch(:location).split(":")
92
+
93
+ # On Windows, file paths will look something like `C:/path/to/file.rb:123`. Only the last colon is the line
94
+ # number and all other parts compose the file path
95
+ file_path = file_parts.join(":")
96
+
97
+ @response_builder << Interface::Location.new(
98
+ uri: URI::Generic.from_path(path: file_path).to_s,
99
+ range: Interface::Range.new(
100
+ start: Interface::Position.new(line: Integer(line) - 1, character: 0),
101
+ end: Interface::Position.new(line: Integer(line) - 1, character: 0),
102
+ ),
103
+ )
104
+ end
66
105
 
67
106
  sig { params(name: String).void }
68
107
  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
- dispatcher.register(self, :on_call_node_enter)
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", "has_and_belongs_to_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
@@ -77,13 +77,14 @@ module RubyLsp
77
77
  schema_file = model[:schema_file]
78
78
 
79
79
  @response_builder.push(
80
- "[Schema](#{URI::Generic.build(scheme: "file", path: schema_file)})",
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
- "**#{name}**: #{type}\n"
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
- Process.kill(T.must(Signal.list["TERM"]), @wait_thread.pid)
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,14 @@ module RubyLsp
95
98
  nil
96
99
  end
97
100
 
101
+ sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
102
+ def route_location(name)
103
+ make_request("route_location", name: name)
104
+ rescue IncompleteMessageError
105
+ $stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
106
+ nil
107
+ end
108
+
98
109
  sig { void }
99
110
  def trigger_reload
100
111
  $stderr.puts("Reloading Rails application")
@@ -1,24 +1,8 @@
1
- # typed: strict
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,16 +11,14 @@ 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
- @running = T.let(true, T::Boolean)
17
+ $stdin.binmode
18
+ $stdout.binmode
19
+ @running = true
37
20
  end
38
21
 
39
- sig { void }
40
22
  def start
41
23
  initialize_result = { result: { message: "ok" } }.to_json
42
24
  $stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
@@ -54,22 +36,18 @@ module RubyLsp
54
36
  end
55
37
  end
56
38
 
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
39
  def execute(request, params)
64
40
  case request
65
41
  when "shutdown"
66
42
  @running = false
67
43
  VOID
68
44
  when "model"
69
- resolve_database_info_from_model(T.must(params).fetch(:name))
45
+ resolve_database_info_from_model(params.fetch(:name))
70
46
  when "reload"
71
47
  ::Rails.application.reloader.reload!
72
48
  VOID
49
+ when "route_location"
50
+ route_location(params.fetch(:name))
73
51
  else
74
52
  VOID
75
53
  end
@@ -79,10 +57,37 @@ module RubyLsp
79
57
 
80
58
  private
81
59
 
82
- sig { params(model_name: String).returns(T::Hash[Symbol, T.untyped]) }
60
+ # Older versions of Rails don't support `route_source_locations`.
61
+ # We also check that it's enabled.
62
+ if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
63
+ ActionDispatch::Routing::Mapper.route_source_locations
64
+ def route_location(name)
65
+ match_data = name.match(/^(.+)(_path|_url)$/)
66
+ return { result: nil } unless match_data
67
+
68
+ key = match_data[1]
69
+
70
+ # A token could match the _path or _url pattern, but not be an actual route.
71
+ route = ::Rails.application.routes.named_routes.get(key)
72
+ return { result: nil } unless route&.source_location
73
+
74
+ {
75
+ result: {
76
+ location: ::Rails.root.join(route.source_location).to_s,
77
+ },
78
+ }
79
+ rescue => e
80
+ { error: e.full_message(highlight: false) }
81
+ end
82
+ else
83
+ def route_location(name)
84
+ { result: nil }
85
+ end
86
+ end
87
+
83
88
  def resolve_database_info_from_model(model_name)
84
89
  const = ActiveSupport::Inflector.safe_constantize(model_name)
85
- unless const && defined?(ActiveRecord) && const < ActiveRecord::Base && !const.abstract_class?
90
+ unless active_record_model?(const)
86
91
  return {
87
92
  result: nil,
88
93
  }
@@ -91,6 +96,7 @@ module RubyLsp
91
96
  info = {
92
97
  result: {
93
98
  columns: const.columns.map { |column| [column.name, column.type] },
99
+ primary_keys: Array(const.primary_key),
94
100
  },
95
101
  }
96
102
 
@@ -103,6 +109,15 @@ module RubyLsp
103
109
  rescue => e
104
110
  { error: e.full_message(highlight: false) }
105
111
  end
112
+
113
+ def active_record_model?(const)
114
+ !!(
115
+ const &&
116
+ defined?(ActiveRecord) &&
117
+ ActiveRecord::Base > const && # We do this 'backwards' in case the class overwrites `<`
118
+ !const.abstract_class?
119
+ )
120
+ end
106
121
  end
107
122
  end
108
123
  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 Documents...")
69
+ $stderr.puts("Fetching search index for Rails documentation")
70
70
 
71
- response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"))
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
- response.body if response.is_a?(Net::HTTPSuccess)
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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.5"
6
+ VERSION = "0.3.6"
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.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-05 00:00:00.000000000 Z
11
+ date: 2024-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-lsp
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.16.0
19
+ version: 0.16.5
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: 0.17.0
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.16.0
29
+ version: 0.16.5
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.17.0
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 3.5.7
94
+ rubygems_version: 3.5.9
95
95
  signing_key:
96
96
  specification_version: 4
97
97
  summary: A Ruby LSP addon for Rails