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 inlay_hint_insert_select(hints, columns, insert.select_variant()?)
146}
147
148fn inlay_hint_insert_select(
149 hints: &mut Vec<InlayHint>,
150 columns: Vec<(Name, Option<TextRange>, Option<File>)>,
151 select_variant: ast::SelectVariant,
152) -> Option<()> {
153 if let ast::SelectVariant::Values(values) = &select_variant {
154 for row in values.row_list()?.rows() {
156 for ((column_name, target, file_id), expr) in columns.iter().zip(row.exprs()) {
157 let expr_start = expr.syntax().text_range().start();
158 hints.push(InlayHint {
159 position: expr_start,
160 label: format!("{}: ", column_name),
161 kind: InlayHintKind::Parameter,
162 target: *target,
163 file: *file_id,
164 });
165 }
166 }
167 return Some(());
168 }
169
170 let target_list = select_variant.target_list()?;
172 for ((column_name, target, file_id), target_expr) in columns.iter().zip(target_list.targets()) {
173 let expr = target_expr.expr()?;
174 let expr_start = expr.syntax().text_range().start();
175 hints.push(InlayHint {
176 position: expr_start,
177 label: format!("{}: ", column_name),
178 kind: InlayHintKind::Parameter,
179 target: *target,
180 file: *file_id,
181 });
182 }
183
184 Some(())
185}
186
187#[cfg(test)]
188mod test {
189 use crate::db::{Database, File};
190 use crate::inlay_hints::inlay_hints;
191 use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
192 use insta::assert_snapshot;
193
194 #[must_use]
195 #[track_caller]
196 fn check_inlay_hints(sql: &str) -> String {
197 let db = Database::default();
198 let file = File::new(&db, sql.to_string().into());
199
200 assert_eq!(crate::db::parse(&db, file).errors(), vec![]);
201
202 let hints = inlay_hints(&db, file);
203
204 if hints.is_empty() {
205 return String::new();
206 }
207
208 let mut modified_sql = sql.to_string();
209 let mut insertions: Vec<(usize, String)> = hints
210 .iter()
211 .map(|hint| {
212 let offset: usize = hint.position.into();
213 (offset, hint.label.clone())
214 })
215 .collect();
216
217 insertions.sort_by(|a, b| b.0.cmp(&a.0));
218
219 for (offset, label) in &insertions {
220 modified_sql.insert_str(*offset, label);
221 }
222
223 let mut annotations = vec![];
224 let mut cumulative_offset = 0;
225
226 insertions.reverse();
227 for (original_offset, label) in insertions {
228 let new_offset = original_offset + cumulative_offset;
229 annotations.push((new_offset, label.len()));
230 cumulative_offset += label.len();
231 }
232
233 let mut snippet = Snippet::source(&modified_sql).fold(true);
234
235 for (offset, len) in annotations {
236 snippet = snippet.annotation(AnnotationKind::Context.span(offset..offset + len));
237 }
238
239 let group = Level::INFO.primary_title("inlay hints").element(snippet);
240
241 let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
242 renderer
243 .render(&[group])
244 .to_string()
245 .replace("info: inlay hints", "inlay hints:")
246 }
247
248 #[test]
249 fn single_param() {
250 assert_snapshot!(check_inlay_hints("
251create function foo(a int) returns int as 'select $$1' language sql;
252select foo(1);
253"), @r"
254 inlay hints:
255 ╭▸
256 3 │ select foo(a: 1);
257 ╰╴ ───
258 ");
259 }
260
261 #[test]
262 fn multiple_params() {
263 assert_snapshot!(check_inlay_hints("
264create function add(a int, b int) returns int as 'select $$1 + $$2' language sql;
265select add(1, 2);
266"), @r"
267 inlay hints:
268 ╭▸
269 3 │ select add(a: 1, b: 2);
270 ╰╴ ─── ───
271 ");
272 }
273
274 #[test]
275 fn no_params() {
276 assert_snapshot!(check_inlay_hints("
277create function foo() returns int as 'select 1' language sql;
278select foo();
279"), @"");
280 }
281
282 #[test]
283 fn with_schema() {
284 assert_snapshot!(check_inlay_hints("
285create function public.foo(x int) returns int as 'select $$1' language sql;
286select public.foo(42);
287"), @r"
288 inlay hints:
289 ╭▸
290 3 │ select public.foo(x: 42);
291 ╰╴ ───
292 ");
293 }
294
295 #[test]
296 fn with_search_path() {
297 assert_snapshot!(check_inlay_hints(r#"
298set search_path to myschema;
299create function foo(val int) returns int as 'select $$1' language sql;
300select foo(100);
301"#), @r"
302 inlay hints:
303 ╭▸
304 4 │ select foo(val: 100);
305 ╰╴ ─────
306 ");
307 }
308
309 #[test]
310 fn multiple_calls() {
311 assert_snapshot!(check_inlay_hints("
312create function inc(n int) returns int as 'select $$1 + 1' language sql;
313select inc(1), inc(2);
314"), @r"
315 inlay hints:
316 ╭▸
317 3 │ select inc(n: 1), inc(n: 2);
318 ╰╴ ─── ───
319 ");
320 }
321
322 #[test]
323 fn more_args_than_params() {
324 assert_snapshot!(check_inlay_hints("
325create function foo(a int) returns int as 'select $$1' language sql;
326select foo(1, 2);
327"), @r"
328 inlay hints:
329 ╭▸
330 3 │ select foo(a: 1, 2);
331 ╰╴ ───
332 ");
333 }
334
335 #[test]
336 fn builtin_function() {
337 assert_snapshot!(check_inlay_hints("
338select json_strip_nulls('[1, null]', true);
339"), @r"
340 inlay hints:
341 ╭▸
342 2 │ select json_strip_nulls(target: '[1, null]', strip_in_arrays: true);
343 ╰╴ ──────── ─────────────────
344 ");
345 }
346
347 #[test]
348 fn insert_with_column_list() {
349 assert_snapshot!(check_inlay_hints("
350create table t (column_a int, column_b int, column_c text);
351insert into t (column_a, column_c) values (1, 'foo');
352"), @r"
353 inlay hints:
354 ╭▸
355 3 │ insert into t (column_a, column_c) values (column_a: 1, column_c: 'foo');
356 ╰╴ ────────── ──────────
357 ");
358 }
359
360 #[test]
361 fn insert_without_column_list() {
362 assert_snapshot!(check_inlay_hints("
363create table t (column_a int, column_b int, column_c text);
364insert into t values (1, 2, 'foo');
365"), @r"
366 inlay hints:
367 ╭▸
368 3 │ insert into t values (column_a: 1, column_b: 2, column_c: 'foo');
369 ╰╴ ────────── ────────── ──────────
370 ");
371 }
372
373 #[test]
374 fn insert_multiple_rows() {
375 assert_snapshot!(check_inlay_hints("
376create table t (x int, y int);
377insert into t values (1, 2), (3, 4);
378"), @r"
379 inlay hints:
380 ╭▸
381 3 │ insert into t values (x: 1, y: 2), (x: 3, y: 4);
382 ╰╴ ─── ─── ─── ───
383 ");
384 }
385
386 #[test]
387 fn insert_no_create_table() {
388 assert_snapshot!(check_inlay_hints("
389insert into t (a, b) values (1, 2);
390"), @r"
391 inlay hints:
392 ╭▸
393 2 │ insert into t (a, b) values (a: 1, b: 2);
394 ╰╴ ─── ───
395 ");
396 }
397
398 #[test]
399 fn insert_more_values_than_columns() {
400 assert_snapshot!(check_inlay_hints("
401create table t (a int, b int);
402insert into t values (1, 2, 3);
403"), @r"
404 inlay hints:
405 ╭▸
406 3 │ insert into t values (a: 1, b: 2, 3);
407 ╰╴ ─── ───
408 ");
409 }
410
411 #[test]
412 fn insert_table_inherits_select() {
413 assert_snapshot!(check_inlay_hints("
414create table t (a int, b int);
415create table u (c int) inherits (t);
416insert into u select 1, 2, 3;
417"), @"
418 inlay hints:
419 ╭▸
420 4 │ insert into u select a: 1, b: 2, c: 3;
421 ╰╴ ─── ─── ───
422 ");
423 }
424
425 #[test]
426 fn insert_table_inherits_builtin_values() {
427 assert_snapshot!(check_inlay_hints("
428create table t ()
429inherits (information_schema.sql_features);
430insert into t values (1, 2, 3, 4, 5, 6, 7);
431"), @"
432 inlay hints:
433 ╭▸
434 4 │ …values (feature_id: 1, feature_name: 2, sub_feature_id: 3, sub_feature_name: 4, is_supported: 5, is_verified_by: 6, comments: 7);
435 ╰╴ ──────────── ────────────── ──────────────── ────────────────── ────────────── ──────────────── ──────────
436 ");
437 }
438
439 #[test]
440 fn insert_table_inherits_create_table_as_values() {
441 assert_snapshot!(check_inlay_hints("
442create table parent as select 1 a, 'x'::text b;
443create table child (c int) inherits (parent);
444insert into child values (1, 2, 3);
445"), @"
446 inlay hints:
447 ╭▸
448 4 │ insert into child values (a: 1, b: 2, c: 3);
449 ╰╴ ─── ─── ───
450 ");
451 }
452
453 #[test]
454 fn insert_table_inherits_create_table_as_select_star() {
455 assert_snapshot!(check_inlay_hints("
456create table base (a int, b text);
457create table parent as select * from base;
458create table child (c int) inherits (parent);
459insert into child values (1, 2, 3);
460"), @"
461 inlay hints:
462 ╭▸
463 5 │ insert into child values (a: 1, b: 2, c: 3);
464 ╰╴ ─── ─── ───
465 ");
466 }
467
468 #[test]
469 fn insert_table_like_select() {
470 assert_snapshot!(check_inlay_hints("
471create table x (a int, b int);
472create table y (c int, like x);
473insert into y select 1, 2, 3;
474"), @r"
475 inlay hints:
476 ╭▸
477 4 │ insert into y select c: 1, a: 2, b: 3;
478 ╰╴ ─── ─── ───
479 ");
480 }
481
482 #[test]
483 fn insert_select() {
484 assert_snapshot!(check_inlay_hints("
485create table t (a int, b int);
486insert into t select 1, 2;
487"), @r"
488 inlay hints:
489 ╭▸
490 3 │ insert into t select a: 1, b: 2;
491 ╰╴ ─── ───
492 ");
493 }
494
495 #[test]
496 fn insert_table_like_builtin_values() {
497 assert_snapshot!(check_inlay_hints("
498create table t (like information_schema.sql_features);
499insert into t values (1, 2, 3, 4, 5, 6, 7);
500"), @"
501 inlay hints:
502 ╭▸
503 3 │ …values (feature_id: 1, feature_name: 2, sub_feature_id: 3, sub_feature_name: 4, is_supported: 5, is_verified_by: 6, comments: 7);
504 ╰╴ ──────────── ────────────── ──────────────── ────────────────── ────────────── ──────────────── ──────────
505 ");
506 }
507
508 #[test]
509 fn insert_table_like_select_into_values() {
510 assert_snapshot!(check_inlay_hints("
511select 1 a, 'x'::text b into parent;
512create table child (like parent);
513insert into child values (1, 2);
514"), @"
515 inlay hints:
516 ╭▸
517 4 │ insert into child values (a: 1, b: 2);
518 ╰╴ ─── ───
519 ");
520 }
521}