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