1use rowan::TextSize;
2use squawk_syntax::ast::{self, AstNode};
3use squawk_syntax::{SyntaxKind, SyntaxToken};
4
5use crate::binder;
6use crate::resolve;
7use crate::symbols::{Name, Schema, SymbolKind};
8use crate::tokens::is_string_or_comment;
9
10const COMPLETION_MARKER: &str = "squawkCompletionMarker";
11
12pub fn completion(file: &ast::SourceFile, offset: TextSize) -> Vec<CompletionItem> {
13 let file = file_with_completion_marker(file, offset);
14 let Some(token) = token_at_offset(&file, offset) else {
15 return default_completions();
17 };
18 if is_string_or_comment(token.kind()) {
23 return vec![];
24 }
25
26 match completion_context(&token) {
27 CompletionContext::TableOnly => table_completions(&file, &token),
28 CompletionContext::Default => default_completions(),
29 CompletionContext::SelectClause(select_clause) => {
30 select_completions(&file, select_clause, &token)
31 }
32 CompletionContext::SelectClauses(select) => select_clauses_completions(&select),
33 CompletionContext::SelectExpr(select) => select_expr_completions(&file, &select, &token),
34 CompletionContext::LimitClause => limit_completions(&file, &token),
35 CompletionContext::OffsetClause => offset_completions(&file, &token),
36 CompletionContext::DeleteClauses(delete) => {
37 delete_clauses_completions(&file, &delete, &token)
38 }
39 CompletionContext::DeleteExpr(delete) => delete_expr_completions(&file, &delete, &token),
40 }
41}
42
43fn select_completions(
44 file: &ast::SourceFile,
45 select_clause: ast::SelectClause,
46 token: &SyntaxToken,
47) -> Vec<CompletionItem> {
48 let binder = binder::bind(file);
49 let mut completions = vec![];
50 let schema = schema_qualifier_at_token(token);
51 let position = token.text_range().start();
52
53 completions.extend(function_completions(&binder, file, &schema, position));
54
55 let tables = binder.all_symbols_by_kind(SymbolKind::Table, schema.as_ref());
56 completions.extend(tables.into_iter().map(|name| CompletionItem {
57 label: name.to_string(),
58 kind: CompletionItemKind::Table,
59 detail: None,
60 insert_text: None,
61 insert_text_format: None,
62 trigger_completion_after_insert: false,
63 sort_text: None,
64 }));
65
66 if schema.is_none() {
67 completions.extend(schema_completions(&binder));
68 }
69
70 if let Some(parent) = select_clause.syntax().parent()
71 && let Some(select) = ast::Select::cast(parent)
72 {
73 if let Some(from_clause) = select.from_clause() {
74 completions.push(CompletionItem {
75 label: "*".to_string(),
76 kind: CompletionItemKind::Operator,
77 detail: None,
78 insert_text: None,
79 insert_text_format: None,
80 trigger_completion_after_insert: false,
81 sort_text: None,
82 });
83 completions.extend(column_completions_from_clause(&binder, file, &from_clause));
84 } else if schema.is_none() {
85 completions.extend(select_clauses_completions(&select));
86 }
87 }
88
89 completions
90}
91
92fn select_clauses_completions(select: &ast::Select) -> Vec<CompletionItem> {
93 let mut completions = vec![];
94
95 if select.from_clause().is_none() {
96 completions.push(CompletionItem {
97 label: "from".to_owned(),
98 kind: CompletionItemKind::Snippet,
99 detail: None,
100 insert_text: Some("from $0".to_owned()),
101 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
102 trigger_completion_after_insert: true,
103 sort_text: None,
104 });
105 }
106
107 if select.where_clause().is_none() {
108 completions.push(CompletionItem {
109 label: "where".to_owned(),
110 kind: CompletionItemKind::Snippet,
111 detail: None,
112 insert_text: Some("where $0".to_owned()),
113 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
114 trigger_completion_after_insert: true,
115 sort_text: None,
116 });
117 }
118
119 if select.group_by_clause().is_none() {
120 completions.push(CompletionItem {
121 label: "group by".to_owned(),
122 kind: CompletionItemKind::Snippet,
123 detail: None,
124 insert_text: Some("group by $0".to_owned()),
125 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
126 trigger_completion_after_insert: true,
127 sort_text: None,
128 });
129 }
130
131 if select.having_clause().is_none() {
132 completions.push(CompletionItem {
133 label: "having".to_owned(),
134 kind: CompletionItemKind::Snippet,
135 detail: None,
136 insert_text: Some("having $0".to_owned()),
137 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
138 trigger_completion_after_insert: true,
139 sort_text: None,
140 });
141 }
142
143 if select.order_by_clause().is_none() {
144 completions.push(CompletionItem {
145 label: "order by".to_owned(),
146 kind: CompletionItemKind::Snippet,
147 detail: None,
148 insert_text: Some("order by $0".to_owned()),
149 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
150 trigger_completion_after_insert: true,
151 sort_text: None,
152 });
153 }
154
155 if select.limit_clause().is_none() {
156 completions.push(CompletionItem {
157 label: "limit".to_owned(),
158 kind: CompletionItemKind::Snippet,
159 detail: None,
160 insert_text: Some("limit $0".to_owned()),
161 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
162 trigger_completion_after_insert: true,
163 sort_text: None,
164 });
165 }
166
167 if select.offset_clause().is_none() {
168 completions.push(CompletionItem {
169 label: "offset".to_owned(),
170 kind: CompletionItemKind::Snippet,
171 detail: None,
172 insert_text: Some("offset $0".to_owned()),
173 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
174 trigger_completion_after_insert: true,
175 sort_text: None,
176 });
177 }
178
179 if select.fetch_clause().is_none() {
180 completions.push(CompletionItem {
181 label: "fetch".to_owned(),
182 kind: CompletionItemKind::Snippet,
183 detail: None,
184 insert_text: Some(
185 "fetch ${1|first,next|} $2 ${3|row,rows|} ${4|only,with ties|}".to_owned(),
186 ),
187 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
188 trigger_completion_after_insert: true,
189 sort_text: None,
190 });
191 }
192
193 if select.locking_clauses().next().is_none() {
194 completions.push(CompletionItem {
195 label: "for".to_owned(),
196 kind: CompletionItemKind::Snippet,
197 detail: None,
198 insert_text: Some("for ${1|update,no key update,share,key share|} $2".to_owned()),
199 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
200 trigger_completion_after_insert: true,
201 sort_text: None,
202 });
203 }
204
205 if select.window_clause().is_none() {
206 completions.push(CompletionItem {
207 label: "window".to_owned(),
208 kind: CompletionItemKind::Snippet,
209 detail: None,
210 insert_text: Some("window $1 as ($0)".to_owned()),
211 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
212 trigger_completion_after_insert: true,
213 sort_text: None,
214 });
215 }
216
217 completions.push(CompletionItem {
218 label: "union".to_owned(),
219 kind: CompletionItemKind::Snippet,
220 detail: None,
221 insert_text: Some("union $0".to_owned()),
222 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
223 trigger_completion_after_insert: true,
224 sort_text: None,
225 });
226 completions.push(CompletionItem {
227 label: "intersect".to_owned(),
228 kind: CompletionItemKind::Snippet,
229 detail: None,
230 insert_text: Some("intersect $0".to_owned()),
231 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
232 trigger_completion_after_insert: true,
233 sort_text: None,
234 });
235 completions.push(CompletionItem {
236 label: "except".to_owned(),
237 kind: CompletionItemKind::Snippet,
238 detail: None,
239 insert_text: Some("except $0".to_owned()),
240 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
241 trigger_completion_after_insert: true,
242 sort_text: None,
243 });
244
245 completions
246}
247
248fn limit_completions(file: &ast::SourceFile, token: &SyntaxToken) -> Vec<CompletionItem> {
249 let binder = binder::bind(file);
250 let schema = schema_qualifier_at_token(token);
251 let position = token.text_range().start();
252
253 let mut completions = vec![CompletionItem {
254 label: "all".to_owned(),
255 kind: CompletionItemKind::Keyword,
256 detail: None,
257 insert_text: None,
258 insert_text_format: None,
259 trigger_completion_after_insert: false,
260 sort_text: None,
261 }];
262
263 completions.extend(function_completions(&binder, file, &schema, position));
264 completions
265}
266
267fn offset_completions(file: &ast::SourceFile, token: &SyntaxToken) -> Vec<CompletionItem> {
268 let binder = binder::bind(file);
269 let schema = schema_qualifier_at_token(token);
270 let position = token.text_range().start();
271
272 function_completions(&binder, file, &schema, position)
273}
274
275fn select_expr_completions(
276 file: &ast::SourceFile,
277 select: &ast::Select,
278 token: &SyntaxToken,
279) -> Vec<CompletionItem> {
280 let binder = binder::bind(file);
281 let mut completions = vec![];
282 let schema = schema_qualifier_at_token(token);
283 let position = token.text_range().start();
284
285 completions.extend(function_completions(&binder, file, &schema, position));
286
287 if let Some(from_clause) = select.from_clause() {
288 for from_item in from_clause.from_items() {
289 if let Some(table_name) = table_name_from_from_item(&from_item) {
290 completions.push(CompletionItem {
291 label: table_name.to_string(),
292 kind: CompletionItemKind::Table,
293 detail: None,
294 insert_text: None,
295 insert_text_format: None,
296 trigger_completion_after_insert: false,
297 sort_text: None,
298 });
299 }
300 }
301
302 completions.extend(column_completions_from_clause(&binder, file, &from_clause));
303 }
304
305 completions
306}
307
308fn function_completions(
309 binder: &binder::Binder,
310 file: &ast::SourceFile,
311 schema: &Option<Schema>,
312 position: TextSize,
313) -> Vec<CompletionItem> {
314 binder
315 .all_symbols_by_kind(SymbolKind::Function, schema.as_ref())
316 .into_iter()
317 .map(|name| CompletionItem {
318 label: format!("{name}()"),
319 kind: CompletionItemKind::Function,
320 detail: function_detail(binder, file, name, schema, position),
321 insert_text: None,
322 insert_text_format: None,
323 trigger_completion_after_insert: false,
324 sort_text: None,
325 })
326 .collect()
327}
328
329fn column_completions_from_clause(
330 binder: &binder::Binder,
331 file: &ast::SourceFile,
332 from_clause: &ast::FromClause,
333) -> Vec<CompletionItem> {
334 let mut completions = vec![];
335 for table_ptr in resolve::table_ptrs_from_clause(binder, from_clause) {
336 let table_node = table_ptr.to_node(file.syntax());
337 match resolve::find_table_source(&table_node) {
338 Some(resolve::TableSource::CreateTable(create_table)) => {
339 let columns = resolve::collect_table_columns(binder, file.syntax(), &create_table);
340 completions.extend(columns.into_iter().filter_map(|column| {
341 let name = column.name()?;
342 let detail = column.ty().map(|t| t.syntax().text().to_string());
343 Some(CompletionItem {
344 label: Name::from_node(&name).to_string(),
345 kind: CompletionItemKind::Column,
346 detail,
347 insert_text: None,
348 insert_text_format: None,
349 trigger_completion_after_insert: false,
350 sort_text: None,
351 })
352 }));
353 }
354 Some(resolve::TableSource::WithTable(with_table)) => {
355 let columns = resolve::collect_with_table_columns_with_types(&with_table);
356 completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
357 label: name.to_string(),
358 kind: CompletionItemKind::Column,
359 detail: ty.map(|t| t.to_string()),
360 insert_text: None,
361 insert_text_format: None,
362 trigger_completion_after_insert: false,
363 sort_text: None,
364 }));
365 }
366 Some(resolve::TableSource::CreateView(create_view)) => {
367 let columns = resolve::collect_view_columns_with_types(&create_view);
368 completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
369 label: name.to_string(),
370 kind: CompletionItemKind::Column,
371 detail: ty.map(|t| t.to_string()),
372 insert_text: None,
373 insert_text_format: None,
374 trigger_completion_after_insert: false,
375 sort_text: None,
376 }));
377 }
378 Some(resolve::TableSource::CreateMaterializedView(create_materialized_view)) => {
379 let columns = resolve::collect_materialized_view_columns_with_types(
380 &create_materialized_view,
381 );
382 completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
383 label: name.to_string(),
384 kind: CompletionItemKind::Column,
385 detail: ty.map(|t| t.to_string()),
386 insert_text: None,
387 insert_text_format: None,
388 trigger_completion_after_insert: false,
389 sort_text: None,
390 }));
391 }
392 Some(resolve::TableSource::ParenSelect(paren_select)) => {
393 let columns = resolve::collect_paren_select_columns_with_types(
394 binder,
395 file.syntax(),
396 &paren_select,
397 );
398 completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
399 label: name.to_string(),
400 kind: CompletionItemKind::Column,
401 detail: ty.map(|t| t.to_string()),
402 insert_text: None,
403 insert_text_format: None,
404 trigger_completion_after_insert: false,
405 sort_text: None,
406 }));
407 }
408 None => {}
409 }
410 }
411 completions
412}
413
414fn schema_completions(binder: &binder::Binder) -> Vec<CompletionItem> {
415 let builtin_schemas = [
416 "public",
417 "pg_catalog",
418 "pg_temp",
419 "pg_toast",
420 "information_schema",
421 ];
422 let mut completions: Vec<CompletionItem> = builtin_schemas
423 .into_iter()
424 .enumerate()
425 .map(|(i, name)| CompletionItem {
426 label: name.to_string(),
427 kind: CompletionItemKind::Schema,
428 detail: None,
429 insert_text: None,
430 insert_text_format: None,
431 trigger_completion_after_insert: false,
432 sort_text: Some(format!("{i}")),
433 })
434 .collect();
435
436 for name in binder.all_symbols_by_kind(SymbolKind::Schema, None) {
437 completions.push(CompletionItem {
438 label: name.to_string(),
439 kind: CompletionItemKind::Schema,
440 detail: None,
441 insert_text: None,
442 insert_text_format: None,
443 trigger_completion_after_insert: false,
444 sort_text: None,
445 });
446 }
447
448 completions
449}
450
451fn table_completions(file: &ast::SourceFile, token: &SyntaxToken) -> Vec<CompletionItem> {
452 let binder = binder::bind(file);
453 let schema = schema_qualifier_at_token(token);
454 let tables = binder.all_symbols_by_kind(SymbolKind::Table, schema.as_ref());
455 let mut completions: Vec<CompletionItem> = tables
456 .into_iter()
457 .map(|name| CompletionItem {
458 label: name.to_string(),
459 kind: CompletionItemKind::Table,
460 detail: None,
461 insert_text: None,
462 insert_text_format: None,
463 trigger_completion_after_insert: false,
464 sort_text: None,
465 })
466 .collect();
467
468 if schema.is_none() {
469 completions.extend(schema_completions(&binder));
470 }
471
472 completions
473}
474
475fn delete_clauses_completions(
476 file: &ast::SourceFile,
477 delete: &ast::Delete,
478 token: &SyntaxToken,
479) -> Vec<CompletionItem> {
480 let mut completions = vec![];
481
482 if token.kind() == SyntaxKind::FROM_KW {
484 return table_completions(file, token);
485 }
486
487 if delete.using_clause().is_none() {
488 completions.push(CompletionItem {
489 label: "using".to_owned(),
490 kind: CompletionItemKind::Snippet,
491 detail: None,
492 insert_text: Some("using $0".to_owned()),
493 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
494 trigger_completion_after_insert: true,
495 sort_text: None,
496 });
497 }
498
499 if delete.where_clause().is_none() {
500 completions.push(CompletionItem {
501 label: "where".to_owned(),
502 kind: CompletionItemKind::Snippet,
503 detail: None,
504 insert_text: Some("where $0".to_owned()),
505 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
506 trigger_completion_after_insert: true,
507 sort_text: None,
508 });
509 }
510
511 if delete.returning_clause().is_none() {
512 completions.push(CompletionItem {
513 label: "returning".to_owned(),
514 kind: CompletionItemKind::Snippet,
515 detail: None,
516 insert_text: Some("returning $0".to_owned()),
517 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
518 trigger_completion_after_insert: true,
519 sort_text: None,
520 });
521 }
522
523 completions
524}
525
526fn delete_expr_completions(
527 file: &ast::SourceFile,
528 delete: &ast::Delete,
529 token: &SyntaxToken,
530) -> Vec<CompletionItem> {
531 let binder = binder::bind(file);
532 let mut completions = vec![];
533
534 let Some(path) = delete.relation_name().and_then(|r| r.path()) else {
535 return completions;
536 };
537
538 let Some(delete_table_name) = resolve::extract_table_name(&path) else {
539 return completions;
540 };
541
542 let has_table_qualifier = qualifier_at_token(token).is_some_and(|q| q == delete_table_name);
543 let schema = schema_qualifier_at_token(token);
544 let position = token.text_range().start();
545
546 if has_table_qualifier {
547 let functions = binder.functions_with_single_param(&delete_table_name);
548 completions.extend(functions.into_iter().map(|name| CompletionItem {
549 label: name.to_string(),
550 kind: CompletionItemKind::Function,
551 detail: function_detail(&binder, file, name, &schema, position),
552 insert_text: None,
553 insert_text_format: None,
554 trigger_completion_after_insert: false,
555 sort_text: None,
556 }));
557 } else {
558 let functions = binder.all_symbols_by_kind(SymbolKind::Function, None);
559 completions.extend(functions.into_iter().map(|name| CompletionItem {
560 label: format!("{name}()"),
561 kind: CompletionItemKind::Function,
562 detail: function_detail(&binder, file, name, &schema, position),
563 insert_text: None,
564 insert_text_format: None,
565 trigger_completion_after_insert: false,
566 sort_text: None,
567 }));
568
569 completions.push(CompletionItem {
570 label: delete_table_name.to_string(),
571 kind: CompletionItemKind::Table,
572 detail: None,
573 insert_text: None,
574 insert_text_format: None,
575 trigger_completion_after_insert: false,
576 sort_text: None,
577 });
578 }
579
580 let schema = resolve::extract_schema_name(&path);
581 if let Some(table_ptr) =
582 binder.lookup_with(&delete_table_name, SymbolKind::Table, position, &schema)
583 && let Some(create_table) = table_ptr
584 .to_node(file.syntax())
585 .ancestors()
586 .find_map(ast::CreateTableLike::cast)
587 {
588 let columns = resolve::collect_table_columns(&binder, file.syntax(), &create_table);
589 completions.extend(columns.into_iter().filter_map(|column| {
590 let name = column.name()?;
591 let detail = column.ty().map(|t| t.syntax().text().to_string());
592 Some(CompletionItem {
593 label: Name::from_node(&name).to_string(),
594 kind: CompletionItemKind::Column,
595 detail,
596 insert_text: None,
597 insert_text_format: None,
598 trigger_completion_after_insert: false,
599 sort_text: None,
600 })
601 }));
602 }
603
604 completions
605}
606
607fn table_name_from_from_item(from_item: &ast::FromItem) -> Option<Name> {
608 if let Some(alias) = from_item.alias()
609 && let Some(alias_name) = alias.name()
610 {
611 return Some(Name::from_node(&alias_name));
612 }
613 if let Some(name_ref) = from_item.name_ref() {
614 return Some(Name::from_node(&name_ref));
615 }
616 None
617}
618
619fn qualifier_at_token(token: &SyntaxToken) -> Option<Name> {
620 let qualifier_token = if token.kind() == SyntaxKind::DOT {
621 token.prev_token()
622 } else if token.kind() == SyntaxKind::IDENT
623 && let Some(prev) = token.prev_token()
624 && prev.kind() == SyntaxKind::DOT
625 {
626 prev.prev_token()
627 } else {
628 None
629 };
630
631 qualifier_token
632 .filter(|tk| tk.kind() == SyntaxKind::IDENT)
633 .map(|tk| Name::from_string(tk.text().to_string()))
634}
635
636#[derive(Debug)]
637enum CompletionContext {
638 TableOnly,
639 Default,
640 SelectClause(ast::SelectClause),
641 SelectClauses(ast::Select),
642 SelectExpr(ast::Select),
643 LimitClause,
644 OffsetClause,
645 DeleteClauses(ast::Delete),
646 DeleteExpr(ast::Delete),
647}
648
649fn completion_context(token: &SyntaxToken) -> CompletionContext {
650 if let Some(node) = token.parent() {
651 let mut inside_delete_clause = false;
652 let mut inside_from_item = false;
653 let mut inside_paren_expr = false;
654 let mut inside_select_expr_clause = false;
655 let mut inside_limit_clause = false;
656 let mut inside_offset_clause = false;
657 for a in node.ancestors() {
658 if ast::Truncate::can_cast(a.kind()) || ast::Table::can_cast(a.kind()) {
659 return CompletionContext::TableOnly;
660 }
661 if ast::WhereClause::can_cast(a.kind())
662 || ast::UsingClause::can_cast(a.kind())
663 || ast::ReturningClause::can_cast(a.kind())
664 {
665 inside_delete_clause = true;
666 }
667 if ast::LimitClause::can_cast(a.kind()) {
668 inside_limit_clause = true;
669 }
670 if ast::OffsetClause::can_cast(a.kind()) {
671 inside_offset_clause = true;
672 }
673 if ast::WhereClause::can_cast(a.kind())
674 || ast::GroupByClause::can_cast(a.kind())
675 || ast::HavingClause::can_cast(a.kind())
676 || ast::OrderByClause::can_cast(a.kind())
677 {
678 inside_select_expr_clause = true;
679 }
680 if ast::FromItem::can_cast(a.kind()) {
681 inside_from_item = true;
682 }
683 if ast::ParenExpr::can_cast(a.kind()) {
684 inside_paren_expr = true;
685 }
686 if let Some(delete) = ast::Delete::cast(a.clone()) {
687 if inside_delete_clause {
688 return CompletionContext::DeleteExpr(delete);
689 }
690 if delete.relation_name().is_some() {
691 return CompletionContext::DeleteClauses(delete);
692 }
693 return CompletionContext::TableOnly;
694 }
695 if let Some(select) = ast::Select::cast(a.clone()) {
696 if inside_limit_clause {
697 return CompletionContext::LimitClause;
698 }
699 if inside_offset_clause {
700 return CompletionContext::OffsetClause;
701 }
702 if inside_select_expr_clause {
703 return CompletionContext::SelectExpr(select);
704 }
705 if inside_from_item && !inside_paren_expr && select.from_clause().is_some() {
706 return CompletionContext::SelectClauses(select);
707 }
708 }
709 if let Some(select_clause) = ast::SelectClause::cast(a.clone()) {
710 return CompletionContext::SelectClause(select_clause);
711 }
712 }
713 }
714 CompletionContext::Default
715}
716
717fn token_at_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
718 let Some(mut token) = file.syntax().token_at_offset(offset).left_biased() else {
719 return None;
721 };
722 while token.kind() == SyntaxKind::WHITESPACE {
723 if let Some(tk) = token.prev_token() {
724 token = tk;
725 }
726 }
727 Some(token)
728}
729
730fn file_with_completion_marker(file: &ast::SourceFile, offset: TextSize) -> ast::SourceFile {
737 let mut sql = file.syntax().text().to_string();
738 let offset = u32::from(offset) as usize;
739 let offset = offset.min(sql.len());
740 sql.insert_str(offset, COMPLETION_MARKER);
741 ast::SourceFile::parse(&sql).tree()
742}
743
744fn schema_qualifier_at_token(token: &SyntaxToken) -> Option<Schema> {
745 qualifier_at_token(token).map(Schema)
746}
747
748fn function_detail(
749 binder: &binder::Binder,
750 file: &ast::SourceFile,
751 function_name: &Name,
752 schema: &Option<Schema>,
753 position: TextSize,
754) -> Option<String> {
755 let create_function = binder
756 .lookup_with(function_name, SymbolKind::Function, position, schema)?
757 .to_node(file.syntax())
758 .ancestors()
759 .find_map(ast::CreateFunction::cast)?;
760 let path = create_function.path()?;
761 let (schema, function_name) = resolve::resolve_function_info(binder, &path)?;
762
763 let param_list = create_function.param_list()?;
764 let params = param_list.syntax().text().to_string();
765
766 let ret_type = create_function.ret_type()?;
767 let return_type = ret_type.syntax().text().to_string();
768
769 Some(format!(
770 "{}.{}{} {}",
771 schema, function_name, params, return_type
772 ))
773}
774
775fn default_completions() -> Vec<CompletionItem> {
776 ["delete from", "select", "table", "truncate"]
777 .map(|stmt| CompletionItem {
778 label: stmt.to_owned(),
779 kind: CompletionItemKind::Snippet,
780 detail: None,
781 insert_text: Some(format!("{stmt} $0;")),
782 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
783 trigger_completion_after_insert: true,
784 sort_text: None,
785 })
786 .into_iter()
787 .collect()
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub enum CompletionItemKind {
792 Keyword,
793 Table,
794 Column,
795 Function,
796 Schema,
797 Type,
798 Snippet,
799 Operator,
800}
801
802impl CompletionItemKind {
803 fn sort_prefix(self) -> &'static str {
804 match self {
805 Self::Column => "0",
806 Self::Keyword => "1",
807 Self::Table => "1",
808 Self::Type => "1",
809 Self::Snippet => "1",
810 Self::Function => "2",
811 Self::Operator => "8",
812 Self::Schema => "9",
813 }
814 }
815}
816
817impl CompletionItem {
818 pub fn sort_text(&self) -> String {
819 let prefix = self.kind.sort_prefix();
820 let suffix = self.sort_text.as_ref().unwrap_or(&self.label);
821 format!("{prefix}_{suffix}")
822 }
823}
824
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum CompletionInsertTextFormat {
827 PlainText,
828 Snippet,
829}
830
831#[derive(Debug, Clone, PartialEq, Eq)]
832pub struct CompletionItem {
833 pub label: String,
834 pub kind: CompletionItemKind,
835 pub detail: Option<String>,
836 pub insert_text: Option<String>,
837 pub insert_text_format: Option<CompletionInsertTextFormat>,
838 pub trigger_completion_after_insert: bool,
839 pub sort_text: Option<String>,
840}
841
842#[cfg(test)]
843mod tests {
844 use super::completion;
845 use crate::test_utils::fixture;
846 use insta::assert_snapshot;
847 use squawk_syntax::ast;
848 use tabled::builder::Builder;
849 use tabled::settings::Style;
850
851 fn completions(sql: &str) -> String {
852 let (offset, sql) = fixture(sql);
853 let parse = ast::SourceFile::parse(&sql);
854 let file = parse.tree();
855 let items = completion(&file, offset);
856 assert!(
857 !items.is_empty(),
858 "No completions found. If this was intended, use `completions_not_found` instead."
859 );
860 format_items(items)
861 }
862
863 fn completions_not_found(sql: &str) {
864 let (offset, sql) = fixture(sql);
865 let parse = ast::SourceFile::parse(&sql);
866 let file = parse.tree();
867 let items = completion(&file, offset);
868 assert_eq!(
869 items,
870 vec![],
871 "Completions found. If this was unintended, use `completions` instead."
872 )
873 }
874
875 fn format_items(mut items: Vec<super::CompletionItem>) -> String {
876 items.sort_by_key(|a| a.sort_text());
877
878 let rows: Vec<Vec<String>> = items
879 .into_iter()
880 .map(|item| {
881 vec![
882 item.label,
883 format!("{:?}", item.kind),
884 item.detail.unwrap_or_default(),
885 ]
886 })
887 .collect();
888
889 let mut builder = Builder::default();
890 builder.push_record(["label", "kind", "detail"]);
891 for row in rows {
892 builder.push_record(row);
893 }
894
895 let mut table = builder.build();
896 table.with(Style::psql());
897 table.to_string()
898 }
899
900 #[test]
901 fn completion_at_start() {
902 assert_snapshot!(completions("$0"), @r"
903 label | kind | detail
904 -------------+---------+--------
905 delete from | Snippet |
906 select | Snippet |
907 table | Snippet |
908 truncate | Snippet |
909 ");
910 }
911
912 #[test]
913 fn completion_at_top_level() {
914 assert_snapshot!(completions("
915create table t(a int);
916$0
917"), @r"
918 label | kind | detail
919 -------------+---------+--------
920 delete from | Snippet |
921 select | Snippet |
922 table | Snippet |
923 truncate | Snippet |
924 ");
925 }
926
927 #[test]
928 fn completion_in_string() {
929 completions_not_found("select '$0';");
930 }
931
932 #[test]
933 fn completion_in_comment() {
934 completions_not_found("-- $0 ");
935 }
936
937 #[test]
938 fn completion_after_truncate() {
939 assert_snapshot!(completions("
940create table users (id int);
941truncate $0;
942"), @r"
943 label | kind | detail
944 --------------------+--------+--------
945 users | Table |
946 public | Schema |
947 pg_catalog | Schema |
948 pg_temp | Schema |
949 pg_toast | Schema |
950 information_schema | Schema |
951 ");
952 }
953
954 #[test]
955 fn completion_table_at_top_level() {
956 assert_snapshot!(completions("$0"), @r"
957 label | kind | detail
958 -------------+---------+--------
959 delete from | Snippet |
960 select | Snippet |
961 table | Snippet |
962 truncate | Snippet |
963 ");
964 }
965
966 #[test]
967 fn completion_table_nested() {
968 assert_snapshot!(completions("select * from ($0)"), @r"
969 label | kind | detail
970 -------------+---------+--------
971 delete from | Snippet |
972 select | Snippet |
973 table | Snippet |
974 truncate | Snippet |
975 ");
976 }
977
978 #[test]
979 fn completion_after_table() {
980 assert_snapshot!(completions("
981create table users (id int);
982table $0;
983"), @r"
984 label | kind | detail
985 --------------------+--------+--------
986 users | Table |
987 public | Schema |
988 pg_catalog | Schema |
989 pg_temp | Schema |
990 pg_toast | Schema |
991 information_schema | Schema |
992 ");
993 }
994
995 #[test]
996 fn completion_select_without_from() {
997 assert_snapshot!(completions("
998create table t (a int);
999select $0;
1000"), @r"
1001 label | kind | detail
1002 --------------------+---------+--------
1003 except | Snippet |
1004 fetch | Snippet |
1005 for | Snippet |
1006 from | Snippet |
1007 group by | Snippet |
1008 having | Snippet |
1009 intersect | Snippet |
1010 limit | Snippet |
1011 offset | Snippet |
1012 order by | Snippet |
1013 t | Table |
1014 union | Snippet |
1015 where | Snippet |
1016 window | Snippet |
1017 public | Schema |
1018 pg_catalog | Schema |
1019 pg_temp | Schema |
1020 pg_toast | Schema |
1021 information_schema | Schema |
1022 ");
1023 }
1024
1025 #[test]
1026 fn completion_after_select() {
1027 assert_snapshot!(completions("
1028create table t(a text, b int);
1029create function f() returns text as 'select 1::text' language sql;
1030select $0 from t;
1031"), @r"
1032 label | kind | detail
1033 --------------------+----------+-------------------------
1034 a | Column | text
1035 b | Column | int
1036 t | Table |
1037 f() | Function | public.f() returns text
1038 * | Operator |
1039 public | Schema |
1040 pg_catalog | Schema |
1041 pg_temp | Schema |
1042 pg_toast | Schema |
1043 information_schema | Schema |
1044 ");
1045 }
1046
1047 #[test]
1048 fn completion_select_table_qualified() {
1049 assert_snapshot!(completions("
1050create table t (c int);
1051select t.$0 from t;
1052"), @r"
1053 label | kind | detail
1054 -------+----------+--------
1055 c | Column | int
1056 * | Operator |
1057 ");
1058 }
1059
1060 #[test]
1061 fn completion_after_select_with_cte() {
1062 assert_snapshot!(completions("
1063with t as (select 1 a)
1064select $0 from t;
1065"), @r"
1066 label | kind | detail
1067 --------------------+----------+---------
1068 a | Column | integer
1069 * | Operator |
1070 public | Schema |
1071 pg_catalog | Schema |
1072 pg_temp | Schema |
1073 pg_toast | Schema |
1074 information_schema | Schema |
1075 ");
1076 }
1077
1078 #[test]
1079 fn completion_values_cte() {
1080 assert_snapshot!(completions("
1081with t as (values (1, 'foo', false))
1082select $0 from t;
1083"), @r"
1084 label | kind | detail
1085 --------------------+----------+---------
1086 column1 | Column | integer
1087 column2 | Column | text
1088 column3 | Column | boolean
1089 * | Operator |
1090 public | Schema |
1091 pg_catalog | Schema |
1092 pg_temp | Schema |
1093 pg_toast | Schema |
1094 information_schema | Schema |
1095 ");
1096 }
1097
1098 #[test]
1099 fn completion_values_subquery() {
1100 assert_snapshot!(completions("
1101select $0 from (values (1, 'foo', 1.5, false));
1102"), @r"
1103 label | kind | detail
1104 --------------------+----------+---------
1105 column1 | Column | integer
1106 column2 | Column | text
1107 column3 | Column | numeric
1108 column4 | Column | boolean
1109 * | Operator |
1110 public | Schema |
1111 pg_catalog | Schema |
1112 pg_temp | Schema |
1113 pg_toast | Schema |
1114 information_schema | Schema |
1115 ");
1116 }
1117
1118 #[test]
1119 fn completion_with_schema_qualifier() {
1120 assert_snapshot!(completions("
1121create function f() returns int8 as 'select 1' language sql;
1122create function foo.b() returns int8 as 'select 2' language sql;
1123select public.$0;
1124"), @r"
1125 label | kind | detail
1126 -------+----------+-------------------------
1127 f() | Function | public.f() returns int8
1128 ");
1129 }
1130
1131 #[test]
1132 fn completion_truncate_with_schema_qualifier() {
1133 assert_snapshot!(completions("
1134create table users (id int);
1135truncate public.$0;
1136"), @r"
1137 label | kind | detail
1138 -------+-------+--------
1139 users | Table |
1140 ");
1141 }
1142
1143 #[test]
1144 fn completion_after_delete_from() {
1145 assert_snapshot!(completions("
1146create table users (id int);
1147delete from $0;
1148"), @r"
1149 label | kind | detail
1150 --------------------+--------+--------
1151 users | Table |
1152 public | Schema |
1153 pg_catalog | Schema |
1154 pg_temp | Schema |
1155 pg_toast | Schema |
1156 information_schema | Schema |
1157 ");
1158 }
1159
1160 #[test]
1161 fn completion_delete_clauses() {
1162 assert_snapshot!(completions("
1163create table t (id int);
1164delete from t $0;
1165"), @r"
1166 label | kind | detail
1167 -----------+---------+--------
1168 returning | Snippet |
1169 using | Snippet |
1170 where | Snippet |
1171 ");
1172 }
1173
1174 #[test]
1175 fn completion_delete_where_expr() {
1176 assert_snapshot!(completions("
1177create table t (id int, name text);
1178create function is_active() returns bool as 'select true' language sql;
1179delete from t where $0;
1180"), @r"
1181 label | kind | detail
1182 -------------+----------+---------------------------------
1183 id | Column | int
1184 name | Column | text
1185 t | Table |
1186 is_active() | Function | public.is_active() returns bool
1187 ")
1188 }
1189
1190 #[test]
1191 fn completion_delete_returning_expr() {
1192 assert_snapshot!(completions("
1193create table t (id int, name text);
1194delete from t returning $0;
1195"), @r"
1196 label | kind | detail
1197 -------+--------+--------
1198 id | Column | int
1199 name | Column | text
1200 t | Table |
1201 ");
1202 }
1203
1204 #[test]
1205 fn completion_delete_where_qualified() {
1206 assert_snapshot!(completions("
1207-- different type than the table, so we shouldn't show this
1208create function b(diff_type) returns int8
1209 as 'select 1'
1210 language sql;
1211create function f(t) returns int8
1212 as 'select 1'
1213 language sql;
1214create table t (a int, b text);
1215delete from t where t.$0;
1216"), @r"
1217 label | kind | detail
1218 -------+----------+--------
1219 a | Column | int
1220 b | Column | text
1221 f | Function |
1222 ");
1223 }
1224
1225 #[test]
1226 fn completion_select_clauses() {
1227 assert_snapshot!(completions("
1228with t as (select 1 a)
1229select a from t $0;
1230"), @r"
1231 label | kind | detail
1232 -----------+---------+--------
1233 except | Snippet |
1234 fetch | Snippet |
1235 for | Snippet |
1236 group by | Snippet |
1237 having | Snippet |
1238 intersect | Snippet |
1239 limit | Snippet |
1240 offset | Snippet |
1241 order by | Snippet |
1242 union | Snippet |
1243 where | Snippet |
1244 window | Snippet |
1245 ");
1246 }
1247
1248 #[test]
1249 fn completion_select_clauses_simple() {
1250 assert_snapshot!(completions("
1251select 1 from t $0;
1252"), @r"
1253 label | kind | detail
1254 -----------+---------+--------
1255 except | Snippet |
1256 fetch | Snippet |
1257 for | Snippet |
1258 group by | Snippet |
1259 having | Snippet |
1260 intersect | Snippet |
1261 limit | Snippet |
1262 offset | Snippet |
1263 order by | Snippet |
1264 union | Snippet |
1265 where | Snippet |
1266 window | Snippet |
1267 ");
1268 }
1269
1270 #[test]
1271 fn completion_select_group_by_expr() {
1272 assert_snapshot!(completions("
1273with t as (select 1 a)
1274select a from t group by $0;
1275"), @r"
1276 label | kind | detail
1277 -------+--------+---------
1278 a | Column | integer
1279 t | Table |
1280 ");
1281 }
1282
1283 #[test]
1284 fn completion_select_where_expr() {
1285 assert_snapshot!(completions("
1286create table t (id int, name text);
1287select * from t where $0;
1288"), @r"
1289 label | kind | detail
1290 -------+--------+--------
1291 id | Column | int
1292 name | Column | text
1293 t | Table |
1294 ");
1295 }
1296
1297 #[test]
1298 fn completion_select_limit() {
1299 assert_snapshot!(completions("
1300create function get_limit() returns int as 'select 10' language sql;
1301select 1 from t limit $0;
1302"), @r"
1303 label | kind | detail
1304 -------------+----------+--------------------------------
1305 all | Keyword |
1306 get_limit() | Function | public.get_limit() returns int
1307 ");
1308 }
1309
1310 #[test]
1311 fn completion_select_offset() {
1312 assert_snapshot!(completions("
1313create function get_offset() returns int as 'select 10' language sql;
1314select 1 from t offset $0;
1315"), @r"
1316 label | kind | detail
1317 --------------+----------+---------------------------------
1318 get_offset() | Function | public.get_offset() returns int
1319 ");
1320 }
1321}