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