Skip to content

Commit 7b27cdc

Browse files
fix(completions): improved completion in delete/update clauses (#371)
* ok * fixie fixie * Update crates/pgt_completions/src/relevance/filtering.rs * set to workspace cargo.toml * OCD * seems valid * just some help * fixies * hope this works * ok * takin shape * ok * why * format too * this
1 parent 2820bb5 commit 7b27cdc

File tree

8 files changed

+323
-59
lines changed

8 files changed

+323
-59
lines changed

crates/pgt_cli/src/execute/mod.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,11 @@ pub enum TraversalMode {
7676
Dummy,
7777
/// This mode is enabled when running the command `check`
7878
Check {
79-
/// The type of fixes that should be applied when analyzing a file.
80-
///
81-
/// It's [None] if the `check` command is called without `--apply` or `--apply-suggested`
82-
/// arguments.
79+
// The type of fixes that should be applied when analyzing a file.
80+
//
81+
// It's [None] if the `check` command is called without `--apply` or `--apply-suggested`
82+
// arguments.
8383
// fix_file_mode: Option<FixFileMode>,
84-
8584
/// An optional tuple.
8685
/// 1. The virtual path to the file
8786
/// 2. The content of the file

crates/pgt_completions/src/context.rs

+71-16
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl TryFrom<&str> for ClauseType {
3030
match value {
3131
"select" => Ok(Self::Select),
3232
"where" => Ok(Self::Where),
33-
"from" | "keyword_from" => Ok(Self::From),
33+
"from" => Ok(Self::From),
3434
"update" => Ok(Self::Update),
3535
"delete" => Ok(Self::Delete),
3636
_ => {
@@ -49,8 +49,52 @@ impl TryFrom<&str> for ClauseType {
4949

5050
impl TryFrom<String> for ClauseType {
5151
type Error = String;
52-
fn try_from(value: String) -> Result<ClauseType, Self::Error> {
53-
ClauseType::try_from(value.as_str())
52+
fn try_from(value: String) -> Result<Self, Self::Error> {
53+
Self::try_from(value.as_str())
54+
}
55+
}
56+
57+
/// We can map a few nodes, such as the "update" node, to actual SQL clauses.
58+
/// That gives us a lot of insight for completions.
59+
/// Other nodes, such as the "relation" node, gives us less but still
60+
/// relevant information.
61+
/// `WrappingNode` maps to such nodes.
62+
///
63+
/// Note: This is not the direct parent of the `node_under_cursor`, but the closest
64+
/// *relevant* parent.
65+
#[derive(Debug, PartialEq, Eq)]
66+
pub enum WrappingNode {
67+
Relation,
68+
BinaryExpression,
69+
Assignment,
70+
}
71+
72+
impl TryFrom<&str> for WrappingNode {
73+
type Error = String;
74+
75+
fn try_from(value: &str) -> Result<Self, Self::Error> {
76+
match value {
77+
"relation" => Ok(Self::Relation),
78+
"assignment" => Ok(Self::Assignment),
79+
"binary_expression" => Ok(Self::BinaryExpression),
80+
_ => {
81+
let message = format!("Unimplemented Relation: {}", value);
82+
83+
// Err on tests, so we notice that we're lacking an implementation immediately.
84+
if cfg!(test) {
85+
panic!("{}", message);
86+
}
87+
88+
Err(message)
89+
}
90+
}
91+
}
92+
}
93+
94+
impl TryFrom<String> for WrappingNode {
95+
type Error = String;
96+
fn try_from(value: String) -> Result<Self, Self::Error> {
97+
Self::try_from(value.as_str())
5498
}
5599
}
56100

@@ -64,6 +108,9 @@ pub(crate) struct CompletionContext<'a> {
64108

65109
pub schema_name: Option<String>,
66110
pub wrapping_clause_type: Option<ClauseType>,
111+
112+
pub wrapping_node_kind: Option<WrappingNode>,
113+
67114
pub is_invocation: bool,
68115
pub wrapping_statement_range: Option<tree_sitter::Range>,
69116

@@ -80,6 +127,7 @@ impl<'a> CompletionContext<'a> {
80127
node_under_cursor: None,
81128
schema_name: None,
82129
wrapping_clause_type: None,
130+
wrapping_node_kind: None,
83131
wrapping_statement_range: None,
84132
is_invocation: false,
85133
mentioned_relations: HashMap::new(),
@@ -133,6 +181,15 @@ impl<'a> CompletionContext<'a> {
133181
})
134182
}
135183

184+
pub fn get_node_under_cursor_content(&self) -> Option<String> {
185+
self.node_under_cursor
186+
.and_then(|n| self.get_ts_node_content(n))
187+
.and_then(|txt| match txt {
188+
NodeText::Replaced => None,
189+
NodeText::Original(c) => Some(c.to_string()),
190+
})
191+
}
192+
136193
fn gather_tree_context(&mut self) {
137194
let mut cursor = self.tree.root_node().walk();
138195

@@ -163,23 +220,26 @@ impl<'a> CompletionContext<'a> {
163220
) {
164221
let current_node = cursor.node();
165222

223+
let parent_node_kind = parent_node.kind();
224+
let current_node_kind = current_node.kind();
225+
166226
// prevent infinite recursion – this can happen if we only have a PROGRAM node
167-
if current_node.kind() == parent_node.kind() {
227+
if current_node_kind == parent_node_kind {
168228
self.node_under_cursor = Some(current_node);
169229
return;
170230
}
171231

172-
match parent_node.kind() {
232+
match parent_node_kind {
173233
"statement" | "subquery" => {
174-
self.wrapping_clause_type = current_node.kind().try_into().ok();
234+
self.wrapping_clause_type = current_node_kind.try_into().ok();
175235
self.wrapping_statement_range = Some(parent_node.range());
176236
}
177237
"invocation" => self.is_invocation = true,
178238

179239
_ => {}
180240
}
181241

182-
match current_node.kind() {
242+
match current_node_kind {
183243
"object_reference" => {
184244
let content = self.get_ts_node_content(current_node);
185245
if let Some(node_txt) = content {
@@ -195,13 +255,12 @@ impl<'a> CompletionContext<'a> {
195255
}
196256
}
197257

198-
// in Treesitter, the Where clause is nested inside other clauses
199-
"where" => {
200-
self.wrapping_clause_type = "where".try_into().ok();
258+
"where" | "update" | "select" | "delete" | "from" => {
259+
self.wrapping_clause_type = current_node_kind.try_into().ok();
201260
}
202261

203-
"keyword_from" => {
204-
self.wrapping_clause_type = "keyword_from".try_into().ok();
262+
"relation" | "binary_expression" | "assignment" => {
263+
self.wrapping_node_kind = current_node_kind.try_into().ok();
205264
}
206265

207266
_ => {}
@@ -406,10 +465,6 @@ mod tests {
406465
ctx.get_ts_node_content(node),
407466
Some(NodeText::Original("from"))
408467
);
409-
assert_eq!(
410-
ctx.wrapping_clause_type,
411-
Some(crate::context::ClauseType::From)
412-
);
413468
}
414469

415470
#[test]

crates/pgt_completions/src/providers/schemas.rs

+52-20
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ pub fn complete_schemas<'a>(ctx: &'a CompletionContext, builder: &mut Completion
2727
mod tests {
2828

2929
use crate::{
30-
CompletionItemKind, complete,
31-
test_helper::{CURSOR_POS, get_test_deps, get_test_params},
30+
CompletionItemKind,
31+
test_helper::{CURSOR_POS, CompletionAssertion, assert_complete_results},
3232
};
3333

3434
#[tokio::test]
@@ -46,27 +46,59 @@ mod tests {
4646
);
4747
"#;
4848

49-
let query = format!("select * from {}", CURSOR_POS);
49+
assert_complete_results(
50+
format!("select * from {}", CURSOR_POS).as_str(),
51+
vec![
52+
CompletionAssertion::LabelAndKind("public".to_string(), CompletionItemKind::Schema),
53+
CompletionAssertion::LabelAndKind("auth".to_string(), CompletionItemKind::Schema),
54+
CompletionAssertion::LabelAndKind(
55+
"internal".to_string(),
56+
CompletionItemKind::Schema,
57+
),
58+
CompletionAssertion::LabelAndKind(
59+
"private".to_string(),
60+
CompletionItemKind::Schema,
61+
),
62+
CompletionAssertion::LabelAndKind(
63+
"information_schema".to_string(),
64+
CompletionItemKind::Schema,
65+
),
66+
CompletionAssertion::LabelAndKind(
67+
"pg_catalog".to_string(),
68+
CompletionItemKind::Schema,
69+
),
70+
CompletionAssertion::LabelAndKind(
71+
"pg_toast".to_string(),
72+
CompletionItemKind::Schema,
73+
),
74+
CompletionAssertion::LabelAndKind("users".to_string(), CompletionItemKind::Table),
75+
],
76+
setup,
77+
)
78+
.await;
79+
}
5080

51-
let (tree, cache) = get_test_deps(setup, query.as_str().into()).await;
52-
let params = get_test_params(&tree, &cache, query.as_str().into());
53-
let items = complete(params);
81+
#[tokio::test]
82+
async fn suggests_tables_and_schemas_with_matching_keys() {
83+
let setup = r#"
84+
create schema ultimate;
5485
55-
assert!(!items.is_empty());
86+
-- add a table to compete against schemas
87+
create table users (
88+
id serial primary key,
89+
name text,
90+
password text
91+
);
92+
"#;
5693

57-
assert_eq!(
58-
items
59-
.into_iter()
60-
.take(5)
61-
.map(|i| (i.label, i.kind))
62-
.collect::<Vec<(String, CompletionItemKind)>>(),
94+
assert_complete_results(
95+
format!("select * from u{}", CURSOR_POS).as_str(),
6396
vec![
64-
("public".to_string(), CompletionItemKind::Schema),
65-
("auth".to_string(), CompletionItemKind::Schema),
66-
("internal".to_string(), CompletionItemKind::Schema),
67-
("private".to_string(), CompletionItemKind::Schema),
68-
("users".to_string(), CompletionItemKind::Table),
69-
]
70-
);
97+
CompletionAssertion::LabelAndKind("users".into(), CompletionItemKind::Table),
98+
CompletionAssertion::LabelAndKind("ultimate".into(), CompletionItemKind::Schema),
99+
],
100+
setup,
101+
)
102+
.await;
71103
}
72104
}

crates/pgt_completions/src/providers/tables.rs

+96-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ mod tests {
3131

3232
use crate::{
3333
CompletionItem, CompletionItemKind, complete,
34-
test_helper::{CURSOR_POS, get_test_deps, get_test_params},
34+
test_helper::{
35+
CURSOR_POS, CompletionAssertion, assert_complete_results, assert_no_complete_results,
36+
get_test_deps, get_test_params,
37+
},
3538
};
3639

3740
#[tokio::test]
@@ -178,4 +181,96 @@ mod tests {
178181
assert_eq!(label, "coos");
179182
assert_eq!(kind, CompletionItemKind::Table);
180183
}
184+
185+
#[tokio::test]
186+
async fn suggests_tables_in_update() {
187+
let setup = r#"
188+
create table coos (
189+
id serial primary key,
190+
name text
191+
);
192+
"#;
193+
194+
assert_complete_results(
195+
format!("update {}", CURSOR_POS).as_str(),
196+
vec![CompletionAssertion::LabelAndKind(
197+
"public".into(),
198+
CompletionItemKind::Schema,
199+
)],
200+
setup,
201+
)
202+
.await;
203+
204+
assert_complete_results(
205+
format!("update public.{}", CURSOR_POS).as_str(),
206+
vec![CompletionAssertion::LabelAndKind(
207+
"coos".into(),
208+
CompletionItemKind::Table,
209+
)],
210+
setup,
211+
)
212+
.await;
213+
214+
assert_no_complete_results(format!("update public.coos {}", CURSOR_POS).as_str(), setup)
215+
.await;
216+
217+
assert_complete_results(
218+
format!("update coos set {}", CURSOR_POS).as_str(),
219+
vec![
220+
CompletionAssertion::Label("id".into()),
221+
CompletionAssertion::Label("name".into()),
222+
],
223+
setup,
224+
)
225+
.await;
226+
227+
assert_complete_results(
228+
format!("update coos set name = 'cool' where {}", CURSOR_POS).as_str(),
229+
vec![
230+
CompletionAssertion::Label("id".into()),
231+
CompletionAssertion::Label("name".into()),
232+
],
233+
setup,
234+
)
235+
.await;
236+
}
237+
238+
#[tokio::test]
239+
async fn suggests_tables_in_delete() {
240+
let setup = r#"
241+
create table coos (
242+
id serial primary key,
243+
name text
244+
);
245+
"#;
246+
247+
assert_no_complete_results(format!("delete {}", CURSOR_POS).as_str(), setup).await;
248+
249+
assert_complete_results(
250+
format!("delete from {}", CURSOR_POS).as_str(),
251+
vec![
252+
CompletionAssertion::LabelAndKind("public".into(), CompletionItemKind::Schema),
253+
CompletionAssertion::LabelAndKind("coos".into(), CompletionItemKind::Table),
254+
],
255+
setup,
256+
)
257+
.await;
258+
259+
assert_complete_results(
260+
format!("delete from public.{}", CURSOR_POS).as_str(),
261+
vec![CompletionAssertion::Label("coos".into())],
262+
setup,
263+
)
264+
.await;
265+
266+
assert_complete_results(
267+
format!("delete from public.coos where {}", CURSOR_POS).as_str(),
268+
vec![
269+
CompletionAssertion::Label("id".into()),
270+
CompletionAssertion::Label("name".into()),
271+
],
272+
setup,
273+
)
274+
.await;
275+
}
181276
}

crates/pgt_completions/src/relevance/filtering.rs

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ impl CompletionFilter<'_> {
3535
return None;
3636
}
3737

38+
// No autocompletions if there are two identifiers without a separator.
39+
if ctx.node_under_cursor.is_some_and(|n| {
40+
n.prev_sibling().is_some_and(|p| {
41+
(p.kind() == "identifier" || p.kind() == "object_reference")
42+
&& n.kind() == "identifier"
43+
})
44+
}) {
45+
return None;
46+
}
47+
3848
Some(())
3949
}
4050

0 commit comments

Comments
 (0)