ruby-lsp-rails icon indicating copy to clipboard operation
ruby-lsp-rails copied to clipboard

Enhance our schema hover link to jump to the right table

Open vinistock opened this issue 2 years ago • 6 comments

Currently, our schema link just points to the file. However, we can link to the specific table for the model, which is a richer experience.

One possibility of how we can do this is by

  1. Start returning the table name from our middleware
  2. During the activate call parse schema.rb and create a hash of table names to locations
  3. When executing hover, search the hash for the right line to jump to

Concerns

  • If there are modifications in schema.rb, we would need to rebuild the hash. For that, we need file watching to be merged in the Ruby LSP and then we have to design a way in which extensions can ask the Ruby LSP to notify them when certain files are changed.
  • This won't work for structure.sql. We probably don't want a SQL parser, but we might be able to achieve something with regexes

vinistock avatar Apr 24 '23 15:04 vinistock

I've done some similar work in a personal project and I would love to help out with this issue

faraazahmad avatar Dec 09 '23 00:12 faraazahmad

That's awesome! Can you describe the approach to discover the tables and keep track of their location in the schema file?

vinistock avatar Dec 11 '23 14:12 vinistock

Thanks! Although in this project I'm not storing the locations of the tables, I'm using rubocop-ast to parse the source. This gives you access to the path and location in the file for every node.

So in the schema.rb the approach would be to:

  1. Look for create_table method calls (lets call it the ddl_node)
  2. Constantize its first argument using ActiveSupport's string helpers and get the model name
  3. Get children line, column, last_line, last_column from ddl_node.location

I have a Visitor::Schema that I borrowed from rubocop-rails's SchemaLoader that basically does that, except for the location storing part.

I hope this makes sense.

faraazahmad avatar Dec 11 '23 17:12 faraazahmad

Yeah, that makes sense. Although we'd use Prism and not rubocop-ast here. So you can probably do something like

class SchemaCollector < Prism::Visitor
  def initialize
    @tables = {}
  end

  def visit_call_node(node)
    # check if it's a `create_table` call
    # add it to the hash of tables
  end
end

Feel free to put a PR up and thanks for the interest!

vinistock avatar Dec 11 '23 19:12 vinistock

@vinistock Is it fine if I use the SyntaxTree Visitor? since the gem is installed anyway and the code becomes simpler

require 'syntax_tree'
require 'active_support/inflector'

class SchemaCollector < SyntaxTree::Visitor
  attr_reader :tables
  
  def initialize
    @tables = {}
  end

  def visit_command(node)
    # check if it's a `create_table` call
    # add it to the hash of tables

    case node
    in {
      message: { value: 'create_table' },
      arguments: { parts: [{ parts: [table_name_literal] }, *ignored_args] }
    }
      @tables[table_name_literal.value.classify] = node.location
    else
      # No matching pattern
    end
  end
end

raw_schema = File.read('schema.rb')
visitor = SchemaCollector.new
visitor.visit(SyntaxTree.parse(raw_schema))

faraazahmad avatar Dec 13 '23 14:12 faraazahmad

since the gem is installed anyway and the code becomes simpler

We don't have a dependency on Syntax Tree so it wouldn't be available on every project - and the new official parser is Prism.

For this case, we have to use Prism. I should also mention that for language servers performance is always really critical, so please don't use pattern matching in the visitor.

We can probably do it with a few early returns. This is not the exact code, but something like this.

class SchemaCollector < Prism::Visitor
  def visit_call_node(node)
    return unless node.message == "create_table"

    arguments = node.arguments&.arguments
    return unless arguments

    first_arg = arguments.first
    return unless first_arg.is_a?(Prism::StringNode) # can't remember if it's a string or a symbol

    @table[first_arg.content] = node.location
  end
end

vinistock avatar Dec 13 '23 14:12 vinistock