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