Skip to content

Commit

Permalink
Merge pull request #157 from Shopify/migrate-to-yarp
Browse files Browse the repository at this point in the history
Use YARP-based ruby-lsp
  • Loading branch information
st0012 authored Oct 4, 2023
2 parents 3f77449 + 8320daa commit 379dfc4
Show file tree
Hide file tree
Showing 17 changed files with 15,461 additions and 21,002 deletions.
13 changes: 6 additions & 7 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ PATH
specs:
ruby-lsp-rails (0.2.5)
rails (>= 6.0)
ruby-lsp (>= 0.10.0, < 0.11.0)
ruby-lsp (>= 0.11.0, < 0.12.0)
sorbet-runtime (>= 0.5.9897)

GEM
Expand Down Expand Up @@ -221,11 +221,10 @@ GEM
rubocop (~> 1.51)
rubocop-sorbet (0.7.4)
rubocop (>= 0.90.0)
ruby-lsp (0.10.1)
ruby-lsp (0.11.0)
language_server-protocol (~> 3.17.0)
sorbet-runtime
syntax_tree (>= 6.1.1, < 7)
yarp (>= 0.11, < 0.12)
sorbet-runtime (>= 0.5.5685)
yarp (>= 0.12, < 0.13)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sorbet (0.5.11054)
Expand All @@ -244,7 +243,7 @@ GEM
sqlite3 (1.6.6)
mini_portile2 (~> 2.8.0)
stringio (3.0.8)
syntax_tree (6.1.1)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tapioca (0.11.9)
bundler (>= 2.2.25)
Expand All @@ -270,7 +269,7 @@ GEM
yard-sorbet (0.8.1)
sorbet-runtime (>= 0.5)
yard (>= 0.9)
yarp (0.11.0)
yarp (0.12.0)
zeitwerk (2.6.12)

PLATFORMS
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ruby LSP Rails

Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) extension for extra Rails editor features, such as:
Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for extra Rails editor features, such as:

- Displaying an ActiveRecord model's database columns and types when hovering over it
- Running tests and debugging tests through the terminal or the editor's UI
Expand Down Expand Up @@ -56,7 +56,7 @@ cause the test runner to hang.
This gem consists of two components that enable enhanced Rails functionality in the editor:

1. A Rack app that automatically exposes APIs when Rails server is running
1. A Ruby LSP extension that connects to the exposed APIs to fetch runtime information from the Rails server
1. A Ruby LSP addon that connects to the exposed APIs to fetch runtime information from the Rails server

This is why the Rails server needs to be running for some features to work.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# typed: strict
# frozen_string_literal: true

require "ruby_lsp/extension"
require "ruby_lsp/addon"

require_relative "rails_client"
require_relative "hover"
require_relative "code_lens"

module RubyLsp
module Rails
class Extension < ::RubyLsp::Extension
class Addon < ::RubyLsp::Addon
extend T::Sig

sig { returns(RailsClient) }
Expand Down Expand Up @@ -39,11 +39,13 @@ def create_code_lens_listener(uri, emitter, message_queue)

sig do
override.params(
nesting: T::Array[String],
index: RubyIndexer::Index,
emitter: EventEmitter,
message_queue: Thread::Queue,
).returns(T.nilable(Listener[T.nilable(Interface::Hover)]))
end
def create_hover_listener(emitter, message_queue)
def create_hover_listener(nesting, index, emitter, message_queue)
Hover.new(client, emitter, message_queue)
end

Expand Down
52 changes: 25 additions & 27 deletions lib/ruby_lsp/ruby_lsp_rails/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,57 +46,55 @@ class CodeLens < ::RubyLsp::Listener
def initialize(uri, emitter, message_queue)
@_response = T.let([], ResponseType)
@path = T.let(uri.to_standardized_path, T.nilable(String))
emitter.register(self, :on_command, :on_class, :on_def)
emitter.register(self, :on_call, :on_class, :on_def)

super(emitter, message_queue)
end

sig { params(node: SyntaxTree::Command).void }
def on_command(node)
message_value = node.message.value
return unless message_value == "test" && node.arguments.parts.any?
sig { params(node: YARP::CallNode).void }
def on_call(node)
message_value = node.message
return unless message_value == "test"

first_argument = node.arguments.parts.first
arguments = node.arguments&.arguments
return unless arguments&.any?

parts = case first_argument
when SyntaxTree::StringConcat
first_argument = arguments.first

content = case first_argument
when YARP::StringConcatNode
left = first_argument.left
right = first_argument.right
# We only support two lines of concatenation on test names
if first_argument.left.is_a?(SyntaxTree::StringLiteral) &&
first_argument.right.is_a?(SyntaxTree::StringLiteral)
[*first_argument.left.parts, *first_argument.right.parts]
if left.is_a?(YARP::StringNode) &&
right.is_a?(YARP::StringNode)
left.content + right.content
end
when SyntaxTree::StringLiteral
first_argument.parts
when YARP::StringNode
first_argument.content
end

# The test name may be a blank string while the code is being typed
return if parts.nil? || parts.empty?

# We can't handle interpolation yet
return unless parts.all? { |part| part.is_a?(SyntaxTree::TStringContent) }

test_name = parts.map(&:value).join
return if test_name.empty?
return unless content && !content.empty?

line_number = node.location.start_line
command = "#{BASE_COMMAND} #{@path}:#{line_number}"
add_test_code_lens(node, name: test_name, command: command, kind: :example)
add_test_code_lens(node, name: content, command: command, kind: :example)
end

# Although uncommon, Rails tests can be written with the classic "def test_name" syntax.
sig { params(node: SyntaxTree::DefNode).void }
sig { params(node: YARP::DefNode).void }
def on_def(node)
method_name = node.name.value
method_name = node.name.to_s
if method_name.start_with?("test_")
line_number = node.location.start_line
command = "#{BASE_COMMAND} #{@path}:#{line_number}"
add_test_code_lens(node, name: method_name, command: command, kind: :example)
end
end

sig { params(node: SyntaxTree::ClassDeclaration).void }
sig { params(node: YARP::ClassNode).void }
def on_class(node)
class_name = node.constant.constant.value
class_name = node.constant_path.slice
if class_name.end_with?("Test")
command = "#{BASE_COMMAND} #{@path}"
add_test_code_lens(node, name: class_name, command: command, kind: :group)
Expand All @@ -105,7 +103,7 @@ def on_class(node)

private

sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
sig { params(node: YARP::Node, name: String, command: String, kind: Symbol).void }
def add_test_code_lens(node, name:, command:, kind:)
return unless @path

Expand Down
42 changes: 19 additions & 23 deletions lib/ruby_lsp/ruby_lsp_rails/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ def initialize(client, emitter, message_queue)

@_response = T.let(nil, ResponseType)
@client = client
emitter.register(self, :on_const, :on_command, :on_const_path_ref, :on_call)
emitter.register(self, :on_constant_path, :on_constant_read, :on_call)
end

sig { params(node: SyntaxTree::Const).void }
def on_const(node)
model = @client.model(node.value)
sig { params(node: YARP::ConstantPathNode).void }
def on_constant_path(node)
@_response = generate_rails_document_link_hover(node.slice, node.location)
end

sig { params(node: YARP::ConstantReadNode).void }
def on_constant_read(node)
model = @client.model(node.name.to_s)
return if model.nil?

schema_file = model[:schema_file]
Expand All @@ -46,37 +51,28 @@ def on_const(node)
end
content << model[:columns].map { |name, type| "**#{name}**: #{type}\n" }.join("\n")
contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: content)
@_response = RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
end

sig { params(node: SyntaxTree::Command).void }
def on_command(node)
message = node.message
@_response = generate_rails_document_link_hover(message.value, message)
@_response = RubyLsp::Interface::Hover.new(range: range_from_node(node), contents: contents)
end

sig { params(node: SyntaxTree::ConstPathRef).void }
def on_const_path_ref(node)
@_response = generate_rails_document_link_hover(full_constant_name(node), node)
end

sig { params(node: SyntaxTree::CallNode).void }
sig { params(node: YARP::CallNode).void }
def on_call(node)
message = node.message
return if message.is_a?(Symbol)
message_value = node.message
message_loc = node.message_loc

return unless message_value && message_loc

@_response = generate_rails_document_link_hover(message.value, message)
@_response = generate_rails_document_link_hover(message_value, message_loc)
end

private

sig { params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover)) }
def generate_rails_document_link_hover(name, node)
sig { params(name: String, location: YARP::Location).returns(T.nilable(Interface::Hover)) }
def generate_rails_document_link_hover(name, location)
urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
return if urls.empty?

contents = RubyLsp::Interface::MarkupContent.new(kind: "markdown", value: urls.join("\n\n"))
RubyLsp::Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
RubyLsp::Interface::Hover.new(range: range_from_location(location), contents: contents)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/ruby_lsp_rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class Railtie < ::Rails::Railtie
app_uri_path.write(app_uri)

at_exit do
# The app_uri.txt file should only exist when the server is running. The extension uses its presence to
# report if the server is running or not. If the server is not running, some of the extension features
# The app_uri.txt file should only exist when the server is running. The addon uses its presence to
# report if the server is running or not. If the server is not running, some of the addon features
# will not be available.
File.delete(app_uri_path) if File.exist?(app_uri_path)
end
Expand Down
6 changes: 3 additions & 3 deletions ruby-lsp-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
spec.authors = ["Shopify"]
spec.email = ["[email protected]"]
spec.homepage = "https://github.com/Shopify/ruby-lsp-rails"
spec.summary = "A Ruby LSP extension for Rails"
spec.description = "A Ruby LSP extension that adds extra editor functionality for Rails applications"
spec.summary = "A Ruby LSP addon for Rails"
spec.description = "A Ruby LSP addon that adds extra editor functionality for Rails applications"
spec.license = "MIT"

spec.metadata["allowed_push_host"] = "https://rubygems.org"
Expand All @@ -22,6 +22,6 @@ Gem::Specification.new do |spec|
end

spec.add_dependency("rails", ">= 6.0")
spec.add_dependency("ruby-lsp", ">= 0.10.0", "< 0.11.0")
spec.add_dependency("ruby-lsp", ">= 0.11.0", "< 0.12.0")
spec.add_dependency("sorbet-runtime", ">= 0.5.9897")
end
Loading

0 comments on commit 379dfc4

Please sign in to comment.