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

Completion for Active Record .where queries #526

Merged
merged 4 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require_relative "code_lens"
require_relative "document_symbol"
require_relative "definition"
require_relative "completion"
require_relative "indexing_enhancement"

module RubyLsp
Expand Down Expand Up @@ -119,6 +120,18 @@ def create_definition_listener(response_builder, uri, node_context, dispatcher)
Definition.new(@rails_runner_client, response_builder, node_context, index, dispatcher)
end

sig do
override.params(
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
uri: URI::Generic,
).void
end
def create_completion_listener(response_builder, node_context, dispatcher, uri)
Completion.new(@rails_runner_client, response_builder, node_context, dispatcher, uri)
end

sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
def workspace_did_change_watched_files(changes)
if changes.any? { |c| c[:uri].end_with?("db/schema.rb") || c[:uri].end_with?("structure.sql") }
Expand Down
90 changes: 90 additions & 0 deletions lib/ruby_lsp/ruby_lsp_rails/completion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
module Rails
class Completion
extend T::Sig
include Requests::Support::Common

sig do
override.params(
client: RunnerClient,
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
uri: URI::Generic,
).void
end
def initialize(client, response_builder, node_context, dispatcher, uri)
@response_builder = response_builder
@client = client
@node_context = node_context
dispatcher.register(
self,
:on_call_node_enter,
)
end

sig { params(node: Prism::CallNode).void }
def on_call_node_enter(node)
if @node_context.call_node && @node_context.call_node&.name == :where
ChallaHalla marked this conversation as resolved.
Show resolved Hide resolved
handle_active_record_where_completions(node)
end
end

private

sig { params(node: Prism::CallNode).void }
def handle_active_record_where_completions(node)
receiver = T.must(@node_context.call_node).receiver
ChallaHalla marked this conversation as resolved.
Show resolved Hide resolved
return if receiver.nil?
return unless receiver.is_a?(Prism::ConstantReadNode)
ChallaHalla marked this conversation as resolved.
Show resolved Hide resolved

resolved_class = @client.model(receiver.name.to_s)
return if resolved_class.nil?

arguments = T.must(@node_context.call_node).arguments&.arguments
indexed_call_node_args = T.let({}, T::Hash[String, Prism::Node])

if arguments&.is_a?(Array)
ChallaHalla marked this conversation as resolved.
Show resolved Hide resolved
indexed_call_node_args = index_call_node_args(arguments: arguments)
return if indexed_call_node_args.values.any? { |v| v == node }
end

resolved_class[:columns].each do |column|
next unless column[0].start_with?(node.name.to_s)
next if indexed_call_node_args.key?(column[0])

@response_builder << Interface::CompletionItem.new(
label: column[0],
filter_text: column[0],
label_details: Interface::CompletionItemLabelDetails.new(
description: "Filter #{receiver.name} records by #{column[0]}",
),
text_edit: Interface::TextEdit.new(range: range_from_location(node.location), new_text: "#{column[0]}: "),
ChallaHalla marked this conversation as resolved.
Show resolved Hide resolved
kind: Constant::CompletionItemKind::FIELD,
)
end
end

sig { params(arguments: T::Array[Prism::Node]).returns(T::Hash[String, Prism::Node]) }
def index_call_node_args(arguments:)
indexed_call_node_args = {}
arguments.each do |argument|
next unless argument.is_a?(Prism::KeywordHashNode)

argument.elements.each do |e|
next unless e.is_a?(Prism::AssocNode)

key = e.key
if key.is_a?(Prism::SymbolNode)
indexed_call_node_args[key.value] = e.value
end
end
end
indexed_call_node_args
end
end
end
end
59 changes: 59 additions & 0 deletions test/ruby_lsp_rails/completion_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RubyLsp
module Rails
class CompletionTest < ActiveSupport::TestCase
test "Does not suggest column if it already exists within .where as an arg and parantheses are not closed" do
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 37 })
# typed: false
User.where(id:, first_name:, first_na
RUBY

assert_equal(0, response.size)
end

test "Provides suggestions when typing column name partially" do
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 17 })
# typed: false
User.where(first_
RUBY

assert_equal(1, response.size)
assert_equal("first_name", response[0].label)
assert_equal("first_name", response[0].filter_text)
assert_equal(11, response[0].text_edit.range.start.character)
assert_equal(1, response[0].text_edit.range.start.line)
assert_equal(17, response[0].text_edit.range.end.character)
assert_equal(1, response[0].text_edit.range.end.line)
end

test "Does not provide suggestion when typing argument value" do
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 20 })
# typed: false
User.where(id: creat
RUBY
assert_equal(0, response.size)
end

private

def generate_completions_for_source(source, position)
with_server(source) do |server, uri|
sleep(0.1) while RubyLsp::Addon.addons.first.instance_variable_get(:@rails_runner_client).is_a?(NullClient)

server.process_message(
id: 1,
method: "textDocument/completion",
params: { textDocument: { uri: uri }, position: position },
)

result = pop_result(server)
result.response
end
end
end
end
end
Loading