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