Skip to main content

squawk_ide/
inlay_hints.rs

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/// `VSCode` has some theming options based on these types.
11#[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    // Need this to be an Option because we can still inlay hints when we don't
23    // have the destination.
24    // For example: `insert into t(a, b) values (1, 2)`
25    pub target: Option<TextRange>,
26    // TODO: combine with the target range above
27    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    // We need to support the table definition not being found since we can
104    // still provide inlay hints when a column list is provided
105    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        // `insert into t(a, b, c) values (1, 2, 3)`
122        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        // `insert into t values (1, 2, 3)`
136        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        // `insert into t values (1, 2);`
155        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    // `insert into t select 1, 2;`
171    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}