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