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 (offset, sql) = fixture(sql);
942        let db = Database::default();
943        let file = File::new(&db, sql.into());
944        let items = completion(&db, file, offset);
945        assert!(
946            !items.is_empty(),
947            "No completions found. If this was intended, use `completions_not_found` instead."
948        );
949        format_items(items)
950    }
951
952    fn completions_not_found(sql: &str) {
953        let (offset, sql) = fixture(sql);
954        let db = Database::default();
955        let file = File::new(&db, sql.into());
956        let items = completion(&db, file, offset);
957        assert_eq!(
958            items,
959            vec![],
960            "Completions found. If this was unintended, use `completions` instead."
961        )
962    }
963
964    fn format_items(mut items: Vec<super::CompletionItem>) -> String {
965        items.sort_by_key(|a| a.sort_text());
966
967        let rows: Vec<Vec<String>> = items
968            .into_iter()
969            .map(|item| {
970                vec![
971                    item.label,
972                    format!("{:?}", item.kind),
973                    item.detail.unwrap_or_default(),
974                ]
975            })
976            .collect();
977
978        let mut builder = Builder::default();
979        builder.push_record(["label", "kind", "detail"]);
980        for row in rows {
981            builder.push_record(row);
982        }
983
984        let mut table = builder.build();
985        table.with(Style::psql());
986        table.to_string()
987    }
988
989    #[test]
990    fn completion_at_start() {
991        assert_snapshot!(completions("$0"), @r"
992         label       | kind    | detail 
993        -------------+---------+--------
994         delete from | Snippet |        
995         select      | Snippet |        
996         table       | Snippet |        
997         truncate    | Snippet |
998        ");
999    }
1000
1001    #[test]
1002    fn completion_at_top_level() {
1003        assert_snapshot!(completions("
1004create table t(a int);
1005$0
1006"), @r"
1007         label       | kind    | detail 
1008        -------------+---------+--------
1009         delete from | Snippet |        
1010         select      | Snippet |        
1011         table       | Snippet |        
1012         truncate    | Snippet |
1013        ");
1014    }
1015
1016    #[test]
1017    fn completion_in_string() {
1018        completions_not_found("select '$0';");
1019    }
1020
1021    #[test]
1022    fn completion_in_comment() {
1023        completions_not_found("-- $0 ");
1024    }
1025
1026    #[test]
1027    fn completion_after_truncate() {
1028        assert_snapshot!(completions("
1029create table users (id int);
1030truncate $0;
1031"), @r"
1032         label              | kind   | detail 
1033        --------------------+--------+--------
1034         users              | Table  |        
1035         public             | Schema |        
1036         pg_catalog         | Schema |        
1037         pg_temp            | Schema |        
1038         pg_toast           | Schema |        
1039         information_schema | Schema |
1040        ");
1041    }
1042
1043    #[test]
1044    fn completion_table_at_top_level() {
1045        assert_snapshot!(completions("$0"), @r"
1046         label       | kind    | detail 
1047        -------------+---------+--------
1048         delete from | Snippet |        
1049         select      | Snippet |        
1050         table       | Snippet |        
1051         truncate    | Snippet |
1052        ");
1053    }
1054
1055    #[test]
1056    fn completion_table_nested() {
1057        assert_snapshot!(completions("select * from ($0)"), @r"
1058         label       | kind    | detail 
1059        -------------+---------+--------
1060         delete from | Snippet |        
1061         select      | Snippet |        
1062         table       | Snippet |        
1063         truncate    | Snippet |
1064        ");
1065    }
1066
1067    #[test]
1068    fn completion_after_table() {
1069        assert_snapshot!(completions("
1070create table users (id int);
1071table $0;
1072"), @r"
1073         label              | kind   | detail 
1074        --------------------+--------+--------
1075         users              | Table  |        
1076         public             | Schema |        
1077         pg_catalog         | Schema |        
1078         pg_temp            | Schema |        
1079         pg_toast           | Schema |        
1080         information_schema | Schema |
1081        ");
1082    }
1083
1084    #[test]
1085    fn completion_select_without_from() {
1086        assert_snapshot!(completions("
1087create table t (a int);
1088select $0;
1089"), @r"
1090         label              | kind    | detail 
1091        --------------------+---------+--------
1092         except             | Snippet |        
1093         fetch              | Snippet |        
1094         for                | Snippet |        
1095         from               | Snippet |        
1096         group by           | Snippet |        
1097         having             | Snippet |        
1098         intersect          | Snippet |        
1099         limit              | Snippet |        
1100         offset             | Snippet |        
1101         order by           | Snippet |        
1102         t                  | Table   |        
1103         union              | Snippet |        
1104         where              | Snippet |        
1105         window             | Snippet |        
1106         public             | Schema  |        
1107         pg_catalog         | Schema  |        
1108         pg_temp            | Schema  |        
1109         pg_toast           | Schema  |        
1110         information_schema | Schema  |
1111        ");
1112    }
1113
1114    #[test]
1115    fn completion_after_select() {
1116        assert_snapshot!(completions("
1117create table t(a text, b int);
1118create function f() returns text as 'select 1::text' language sql;
1119select $0 from t;
1120"), @r"
1121         label              | kind     | detail                  
1122        --------------------+----------+-------------------------
1123         a                  | Column   | text                    
1124         b                  | Column   | int                     
1125         t                  | Table    |                         
1126         f()                | Function | public.f() returns text 
1127         *                  | Operator |                         
1128         public             | Schema   |                         
1129         pg_catalog         | Schema   |                         
1130         pg_temp            | Schema   |                         
1131         pg_toast           | Schema   |                         
1132         information_schema | Schema   |
1133        ");
1134    }
1135
1136    #[test]
1137    fn completion_select_table_qualified() {
1138        assert_snapshot!(completions("
1139create table t (c int);
1140select t.$0 from t;
1141"), @r"
1142         label | kind     | detail 
1143        -------+----------+--------
1144         c     | Column   | int    
1145         *     | Operator |
1146        ");
1147    }
1148
1149    #[test]
1150    fn completion_after_select_with_cte() {
1151        assert_snapshot!(completions("
1152with t as (select 1 a)
1153select $0 from t;
1154"), @r"
1155         label              | kind     | detail  
1156        --------------------+----------+---------
1157         a                  | Column   | integer 
1158         *                  | Operator |         
1159         public             | Schema   |         
1160         pg_catalog         | Schema   |         
1161         pg_temp            | Schema   |         
1162         pg_toast           | Schema   |         
1163         information_schema | Schema   |
1164        ");
1165    }
1166
1167    #[test]
1168    fn completion_after_select_with_cte_alias_column_list() {
1169        assert_snapshot!(completions("
1170with t as (select 1 a, 2 b, 3 c)
1171select $0 from t as u(x, y);
1172"), @r"
1173         label              | kind     | detail  
1174        --------------------+----------+---------
1175         x                  | Column   | integer 
1176         y                  | Column   | integer 
1177         c                  | Column   | integer 
1178         *                  | Operator |         
1179         public             | Schema   |         
1180         pg_catalog         | Schema   |         
1181         pg_temp            | Schema   |         
1182         pg_toast           | Schema   |         
1183         information_schema | Schema   |
1184        ");
1185    }
1186
1187    #[test]
1188    fn completion_values_cte() {
1189        assert_snapshot!(completions("
1190with t as (values (1, 'foo', false))
1191select $0 from t;
1192"), @r"
1193         label              | kind     | detail  
1194        --------------------+----------+---------
1195         column1            | Column   | integer 
1196         column2            | Column   | text    
1197         column3            | Column   | boolean 
1198         *                  | Operator |         
1199         public             | Schema   |         
1200         pg_catalog         | Schema   |         
1201         pg_temp            | Schema   |         
1202         pg_toast           | Schema   |         
1203         information_schema | Schema   |
1204        ");
1205    }
1206
1207    #[test]
1208    fn completion_values_subquery() {
1209        assert_snapshot!(completions("
1210select $0 from (values (1, 'foo', 1.5, false));
1211"), @r"
1212         label              | kind     | detail  
1213        --------------------+----------+---------
1214         column1            | Column   | integer 
1215         column2            | Column   | text    
1216         column3            | Column   | numeric 
1217         column4            | Column   | boolean 
1218         *                  | Operator |         
1219         public             | Schema   |         
1220         pg_catalog         | Schema   |         
1221         pg_temp            | Schema   |         
1222         pg_toast           | Schema   |         
1223         information_schema | Schema   |
1224        ");
1225    }
1226
1227    #[test]
1228    fn completion_with_schema_qualifier() {
1229        assert_snapshot!(completions("
1230create function f() returns int8 as 'select 1' language sql;
1231create function foo.b() returns int8 as 'select 2' language sql;
1232select public.$0;
1233"), @r"
1234         label | kind     | detail                  
1235        -------+----------+-------------------------
1236         f()   | Function | public.f() returns int8
1237        ");
1238    }
1239
1240    #[test]
1241    fn completion_truncate_with_schema_qualifier() {
1242        assert_snapshot!(completions("
1243create table users (id int);
1244truncate public.$0;
1245"), @r"
1246         label | kind  | detail 
1247        -------+-------+--------
1248         users | Table |
1249        ");
1250    }
1251
1252    #[test]
1253    fn completion_after_delete_from() {
1254        assert_snapshot!(completions("
1255create table users (id int);
1256delete from $0;
1257"), @r"
1258         label              | kind   | detail 
1259        --------------------+--------+--------
1260         users              | Table  |        
1261         public             | Schema |        
1262         pg_catalog         | Schema |        
1263         pg_temp            | Schema |        
1264         pg_toast           | Schema |        
1265         information_schema | Schema |
1266        ");
1267    }
1268
1269    #[test]
1270    fn completion_delete_clauses() {
1271        assert_snapshot!(completions("
1272create table t (id int);
1273delete from t $0;
1274"), @r"
1275         label     | kind    | detail 
1276        -----------+---------+--------
1277         returning | Snippet |        
1278         using     | Snippet |        
1279         where     | Snippet |
1280        ");
1281    }
1282
1283    #[test]
1284    fn completion_delete_where_expr() {
1285        assert_snapshot!(completions("
1286create table t (id int, name text);
1287create function is_active() returns bool as 'select true' language sql;
1288delete from t where $0;
1289"), @r"
1290         label       | kind     | detail                          
1291        -------------+----------+---------------------------------
1292         id          | Column   | int                             
1293         name        | Column   | text                            
1294         t           | Table    |                                 
1295         is_active() | Function | public.is_active() returns bool
1296        ")
1297    }
1298
1299    #[test]
1300    fn completion_delete_returning_expr() {
1301        assert_snapshot!(completions("
1302create table t (id int, name text);
1303delete from t returning $0;
1304"), @r"
1305         label | kind   | detail 
1306        -------+--------+--------
1307         id    | Column | int    
1308         name  | Column | text   
1309         t     | Table  |
1310        ");
1311    }
1312
1313    #[test]
1314    fn completion_delete_where_qualified() {
1315        assert_snapshot!(completions("
1316-- different type than the table, so we shouldn't show this
1317create function b(diff_type) returns int8
1318  as 'select 1'
1319  language sql;
1320create function f(t) returns int8
1321  as 'select 1'
1322  language sql;
1323create table t (a int, b text);
1324delete from t where t.$0;
1325"), @r"
1326         label | kind     | detail 
1327        -------+----------+--------
1328         a     | Column   | int    
1329         b     | Column   | text   
1330         f     | Function |
1331        ");
1332    }
1333
1334    #[test]
1335    fn completion_select_clauses() {
1336        assert_snapshot!(completions("
1337with t as (select 1 a)
1338select a from t $0;
1339"), @r"
1340         label     | kind    | detail 
1341        -----------+---------+--------
1342         except    | Snippet |        
1343         fetch     | Snippet |        
1344         for       | Snippet |        
1345         group by  | Snippet |        
1346         having    | Snippet |        
1347         intersect | Snippet |        
1348         limit     | Snippet |        
1349         offset    | Snippet |        
1350         order by  | Snippet |        
1351         union     | Snippet |        
1352         where     | Snippet |        
1353         window    | Snippet |
1354        ");
1355    }
1356
1357    #[test]
1358    fn completion_select_clauses_simple() {
1359        assert_snapshot!(completions("
1360select 1 from t $0;
1361"), @r"
1362         label     | kind    | detail 
1363        -----------+---------+--------
1364         except    | Snippet |        
1365         fetch     | Snippet |        
1366         for       | Snippet |        
1367         group by  | Snippet |        
1368         having    | Snippet |        
1369         intersect | Snippet |        
1370         limit     | Snippet |        
1371         offset    | Snippet |        
1372         order by  | Snippet |        
1373         union     | Snippet |        
1374         where     | Snippet |        
1375         window    | Snippet |
1376        ");
1377    }
1378
1379    #[test]
1380    fn completion_select_group_by_expr() {
1381        assert_snapshot!(completions("
1382with t as (select 1 a)
1383select a from t group by $0;
1384"), @r"
1385         label | kind   | detail  
1386        -------+--------+---------
1387         a     | Column | integer 
1388         t     | Table  |
1389        ");
1390    }
1391
1392    #[test]
1393    fn completion_select_where_expr() {
1394        assert_snapshot!(completions("
1395create table t (id int, name text);
1396select * from t where $0;
1397"), @r"
1398         label | kind   | detail 
1399        -------+--------+--------
1400         id    | Column | int    
1401         name  | Column | text   
1402         t     | Table  |
1403        ");
1404    }
1405
1406    #[test]
1407    fn completion_select_limit() {
1408        assert_snapshot!(completions("
1409create function get_limit() returns int as 'select 10' language sql;
1410select 1 from t limit $0;
1411"), @r"
1412         label       | kind     | detail                         
1413        -------------+----------+--------------------------------
1414         all         | Keyword  |                                
1415         get_limit() | Function | public.get_limit() returns int
1416        ");
1417    }
1418
1419    #[test]
1420    fn completion_select_offset() {
1421        assert_snapshot!(completions("
1422create function get_offset() returns int as 'select 10' language sql;
1423select 1 from t offset $0;
1424"), @r"
1425         label        | kind     | detail                          
1426        --------------+----------+---------------------------------
1427         get_offset() | Function | public.get_offset() returns int
1428        ");
1429    }
1430}