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