ruby-lsp-rails 0.2.2 → 0.2.4

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