Skip to content

Commit 44cbede

Browse files
committed
Add completion for AR .where queries using AR model's column names
1 parent 955dc27 commit 44cbede

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

lib/ruby_lsp/ruby_lsp_rails/addon.rb

+13
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require_relative "code_lens"
1414
require_relative "document_symbol"
1515
require_relative "definition"
16+
require_relative "completion"
1617
require_relative "indexing_enhancement"
1718

1819
module RubyLsp
@@ -119,6 +120,18 @@ def create_definition_listener(response_builder, uri, node_context, dispatcher)
119120
Definition.new(@rails_runner_client, response_builder, node_context, index, dispatcher)
120121
end
121122

123+
sig do
124+
override.params(
125+
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
126+
node_context: NodeContext,
127+
dispatcher: Prism::Dispatcher,
128+
uri: URI::Generic,
129+
).void
130+
end
131+
def create_completion_listener(response_builder, node_context, dispatcher, uri)
132+
Completion.new(@rails_runner_client, response_builder, node_context, dispatcher, uri)
133+
end
134+
122135
sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
123136
def workspace_did_change_watched_files(changes)
124137
if changes.any? { |c| c[:uri].end_with?("db/schema.rb") || c[:uri].end_with?("structure.sql") }
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module RubyLsp
5+
module Rails
6+
class Completion
7+
extend T::Sig
8+
include Requests::Support::Common
9+
10+
sig do
11+
override.params(
12+
client: RunnerClient,
13+
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
14+
node_context: NodeContext,
15+
dispatcher: Prism::Dispatcher,
16+
uri: URI::Generic,
17+
).void
18+
end
19+
def initialize(client, response_builder, node_context, dispatcher, uri)
20+
@response_builder = response_builder
21+
@client = client
22+
@node_context = node_context
23+
dispatcher.register(
24+
self,
25+
:on_call_node_enter,
26+
)
27+
end
28+
29+
sig { params(node: Prism::CallNode).void }
30+
def on_call_node_enter(node)
31+
if @node_context.call_node && @node_context.call_node&.name == :where
32+
handle_active_record_where_completions(node)
33+
end
34+
end
35+
36+
private
37+
38+
sig { params(node: Prism::CallNode).void }
39+
def handle_active_record_where_completions(node)
40+
receiver = T.must(@node_context.call_node).receiver
41+
return if receiver.nil?
42+
return unless receiver.is_a?(Prism::ConstantReadNode)
43+
44+
resolved_class = @client.model(receiver.name.to_s)
45+
return if resolved_class.nil?
46+
47+
arguments = T.must(@node_context.call_node).arguments&.arguments
48+
indexed_call_node_args = T.let({}, T::Hash[String, Prism::Node])
49+
50+
if arguments&.is_a?(Array)
51+
indexed_call_node_args = index_call_node_args(arguments: arguments)
52+
binding.irb
53+
return if indexed_call_node_args.values.any? { |v| v == node }
54+
end
55+
56+
resolved_class[:columns].each do |column|
57+
next unless column[0].start_with?(node.name.to_s)
58+
next if indexed_call_node_args.key?(column[0])
59+
60+
@response_builder << Interface::CompletionItem.new(
61+
label: column[0],
62+
filter_text: column[0],
63+
label_details: Interface::CompletionItemLabelDetails.new(
64+
description: "Filter #{receiver.name} records by #{column[0]}",
65+
),
66+
text_edit: Interface::TextEdit.new(range: range_from_location(node.location), new_text: "#{column[0]}: "),
67+
kind: Constant::CompletionItemKind::FIELD,
68+
)
69+
end
70+
end
71+
72+
sig { params(arguments: T::Array[Prism::Node]).returns(T::Hash[String, Prism::Node]) }
73+
def index_call_node_args(arguments:)
74+
indexed_call_node_args = {}
75+
arguments.each do |argument|
76+
next unless argument.is_a?(Prism::KeywordHashNode)
77+
78+
argument.elements.each do |e|
79+
next unless e.is_a?(Prism::AssocNode)
80+
81+
key = e.key
82+
if key.is_a?(Prism::SymbolNode)
83+
indexed_call_node_args[key.value] = e.value
84+
end
85+
end
86+
end
87+
indexed_call_node_args
88+
end
89+
end
90+
end
91+
end
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require "test_helper"
5+
6+
module RubyLsp
7+
module Rails
8+
class CompletionTest < ActiveSupport::TestCase
9+
test "Does not suggest column if it already exists within .where as an arg and parantheses are not closed" do
10+
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 37 })
11+
# typed: false
12+
User.where(id:, first_name:, first_na
13+
RUBY
14+
15+
assert_equal(0, response.size)
16+
end
17+
18+
test "Provides suggestions when typing column name partially" do
19+
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 17 })
20+
# typed: false
21+
User.where(first_
22+
RUBY
23+
24+
assert_equal(1, response.size)
25+
assert_equal("first_name", response[0].label)
26+
assert_equal("first_name", response[0].filter_text)
27+
assert_equal(11, response[0].text_edit.range.start.character)
28+
assert_equal(1, response[0].text_edit.range.start.line)
29+
assert_equal(17, response[0].text_edit.range.end.character)
30+
assert_equal(1, response[0].text_edit.range.end.line)
31+
end
32+
33+
test "Does not provide suggestion when typing argument value" do
34+
response = generate_completions_for_source(<<~RUBY, { line: 1, character: 20 })
35+
# typed: false
36+
User.where(id: creat
37+
RUBY
38+
assert_equal(0, response.size)
39+
end
40+
41+
private
42+
43+
def generate_completions_for_source(source, position)
44+
with_server(source) do |server, uri|
45+
sleep(0.1) while RubyLsp::Addon.addons.first.instance_variable_get(:@rails_runner_client).is_a?(NullClient)
46+
47+
server.process_message(
48+
id: 1,
49+
method: "textDocument/completion",
50+
params: { textDocument: { uri: uri }, position: position },
51+
)
52+
53+
result = pop_result(server)
54+
result.response
55+
end
56+
end
57+
end
58+
end
59+
end

0 commit comments

Comments
 (0)