Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance schema hover link to jump to the right table #212

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
9 changes: 8 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rails/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "ruby_lsp/addon"

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

Expand All @@ -17,9 +18,15 @@ def client
@client ||= T.let(RailsClient.new, T.nilable(RailsClient))
end

sig { returns(SchemaCollector) }
def schema_collector
@schema_collector ||= T.let(SchemaCollector.new(client.root), T.nilable(SchemaCollector))
end

sig { override.params(message_queue: Thread::Queue).void }
def activate(message_queue)
client.check_if_server_is_running!
schema_collector.parse_schema
end

sig { override.void }
Expand All @@ -44,7 +51,7 @@ def create_code_lens_listener(uri, dispatcher)
).returns(T.nilable(Listener[T.nilable(Interface::Hover)]))
end
def create_hover_listener(nesting, index, dispatcher)
Hover.new(client, nesting, index, dispatcher)
Hover.new(client, schema_collector, nesting, index, dispatcher)
end

sig { override.returns(String) }
Expand Down
15 changes: 13 additions & 2 deletions lib/ruby_lsp/ruby_lsp_rails/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ class Hover < ::RubyLsp::Listener
sig do
params(
client: RailsClient,
schema_collector: SchemaCollector,
nesting: T::Array[String],
index: RubyIndexer::Index,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(client, nesting, index, dispatcher)
def initialize(client, schema_collector, nesting, index, dispatcher)
super(dispatcher)

@_response = T.let(nil, ResponseType)
@client = client
@schema_collector = schema_collector
@nesting = nesting
@index = index
dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter, :on_call_node_enter)
Expand Down Expand Up @@ -91,8 +93,17 @@ def generate_column_content(name)
return if model.nil?

schema_file = model[:schema_file]
if schema_file
location = @schema_collector.tables[model[:schema_table]]
fragment = "L#{location.start_line},#{location.start_column}-"\
"#{location.end_line},#{location.end_column}" if location
faraazahmad marked this conversation as resolved.
Show resolved Hide resolved
schema_uri = URI::Generic.from_path(
path: schema_file,
fragment: fragment,
)
end
content = +""
content << "[Schema](#{URI::Generic.build(scheme: "file", path: schema_file)})\n\n" if schema_file
content << "[Schema](#{schema_uri})\n\n" if schema_uri
content << model[:columns].map { |name, type| "**#{name}**: #{type}\n" }.join("\n")
content
end
Expand Down
40 changes: 40 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/schema_collector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
module Rails
class SchemaCollector < Prism::Visitor
extend T::Sig

sig { returns(T::Hash[String, Prism::Location]) }
attr_reader :tables

sig { params(project_root: Pathname).void }
def initialize(project_root)
super()

@tables = T.let({}, T::Hash[String, Prism::Location])
@schema_path = T.let(project_root.join("db", "schema.rb").to_s, String)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite right as some apps use structure.sql instead.

If you search you'll see we use schema_dump_path elsewhere to get the path.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means this collector will need to communicate with the server. The path for the schema is unlikely to change, so maybe there's room for a refactor where we fetch the schema path once and reuse it here and in hover.

Maybe we can move forward with this and simply not populate anything if db/schema.rb doesn't exist? That way we can provide the feature for schema users and enhance it later.

end

sig { void }
def parse_schema
parse_result = Prism.parse_file(@schema_path)
parse_result.value.accept(self)
end

sig { params(node: Prism::CallNode).void }
def visit_call_node(node)
if node.message == "create_table"
first_argument = node.arguments&.arguments&.first

if first_argument&.is_a?(Prism::StringNode)
@tables[first_argument.content] = node.location
end
end

super
end
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_lsp_rails/rack_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def resolve_database_info_from_model(model_name)
body = JSON.dump({
columns: const.columns.map { |column| [column.name, column.type] },
schema_file: schema_file,
schema_table: const.table_name,
})

[200, { "Content-Type" => "application/json" }, [body]]
Expand Down
1 change: 1 addition & 0 deletions test/ruby_lsp_rails/rack_app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class RackAppTest < ActionDispatch::IntegrationTest
["created_at", "datetime"],
["updated_at", "datetime"],
],
"schema_table" => "users",
},
JSON.parse(response.body),
)
Expand Down
27 changes: 27 additions & 0 deletions test/ruby_lsp_rails/schema_collector_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RubyLsp
module Rails
class SchemaCollectorTest < ActiveSupport::TestCase
SCHEMA_FILE = <<~RUBY
ActiveRecord::Schema[7.1].define(version: 2023_12_09_114241) do
create_table "cats", force: :cascade do |t|
end

create_table "dogs", force: :cascade do |t|
end
end
RUBY

test "store locations of models by parsing create_table calls" do
collector = RubyLsp::Rails::SchemaCollector.new(Pathname.new("example_app"))
Prism.parse(SCHEMA_FILE).value.accept(collector)

assert_equal(["cats", "dogs"], collector.tables.keys)
end
end
end
end
Loading