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