ruby-lsp-rails 0.3.5 → 0.3.7

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: 6bb6fcc96e3d0fd9357e7f8700acbae09e0d792499ed0d8b92fd577a97949108
4
+ data.tar.gz: cbb971feed21416f187195d0b23295801dbbc5c875c5d64961f2591e253f375a
5
5
  SHA512:
6
- metadata.gz: 59a4059ffdeb381dd3b8c4858e91b64a624df5e424807540b99aa6182f908f944cc6ea0ddf677776eb2d4680f8bc7c7944883fea2eb346da8252125eedd2c788
7
- data.tar.gz: 8bdc777be1f0663cc6d7e2a450b42ceef091f5e75769cf74570ce536bc914de2c2b559cbdcd05989b55d3fc6a58a843f637b2caaf1ec165a97b4c3d0b2c7db11
6
+ metadata.gz: dc8c684b4900726219d73c2855a1afe159e0ba02f8a953de86356e1cfb9667fe11dff4cbaf3446d5dd471689151f34072f1a099f82033c959516db30dcf81ce7
7
+ data.tar.gz: 72b8a8ba741fbccd174dc46bb86ef683634630866f209f0968eb6d228992f4986f46bac7f0de249a936649eef80a799c5505f9c8a3514f76d44e77a511b70325
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q)
2
+
1
3
  # Ruby LSP Rails
2
4
 
3
5
  Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for extra Rails editor features, such as:
@@ -6,6 +8,7 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
6
8
  * Run or debug a test by clicking on the code lens which appears above the test class, or an individual test.
7
9
  * Navigate to associations, validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
8
10
  * Jump to the definition of callbacks using your editor's "Go to Definition" feature.
11
+ * Jump to the declaration of a route.
9
12
 
10
13
  ## Installation
11
14
 
@@ -36,9 +39,7 @@ When extension is stopped (e.g. by quitting the editor), the server instance is
36
39
 
37
40
  ## Contributing
38
41
 
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.
42
+ See [CONTRIBUTING.md](https://github.com/Shopify/ruby-lsp-rails/blob/main/CONTRIBUTING.md)
42
43
 
43
44
  ## License
44
45
 
data/Rakefile CHANGED
@@ -30,4 +30,4 @@ end
30
30
 
31
31
  RubyLsp::CheckDocs.new(FileList["#{__dir__}/lib/ruby_lsp/**/*.rb"], FileList["#{__dir__}/misc/**/*.gif"])
32
32
 
33
- task default: :test
33
+ task default: [:"db:setup", :test]
@@ -3,8 +3,11 @@
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"
8
+ require_relative "support/associations"
7
9
  require_relative "support/callbacks"
10
+ require_relative "support/location_builder"
8
11
  require_relative "runner_client"
9
12
  require_relative "hover"
10
13
  require_relative "code_lens"
@@ -31,6 +34,7 @@ module RubyLsp
31
34
  $stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
32
35
  # Start booting the real client in a background thread. Until this completes, the client will be a NullClient
33
36
  Thread.new { @client = RunnerClient.create_client }
37
+ register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
34
38
  end
35
39
 
36
40
  sig { override.void }
@@ -55,12 +59,12 @@ module RubyLsp
55
59
  sig do
56
60
  override.params(
57
61
  response_builder: ResponseBuilders::Hover,
58
- nesting: T::Array[String],
62
+ node_context: NodeContext,
59
63
  dispatcher: Prism::Dispatcher,
60
64
  ).void
61
65
  end
62
- def create_hover_listener(response_builder, nesting, dispatcher)
63
- Hover.new(@client, response_builder, nesting, T.must(@global_state), dispatcher)
66
+ def create_hover_listener(response_builder, node_context, dispatcher)
67
+ Hover.new(@client, response_builder, node_context, T.must(@global_state), dispatcher)
64
68
  end
65
69
 
66
70
  sig do
@@ -77,13 +81,13 @@ module RubyLsp
77
81
  override.params(
78
82
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
79
83
  uri: URI::Generic,
80
- nesting: T::Array[String],
84
+ node_context: NodeContext,
81
85
  dispatcher: Prism::Dispatcher,
82
86
  ).void
83
87
  end
84
- def create_definition_listener(response_builder, uri, nesting, dispatcher)
88
+ def create_definition_listener(response_builder, uri, node_context, dispatcher)
85
89
  index = T.must(@global_state).index
86
- Definition.new(response_builder, nesting, index, dispatcher)
90
+ Definition.new(@client, response_builder, node_context, index, dispatcher)
87
91
  end
88
92
 
89
93
  sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
@@ -95,6 +99,32 @@ module RubyLsp
95
99
  end
96
100
  end
97
101
 
102
+ sig { params(global_state: GlobalState, message_queue: Thread::Queue).void }
103
+ def register_additional_file_watchers(global_state:, message_queue:)
104
+ return unless global_state.supports_watching_files
105
+
106
+ message_queue << Request.new(
107
+ id: "ruby-lsp-rails-file-watcher",
108
+ method: "client/registerCapability",
109
+ params: Interface::RegistrationParams.new(
110
+ registrations: [
111
+ Interface::Registration.new(
112
+ id: "workspace/didChangeWatchedFilesRails",
113
+ method: "workspace/didChangeWatchedFiles",
114
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
115
+ watchers: [
116
+ Interface::FileSystemWatcher.new(
117
+ glob_pattern: "**/*structure.sql",
118
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
119
+ ),
120
+ ],
121
+ ),
122
+ ),
123
+ ],
124
+ ),
125
+ )
126
+ end
127
+
98
128
  sig { override.returns(String) }
99
129
  def name
100
130
  "Ruby LSP Rails"
@@ -6,6 +6,7 @@ module RubyLsp
6
6
  # ![CodeLens demo](../../code_lens.gif)
7
7
  #
8
8
  # This feature adds several CodeLens features for Rails applications using Active Support test cases:
9
+ #
9
10
  # - Run tests in the VS Terminal
10
11
  # - Run tests in the VS Code Test Explorer
11
12
  # - Debug tests
@@ -42,8 +43,6 @@ module RubyLsp
42
43
  include Requests::Support::Common
43
44
  include ActiveSupportTestCaseHelper
44
45
 
45
- BASE_COMMAND = "bin/rails test"
46
-
47
46
  sig do
48
47
  params(
49
48
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
@@ -67,7 +66,7 @@ module RubyLsp
67
66
  return unless content
68
67
 
69
68
  line_number = node.location.start_line
70
- command = "#{BASE_COMMAND} #{@path}:#{line_number}"
69
+ command = "#{test_command} #{@path}:#{line_number}"
71
70
  add_test_code_lens(node, name: content, command: command, kind: :example)
72
71
  end
73
72
 
@@ -77,7 +76,7 @@ module RubyLsp
77
76
  method_name = node.name.to_s
78
77
  if method_name.start_with?("test_")
79
78
  line_number = node.location.start_line
80
- command = "#{BASE_COMMAND} #{@path}:#{line_number}"
79
+ command = "#{test_command} #{@path}:#{line_number}"
81
80
  add_test_code_lens(node, name: method_name, command: command, kind: :example)
82
81
  end
83
82
  end
@@ -86,7 +85,7 @@ module RubyLsp
86
85
  def on_class_node_enter(node)
87
86
  class_name = node.constant_path.slice
88
87
  if class_name.end_with?("Test")
89
- command = "#{BASE_COMMAND} #{@path}"
88
+ command = "#{test_command} #{@path}"
90
89
  add_test_code_lens(node, name: class_name, command: command, kind: :group)
91
90
  @group_id_stack.push(@group_id)
92
91
  @group_id += 1
@@ -103,6 +102,15 @@ module RubyLsp
103
102
 
104
103
  private
105
104
 
105
+ sig { returns(String) }
106
+ def test_command
107
+ if Gem.win_platform?
108
+ "ruby bin/rails test"
109
+ else
110
+ "bin/rails test"
111
+ end
112
+ end
113
+
106
114
  sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
107
115
  def add_test_code_lens(node, name:, command:, kind:)
108
116
  return unless @path
@@ -10,28 +10,40 @@ module RubyLsp
10
10
  # definition of the symbol under the cursor.
11
11
  #
12
12
  # Currently supported targets:
13
+ #
13
14
  # - Callbacks
15
+ # - Named routes (e.g. `users_path`)
14
16
  #
15
17
  # # Example
16
18
  #
17
19
  # ```ruby
18
20
  # before_action :foo # <- Go to definition on this symbol will jump to the method if it is defined in the same class
19
21
  # ```
22
+ #
23
+ # Notes for named routes:
24
+ #
25
+ # - It is available only in Rails 7.1 or newer.
26
+ # - Route may be defined across multiple files, e.g. using `draw`, rather than in `routes.rb`.
27
+ # - Routes won't be found if not defined for the Rails development environment.
28
+ # - If using `constraints`, the route can only be found if the constraints are met.
29
+ # - Changes to routes won't be picked up until the server is restarted.
20
30
  class Definition
21
31
  extend T::Sig
22
32
  include Requests::Support::Common
23
33
 
24
34
  sig do
25
35
  params(
36
+ client: RunnerClient,
26
37
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
27
- nesting: T::Array[String],
38
+ node_context: NodeContext,
28
39
  index: RubyIndexer::Index,
29
40
  dispatcher: Prism::Dispatcher,
30
41
  ).void
31
42
  end
32
- def initialize(response_builder, nesting, index, dispatcher)
43
+ def initialize(client, response_builder, node_context, index, dispatcher)
44
+ @client = client
33
45
  @response_builder = response_builder
34
- @nesting = nesting
46
+ @nesting = T.let(node_context.nesting, T::Array[String])
35
47
  @index = index
36
48
 
37
49
  dispatcher.register(self, :on_call_node_enter)
@@ -43,8 +55,21 @@ module RubyLsp
43
55
 
44
56
  message = node.message
45
57
 
46
- return unless message && Support::Callbacks::ALL.include?(message)
58
+ return unless message
59
+
60
+ if Support::Associations::ALL.include?(message)
61
+ handle_association(node)
62
+ elsif Support::Callbacks::ALL.include?(message)
63
+ handle_callback(node)
64
+ elsif message.end_with?("_path") || message.end_with?("_url")
65
+ handle_route(node)
66
+ end
67
+ end
68
+
69
+ private
47
70
 
71
+ sig { params(node: Prism::CallNode).void }
72
+ def handle_callback(node)
48
73
  arguments = node.arguments&.arguments
49
74
  return unless arguments&.any?
50
75
 
@@ -62,7 +87,30 @@ module RubyLsp
62
87
  end
63
88
  end
64
89
 
65
- private
90
+ sig { params(node: Prism::CallNode).void }
91
+ def handle_association(node)
92
+ first_argument = node.arguments&.arguments&.first
93
+ return unless first_argument.is_a?(Prism::SymbolNode)
94
+
95
+ association_name = first_argument.unescaped
96
+
97
+ result = @client.association_target_location(
98
+ model_name: @nesting.join("::"),
99
+ association_name: association_name,
100
+ )
101
+
102
+ return unless result
103
+
104
+ @response_builder << Support::LocationBuilder.line_location_from_s(result.fetch(:location))
105
+ end
106
+
107
+ sig { params(node: Prism::CallNode).void }
108
+ def handle_route(node)
109
+ result = @client.route_location(T.must(node.message))
110
+ return unless result
111
+
112
+ @response_builder << Support::LocationBuilder.line_location_from_s(result.fetch(:location))
113
+ end
66
114
 
67
115
  sig { params(name: String).void }
68
116
  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
@@ -24,15 +24,15 @@ module RubyLsp
24
24
  params(
25
25
  client: RunnerClient,
26
26
  response_builder: ResponseBuilders::Hover,
27
- nesting: T::Array[String],
27
+ node_context: NodeContext,
28
28
  global_state: GlobalState,
29
29
  dispatcher: Prism::Dispatcher,
30
30
  ).void
31
31
  end
32
- def initialize(client, response_builder, nesting, global_state, dispatcher)
32
+ def initialize(client, response_builder, node_context, global_state, dispatcher)
33
33
  @client = client
34
34
  @response_builder = response_builder
35
- @nesting = nesting
35
+ @nesting = T.let(node_context.nesting, T::Array[String])
36
36
  @index = T.let(global_state.index, RubyIndexer::Index)
37
37
  dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter, :on_call_node_enter)
38
38
  end
@@ -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,30 @@ module RubyLsp
95
98
  nil
96
99
  end
97
100
 
101
+ sig do
102
+ params(
103
+ model_name: String,
104
+ association_name: String,
105
+ ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
106
+ end
107
+ def association_target_location(model_name:, association_name:)
108
+ make_request(
109
+ "association_target_location",
110
+ model_name: model_name,
111
+ association_name: association_name,
112
+ )
113
+ rescue => e
114
+ $stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
115
+ end
116
+
117
+ sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
118
+ def route_location(name)
119
+ make_request("route_location", name: name)
120
+ rescue IncompleteMessageError
121
+ $stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
122
+ nil
123
+ end
124
+
98
125
  sig { void }
99
126
  def trigger_reload
100
127
  $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,17 +11,19 @@ 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
23
+ # Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
24
+ routes_reloader = ::Rails.application.routes_reloader
25
+ routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
26
+
41
27
  initialize_result = { result: { message: "ok" } }.to_json
42
28
  $stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
43
29
 
@@ -54,22 +40,20 @@ module RubyLsp
54
40
  end
55
41
  end
56
42
 
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
43
  def execute(request, params)
64
44
  case request
65
45
  when "shutdown"
66
46
  @running = false
67
47
  VOID
68
48
  when "model"
69
- resolve_database_info_from_model(T.must(params).fetch(:name))
49
+ resolve_database_info_from_model(params.fetch(:name))
50
+ when "association_target_location"
51
+ resolve_association_target(params)
70
52
  when "reload"
71
53
  ::Rails.application.reloader.reload!
72
54
  VOID
55
+ when "route_location"
56
+ route_location(params.fetch(:name))
73
57
  else
74
58
  VOID
75
59
  end
@@ -79,10 +63,37 @@ module RubyLsp
79
63
 
80
64
  private
81
65
 
82
- sig { params(model_name: String).returns(T::Hash[Symbol, T.untyped]) }
66
+ # Older versions of Rails don't support `route_source_locations`.
67
+ # We also check that it's enabled.
68
+ if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
69
+ ActionDispatch::Routing::Mapper.route_source_locations
70
+ def route_location(name)
71
+ match_data = name.match(/^(.+)(_path|_url)$/)
72
+ return { result: nil } unless match_data
73
+
74
+ key = match_data[1]
75
+
76
+ # A token could match the _path or _url pattern, but not be an actual route.
77
+ route = ::Rails.application.routes.named_routes.get(key)
78
+ return { result: nil } unless route&.source_location
79
+
80
+ {
81
+ result: {
82
+ location: ::Rails.root.join(route.source_location).to_s,
83
+ },
84
+ }
85
+ rescue => e
86
+ { error: e.full_message(highlight: false) }
87
+ end
88
+ else
89
+ def route_location(name)
90
+ { result: nil }
91
+ end
92
+ end
93
+
83
94
  def resolve_database_info_from_model(model_name)
84
95
  const = ActiveSupport::Inflector.safe_constantize(model_name)
85
- unless const && defined?(ActiveRecord) && const < ActiveRecord::Base && !const.abstract_class?
96
+ unless active_record_model?(const)
86
97
  return {
87
98
  result: nil,
88
99
  }
@@ -91,6 +102,7 @@ module RubyLsp
91
102
  info = {
92
103
  result: {
93
104
  columns: const.columns.map { |column| [column.name, column.type] },
105
+ primary_keys: Array(const.primary_key),
94
106
  },
95
107
  }
96
108
 
@@ -103,6 +115,38 @@ module RubyLsp
103
115
  rescue => e
104
116
  { error: e.full_message(highlight: false) }
105
117
  end
118
+
119
+ def resolve_association_target(params)
120
+ const = ActiveSupport::Inflector.safe_constantize(params[:model_name])
121
+ unless active_record_model?(const)
122
+ return {
123
+ result: nil,
124
+ }
125
+ end
126
+
127
+ association_klass = const.reflect_on_association(params[:association_name].intern).klass
128
+
129
+ source_location = Object.const_source_location(association_klass.to_s)
130
+
131
+ {
132
+ result: {
133
+ location: source_location.first + ":" + source_location.second.to_s,
134
+ },
135
+ }
136
+ rescue NameError
137
+ {
138
+ result: nil,
139
+ }
140
+ end
141
+
142
+ def active_record_model?(const)
143
+ !!(
144
+ const &&
145
+ defined?(ActiveRecord) &&
146
+ ActiveRecord::Base > const && # We do this 'backwards' in case the class overwrites `<`
147
+ !const.abstract_class?
148
+ )
149
+ end
106
150
  end
107
151
  end
108
152
  end
@@ -0,0 +1,20 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ module Support
7
+ module Associations
8
+ ALL = T.let(
9
+ [
10
+ "belongs_to",
11
+ "has_many",
12
+ "has_one",
13
+ "has_and_belongs_to_many",
14
+ ].freeze,
15
+ T::Array[String],
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ module Support
7
+ class LocationBuilder
8
+ class << self
9
+ extend T::Sig
10
+
11
+ sig { params(location_string: String).returns(Interface::Location) }
12
+ def line_location_from_s(location_string)
13
+ *file_parts, line = location_string.split(":")
14
+
15
+ raise ArgumentError, "Invalid location string given" unless file_parts
16
+
17
+ # On Windows, file paths will look something like `C:/path/to/file.rb:123`. Only the last colon is the line
18
+ # number and all other parts compose the file path
19
+ file_path = file_parts.join(":")
20
+
21
+ Interface::Location.new(
22
+ uri: URI::Generic.from_path(path: file_path).to_s,
23
+ range: Interface::Range.new(
24
+ start: Interface::Position.new(line: Integer(line) - 1, character: 0),
25
+ end: Interface::Position.new(line: Integer(line) - 1, character: 0),
26
+ ),
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ 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.7"
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.7
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-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-lsp
@@ -16,34 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.16.0
19
+ version: 0.17.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: 0.17.0
22
+ version: 0.18.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 0.16.0
30
- - - "<"
31
28
  - !ruby/object:Gem::Version
32
29
  version: 0.17.0
33
- - !ruby/object:Gem::Dependency
34
- name: sorbet-runtime
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 0.5.9897
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
30
+ - - "<"
45
31
  - !ruby/object:Gem::Version
46
- version: 0.5.9897
32
+ version: 0.18.0
47
33
  description: A Ruby LSP addon that adds extra editor functionality for Rails applications
48
34
  email:
49
35
  - ruby@shopify.com
@@ -63,7 +49,9 @@ files:
63
49
  - lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
64
50
  - lib/ruby_lsp/ruby_lsp_rails/server.rb
65
51
  - lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb
52
+ - lib/ruby_lsp/ruby_lsp_rails/support/associations.rb
66
53
  - lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb
54
+ - lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb
67
55
  - lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
68
56
  - lib/ruby_lsp_rails/railtie.rb
69
57
  - lib/ruby_lsp_rails/version.rb
@@ -91,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
79
  - !ruby/object:Gem::Version
92
80
  version: '0'
93
81
  requirements: []
94
- rubygems_version: 3.5.7
82
+ rubygems_version: 3.5.10
95
83
  signing_key:
96
84
  specification_version: 4
97
85
  summary: A Ruby LSP addon for Rails