ruby-lsp-rails 0.3.5 → 0.3.7

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: 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