1use crate::collect;
2use crate::db::{File, parse};
3use crate::goto_definition;
4use crate::resolve;
5use crate::symbols::Name;
6use rowan::{TextRange, TextSize};
7use salsa::Database as Db;
8use squawk_syntax::ast::{self, AstNode};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InlayHintKind {
13 Type,
14 Parameter,
15}
16
17#[derive(Clone, PartialEq, Eq)]
18pub struct InlayHint {
19 pub position: TextSize,
20 pub label: String,
21 pub kind: InlayHintKind,
22 pub target: Option<TextRange>,
26 pub file: Option<File>,
28}
29
30#[salsa::tracked]
31pub fn inlay_hints(db: &dyn Db, file: File) -> Vec<InlayHint> {
32 let mut hints = vec![];
33 for node in parse(db, file).tree().syntax().descendants() {
34 if let Some(call_expr) = ast::CallExpr::cast(node.clone()) {
35 inlay_hint_call_expr(db, &mut hints, file, call_expr);
36 } else if let Some(insert) = ast::Insert::cast(node) {
37 inlay_hint_insert(db, &mut hints, file, insert);
38 }
39 }
40 hints
41}
42
43fn inlay_hint_call_expr(
44 db: &dyn Db,
45 hints: &mut Vec<InlayHint>,
46 file_id: File,
47 call_expr: ast::CallExpr,
48) -> Option<()> {
49 let arg_list = call_expr.arg_list()?;
50 let expr = call_expr.expr()?;
51
52 let name_ref = if let Some(name_ref) = ast::NameRef::cast(expr.syntax().clone()) {
53 name_ref
54 } else {
55 ast::FieldExpr::cast(expr.syntax().clone())?.field()?
56 };
57
58 let location =
59 goto_definition::goto_definition(db, file_id, name_ref.syntax().text_range().start())
60 .into_iter()
61 .next()?;
62
63 let def_file = parse(db, location.file).tree();
64
65 let function_name_node = def_file.syntax().covering_element(location.range);
66
67 if let Some(create_function) = function_name_node
68 .ancestors()
69 .find_map(ast::CreateFunction::cast)
70 && let Some(param_list) = create_function.param_list()
71 {
72 for (param, arg) in param_list.params().zip(arg_list.args()) {
73 if let Some(param_name) = param.name() {
74 let arg_start = arg.syntax().text_range().start();
75 let target = Some(param_name.syntax().text_range());
76 hints.push(InlayHint {
77 position: arg_start,
78 label: format!("{}: ", param_name.syntax().text()),
79 kind: InlayHintKind::Parameter,
80 target,
81 file: Some(location.file),
82 });
83 }
84 }
85 };
86
87 Some(())
88}
89
90fn inlay_hint_insert(
91 db: &dyn Db,
92 hints: &mut Vec<InlayHint>,
93 file_id: File,
94 insert: ast::Insert,
95) -> Option<()> {
96 let name_start = insert
97 .path()?
98 .segment()?
99 .name_ref()?
100 .syntax()
101 .text_range()
102 .start();
103 let location = goto_definition::goto_definition(db, file_id, name_start)
106 .into_iter()
107 .next();
108
109 let def_file = location.as_ref().map(|loc| loc.file).unwrap_or(file_id);
110 let def_tree = parse(db, def_file).tree();
111
112 let create_table = location.as_ref().and_then(|loc| {
113 def_tree
114 .syntax()
115 .covering_element(loc.range)
116 .ancestors()
117 .find_map(ast::CreateTableLike::cast)
118 });
119
120 let columns = if let Some(column_list) = insert.column_list() {
121 column_list
123 .columns()
124 .filter_map(|col| {
125 let col_name = col.name_ref().map(|x| Name::from_node(&x))?;
126 let target = create_table
127 .as_ref()
128 .and_then(|x| resolve::find_column_in_create_table(db, def_file, x, &col_name))
129 .and_then(|x| x.into_iter().next())
130 .map(|x| x.range);
131 Some((col_name, target, location.as_ref().map(|x| x.file)))
132 })
133 .collect()
134 } else {
135 collect::columns_from_create_table(db, def_file, &create_table?)
137 .into_iter()
138 .map(|(col_name, ptr)| {
139 let target = ptr.map(|p| p.text_range());
140 (col_name, target, location.as_ref().map(|x| x.file))
141 })
142 .collect()
143 };
144
145 let Some(values) = insert.values() else {
146 return inlay_hint_insert_select(hints, columns, insert.stmt()?);
148 };
149 for row in values.row_list()?.rows() {
151 for ((column_name, target, file_id), expr) in columns.iter().zip(row.exprs()) {
152 let expr_start = expr.syntax().text_range().start();
153 hints.push(InlayHint {
154 position: expr_start,
155 label: format!("{}: ", column_name),
156 kind: InlayHintKind::Parameter,
157 target: *target,
158 file: *file_id,
159 });
160 }
161 }
162
163 Some(())
164}
165
166fn inlay_hint_insert_select(
167 hints: &mut Vec<InlayHint>,
168 columns: Vec<(Name, Option<TextRange>, Option<File>)>,
169 stmt: ast::Stmt,
170) -> Option<()> {
171 let target_list = match stmt {
172 ast::Stmt::Select(select) => select.select_clause()?.target_list(),
173 ast::Stmt::SelectInto(select_into) => select_into.select_clause()?.target_list(),
174 ast::Stmt::ParenSelect(paren_select) => paren_select.select()?.target_list(),
175 _ => None,
176 }?;
177
178 for ((column_name, target, file_id), target_expr) in columns.iter().zip(target_list.targets()) {
179 let expr = target_expr.expr()?;
180 let expr_start = expr.syntax().text_range().start();
181 hints.push(InlayHint {
182 position: expr_start,
183 label: format!("{}: ", column_name),
184 kind: InlayHintKind::Parameter,
185 target: *target,
186 file: *file_id,
187 });
188 }
189
190 Some(())
191}
192
193#[cfg(test)]
194mod test {
195 use crate::db::{Database, File};
196 use crate::inlay_hints::inlay_hints;
197 use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
198 use insta::assert_snapshot;
199
200 #[track_caller]
201 fn check_inlay_hints(sql: &str) -> String {
202 let db = Database::default();
203 let file = File::new(&db, sql.to_string().into());
204
205 assert_eq!(crate::db::parse(&db, file).errors(), vec![]);
206
207 let hints = inlay_hints(&db, file);
208
209 if hints.is_empty() {
210 return String::new();
211 }
212
213 let mut modified_sql = sql.to_string();
214 let mut insertions: Vec<(usize, String)> = hints
215 .iter()
216 .map(|hint| {
217 let offset: usize = hint.position.into();
218 (offset, hint.label.clone())
219 })
220 .collect();
221
222 insertions.sort_by(|a, b| b.0.cmp(&a.0));
223
224 for (offset, label) in &insertions {
225 modified_sql.insert_str(*offset, label);
226 }
227
228 let mut annotations = vec![];
229 let mut cumulative_offset = 0;
230
231 insertions.reverse();
232 for (original_offset, label) in insertions {
233 let new_offset = original_offset + cumulative_offset;
234 annotations.push((new_offset, label.len()));
235 cumulative_offset += label.len();
236 }
237
238 let mut snippet = Snippet::source(&modified_sql).fold(true);
239
240 for (offset, len) in annotations {
241 snippet = snippet.annotation(AnnotationKind::Context.span(offset..offset + len));
242 }
243
244 let group = Level::INFO.primary_title("inlay hints").element(snippet);
245
246 let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
247 renderer
248 .render(&[group])
249 .to_string()
250 .replace("info: inlay hints", "inlay hints:")
251 }
252
253 #[test]
254 fn single_param() {
255 assert_snapshot!(check_inlay_hints("
256create function foo(a int) returns int as 'select $$1' language sql;
257select foo(1);
258"), @r"
259 inlay hints:
260 ╭▸
261 3 │ select foo(a: 1);
262 ╰╴ ───
263 ");
264 }
265
266 #[test]
267 fn multiple_params() {
268 assert_snapshot!(check_inlay_hints("
269create function add(a int, b int) returns int as 'select $$1 + $$2' language sql;
270select add(1, 2);
271"), @r"
272 inlay hints:
273 ╭▸
274 3 │ select add(a: 1, b: 2);
275 ╰╴ ─── ───
276 ");
277 }
278
279 #[test]
280 fn no_params() {
281 assert_snapshot!(check_inlay_hints("
282create function foo() returns int as 'select 1' language sql;
283select foo();
284"), @"");
285 }
286
287 #[test]
288 fn with_schema() {
289 assert_snapshot!(check_inlay_hints("
290create function public.foo(x int) returns int as 'select $$1' language sql;
291select public.foo(42);
292"), @r"
293 inlay hints:
294 ╭▸
295 3 │ select public.foo(x: 42);
296 ╰╴ ───
297 ");
298 }
299
300 #[test]
301 fn with_search_path() {
302 assert_snapshot!(check_inlay_hints(r#"
303set search_path to myschema;
304create function foo(val int) returns int as 'select $$1' language sql;
305select foo(100);
306"#), @r"
307 inlay hints:
308 ╭▸
309 4 │ select foo(val: 100);
310 ╰╴ ─────
311 ");
312 }
313
314 #[test]
315 fn multiple_calls() {
316 assert_snapshot!(check_inlay_hints("
317create function inc(n int) returns int as 'select $$1 + 1' language sql;
318select inc(1), inc(2);
319"), @r"
320 inlay hints:
321 ╭▸
322 3 │ select inc(n: 1), inc(n: 2);
323 ╰╴ ─── ───
324 ");
325 }
326
327 #[test]
328 fn more_args_than_params() {
329 assert_snapshot!(check_inlay_hints("
330create function foo(a int) returns int as 'select $$1' language sql;
331select foo(1, 2);
332"), @r"
333 inlay hints:
334 ╭▸
335 3 │ select foo(a: 1, 2);
336 ╰╴ ───
337 ");
338 }
339
340 #[test]
341 fn builtin_function() {
342 assert_snapshot!(check_inlay_hints("
343select json_strip_nulls('[1, null]', true);
344"), @r"
345 inlay hints:
346 ╭▸
347 2 │ select json_strip_nulls(target: '[1, null]', strip_in_arrays: true);
348 ╰╴ ──────── ─────────────────
349 ");
350 }
351
352 #[test]
353 fn insert_with_column_list() {
354 assert_snapshot!(check_inlay_hints("
355create table t (column_a int, column_b int, column_c text);
356insert into t (column_a, column_c) values (1, 'foo');
357"), @r"
358 inlay hints:
359 ╭▸
360 3 │ insert into t (column_a, column_c) values (column_a: 1, column_c: 'foo');
361 ╰╴ ────────── ──────────
362 ");
363 }
364
365 #[test]
366 fn insert_without_column_list() {
367 assert_snapshot!(check_inlay_hints("
368create table t (column_a int, column_b int, column_c text);
369insert into t values (1, 2, 'foo');
370"), @r"
371 inlay hints:
372 ╭▸
373 3 │ insert into t values (column_a: 1, column_b: 2, column_c: 'foo');
374 ╰╴ ────────── ────────── ──────────
375 ");
376 }
377
378 #[test]
379 fn insert_multiple_rows() {
380 assert_snapshot!(check_inlay_hints("
381create table t (x int, y int);
382insert into t values (1, 2), (3, 4);
383"), @r"
384 inlay hints:
385 ╭▸
386 3 │ insert into t values (x: 1, y: 2), (x: 3, y: 4);
387 ╰╴ ─── ─── ─── ───
388 ");
389 }
390
391 #[test]
392 fn insert_no_create_table() {
393 assert_snapshot!(check_inlay_hints("
394insert into t (a, b) values (1, 2);
395"), @r"
396 inlay hints:
397 ╭▸
398 2 │ insert into t (a, b) values (a: 1, b: 2);
399 ╰╴ ─── ───
400 ");
401 }
402
403 #[test]
404 fn insert_more_values_than_columns() {
405 assert_snapshot!(check_inlay_hints("
406create table t (a int, b int);
407insert into t values (1, 2, 3);
408"), @r"
409 inlay hints:
410 ╭▸
411 3 │ insert into t values (a: 1, b: 2, 3);
412 ╰╴ ─── ───
413 ");
414 }
415
416 #[test]
417 fn insert_table_inherits_select() {
418 assert_snapshot!(check_inlay_hints("
419create table t (a int, b int);
420create table u (c int) inherits (t);
421insert into u select 1, 2, 3;
422"), @"
423 inlay hints:
424 ╭▸
425 4 │ insert into u select a: 1, b: 2, c: 3;
426 ╰╴ ─── ─── ───
427 ");
428 }
429
430 #[test]
431 fn insert_table_like_select() {
432 assert_snapshot!(check_inlay_hints("
433create table x (a int, b int);
434create table y (c int, like x);
435insert into y select 1, 2, 3;
436"), @r"
437 inlay hints:
438 ╭▸
439 4 │ insert into y select c: 1, a: 2, b: 3;
440 ╰╴ ─── ─── ───
441 ");
442 }
443
444 #[test]
445 fn insert_select() {
446 assert_snapshot!(check_inlay_hints("
447create table t (a int, b int);
448insert into t select 1, 2;
449"), @r"
450 inlay hints:
451 ╭▸
452 3 │ insert into t select a: 1, b: 2;
453 ╰╴ ─── ───
454 ");
455 }
456}