ruby-lsp-rails 0.2.2 → 0.2.4

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: 5133a59bd3960baa206580c91b1504a91a94089591ea6211123038f28c40eaf7
4
- data.tar.gz: 618627944a25edf11210afebc4f45112554125f8021787048a159ac13af0d15f
3
+ metadata.gz: 79a5ef8571e1fd77b8bbd08a782ef68f1116ce4b71e206eee890d573dd2b90dd
4
+ data.tar.gz: de8459cb09768c703b0f864f008238a40b78210acdef0e9eba8f4d4fd37c65d7
5
5
  SHA512:
6
- metadata.gz: 6f3e0395d614c55814e33fd52843e714458d7dd17b1460bb0ee64a796159138e42e6d4455d7a0e9cfb5648ca3b9609df9659f663e0d426aeb18938b62ef16517
7
- data.tar.gz: 7f3d0408b2122e4ff0a7f6ac82d4cd5e82e2c7d858f801ea19cac06c4043aca81efe4e2a75de1724d854669d1cd1e9fbe8f6b2e7258cd1e3f442868f48e382e3
6
+ metadata.gz: f0160d705fba4c081925b29efce3b2d8cc9a8243e8b9e53fa8a7820ffe62b5f0ecf12c73d5546b38f3289d807018f7bc7cbd82acbc296a2151c342ffcbf709be
7
+ data.tar.gz: 7d033dea267d94c5068bbbf4698e2c5b8bab63708021f78c67a90eed6b2e43c7a48e731a68fa5bd7c8d3a0c49a55eb600b14f1d40001f70fb050cbb5a538a7d4
data/README.md CHANGED
@@ -45,7 +45,7 @@ See the [documentation](https://shopify.github.io/ruby-lsp-rails) for more in-de
45
45
 
46
46
  ### Running Tests
47
47
 
48
- 1. Open a test which inherits from `ActiveSupport::TestCase` or one if its descendants, such as `ActionDispatch::IntegrationTest`.
48
+ 1. Open a test which inherits from `ActiveSupport::TestCase` or one of its descendants, such as `ActionDispatch::IntegrationTest`.
49
49
  2. Click on the "Run", "Run in Terminal" or "Debug" code lens which appears above the test class, or an individual test.
50
50
 
51
51
  Note: When using the Test Explorer view, if your code contains a statement to pause execution (e.g. `debugger`) it will
@@ -39,15 +39,13 @@ module RubyLsp
39
39
  ResponseType = type_member { { fixed: T::Array[::RubyLsp::Interface::CodeLens] } }
40
40
  BASE_COMMAND = "bin/rails test"
41
41
 
42
- ::RubyLsp::Requests::CodeLens.add_listener(self)
43
-
44
42
  sig { override.returns(ResponseType) }
45
43
  attr_reader :response
46
44
 
47
- sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
45
+ sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
48
46
  def initialize(uri, emitter, message_queue)
49
47
  @response = T.let([], ResponseType)
50
- @path = T.let(URI(uri).path, T.nilable(String))
48
+ @path = T.let(uri.to_standardized_path, T.nilable(String))
51
49
  emitter.register(self, :on_command, :on_class, :on_def)
52
50
 
53
51
  super(emitter, message_queue)
@@ -59,16 +57,26 @@ module RubyLsp
59
57
  return unless message_value == "test" && node.arguments.parts.any?
60
58
 
61
59
  first_argument = node.arguments.parts.first
62
- return unless first_argument.is_a?(SyntaxTree::StringLiteral)
60
+
61
+ parts = case first_argument
62
+ when SyntaxTree::StringConcat
63
+ # We only support two lines of concatenation on test names
64
+ if first_argument.left.is_a?(SyntaxTree::StringLiteral) &&
65
+ first_argument.right.is_a?(SyntaxTree::StringLiteral)
66
+ [*first_argument.left.parts, *first_argument.right.parts]
67
+ end
68
+ when SyntaxTree::StringLiteral
69
+ first_argument.parts
70
+ end
63
71
 
64
72
  # The test name may be a blank string while the code is being typed
65
- return if first_argument.parts.empty?
73
+ return if parts.nil? || parts.empty?
66
74
 
67
75
  # We can't handle interpolation yet
68
- return unless first_argument.parts.all? { |part| part.is_a?(SyntaxTree::TStringContent) }
76
+ return unless parts.all? { |part| part.is_a?(SyntaxTree::TStringContent) }
69
77
 
70
- test_name = first_argument.parts.first.value
71
- return unless test_name
78
+ test_name = parts.map(&:value).join
79
+ return if test_name.empty?
72
80
 
73
81
  line_number = node.location.start_line
74
82
  command = "#{BASE_COMMAND} #{@path}:#{line_number}"
@@ -99,6 +107,8 @@ module RubyLsp
99
107
 
100
108
  sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
101
109
  def add_test_code_lens(node, name:, command:, kind:)
110
+ return unless @path
111
+
102
112
  arguments = [
103
113
  @path,
104
114
  name,
@@ -12,9 +12,39 @@ module RubyLsp
12
12
  class Extension < ::RubyLsp::Extension
13
13
  extend T::Sig
14
14
 
15
+ sig { returns(RailsClient) }
16
+ def client
17
+ @client ||= T.let(RailsClient.new, T.nilable(RailsClient))
18
+ end
19
+
15
20
  sig { override.void }
16
21
  def activate
17
- RubyLsp::Rails::RailsClient.instance.check_if_server_is_running!
22
+ client.check_if_server_is_running!
23
+ end
24
+
25
+ sig { override.void }
26
+ def deactivate; end
27
+
28
+ # Creates a new CodeLens listener. This method is invoked on every CodeLens request
29
+ sig do
30
+ override.params(
31
+ uri: URI::Generic,
32
+ emitter: EventEmitter,
33
+ message_queue: Thread::Queue,
34
+ ).returns(T.nilable(Listener[T::Array[Interface::CodeLens]]))
35
+ end
36
+ def create_code_lens_listener(uri, emitter, message_queue)
37
+ CodeLens.new(uri, emitter, message_queue)
38
+ end
39
+
40
+ sig do
41
+ override.params(
42
+ emitter: EventEmitter,
43
+ message_queue: Thread::Queue,
44
+ ).returns(T.nilable(Listener[T.nilable(Interface::Hover)]))
45
+ end
46
+ def create_hover_listener(emitter, message_queue)
47
+ Hover.new(client, emitter, message_queue)
18
48
  end
19
49
 
20
50
  sig { override.returns(String) }
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "support/rails_document_client"
5
+
4
6
  module RubyLsp
5
7
  module Rails
6
8
  # ![Hover demo](../../hover.gif)
@@ -20,33 +22,62 @@ module RubyLsp
20
22
 
21
23
  ResponseType = type_member { { fixed: T.nilable(::RubyLsp::Interface::Hover) } }
22
24
 
23
- ::RubyLsp::Requests::Hover.add_listener(self)
24
-
25
25
  sig { override.returns(ResponseType) }
26
26
  attr_reader :response
27
27
 
28
- sig { params(emitter: RubyLsp::EventEmitter, message_queue: Thread::Queue).void }
29
- def initialize(emitter, message_queue)
30
- super
28
+ sig { params(client: RailsClient, emitter: RubyLsp::EventEmitter, message_queue: Thread::Queue).void }
29
+ def initialize(client, emitter, message_queue)
30
+ super(emitter, message_queue)
31
31
 
32
32
  @response = T.let(nil, ResponseType)
33
- emitter.register(self, :on_const)
33
+ @client = client
34
+ emitter.register(self, :on_const, :on_command, :on_const_path_ref, :on_call)
34
35
  end
35
36
 
36
37
  sig { params(node: SyntaxTree::Const).void }
37
38
  def on_const(node)
38
- model = RailsClient.instance.model(node.value)
39
+ model = @client.model(node.value)
39
40
  return if model.nil?
40
41
 
41
42
  schema_file = model[:schema_file]
42
43
  content = +""
43
44
  if schema_file
44
- content << "[Schema](file://#{schema_file})\n\n"
45
+ content << "[Schema](#{URI::Generic.build(scheme: "file", path: schema_file)})\n\n"
45
46
  end
46
47
  content << model[:columns].map { |name, type| "**#{name}**: #{type}\n" }.join("\n")
47
48
  contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: content)
48
49
  @response = RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
49
50
  end
51
+
52
+ sig { params(node: SyntaxTree::Command).void }
53
+ def on_command(node)
54
+ message = node.message
55
+ @response = generate_rails_document_link_hover(message.value, message)
56
+ end
57
+
58
+ sig { params(node: SyntaxTree::ConstPathRef).void }
59
+ def on_const_path_ref(node)
60
+ @response = generate_rails_document_link_hover(full_constant_name(node), node)
61
+ end
62
+
63
+ sig { params(node: SyntaxTree::CallNode).void }
64
+ def on_call(node)
65
+ message = node.message
66
+ return if message.is_a?(Symbol)
67
+
68
+ @response = generate_rails_document_link_hover(message.value, message)
69
+ end
70
+
71
+ private
72
+
73
+ sig { params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover)) }
74
+ def generate_rails_document_link_hover(name, node)
75
+ urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
76
+ return if urls.empty?
77
+
78
+ contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: urls.join("\n\n"))
79
+ RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
80
+ end
50
81
  end
51
82
  end
52
83
  end
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "singleton"
5
4
  require "net/http"
6
5
 
7
6
  module RubyLsp
@@ -10,35 +9,30 @@ module RubyLsp
10
9
  class ServerAddressUnknown < StandardError; end
11
10
 
12
11
  extend T::Sig
13
- include Singleton
14
12
 
15
13
  SERVER_NOT_RUNNING_MESSAGE = "Rails server is not running. " \
16
14
  "To get Rails features in the editor, boot the Rails server"
17
15
 
18
- sig { returns(String) }
16
+ sig { returns(Pathname) }
19
17
  attr_reader :root
20
18
 
21
19
  sig { void }
22
20
  def initialize
23
- project_root = Pathname.new(ENV["BUNDLE_GEMFILE"]).dirname
21
+ project_root = T.let(Bundler.with_unbundled_env { Bundler.default_gemfile }.dirname, Pathname)
22
+ dummy_path = project_root.join("test", "dummy")
24
23
 
25
- if project_root.basename.to_s == ".ruby-lsp"
26
- project_root = project_root.join("../")
27
- end
28
-
29
- dummy_path = File.join(project_root, "test", "dummy")
30
- @root = T.let(Dir.exist?(dummy_path) ? dummy_path : project_root.to_s, String)
31
- app_uri_path = "#{@root}/tmp/app_uri.txt"
32
-
33
- if File.exist?(app_uri_path)
34
- url = File.read(app_uri_path).chomp
24
+ @root = T.let(dummy_path.exist? ? dummy_path : project_root, Pathname)
25
+ app_uri_path = @root.join("tmp", "app_uri.txt")
35
26
 
36
- scheme, rest = url.split("://")
37
- uri, port = T.must(rest).split(":")
27
+ if app_uri_path.exist?
28
+ url = URI(app_uri_path.read.chomp)
38
29
 
39
- @ssl = T.let(scheme == "https", T::Boolean)
40
- @uri = T.let(T.must(uri), T.nilable(String))
41
- @port = T.let(T.must(port).to_i, Integer)
30
+ @ssl = T.let(url.scheme == "https", T::Boolean)
31
+ @address = T.let(
32
+ [url.host, url.path].reject { |component| component.nil? || component.empty? }.join("/"),
33
+ T.nilable(String),
34
+ )
35
+ @port = T.let(T.must(url.port).to_i, Integer)
42
36
  end
43
37
  end
44
38
 
@@ -48,16 +42,21 @@ module RubyLsp
48
42
  return unless response.code == "200"
49
43
 
50
44
  JSON.parse(response.body.chomp, symbolize_names: true)
51
- rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Net::ReadTimeout, ServerAddressUnknown
45
+ rescue Errno::ECONNREFUSED,
46
+ Errno::EADDRNOTAVAIL,
47
+ Errno::ECONNRESET,
48
+ Net::ReadTimeout,
49
+ Net::OpenTimeout,
50
+ ServerAddressUnknown
52
51
  nil
53
52
  end
54
53
 
55
54
  sig { void }
56
55
  def check_if_server_is_running!
57
56
  request("activate", 0.2)
58
- rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, ServerAddressUnknown
57
+ rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Errno::ECONNRESET, ServerAddressUnknown
59
58
  warn(SERVER_NOT_RUNNING_MESSAGE)
60
- rescue Net::ReadTimeout
59
+ rescue Net::ReadTimeout, Net::OpenTimeout
61
60
  # If the server is running, but the initial request is taking too long, we don't want to block the
62
61
  # initialization of the Ruby LSP
63
62
  end
@@ -66,9 +65,9 @@ module RubyLsp
66
65
 
67
66
  sig { params(path: String, timeout: T.nilable(Float)).returns(Net::HTTPResponse) }
68
67
  def request(path, timeout = nil)
69
- raise ServerAddressUnknown unless @uri
68
+ raise ServerAddressUnknown unless @address
70
69
 
71
- http = Net::HTTP.new(@uri, @port)
70
+ http = Net::HTTP.new(@address, @port)
72
71
  http.use_ssl = @ssl
73
72
  http.read_timeout = timeout if timeout
74
73
  http.get("/ruby_lsp_rails/#{path}")
@@ -0,0 +1,124 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+
6
+ module RubyLsp
7
+ module Rails
8
+ module Support
9
+ class RailsDocumentClient
10
+ RAILS_DOC_HOST = "https://api.rubyonrails.org"
11
+
12
+ SUPPORTED_RAILS_DOC_NAMESPACES = T.let(
13
+ Regexp.union(
14
+ /ActionDispatch/,
15
+ /ActionController/,
16
+ /AbstractController/,
17
+ /ActiveRecord/,
18
+ /ActiveModel/,
19
+ /ActiveStorage/,
20
+ /ActionText/,
21
+ /ActiveJob/,
22
+ ).freeze,
23
+ Regexp,
24
+ )
25
+
26
+ RAILTIES_VERSION = T.let(
27
+ [*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].find do |s|
28
+ s.name == "railties"
29
+ end&.version&.to_s,
30
+ T.nilable(String),
31
+ )
32
+
33
+ class << self
34
+ extend T::Sig
35
+
36
+ sig { params(name: String).returns(T::Array[String]) }
37
+ def generate_rails_document_urls(name)
38
+ docs = search_index&.fetch(name, nil)
39
+
40
+ return [] unless docs
41
+
42
+ docs.map do |doc|
43
+ owner = doc[:owner]
44
+
45
+ link_name =
46
+ # class/module name
47
+ if owner == name
48
+ name
49
+ else
50
+ "#{owner}##{name}"
51
+ end
52
+
53
+ "[Rails Document: `#{link_name}`](#{doc[:url]})"
54
+ end
55
+ end
56
+
57
+ sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
58
+ private def search_index
59
+ @rails_documents ||= T.let(
60
+ build_search_index,
61
+ T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]]),
62
+ )
63
+ end
64
+
65
+ sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
66
+ private def build_search_index
67
+ return unless RAILTIES_VERSION
68
+
69
+ warn("Fetching Rails Documents...")
70
+
71
+ response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"))
72
+
73
+ body = case response
74
+ when Net::HTTPSuccess
75
+ response.body
76
+ when Net::HTTPRedirection
77
+ # If the version's doc is not found, e.g. Rails main, it'll be redirected
78
+ # In this case, we just fetch the latest doc
79
+ response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
80
+ response.body if response.is_a?(Net::HTTPSuccess)
81
+ else
82
+ warn("Response failed: #{response.inspect}")
83
+ nil
84
+ end
85
+
86
+ process_search_index(body) if body
87
+ rescue StandardError => e
88
+ warn("Exception occurred when fetching Rails document index: #{e.inspect}")
89
+ end
90
+
91
+ sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
92
+ private def process_search_index(js)
93
+ raw_data = js.sub("var search_data = ", "")
94
+ info = JSON.parse(raw_data).dig("index", "info")
95
+
96
+ # An entry looks like this:
97
+ #
98
+ # ["belongs_to", # method or module/class
99
+ # "ActiveRecord::Associations::ClassMethods", # method owner
100
+ # "classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to", # path to the document
101
+ # "(name, scope = nil, **options)", # method's parameters
102
+ # "<p>Specifies a one-to-one association with another class..."] # document preview
103
+ #
104
+ info.each_with_object({}) do |(method_or_class, method_owner, doc_path, _, doc_preview), table|
105
+ # If a method doesn't have documentation, there's no need to generate the link to it.
106
+ next if doc_preview.nil? || doc_preview.empty?
107
+
108
+ # If the method or class/module is not from the supported namespace, reject it
109
+ next unless [method_or_class, method_owner].any? do |elem|
110
+ elem.match?(SUPPORTED_RAILS_DOC_NAMESPACES)
111
+ end
112
+
113
+ owner = method_owner.empty? ? method_or_class : method_owner
114
+ table[method_or_class] ||= []
115
+ # It's possible to have multiple modules defining the same method name. For example,
116
+ # both `ActiveRecord::FinderMethods` and `ActiveRecord::Associations::CollectionProxy` defines `#find`
117
+ table[method_or_class] << { owner: owner, url: "#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/#{doc_path}" }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -23,8 +23,8 @@ module RubyLsp
23
23
  if defined?(::Rails::Server)
24
24
  ssl_enable, host, port = ::Rails::Server::Options.new.parse!(ARGV).values_at(:SSLEnable, :Host, :Port)
25
25
  app_uri = "#{ssl_enable ? "https" : "http"}://#{host}:#{port}"
26
- app_uri_path = "#{::Rails.root}/tmp/app_uri.txt"
27
- File.write(app_uri_path, app_uri)
26
+ app_uri_path = ::Rails.root.join("tmp", "app_uri.txt")
27
+ app_uri_path.write(app_uri)
28
28
 
29
29
  at_exit do
30
30
  # The app_uri.txt file should only exist when the server is running. The extension uses its presence to
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.2.2"
6
+ VERSION = "0.2.4"
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.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-06 00:00:00.000000000 Z
11
+ date: 2023-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -30,20 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.6.2
33
+ version: 0.9.1
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: 0.8.0
36
+ version: 0.10.0
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: 0.6.2
43
+ version: 0.9.1
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.8.0
46
+ version: 0.10.0
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sorbet-runtime
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -73,6 +73,7 @@ files:
73
73
  - lib/ruby_lsp/ruby_lsp_rails/extension.rb
74
74
  - lib/ruby_lsp/ruby_lsp_rails/hover.rb
75
75
  - lib/ruby_lsp/ruby_lsp_rails/rails_client.rb
76
+ - lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
76
77
  - lib/ruby_lsp_rails/rack_app.rb
77
78
  - lib/ruby_lsp_rails/railtie.rb
78
79
  - lib/ruby_lsp_rails/version.rb
@@ -100,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  - !ruby/object:Gem::Version
101
102
  version: '0'
102
103
  requirements: []
103
- rubygems_version: 3.4.14
104
+ rubygems_version: 3.4.18
104
105
  signing_key:
105
106
  specification_version: 4
106
107
  summary: A Ruby LSP extension for Rails