Skip to content

Commit 20d44cc

Browse files
authored
ide: inlay hints for function calls (#769)
1 parent 7008e54 commit 20d44cc

File tree

5 files changed

+296
-14
lines changed

5 files changed

+296
-14
lines changed

β€ŽPLAN.mdβ€Ž

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,15 @@ select a, b from t
568568
group by a, b;
569569
```
570570

571+
### Rule: unresolved column
572+
573+
```sql
574+
create function foo(a int, b int) returns int
575+
as 'select $0'
576+
-- ^^ unresolved column, did you mean `a` or `b`?
577+
language sql;
578+
```
579+
571580
### Rule: unused column
572581

573582
```sql
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
use crate::binder;
2+
use crate::binder::Binder;
3+
use crate::resolve;
4+
use rowan::TextSize;
5+
use squawk_syntax::ast::{self, AstNode};
6+
7+
/// `VSCode` has some theming options based on these types.
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub enum InlayHintKind {
10+
Type,
11+
Parameter,
12+
}
13+
14+
#[derive(Debug, Clone, PartialEq, Eq)]
15+
pub struct InlayHint {
16+
pub position: TextSize,
17+
pub label: String,
18+
pub kind: InlayHintKind,
19+
}
20+
21+
pub fn inlay_hints(file: &ast::SourceFile) -> Vec<InlayHint> {
22+
let mut hints = vec![];
23+
let binder = binder::bind(file);
24+
25+
for node in file.syntax().descendants() {
26+
if let Some(call_expr) = ast::CallExpr::cast(node) {
27+
inlay_hint_call_expr(&mut hints, file, &binder, call_expr);
28+
}
29+
}
30+
31+
hints
32+
}
33+
34+
fn inlay_hint_call_expr(
35+
hints: &mut Vec<InlayHint>,
36+
file: &ast::SourceFile,
37+
binder: &Binder,
38+
call_expr: ast::CallExpr,
39+
) -> Option<()> {
40+
let arg_list = call_expr.arg_list()?;
41+
let expr = call_expr.expr()?;
42+
43+
let name_ref = if let Some(name_ref) = ast::NameRef::cast(expr.syntax().clone()) {
44+
name_ref
45+
} else {
46+
ast::FieldExpr::cast(expr.syntax().clone())?.field()?
47+
};
48+
49+
let function_ptr = resolve::resolve_name_ref(binder, &name_ref)?;
50+
51+
let root = file.syntax();
52+
let function_name_node = function_ptr.to_node(root);
53+
54+
if let Some(create_function) = function_name_node
55+
.ancestors()
56+
.find_map(ast::CreateFunction::cast)
57+
&& let Some(param_list) = create_function.param_list()
58+
{
59+
for (param, arg) in param_list.params().zip(arg_list.args()) {
60+
if let Some(param_name) = param.name() {
61+
let arg_start = arg.syntax().text_range().start();
62+
hints.push(InlayHint {
63+
position: arg_start,
64+
label: format!("{}: ", param_name.syntax().text()),
65+
kind: InlayHintKind::Parameter,
66+
});
67+
}
68+
}
69+
};
70+
71+
Some(())
72+
}
73+
74+
#[cfg(test)]
75+
mod test {
76+
use crate::inlay_hints::inlay_hints;
77+
use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
78+
use insta::assert_snapshot;
79+
use squawk_syntax::ast;
80+
81+
#[track_caller]
82+
fn check_inlay_hints(sql: &str) -> String {
83+
let parse = ast::SourceFile::parse(sql);
84+
assert_eq!(parse.errors(), vec![]);
85+
let file: ast::SourceFile = parse.tree();
86+
87+
let hints = inlay_hints(&file);
88+
89+
if hints.is_empty() {
90+
return String::new();
91+
}
92+
93+
let mut modified_sql = sql.to_string();
94+
let mut insertions: Vec<(usize, String)> = hints
95+
.iter()
96+
.map(|hint| {
97+
let offset: usize = hint.position.into();
98+
(offset, hint.label.clone())
99+
})
100+
.collect();
101+
102+
insertions.sort_by(|a, b| b.0.cmp(&a.0));
103+
104+
for (offset, label) in &insertions {
105+
modified_sql.insert_str(*offset, label);
106+
}
107+
108+
let mut annotations = vec![];
109+
let mut cumulative_offset = 0;
110+
111+
insertions.reverse();
112+
for (original_offset, label) in insertions {
113+
let new_offset = original_offset + cumulative_offset;
114+
annotations.push((new_offset, label.len()));
115+
cumulative_offset += label.len();
116+
}
117+
118+
let mut snippet = Snippet::source(&modified_sql).fold(true);
119+
120+
for (offset, len) in annotations {
121+
snippet = snippet.annotation(AnnotationKind::Context.span(offset..offset + len));
122+
}
123+
124+
let group = Level::INFO.primary_title("inlay hints").element(snippet);
125+
126+
let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
127+
renderer
128+
.render(&[group])
129+
.to_string()
130+
.replace("info: inlay hints", "inlay hints:")
131+
}
132+
133+
#[test]
134+
fn single_param() {
135+
assert_snapshot!(check_inlay_hints("
136+
create function foo(a int) returns int as 'select $$1' language sql;
137+
select foo(1);
138+
"), @r"
139+
inlay hints:
140+
β•­β–Έ
141+
3 β”‚ select foo(a: 1);
142+
β•°β•΄ ───
143+
");
144+
}
145+
146+
#[test]
147+
fn multiple_params() {
148+
assert_snapshot!(check_inlay_hints("
149+
create function add(a int, b int) returns int as 'select $$1 + $$2' language sql;
150+
select add(1, 2);
151+
"), @r"
152+
inlay hints:
153+
β•­β–Έ
154+
3 β”‚ select add(a: 1, b: 2);
155+
β•°β•΄ ─── ───
156+
");
157+
}
158+
159+
#[test]
160+
fn no_params() {
161+
assert_snapshot!(check_inlay_hints("
162+
create function foo() returns int as 'select 1' language sql;
163+
select foo();
164+
"), @"");
165+
}
166+
167+
#[test]
168+
fn with_schema() {
169+
assert_snapshot!(check_inlay_hints("
170+
create function public.foo(x int) returns int as 'select $$1' language sql;
171+
select public.foo(42);
172+
"), @r"
173+
inlay hints:
174+
β•­β–Έ
175+
3 β”‚ select public.foo(x: 42);
176+
β•°β•΄ ───
177+
");
178+
}
179+
180+
#[test]
181+
fn with_search_path() {
182+
assert_snapshot!(check_inlay_hints(r#"
183+
set search_path to myschema;
184+
create function foo(val int) returns int as 'select $$1' language sql;
185+
select foo(100);
186+
"#), @r"
187+
inlay hints:
188+
β•­β–Έ
189+
4 β”‚ select foo(val: 100);
190+
β•°β•΄ ─────
191+
");
192+
}
193+
194+
#[test]
195+
fn multiple_calls() {
196+
assert_snapshot!(check_inlay_hints("
197+
create function inc(n int) returns int as 'select $$1 + 1' language sql;
198+
select inc(1), inc(2);
199+
"), @r"
200+
inlay hints:
201+
β•­β–Έ
202+
3 β”‚ select inc(n: 1), inc(n: 2);
203+
β•°β•΄ ─── ───
204+
");
205+
}
206+
207+
#[test]
208+
fn more_args_than_params() {
209+
assert_snapshot!(check_inlay_hints("
210+
create function foo(a int) returns int as 'select $$1' language sql;
211+
select foo(1, 2);
212+
"), @r"
213+
inlay hints:
214+
β•­β–Έ
215+
3 β”‚ select foo(a: 1, 2);
216+
β•°β•΄ ───
217+
");
218+
}
219+
}

β€Žcrates/squawk_ide/src/lib.rsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod find_references;
66
mod generated;
77
pub mod goto_definition;
88
pub mod hover;
9+
pub mod inlay_hints;
910
mod offsets;
1011
mod resolve;
1112
mod scope;

0 commit comments

Comments
Β (0)