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 .column_list()
404 .into_iter()
405 .flat_map(|column_list| column_list.columns())
406 .filter_map(|column| column.name().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::Alias,
474) -> Vec<(Name, Option<String>)> {
475 let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) else {
476 return vec![];
477 };
478 let Some(table_ptr) = resolve::table_ptr_from_from_item(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().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.relation_name().and_then(|r| r.path()) else {
649 return completions;
650 };
651
652 let Some(delete_table_name) = name::table_name(&path) else {
653 return completions;
654 };
655
656 let has_table_qualifier = qualifier_at_token(token).is_some_and(|q| q == delete_table_name);
657 let schema = schema_qualifier_at_token(token);
658 let position = token.text_range().start();
659
660 if has_table_qualifier {
661 let functions = binder.functions_with_single_param(&delete_table_name);
662 completions.extend(functions.into_iter().map(|name| CompletionItem {
663 label: name.to_string(),
664 kind: CompletionItemKind::Function,
665 detail: function_detail(db, file, name, schema.as_ref(), position),
666 insert_text: None,
667 insert_text_format: None,
668 trigger_completion_after_insert: false,
669 sort_text: None,
670 }));
671 } else {
672 let functions = binder.all_symbols_by_kind(SymbolKind::Function, None);
673 completions.extend(functions.into_iter().map(|name| CompletionItem {
674 label: format!("{name}()"),
675 kind: CompletionItemKind::Function,
676 detail: function_detail(db, file, name, schema.as_ref(), position),
677 insert_text: None,
678 insert_text_format: None,
679 trigger_completion_after_insert: false,
680 sort_text: None,
681 }));
682
683 completions.push(CompletionItem {
684 label: delete_table_name.to_string(),
685 kind: CompletionItemKind::Table,
686 detail: None,
687 insert_text: None,
688 insert_text_format: None,
689 trigger_completion_after_insert: false,
690 sort_text: None,
691 });
692 }
693
694 let schema = name::schema_name(&path);
695 let schemas = binder.resolved_schemas(position, schema.as_ref());
696 if let Some(table_ptr) = binder.lookup_with(&delete_table_name, SymbolKind::Table, &schemas)
697 && let Some(create_table) = table_ptr
698 .to_node(source_file.syntax())
699 .ancestors()
700 .find_map(ast::CreateTableLike::cast)
701 {
702 let columns = collect::table_columns(db, file, &create_table);
703 completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
704 label: name.to_string(),
705 kind: CompletionItemKind::Column,
706 detail: ty.map(|t| t.to_string()),
707 insert_text: None,
708 insert_text_format: None,
709 trigger_completion_after_insert: false,
710 sort_text: None,
711 }));
712 }
713
714 completions
715}
716
717fn table_name_from_from_item(from_item: &ast::FromItem) -> Option<Name> {
718 if let Some(alias) = from_item.alias()
719 && let Some(alias_name) = alias.name()
720 {
721 return Some(Name::from_node(&alias_name));
722 }
723 if let Some(name_ref) = from_item.name_ref() {
724 return Some(Name::from_node(&name_ref));
725 }
726 None
727}
728
729fn qualifier_at_token(token: &SyntaxToken) -> Option<Name> {
730 let qualifier_token = if token.kind() == SyntaxKind::DOT {
731 token.prev_token()
732 } else if token.kind() == SyntaxKind::IDENT
733 && let Some(prev) = token.prev_token()
734 && prev.kind() == SyntaxKind::DOT
735 {
736 prev.prev_token()
737 } else {
738 None
739 };
740
741 qualifier_token
742 .filter(|tk| tk.kind() == SyntaxKind::IDENT)
743 .map(|tk| Name::from_string(tk.text().to_string()))
744}
745
746#[derive(Debug)]
747enum CompletionContext {
748 TableOnly,
749 Default,
750 SelectClause(ast::SelectClause),
751 SelectClauses(ast::Select),
752 SelectExpr(ast::Select),
753 LimitClause,
754 OffsetClause,
755 DeleteClauses(ast::Delete),
756 DeleteExpr(ast::Delete),
757}
758
759fn completion_context(token: &SyntaxToken) -> CompletionContext {
760 if let Some(node) = token.parent() {
761 let mut inside_delete_clause = false;
762 let mut inside_from_item = false;
763 let mut inside_paren_expr = false;
764 let mut inside_select_expr_clause = false;
765 let mut inside_limit_clause = false;
766 let mut inside_offset_clause = false;
767 for a in node.ancestors() {
768 if ast::Truncate::can_cast(a.kind()) || ast::Table::can_cast(a.kind()) {
769 return CompletionContext::TableOnly;
770 }
771 if ast::WhereClause::can_cast(a.kind())
772 || ast::UsingClause::can_cast(a.kind())
773 || ast::ReturningClause::can_cast(a.kind())
774 {
775 inside_delete_clause = true;
776 }
777 if ast::LimitClause::can_cast(a.kind()) {
778 inside_limit_clause = true;
779 }
780 if ast::OffsetClause::can_cast(a.kind()) {
781 inside_offset_clause = true;
782 }
783 if ast::WhereClause::can_cast(a.kind())
784 || ast::GroupByClause::can_cast(a.kind())
785 || ast::HavingClause::can_cast(a.kind())
786 || ast::OrderByClause::can_cast(a.kind())
787 {
788 inside_select_expr_clause = true;
789 }
790 if ast::FromItem::can_cast(a.kind()) {
791 inside_from_item = true;
792 }
793 if ast::ParenExpr::can_cast(a.kind()) {
794 inside_paren_expr = true;
795 }
796 if let Some(delete) = ast::Delete::cast(a.clone()) {
797 if inside_delete_clause {
798 return CompletionContext::DeleteExpr(delete);
799 }
800 if delete.relation_name().is_some() {
801 return CompletionContext::DeleteClauses(delete);
802 }
803 return CompletionContext::TableOnly;
804 }
805 if let Some(select) = ast::Select::cast(a.clone()) {
806 if inside_limit_clause {
807 return CompletionContext::LimitClause;
808 }
809 if inside_offset_clause {
810 return CompletionContext::OffsetClause;
811 }
812 if inside_select_expr_clause {
813 return CompletionContext::SelectExpr(select);
814 }
815 if inside_from_item && !inside_paren_expr && select.from_clause().is_some() {
816 return CompletionContext::SelectClauses(select);
817 }
818 }
819 if let Some(select_clause) = ast::SelectClause::cast(a.clone()) {
820 return CompletionContext::SelectClause(select_clause);
821 }
822 }
823 }
824 CompletionContext::Default
825}
826
827fn token_at_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
828 let Some(mut token) = file.syntax().token_at_offset(offset).left_biased() else {
829 return None;
831 };
832 while token.kind() == SyntaxKind::WHITESPACE {
833 if let Some(tk) = token.prev_token() {
834 token = tk;
835 }
836 }
837 Some(token)
838}
839
840fn file_with_completion_marker(file: &ast::SourceFile, offset: TextSize) -> ast::SourceFile {
847 let mut sql = file.syntax().text().to_string();
848 let offset = u32::from(offset) as usize;
849 let offset = offset.min(sql.len());
850 sql.insert_str(offset, COMPLETION_MARKER);
851 ast::SourceFile::parse(&sql).tree()
852}
853
854fn schema_qualifier_at_token(token: &SyntaxToken) -> Option<Schema> {
855 qualifier_at_token(token).map(Schema)
856}
857
858fn function_detail(
859 db: &dyn Db,
860 file: File,
861 function_name: &Name,
862 schema: Option<&Schema>,
863 position: TextSize,
864) -> Option<String> {
865 let binder = bind(db, file);
866 let source_file = parse(db, file).tree();
867 let schemas = binder.resolved_schemas(position, schema);
868 let create_function = binder
869 .lookup_with(function_name, SymbolKind::Function, &schemas)?
870 .to_node(source_file.syntax())
871 .ancestors()
872 .find_map(ast::CreateFunction::cast)?;
873 let path = create_function.path()?;
874 let (schema, function_name) = resolve::resolve_function_info(db, InFile::new(file, &path))?;
875
876 let param_list = create_function.param_list()?;
877 let params = param_list.syntax().text().to_string();
878
879 let ret_type = create_function.ret_type()?;
880 let return_type = ret_type.syntax().text().to_string();
881
882 Some(format!("{schema}.{function_name}{params} {return_type}"))
883}
884
885fn default_completions() -> Vec<CompletionItem> {
886 ["delete from", "select", "table", "truncate"]
887 .map(|stmt| CompletionItem {
888 label: stmt.to_owned(),
889 kind: CompletionItemKind::Snippet,
890 detail: None,
891 insert_text: Some(format!("{stmt} $0;")),
892 insert_text_format: Some(CompletionInsertTextFormat::Snippet),
893 trigger_completion_after_insert: true,
894 sort_text: None,
895 })
896 .into_iter()
897 .collect()
898}
899
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum CompletionItemKind {
902 Keyword,
903 Table,
904 Column,
905 Function,
906 Schema,
907 Type,
908 Snippet,
909 Operator,
910}
911
912impl CompletionItemKind {
913 fn sort_prefix(self) -> &'static str {
914 match self {
915 Self::Column => "0",
916 Self::Keyword => "1",
917 Self::Table => "1",
918 Self::Type => "1",
919 Self::Snippet => "1",
920 Self::Function => "2",
921 Self::Operator => "8",
922 Self::Schema => "9",
923 }
924 }
925}
926
927impl CompletionItem {
928 pub fn sort_text(&self) -> String {
929 let prefix = self.kind.sort_prefix();
930 let suffix = self.sort_text.as_ref().unwrap_or(&self.label);
931 format!("{prefix}_{suffix}")
932 }
933}
934
935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
936pub enum CompletionInsertTextFormat {
937 PlainText,
938 Snippet,
939}
940
941#[derive(Debug, Clone, PartialEq, Eq)]
942pub struct CompletionItem {
943 pub label: String,
944 pub kind: CompletionItemKind,
945 pub detail: Option<String>,
946 pub insert_text: Option<String>,
947 pub insert_text_format: Option<CompletionInsertTextFormat>,
948 pub trigger_completion_after_insert: bool,
949 pub sort_text: Option<String>,
950}
951
952#[cfg(test)]
953mod tests {
954 use super::completion;
955
956 use crate::test_utils::Fixture;
957 use insta::assert_snapshot;
958 use tabled::builder::Builder;
959 use tabled::settings::Style;
960
961 #[must_use]
962 fn completions(sql: &str) -> String {
963 let fixture = Fixture::new_allow_errors(sql);
964 let offset = fixture.marker().offset();
965 let items = completion(fixture.db(), offset);
966 assert!(
967 !items.is_empty(),
968 "No completions found. If this was intended, use `completions_not_found` instead."
969 );
970 format_items(items)
971 }
972
973 fn completions_not_found(sql: &str) {
974 let fixture = Fixture::new_allow_errors(sql);
975 let offset = fixture.marker().offset();
976 let items = completion(fixture.db(), offset);
977 assert_eq!(
978 items,
979 vec![],
980 "Completions found. If this was unintended, use `completions` instead."
981 )
982 }
983
984 fn format_items(mut items: Vec<super::CompletionItem>) -> String {
985 items.sort_by_key(|a| a.sort_text());
986
987 let rows: Vec<Vec<String>> = items
988 .into_iter()
989 .map(|item| {
990 vec![
991 item.label,
992 format!("{:?}", item.kind),
993 item.detail.unwrap_or_default(),
994 ]
995 })
996 .collect();
997
998 let mut builder = Builder::default();
999 builder.push_record(["label", "kind", "detail"]);
1000 for row in rows {
1001 builder.push_record(row);
1002 }
1003
1004 let mut table = builder.build();
1005 table.with(Style::psql());
1006 table.to_string()
1007 }
1008
1009 #[test]
1010 fn completion_at_start() {
1011 assert_snapshot!(completions("$0"), @r"
1012 label | kind | detail
1013 -------------+---------+--------
1014 delete from | Snippet |
1015 select | Snippet |
1016 table | Snippet |
1017 truncate | Snippet |
1018 ");
1019 }
1020
1021 #[test]
1022 fn completion_at_top_level() {
1023 assert_snapshot!(completions("
1024create table t(a int);
1025$0
1026"), @r"
1027 label | kind | detail
1028 -------------+---------+--------
1029 delete from | Snippet |
1030 select | Snippet |
1031 table | Snippet |
1032 truncate | Snippet |
1033 ");
1034 }
1035
1036 #[test]
1037 fn completion_in_string() {
1038 completions_not_found("select '$0';");
1039 }
1040
1041 #[test]
1042 fn completion_in_comment() {
1043 completions_not_found("-- $0 ");
1044 }
1045
1046 #[test]
1047 fn completion_after_truncate() {
1048 assert_snapshot!(completions("
1049create table users (id int);
1050truncate $0;
1051"), @r"
1052 label | kind | detail
1053 --------------------+--------+--------
1054 users | Table |
1055 public | Schema |
1056 pg_catalog | Schema |
1057 pg_temp | Schema |
1058 pg_toast | Schema |
1059 information_schema | Schema |
1060 ");
1061 }
1062
1063 #[test]
1064 fn completion_table_at_top_level() {
1065 assert_snapshot!(completions("$0"), @r"
1066 label | kind | detail
1067 -------------+---------+--------
1068 delete from | Snippet |
1069 select | Snippet |
1070 table | Snippet |
1071 truncate | Snippet |
1072 ");
1073 }
1074
1075 #[test]
1076 fn completion_table_nested() {
1077 assert_snapshot!(completions("select * from ($0)"), @r"
1078 label | kind | detail
1079 -------------+---------+--------
1080 delete from | Snippet |
1081 select | Snippet |
1082 table | Snippet |
1083 truncate | Snippet |
1084 ");
1085 }
1086
1087 #[test]
1088 fn completion_after_table() {
1089 assert_snapshot!(completions("
1090create table users (id int);
1091table $0;
1092"), @r"
1093 label | kind | detail
1094 --------------------+--------+--------
1095 users | Table |
1096 public | Schema |
1097 pg_catalog | Schema |
1098 pg_temp | Schema |
1099 pg_toast | Schema |
1100 information_schema | Schema |
1101 ");
1102 }
1103
1104 #[test]
1105 fn completion_select_without_from() {
1106 assert_snapshot!(completions("
1107create table t (a int);
1108select $0;
1109"), @r"
1110 label | kind | detail
1111 --------------------+---------+--------
1112 except | Snippet |
1113 fetch | Snippet |
1114 for | Snippet |
1115 from | Snippet |
1116 group by | Snippet |
1117 having | Snippet |
1118 intersect | Snippet |
1119 limit | Snippet |
1120 offset | Snippet |
1121 order by | Snippet |
1122 t | Table |
1123 union | Snippet |
1124 where | Snippet |
1125 window | Snippet |
1126 public | Schema |
1127 pg_catalog | Schema |
1128 pg_temp | Schema |
1129 pg_toast | Schema |
1130 information_schema | Schema |
1131 ");
1132 }
1133
1134 #[test]
1135 fn completion_after_select() {
1136 assert_snapshot!(completions("
1137create table t(a text, b int);
1138create function f() returns text as 'select 1::text' language sql;
1139select $0 from t;
1140"), @r"
1141 label | kind | detail
1142 --------------------+----------+-------------------------
1143 a | Column | text
1144 b | Column | int
1145 t | Table |
1146 f() | Function | public.f() returns text
1147 * | Operator |
1148 public | Schema |
1149 pg_catalog | Schema |
1150 pg_temp | Schema |
1151 pg_toast | Schema |
1152 information_schema | Schema |
1153 ");
1154 }
1155
1156 #[test]
1157 fn completion_after_select_create_table_inherits_builtin() {
1158 assert_snapshot!(completions("
1159-- include-builtins
1160create table t ()
1161inherits (information_schema.sql_features);
1162select $0 from t;
1163"), @"
1164 label | kind | detail
1165 --------------------+----------+----------------
1166 comments | Column | character_data
1167 feature_id | Column | character_data
1168 feature_name | Column | character_data
1169 is_supported | Column | yes_or_no
1170 is_verified_by | Column | character_data
1171 sub_feature_id | Column | character_data
1172 sub_feature_name | Column | character_data
1173 t | Table |
1174 * | Operator |
1175 public | Schema |
1176 pg_catalog | Schema |
1177 pg_temp | Schema |
1178 pg_toast | Schema |
1179 information_schema | Schema |
1180 ");
1181 }
1182
1183 #[test]
1184 fn completion_after_select_create_table_inherits_create_table_as() {
1185 assert_snapshot!(completions("
1186create table parent as select 1 a, 'x'::text b;
1187create table child (c int) inherits (parent);
1188select $0 from child;
1189"), @"
1190 label | kind | detail
1191 --------------------+----------+---------
1192 a | Column | integer
1193 b | Column | text
1194 c | Column | int
1195 child | Table |
1196 parent | Table |
1197 * | Operator |
1198 public | Schema |
1199 pg_catalog | Schema |
1200 pg_temp | Schema |
1201 pg_toast | Schema |
1202 information_schema | Schema |
1203 ");
1204 }
1205
1206 #[test]
1207 fn completion_after_select_create_table_like_select_into() {
1208 assert_snapshot!(completions("
1209select 1 a, 'x'::text b into parent;
1210create table child (like parent);
1211select $0 from child;
1212"), @"
1213 label | kind | detail
1214 --------------------+----------+---------
1215 a | Column | integer
1216 b | Column | text
1217 child | Table |
1218 parent | Table |
1219 * | Operator |
1220 public | Schema |
1221 pg_catalog | Schema |
1222 pg_temp | Schema |
1223 pg_toast | Schema |
1224 information_schema | Schema |
1225 ");
1226 }
1227
1228 #[test]
1229 fn completion_after_select_select_into() {
1230 assert_snapshot!(completions("
1231select 1 a, 'x'::text b into t;
1232select $0 from t;
1233"), @"
1234 label | kind | detail
1235 --------------------+----------+---------
1236 a | Column | integer
1237 b | Column | text
1238 t | Table |
1239 * | Operator |
1240 public | Schema |
1241 pg_catalog | Schema |
1242 pg_temp | Schema |
1243 pg_toast | Schema |
1244 information_schema | Schema |
1245 ");
1246 }
1247
1248 #[test]
1249 fn completion_select_table_qualified() {
1250 assert_snapshot!(completions("
1251create table t (c int);
1252select t.$0 from t;
1253"), @r"
1254 label | kind | detail
1255 -------+----------+--------
1256 c | Column | int
1257 * | Operator |
1258 ");
1259 }
1260
1261 #[test]
1262 fn completion_after_select_with_cte() {
1263 assert_snapshot!(completions("
1264with t as (select 1 a)
1265select $0 from t;
1266"), @r"
1267 label | kind | detail
1268 --------------------+----------+---------
1269 a | Column | integer
1270 * | Operator |
1271 public | Schema |
1272 pg_catalog | Schema |
1273 pg_temp | Schema |
1274 pg_toast | Schema |
1275 information_schema | Schema |
1276 ");
1277 }
1278
1279 #[test]
1280 fn completion_after_select_with_cte_alias_column_list() {
1281 assert_snapshot!(completions("
1282with t as (select 1 a, 2 b, 3 c)
1283select $0 from t as u(x, y);
1284"), @r"
1285 label | kind | detail
1286 --------------------+----------+---------
1287 x | Column | integer
1288 y | Column | integer
1289 c | Column | integer
1290 * | Operator |
1291 public | Schema |
1292 pg_catalog | Schema |
1293 pg_temp | Schema |
1294 pg_toast | Schema |
1295 information_schema | Schema |
1296 ");
1297 }
1298
1299 #[test]
1300 fn completion_values_cte() {
1301 assert_snapshot!(completions("
1302with t as (values (1, 'foo', false))
1303select $0 from t;
1304"), @r"
1305 label | kind | detail
1306 --------------------+----------+---------
1307 column1 | Column | integer
1308 column2 | Column | text
1309 column3 | Column | boolean
1310 * | Operator |
1311 public | Schema |
1312 pg_catalog | Schema |
1313 pg_temp | Schema |
1314 pg_toast | Schema |
1315 information_schema | Schema |
1316 ");
1317 }
1318
1319 #[test]
1320 fn completion_values_subquery() {
1321 assert_snapshot!(completions("
1322select $0 from (values (1, 'foo', 1.5, false));
1323"), @r"
1324 label | kind | detail
1325 --------------------+----------+---------
1326 column1 | Column | integer
1327 column2 | Column | text
1328 column3 | Column | numeric
1329 column4 | Column | boolean
1330 * | Operator |
1331 public | Schema |
1332 pg_catalog | Schema |
1333 pg_temp | Schema |
1334 pg_toast | Schema |
1335 information_schema | Schema |
1336 ");
1337 }
1338
1339 #[test]
1340 fn completion_with_schema_qualifier() {
1341 assert_snapshot!(completions("
1342create function f() returns int8 as 'select 1' language sql;
1343create function foo.b() returns int8 as 'select 2' language sql;
1344select public.$0;
1345"), @r"
1346 label | kind | detail
1347 -------+----------+-------------------------
1348 f() | Function | public.f() returns int8
1349 ");
1350 }
1351
1352 #[test]
1353 fn completion_truncate_with_schema_qualifier() {
1354 assert_snapshot!(completions("
1355create table users (id int);
1356truncate public.$0;
1357"), @r"
1358 label | kind | detail
1359 -------+-------+--------
1360 users | Table |
1361 ");
1362 }
1363
1364 #[test]
1365 fn completion_after_delete_from() {
1366 assert_snapshot!(completions("
1367create table users (id int);
1368delete from $0;
1369"), @r"
1370 label | kind | detail
1371 --------------------+--------+--------
1372 users | Table |
1373 public | Schema |
1374 pg_catalog | Schema |
1375 pg_temp | Schema |
1376 pg_toast | Schema |
1377 information_schema | Schema |
1378 ");
1379 }
1380
1381 #[test]
1382 fn completion_delete_clauses() {
1383 assert_snapshot!(completions("
1384create table t (id int);
1385delete from t $0;
1386"), @r"
1387 label | kind | detail
1388 -----------+---------+--------
1389 returning | Snippet |
1390 using | Snippet |
1391 where | Snippet |
1392 ");
1393 }
1394
1395 #[test]
1396 fn completion_delete_where_expr() {
1397 assert_snapshot!(completions("
1398create table t (id int, name text);
1399create function is_active() returns bool as 'select true' language sql;
1400delete from t where $0;
1401"), @r"
1402 label | kind | detail
1403 -------------+----------+---------------------------------
1404 id | Column | int
1405 name | Column | text
1406 t | Table |
1407 is_active() | Function | public.is_active() returns bool
1408 ")
1409 }
1410
1411 #[test]
1412 fn completion_delete_returning_expr() {
1413 assert_snapshot!(completions("
1414create table t (id int, name text);
1415delete from t returning $0;
1416"), @r"
1417 label | kind | detail
1418 -------+--------+--------
1419 id | Column | int
1420 name | Column | text
1421 t | Table |
1422 ");
1423 }
1424
1425 #[test]
1426 fn completion_delete_where_qualified() {
1427 assert_snapshot!(completions("
1428-- different type than the table, so we shouldn't show this
1429create function b(diff_type) returns int8
1430 as 'select 1'
1431 language sql;
1432create function f(t) returns int8
1433 as 'select 1'
1434 language sql;
1435create table t (a int, b text);
1436delete from t where t.$0;
1437"), @r"
1438 label | kind | detail
1439 -------+----------+--------
1440 a | Column | int
1441 b | Column | text
1442 f | Function |
1443 ");
1444 }
1445
1446 #[test]
1447 fn completion_select_clauses() {
1448 assert_snapshot!(completions("
1449with t as (select 1 a)
1450select a from t $0;
1451"), @r"
1452 label | kind | detail
1453 -----------+---------+--------
1454 except | Snippet |
1455 fetch | Snippet |
1456 for | Snippet |
1457 group by | Snippet |
1458 having | Snippet |
1459 intersect | Snippet |
1460 limit | Snippet |
1461 offset | Snippet |
1462 order by | Snippet |
1463 union | Snippet |
1464 where | Snippet |
1465 window | Snippet |
1466 ");
1467 }
1468
1469 #[test]
1470 fn completion_select_clauses_simple() {
1471 assert_snapshot!(completions("
1472select 1 from t $0;
1473"), @r"
1474 label | kind | detail
1475 -----------+---------+--------
1476 except | Snippet |
1477 fetch | Snippet |
1478 for | Snippet |
1479 group by | Snippet |
1480 having | Snippet |
1481 intersect | Snippet |
1482 limit | Snippet |
1483 offset | Snippet |
1484 order by | Snippet |
1485 union | Snippet |
1486 where | Snippet |
1487 window | Snippet |
1488 ");
1489 }
1490
1491 #[test]
1492 fn completion_select_group_by_expr() {
1493 assert_snapshot!(completions("
1494with t as (select 1 a)
1495select a from t group by $0;
1496"), @r"
1497 label | kind | detail
1498 -------+--------+---------
1499 a | Column | integer
1500 t | Table |
1501 ");
1502 }
1503
1504 #[test]
1505 fn completion_select_where_expr() {
1506 assert_snapshot!(completions("
1507create table t (id int, name text);
1508select * from t where $0;
1509"), @r"
1510 label | kind | detail
1511 -------+--------+--------
1512 id | Column | int
1513 name | Column | text
1514 t | Table |
1515 ");
1516 }
1517
1518 #[test]
1519 fn completion_select_limit() {
1520 assert_snapshot!(completions("
1521create function get_limit() returns int as 'select 10' language sql;
1522select 1 from t limit $0;
1523"), @r"
1524 label | kind | detail
1525 -------------+----------+--------------------------------
1526 all | Keyword |
1527 get_limit() | Function | public.get_limit() returns int
1528 ");
1529 }
1530
1531 #[test]
1532 fn completion_select_offset() {
1533 assert_snapshot!(completions("
1534create function get_offset() returns int as 'select 10' language sql;
1535select 1 from t offset $0;
1536"), @r"
1537 label | kind | detail
1538 --------------+----------+---------------------------------
1539 get_offset() | Function | public.get_offset() returns int
1540 ");
1541 }
1542}