diff --git a/lib/ruby_lsp/ruby_lsp_rails/addon.rb b/lib/ruby_lsp/ruby_lsp_rails/addon.rb index bc869440..f92226c2 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/addon.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/addon.rb @@ -4,6 +4,7 @@ require "ruby_lsp/addon" require_relative "rails_client" +require_relative "schema_collector" require_relative "hover" require_relative "code_lens" @@ -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 } @@ -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) } diff --git a/lib/ruby_lsp/ruby_lsp_rails/hover.rb b/lib/ruby_lsp/ruby_lsp_rails/hover.rb index 18982df1..3168bfe8 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/hover.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/hover.rb @@ -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) @@ -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 - 1},#{location.start_column}-"\ + "#{location.end_line - 1},#{location.end_column}" if location + 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 diff --git a/lib/ruby_lsp/ruby_lsp_rails/schema_collector.rb b/lib/ruby_lsp/ruby_lsp_rails/schema_collector.rb new file mode 100644 index 00000000..a0700c3c --- /dev/null +++ b/lib/ruby_lsp/ruby_lsp_rails/schema_collector.rb @@ -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) + 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 diff --git a/lib/ruby_lsp_rails/rack_app.rb b/lib/ruby_lsp_rails/rack_app.rb index 69ac22ba..c1532e2e 100644 --- a/lib/ruby_lsp_rails/rack_app.rb +++ b/lib/ruby_lsp_rails/rack_app.rb @@ -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]] diff --git a/test/ruby_lsp_rails/rack_app_test.rb b/test/ruby_lsp_rails/rack_app_test.rb index ce645b38..fc954dea 100644 --- a/test/ruby_lsp_rails/rack_app_test.rb +++ b/test/ruby_lsp_rails/rack_app_test.rb @@ -20,6 +20,7 @@ class RackAppTest < ActionDispatch::IntegrationTest ["created_at", "datetime"], ["updated_at", "datetime"], ], + "schema_table" => "users", }, JSON.parse(response.body), ) diff --git a/test/ruby_lsp_rails/schema_collector_test.rb b/test/ruby_lsp_rails/schema_collector_test.rb new file mode 100644 index 00000000..ea05ac8a --- /dev/null +++ b/test/ruby_lsp_rails/schema_collector_test.rb @@ -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