Skip to content

Commit 05df46e

Browse files
refactor: make node identification simpler by updating tree-sitter grammar
1 parent 2317e29 commit 05df46e

35 files changed

+385353
-423537
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pgls_completions/src/providers/columns.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ mod tests {
5858
use crate::{
5959
CompletionItem, CompletionItemKind, complete,
6060
test_helper::{
61-
CompletionAssertion, assert_complete_results, assert_no_complete_results,
62-
get_test_deps, get_test_params,
61+
CompletionAssertion, assert_complete_results, get_test_deps, get_test_params,
6362
},
6463
};
6564

@@ -717,18 +716,6 @@ mod tests {
717716
&pool,
718717
)
719718
.await;
720-
721-
// no completions in the values list!
722-
assert_no_complete_results(
723-
format!(
724-
"insert into instruments (id, name) values ({})",
725-
QueryWithCursorPosition::cursor_marker()
726-
)
727-
.as_str(),
728-
None,
729-
&pool,
730-
)
731-
.await;
732719
}
733720

734721
#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]

crates/pgls_completions/src/providers/helper.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use pgls_text_size::{TextRange, TextSize};
1+
use pgls_text_size::TextRange;
22
use pgls_treesitter::TreesitterContext;
33

44
use crate::{is_sanitized_token_with_quote, remove_sanitized_token};
@@ -9,29 +9,25 @@ pub(crate) fn node_text_surrounded_by_quotes(ctx: &TreesitterContext) -> bool {
99
}
1010

1111
pub(crate) fn get_range_to_replace(ctx: &TreesitterContext) -> TextRange {
12-
match ctx.node_under_cursor.as_ref() {
13-
Some(node) => {
14-
let content = ctx.get_node_under_cursor_content().unwrap_or("".into());
15-
let content = content.as_str();
12+
let node = &ctx.node_under_cursor;
13+
let content = ctx.get_node_under_cursor_content().unwrap_or("".into());
14+
let content = content.as_str();
1615

17-
let sanitized = remove_sanitized_token(content);
18-
let length = sanitized.len();
16+
let sanitized = remove_sanitized_token(content);
17+
let length = sanitized.len();
1918

20-
let mut start = node.start_byte();
21-
let mut end = start + length;
19+
let mut start = node.start_byte();
20+
let mut end = start + length;
2221

23-
if sanitized.starts_with('"') && sanitized.ends_with('"') {
24-
start += 1;
22+
if sanitized.starts_with('"') && sanitized.ends_with('"') {
23+
start += 1;
2524

26-
if sanitized.len() > 1 {
27-
end -= 1;
28-
}
29-
}
30-
31-
TextRange::new(start.try_into().unwrap(), end.try_into().unwrap())
25+
if sanitized.len() > 1 {
26+
end -= 1;
3227
}
33-
None => TextRange::empty(TextSize::new(0)),
3428
}
29+
30+
TextRange::new(start.try_into().unwrap(), end.try_into().unwrap())
3531
}
3632

3733
pub(crate) fn only_leading_quote(ctx: &TreesitterContext) -> bool {
@@ -45,7 +41,7 @@ pub(crate) fn with_schema_or_alias(
4541
item_name: &str,
4642
schema_or_alias_name: Option<&str>,
4743
) -> String {
48-
let is_already_prefixed_with_schema_name = ctx.schema_or_alias_name.is_some();
44+
let is_already_prefixed_with_schema_name = ctx.has_any_qualifier();
4945

5046
let with_quotes = node_text_surrounded_by_quotes(ctx);
5147
let single_leading_quote = only_leading_quote(ctx);

crates/pgls_completions/src/relevance/filtering.rs

Lines changed: 117 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl CompletionFilter<'_> {
1818
pub fn is_relevant(&self, ctx: &TreesitterContext) -> Option<()> {
1919
self.completable_context(ctx)?;
2020

21-
self.check_node_type(ctx)
21+
self.check_specific_node_type(ctx)
2222
// we want to rely on treesitter more, so checking the clause is a fallback
2323
.or_else(|| self.check_clause(ctx))?;
2424

@@ -33,11 +33,7 @@ impl CompletionFilter<'_> {
3333
return None;
3434
}
3535

36-
let current_node_kind = ctx
37-
.node_under_cursor
38-
.as_ref()
39-
.map(|n| n.kind())
40-
.unwrap_or("");
36+
let current_node_kind = ctx.node_under_cursor.kind();
4137

4238
if current_node_kind.starts_with("keyword_")
4339
|| current_node_kind == "="
@@ -70,37 +66,92 @@ impl CompletionFilter<'_> {
7066
}
7167
}
7268

69+
if ctx.node_under_cursor.kind() == "any_identifier"
70+
&& ctx.history_ends_with(&["alias", "any_identifier"])
71+
{
72+
return None;
73+
}
74+
7375
// No autocompletions if there are two identifiers without a separator.
74-
if ctx.node_under_cursor.as_ref().is_some_and(|node| {
75-
node.prev_sibling().is_some_and(|p| {
76-
(p.kind() == "any_identifier" || p.kind() == "object_reference")
77-
&& node.kind() == "any_identifier"
78-
})
76+
if ctx.node_under_cursor.prev_sibling().is_some_and(|p| {
77+
(p.kind() == "any_identifier" || p.kind() == "object_reference")
78+
&& ctx.node_under_cursor.kind() == "any_identifier"
7979
}) {
8080
return None;
8181
}
8282

8383
// no completions if we're right after an asterisk:
8484
// `select * {}`
85-
if ctx.node_under_cursor.as_ref().is_some_and(|node| {
86-
node.prev_sibling()
87-
.is_some_and(|p| (p.kind() == "all_fields") && node.kind() == "any_identifier")
85+
if ctx.node_under_cursor.prev_sibling().is_some_and(|p| {
86+
(p.kind() == "all_fields") && ctx.node_under_cursor.kind() == "any_identifier"
8887
}) {
8988
return None;
9089
}
9190

9291
Some(())
9392
}
9493

95-
fn check_node_type(&self, ctx: &TreesitterContext) -> Option<()> {
96-
let kind = ctx.node_under_cursor.as_ref().map(|n| n.kind())?;
94+
fn check_specific_node_type(&self, ctx: &TreesitterContext) -> Option<()> {
95+
let kind = ctx.node_under_cursor.kind();
9796

9897
let is_allowed = match kind {
99-
"column_identifier" => {
100-
matches!(self.data, CompletionRelevanceData::Column(_))
101-
&& !ctx.matches_ancestor_history(&["insert_values", "field"])
102-
&& !ctx.node_under_cursor_is_within_field_name("binary_expr_right")
103-
}
98+
"column_identifier" => matches!(self.data, CompletionRelevanceData::Column(_)),
99+
"role_identifier" => matches!(self.data, CompletionRelevanceData::Role(_)),
100+
"function_identifier" => matches!(self.data, CompletionRelevanceData::Function(_)),
101+
"schema_identifier" => matches!(self.data, CompletionRelevanceData::Schema(_)),
102+
"table_identifier" => matches!(self.data, CompletionRelevanceData::Table(_)),
103+
"policy_identifier" => matches!(self.data, CompletionRelevanceData::Policy(_)),
104+
105+
"any_identifier" => match self.data {
106+
CompletionRelevanceData::Column(_) => {
107+
ctx.node_under_cursor_is_within_field(&[
108+
"object_reference_1of1",
109+
"object_reference_2of2",
110+
"object_reference_3of3",
111+
"column_reference_1of1",
112+
"column_reference_2of2",
113+
"column_reference_3of3",
114+
]) && !ctx.node_under_cursor_is_within_field(&["binary_expr_right"])
115+
}
116+
117+
CompletionRelevanceData::Schema(_) => ctx.node_under_cursor_is_within_field(&[
118+
"object_reference_1of1",
119+
"object_reference_1of2",
120+
"object_reference_1of3",
121+
"type_reference_1of1",
122+
"table_reference_1of1",
123+
"column_reference_1of1",
124+
"column_reference_1of2",
125+
"function_reference_1of1",
126+
]),
127+
128+
CompletionRelevanceData::Function(f) => {
129+
ctx.node_under_cursor_is_within_field(&[
130+
"object_reference_1of1",
131+
"object_reference_2of2",
132+
"function_reference_1of1",
133+
]) && !(ctx.history_ends_with(&[
134+
"check_or_using_clause",
135+
"binary_expression",
136+
"object_reference",
137+
"any_identifier",
138+
]) && matches!(f.kind, ProcKind::Aggregate))
139+
}
140+
141+
CompletionRelevanceData::Table(_) => ctx.node_under_cursor_is_within_field(&[
142+
"object_reference_1of1",
143+
"object_reference_1of2",
144+
"object_reference_2of2",
145+
"object_reference_2of3",
146+
"table_reference_1of1",
147+
"column_reference_1of1",
148+
"column_reference_1of2",
149+
"column_reference_2of2",
150+
]),
151+
152+
_ => false,
153+
},
154+
104155
_ => false,
105156
};
106157

@@ -116,21 +167,27 @@ impl CompletionFilter<'_> {
116167
WrappingClause::From | WrappingClause::Update => true,
117168

118169
WrappingClause::RevokeStatement | WrappingClause::GrantStatement => ctx
119-
.matches_ancestor_history(&["grantable_on_table", "object_reference"]),
170+
.history_ends_with(&[
171+
"grantable_on_table",
172+
"object_reference",
173+
"any_identifier",
174+
]),
120175

121176
WrappingClause::Join { on_node: None } => true,
122-
WrappingClause::Join { on_node: Some(on) } => ctx
123-
.node_under_cursor
124-
.as_ref()
125-
.is_some_and(|cn| cn.start_byte() < on.end_byte()),
177+
WrappingClause::Join { on_node: Some(on) } => {
178+
ctx.node_under_cursor.start_byte() < on.end_byte()
179+
}
126180

127181
WrappingClause::Insert => {
128182
ctx.wrapping_node_kind
129183
.as_ref()
130184
.is_none_or(|n| n != &WrappingNode::List)
131185
&& (ctx.before_cursor_matches_kind(&["keyword_into"])
132186
|| (ctx.before_cursor_matches_kind(&["."])
133-
&& ctx.matches_ancestor_history(&["object_reference"])))
187+
&& ctx.history_ends_with(&[
188+
"object_reference",
189+
"any_identifier",
190+
])))
134191
}
135192

136193
WrappingClause::DropTable | WrappingClause::AlterTable => ctx
@@ -143,7 +200,7 @@ impl CompletionFilter<'_> {
143200
WrappingClause::CreatePolicy
144201
| WrappingClause::AlterPolicy
145202
| WrappingClause::DropPolicy => {
146-
ctx.matches_ancestor_history(&["object_reference"])
203+
ctx.history_ends_with(&["object_reference", "any_identifier"])
147204
&& ctx.before_cursor_matches_kind(&["keyword_on", "."])
148205
}
149206

@@ -166,10 +223,9 @@ impl CompletionFilter<'_> {
166223

167224
// We can complete columns in JOIN cluases, but only if we are after the
168225
// ON node in the "ON u.id = posts.user_id" part.
169-
WrappingClause::Join { on_node: Some(on) } => ctx
170-
.node_under_cursor
171-
.as_ref()
172-
.is_some_and(|cn| cn.start_byte() >= on.end_byte()),
226+
WrappingClause::Join { on_node: Some(on) } => {
227+
ctx.node_under_cursor.start_byte() >= on.end_byte()
228+
}
173229

174230
// we are in a JOIN, but definitely not after an ON
175231
WrappingClause::Join { on_node: None } => false,
@@ -183,7 +239,7 @@ impl CompletionFilter<'_> {
183239
WrappingClause::Where => {
184240
ctx.before_cursor_matches_kind(&["keyword_and", "keyword_where"])
185241
|| (ctx.before_cursor_matches_kind(&["field_qualifier"])
186-
&& ctx.matches_ancestor_history(&["field"]))
242+
&& ctx.history_ends_with(&["field", "any_identifier"]))
187243
}
188244

189245
WrappingClause::CheckOrUsingClause => {
@@ -216,17 +272,17 @@ impl CompletionFilter<'_> {
216272

217273
CompletionRelevanceData::Schema(_) => match clause {
218274
WrappingClause::Select
219-
| WrappingClause::From
220275
| WrappingClause::Join { .. }
221276
| WrappingClause::Update
222277
| WrappingClause::Delete => true,
223278

224279
WrappingClause::RevokeStatement | WrappingClause::GrantStatement => {
225-
(ctx.matches_ancestor_history(&[
280+
(ctx.history_ends_with(&[
226281
"grantable_on_table",
227282
"object_reference",
228-
]) && ctx.schema_or_alias_name.is_none())
229-
|| ctx.matches_ancestor_history(&["grantable_on_all"])
283+
"any_identifier",
284+
]) && !ctx.has_any_qualifier())
285+
|| ctx.history_ends_with(&["grantable_on_all", "any_identifier"])
230286
}
231287

232288
WrappingClause::Where => {
@@ -271,20 +327,18 @@ impl CompletionFilter<'_> {
271327
.before_cursor_matches_kind(&["keyword_role", "keyword_authorization"]),
272328

273329
WrappingClause::RevokeStatement | WrappingClause::GrantStatement => {
274-
ctx.matches_ancestor_history(&["role_specification"])
275-
|| ctx.node_under_cursor.as_ref().is_some_and(|k| {
276-
k.kind() == "any_identifier"
277-
&& ctx.before_cursor_matches_kind(&[
278-
"keyword_grant",
279-
"keyword_revoke",
280-
"keyword_for",
281-
])
282-
})
330+
ctx.history_ends_with(&["role_specification", "any_identifier"])
331+
|| (ctx.node_under_cursor.kind() == "any_identifier"
332+
&& ctx.before_cursor_matches_kind(&[
333+
"keyword_grant",
334+
"keyword_revoke",
335+
"keyword_for",
336+
]))
283337
}
284338

285339
WrappingClause::AlterPolicy | WrappingClause::CreatePolicy => {
286340
ctx.before_cursor_matches_kind(&["keyword_to"])
287-
&& ctx.matches_ancestor_history(&["policy_to_role"])
341+
&& ctx.history_ends_with(&["policy_to_role", "any_identifier"])
288342
}
289343

290344
_ => false,
@@ -308,18 +362,24 @@ impl CompletionFilter<'_> {
308362
}
309363

310364
fn check_mentioned_schema_or_alias(&self, ctx: &TreesitterContext) -> Option<()> {
311-
if ctx.schema_or_alias_name.is_none() {
312-
return Some(());
313-
}
314-
315-
let schema_or_alias = ctx.schema_or_alias_name.as_ref().unwrap().replace('"', "");
365+
let tail_qualifier = match ctx.tail_qualifier_sanitized() {
366+
Some(q) => q,
367+
None => return Some(()), // no qualifier = this check passes
368+
};
316369

317370
let matches = match self.data {
318-
CompletionRelevanceData::Table(table) => table.schema == schema_or_alias,
319-
CompletionRelevanceData::Function(f) => f.schema == schema_or_alias,
320-
CompletionRelevanceData::Column(col) => ctx
321-
.get_mentioned_table_for_alias(&schema_or_alias)
322-
.is_some_and(|t| t == &col.table_name),
371+
CompletionRelevanceData::Table(table) => table.schema == tail_qualifier,
372+
CompletionRelevanceData::Function(f) => f.schema == tail_qualifier,
373+
CompletionRelevanceData::Column(col) => {
374+
let table = ctx
375+
.get_mentioned_table_for_alias(&tail_qualifier)
376+
.unwrap_or(&tail_qualifier);
377+
378+
col.table_name == table.as_str()
379+
&& ctx
380+
.head_qualifier_sanitized()
381+
.is_none_or(|schema| col.schema_name == schema.as_str())
382+
}
323383

324384
// we should never allow schema suggestions if there already was one.
325385
CompletionRelevanceData::Schema(_) => false,

0 commit comments

Comments
 (0)