Skip to main content

squawk_ide/
completion.rs

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