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