ruby-lsp-rails 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0cdbd01ae2a03195931208bf88504c5e06ae95b321e32e1c07a5fcef993817dc
4
+ data.tar.gz: 8ee3dbd7f7b0bc4851e98a51e624700526f0fbbd7535495e52bd7f9fe43d581a
5
+ SHA512:
6
+ metadata.gz: c2adee12e7cf92359ee6b26b64ade207c3507e2f881d0b2d00ee6b30e45e3bff1025735e616d1da062d6bde2d70bbd72ae73114f1cc13f17a30f47e48171b6a7
7
+ data.tar.gz: 67307e0b0388f38e254f1fbc1822b8814075cd5ded94b73e674b1ef35cc1c989dfd2286bac4eaa901e45caa6335e024d80480470c30d08c74a2a812bf377e365
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023-present, Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Ruby LSP Rails
2
+
3
+ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) extension for extra Rails editor features, such as:
4
+
5
+ - Displaying an ActiveRecord model's database columns and types when hovering over it
6
+ - (More to come!)
7
+
8
+
9
+ ## Installation
10
+
11
+ To install, add the following line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ group :development do
15
+ gem "ruby-lsp-rails"
16
+ end
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ 1. Start your Rails server
22
+ 1. Hover over an ActiveRecord model to see its details
23
+
24
+ ## How It Works
25
+
26
+ This gem consists of two components that enable enhanced Rails functionality in the editor:
27
+
28
+ 1. A Rails engine that automatically exposes APIs when Rails server is running
29
+ 1. A Ruby LSP extension that connects to the exposed APIs to fetch runtime information from the Rails server
30
+
31
+ This is why the Rails server needs to be running for features to work.
32
+
33
+ > **Note**
34
+ >
35
+ > There is no need to restart the Ruby LSP every time the Rails server is booted.
36
+ > If the server is shut down, the extra features will temporarily disappear and reappear once the server is running again.
37
+
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ruby-lsp-rails. This project is
42
+ intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
43
+ [Contributor Covenant](https://github.com/Shopify/ruby-lsp-rails/blob/main/CODE_OF_CONDUCT.md) code of conduct.
44
+
45
+ ## License
46
+
47
+ The gem is available as open source under the terms of the
48
+ [MIT License](https://github.com/Shopify/ruby-lsp-rails/blob/main/LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
+
7
+ load "rails/tasks/engine.rake"
8
+ load "rails/tasks/statistics.rake"
9
+
10
+ require "bundler/gem_tasks"
11
+ require "rake/testtask"
12
+
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << "test"
15
+ t.libs << "lib"
16
+ t.test_files = FileList["test/**/*_test.rb"]
17
+ end
18
+
19
+ task default: :test
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "pathname"
6
+
7
+ require "ruby_lsp_rails/version"
8
+ require "ruby_lsp_rails/railtie"
9
+
10
+ module RubyLSP
11
+ module Rails
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "ruby_lsp/extension"
5
+
6
+ require_relative "rails_client"
7
+ require_relative "hover"
8
+
9
+ module RubyLsp
10
+ module Rails
11
+ class Extension < ::RubyLsp::Extension
12
+ extend T::Sig
13
+
14
+ sig { override.void }
15
+ def activate
16
+ # Must be the last statement in activate since it raises to display a notification for the user
17
+ RubyLsp::Rails::RailsClient.instance.check_if_server_is_running!
18
+ end
19
+
20
+ sig { override.returns(String) }
21
+ def name
22
+ "Ruby LSP Rails"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ class Hover < ::RubyLsp::Listener
7
+ extend T::Sig
8
+ extend T::Generic
9
+
10
+ ResponseType = type_member { { fixed: T.nilable(::RubyLsp::Interface::Hover) } }
11
+
12
+ ::RubyLsp::Requests::Hover.add_listener(self)
13
+
14
+ sig { override.returns(ResponseType) }
15
+ attr_reader :response
16
+
17
+ sig { void }
18
+ def initialize
19
+ @response = T.let(nil, ResponseType)
20
+ super
21
+ end
22
+
23
+ listener_events do
24
+ sig { params(node: SyntaxTree::Const).void }
25
+ def on_const(node)
26
+ model = RailsClient.instance.model(node.value)
27
+ return if model.nil?
28
+
29
+ schema_file = model[:schema_file]
30
+ content = +""
31
+ if schema_file
32
+ content << "[Schema](file://#{schema_file})\n\n"
33
+ end
34
+ content << model[:columns].map { |name, type| "**#{name}**: #{type}\n" }.join("\n")
35
+ contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: content)
36
+ @response = RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "singleton"
5
+ require "net/http"
6
+
7
+ module RubyLsp
8
+ module Rails
9
+ class RailsClient
10
+ class ServerNotRunningError < StandardError; end
11
+ class NeedsRestartError < StandardError; end
12
+
13
+ extend T::Sig
14
+ include Singleton
15
+
16
+ SERVER_NOT_RUNNING_MESSAGE = "Rails server is not running. " \
17
+ "To get Rails features in the editor, boot the Rails server"
18
+
19
+ sig { returns(String) }
20
+ attr_reader :root
21
+
22
+ sig { void }
23
+ def initialize
24
+ project_root = Pathname.new(ENV["BUNDLE_GEMFILE"]).dirname
25
+
26
+ if project_root.basename.to_s == ".ruby-lsp"
27
+ project_root = project_root.join("../")
28
+ end
29
+
30
+ dummy_path = File.join(project_root, "test", "dummy")
31
+ @root = T.let(Dir.exist?(dummy_path) ? dummy_path : project_root.to_s, String)
32
+ app_uri_path = "#{@root}/tmp/app_uri.txt"
33
+
34
+ unless File.exist?(app_uri_path)
35
+ raise NeedsRestartError, <<~MESSAGE
36
+ The Ruby LSP Rails extension needs to be initialized. Please restart the Rails server and the Ruby LSP
37
+ to get Rails features in the editor
38
+ MESSAGE
39
+ end
40
+
41
+ base_uri = File.read(app_uri_path).chomp
42
+ @uri = T.let("#{base_uri}/ruby_lsp_rails", String)
43
+ end
44
+
45
+ sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
46
+ def model(name)
47
+ response = request("models/#{name}")
48
+ return unless response.code == "200"
49
+
50
+ JSON.parse(response.body.chomp, symbolize_names: true)
51
+ rescue Errno::ECONNREFUSED
52
+ raise ServerNotRunningError, SERVER_NOT_RUNNING_MESSAGE
53
+ end
54
+
55
+ sig { void }
56
+ def check_if_server_is_running!
57
+ # Check if the Rails server is running. Warn the user to boot it for Rails features
58
+ pid_file = ENV.fetch("PIDFILE") { File.join(@root, "tmp", "pids", "server.pid") }
59
+
60
+ # If the PID file doesn't exist, then the server hasn't been booted
61
+ raise ServerNotRunningError, SERVER_NOT_RUNNING_MESSAGE unless File.exist?(pid_file)
62
+
63
+ pid = File.read(pid_file).to_i
64
+
65
+ begin
66
+ # Issuing an EXIT signal to an existing process actually doesn't make the server shutdown. But if this
67
+ # call succeeds, then the server is running. If the PID doesn't exist, Errno::ESRCH is raised
68
+ Process.kill(T.must(Signal.list["EXIT"]), pid)
69
+ rescue Errno::ESRCH
70
+ raise ServerNotRunningError, SERVER_NOT_RUNNING_MESSAGE
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ sig { params(path: String).returns(Net::HTTPResponse) }
77
+ def request(path)
78
+ Net::HTTP.get_response(URI("#{@uri}/#{path}"))
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ class Middleware
7
+ extend T::Sig
8
+
9
+ PATH_REGEXP = %r{/ruby_lsp_rails/models/(?<model_name>.+)}
10
+
11
+ sig { params(app: T.untyped).void }
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ sig { params(env: T::Hash[T.untyped, T.untyped]).returns(T::Array[T.untyped]) }
17
+ def call(env)
18
+ request = ActionDispatch::Request.new(env)
19
+ # TODO: improve the model name regex
20
+ match = request.path.match(PATH_REGEXP)
21
+
22
+ if match
23
+ resolve_database_info_from_model(match[:model_name])
24
+ else
25
+ @app.call(env)
26
+ end
27
+ rescue
28
+ @app.call(env)
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(model_name: String).returns(T::Array[T.untyped]) }
34
+ def resolve_database_info_from_model(model_name)
35
+ const = ActiveSupport::Inflector.safe_constantize(model_name)
36
+
37
+ if const && const < ActiveRecord::Base
38
+ begin
39
+ schema_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(const.connection.pool.db_config)
40
+ rescue => e
41
+ warn("Could not locate schema: #{e.message}")
42
+ end
43
+
44
+ body = JSON.dump({
45
+ columns: const.columns.map { |column| [column.name, column.type] },
46
+ schema_file: schema_file,
47
+ })
48
+
49
+ [200, { "Content-Type" => "application/json" }, [body]]
50
+ else
51
+ not_found
52
+ end
53
+ end
54
+
55
+ sig { returns(T::Array[T.untyped]) }
56
+ def not_found
57
+ [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "ruby_lsp_rails/middleware"
5
+
6
+ module RubyLsp
7
+ module Rails
8
+ class Railtie < ::Rails::Railtie
9
+ initializer "ruby_lsp_rails.setup" do |app|
10
+ app.config.middleware.insert_after(ActionDispatch::ShowExceptions, RubyLsp::Rails::Middleware)
11
+
12
+ config.after_initialize do |_app|
13
+ if defined?(::Rails::Server)
14
+ ssl_enable, host, port = ::Rails::Server::Options.new.parse!(ARGV).values_at(:SSLEnable, :Host, :Port)
15
+ app_uri = "#{ssl_enable ? "https" : "http"}://#{host}:#{port}"
16
+ File.write("#{::Rails.root}/tmp/app_uri.txt", app_uri)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :ruby_lsp_rails do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-lsp-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shopify
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-lsp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.4.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.4.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: sorbet-runtime
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.9897
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.9897
55
+ description: A Ruby LSP extension that adds extra editor functionality for Rails applications
56
+ email:
57
+ - ruby@shopify.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE.txt
63
+ - README.md
64
+ - Rakefile
65
+ - lib/ruby-lsp-rails.rb
66
+ - lib/ruby_lsp/ruby_lsp_rails/extension.rb
67
+ - lib/ruby_lsp/ruby_lsp_rails/hover.rb
68
+ - lib/ruby_lsp/ruby_lsp_rails/rails_client.rb
69
+ - lib/ruby_lsp_rails/middleware.rb
70
+ - lib/ruby_lsp_rails/railtie.rb
71
+ - lib/ruby_lsp_rails/version.rb
72
+ - lib/tasks/ruby_lsp_rails_tasks.rake
73
+ homepage: https://github.com/Shopify/ruby-lsp-rails
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ allowed_push_host: https://rubygems.org
78
+ homepage_uri: https://github.com/Shopify/ruby-lsp-rails
79
+ source_code_uri: https://github.com/Shopify/ruby-lsp-rails
80
+ changelog_uri: https://github.com/Shopify/ruby-lsp-rails/releases
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.4.12
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: A Ruby LSP extension for Rails
100
+ test_files: []