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.as_ref(), 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.as_ref(), 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.as_ref(), 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.as_ref(), 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)
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 =
375                    collect::create_table_as_columns_with_types(db, file, &create_table_as);
376                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
377                    label: name.to_string(),
378                    kind: CompletionItemKind::Column,
379                    detail: ty.map(|t| t.to_string()),
380                    insert_text: None,
381                    insert_text_format: None,
382                    trigger_completion_after_insert: false,
383                    sort_text: None,
384                }));
385            }
386            Some(ast_nav::ParentSouce::CreateView(create_view)) => {
387                let columns = collect::view_like_columns_with_types(db, file, &create_view);
388                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
389                    label: name.to_string(),
390                    kind: CompletionItemKind::Column,
391                    detail: ty.map(|t| t.to_string()),
392                    insert_text: None,
393                    insert_text_format: None,
394                    trigger_completion_after_insert: false,
395                    sort_text: None,
396                }));
397            }
398            Some(ast_nav::ParentSouce::Alias(alias)) => {
399                let alias_columns: Vec<Name> = alias
400                    .column_list()
401                    .into_iter()
402                    .flat_map(|column_list| column_list.columns())
403                    .filter_map(|column| column.name().map(|name| Name::from_node(&name)))
404                    .collect();
405
406                let base_columns = alias_base_columns_with_types(db, file, &syntax_root, &alias);
407
408                for (idx, alias_column) in alias_columns.iter().enumerate() {
409                    completions.push(CompletionItem {
410                        label: alias_column.to_string(),
411                        kind: CompletionItemKind::Column,
412                        detail: base_columns.get(idx).and_then(|(_, ty)| ty.clone()),
413                        insert_text: None,
414                        insert_text_format: None,
415                        trigger_completion_after_insert: false,
416                        sort_text: Some(format!("{idx:04}")),
417                    });
418                }
419
420                completions.extend(
421                    base_columns
422                        .into_iter()
423                        .skip(alias_columns.len())
424                        .enumerate()
425                        .map(|(idx, (name, ty))| CompletionItem {
426                            label: name.to_string(),
427                            kind: CompletionItemKind::Column,
428                            detail: ty,
429                            insert_text: None,
430                            insert_text_format: None,
431                            trigger_completion_after_insert: false,
432                            sort_text: Some(format!("{:04}", idx + alias_columns.len())),
433                        }),
434                );
435            }
436            Some(ast_nav::ParentSouce::ParenSelect(paren_select)) => {
437                let columns = collect::paren_select_columns_with_types(db, file, &paren_select);
438                completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
439                    label: name.to_string(),
440                    kind: CompletionItemKind::Column,
441                    detail: ty.map(|t| t.to_string()),
442                    insert_text: None,
443                    insert_text_format: None,
444                    trigger_completion_after_insert: false,
445                    sort_text: None,
446                }));
447            }
448            None => {}
449        }
450    }
451    completions
452}
453
454fn alias_base_columns_with_types(
455    db: &dyn Db,
456    file: File,
457    syntax_root: &SyntaxNode,
458    alias: &ast::Alias,
459) -> Vec<(Name, Option<String>)> {
460    let Some(from_item) = alias.syntax().ancestors().find_map(ast::FromItem::cast) else {
461        return vec![];
462    };
463    let Some(table_ptr) = resolve::table_ptr_from_from_item(db, file, &from_item) else {
464        return vec![];
465    };
466
467    let table_node = table_ptr.to_node(syntax_root);
468
469    match ast_nav::parent_source(&table_node) {
470        Some(ast_nav::ParentSouce::CreateTable(create_table)) => {
471            collect::table_columns(db, file, &create_table)
472                .into_iter()
473                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
474                .collect()
475        }
476        Some(ast_nav::ParentSouce::WithTable(with_table)) => {
477            collect::with_table_columns_with_types(db, file, with_table)
478                .into_iter()
479                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
480                .collect()
481        }
482        Some(ast_nav::ParentSouce::CreateView(create_view)) => {
483            collect::view_like_columns_with_types(db, file, &create_view)
484                .into_iter()
485                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
486                .collect()
487        }
488        Some(ast_nav::ParentSouce::ParenSelect(paren_select)) => {
489            collect::paren_select_columns_with_types(db, file, &paren_select)
490                .into_iter()
491                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
492                .collect()
493        }
494        Some(ast_nav::ParentSouce::CreateTableAs(create_table_as)) => {
495            collect::create_table_as_columns_with_types(db, file, &create_table_as)
496                .into_iter()
497                .map(|(name, ty)| (name, ty.map(|t| t.to_string())))
498                .collect()
499        }
500        Some(ast_nav::ParentSouce::Alias(_)) | None => vec![],
501    }
502}
503
504fn schema_completions(binder: &binder::Binder) -> Vec<CompletionItem> {
505    let builtin_schemas = [
506        "public",
507        "pg_catalog",
508        "pg_temp",
509        "pg_toast",
510        "information_schema",
511    ];
512    let mut completions: Vec<CompletionItem> = builtin_schemas
513        .into_iter()
514        .enumerate()
515        .map(|(i, name)| CompletionItem {
516            label: name.to_string(),
517            kind: CompletionItemKind::Schema,
518            detail: None,
519            insert_text: None,
520            insert_text_format: None,
521            trigger_completion_after_insert: false,
522            sort_text: Some(format!("{i}")),
523        })
524        .collect();
525
526    for name in binder.all_symbols_by_kind(SymbolKind::Schema, None) {
527        completions.push(CompletionItem {
528            label: name.to_string(),
529            kind: CompletionItemKind::Schema,
530            detail: None,
531            insert_text: None,
532            insert_text_format: None,
533            trigger_completion_after_insert: false,
534            sort_text: None,
535        });
536    }
537
538    completions
539}
540
541fn table_completions(file: &ast::SourceFile, token: &SyntaxToken) -> Vec<CompletionItem> {
542    let binder = binder::bind(file);
543    let schema = schema_qualifier_at_token(token);
544    let tables = binder.all_symbols_by_kind(SymbolKind::Table, schema.as_ref());
545    let mut completions: Vec<CompletionItem> = tables
546        .into_iter()
547        .map(|name| CompletionItem {
548            label: name.to_string(),
549            kind: CompletionItemKind::Table,
550            detail: None,
551            insert_text: None,
552            insert_text_format: None,
553            trigger_completion_after_insert: false,
554            sort_text: None,
555        })
556        .collect();
557
558    if schema.is_none() {
559        completions.extend(schema_completions(&binder));
560    }
561
562    completions
563}
564
565fn delete_clauses_completions(
566    file: &ast::SourceFile,
567    delete: &ast::Delete,
568    token: &SyntaxToken,
569) -> Vec<CompletionItem> {
570    let mut completions = vec![];
571
572    // `delete from $0`
573    if token.kind() == SyntaxKind::FROM_KW {
574        return table_completions(file, token);
575    }
576
577    if delete.using_clause().is_none() {
578        completions.push(CompletionItem {
579            label: "using".to_owned(),
580            kind: CompletionItemKind::Snippet,
581            detail: None,
582            insert_text: Some("using $0".to_owned()),
583            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
584            trigger_completion_after_insert: true,
585            sort_text: None,
586        });
587    }
588
589    if delete.where_clause().is_none() {
590        completions.push(CompletionItem {
591            label: "where".to_owned(),
592            kind: CompletionItemKind::Snippet,
593            detail: None,
594            insert_text: Some("where $0".to_owned()),
595            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
596            trigger_completion_after_insert: true,
597            sort_text: None,
598        });
599    }
600
601    if delete.returning_clause().is_none() {
602        completions.push(CompletionItem {
603            label: "returning".to_owned(),
604            kind: CompletionItemKind::Snippet,
605            detail: None,
606            insert_text: Some("returning $0".to_owned()),
607            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
608            trigger_completion_after_insert: true,
609            sort_text: None,
610        });
611    }
612
613    completions
614}
615
616fn delete_expr_completions(
617    db: &dyn Db,
618    file: File,
619    source_file: &ast::SourceFile,
620    delete: &ast::Delete,
621    token: &SyntaxToken,
622) -> Vec<CompletionItem> {
623    let binder = binder::bind(source_file);
624    let mut completions = vec![];
625
626    let Some(path) = delete.relation_name().and_then(|r| r.path()) else {
627        return completions;
628    };
629
630    let Some(delete_table_name) = name::table_name(&path) else {
631        return completions;
632    };
633
634    let has_table_qualifier = qualifier_at_token(token).is_some_and(|q| q == delete_table_name);
635    let schema = schema_qualifier_at_token(token);
636    let position = token.text_range().start();
637
638    if has_table_qualifier {
639        let functions = binder.functions_with_single_param(&delete_table_name);
640        completions.extend(functions.into_iter().map(|name| CompletionItem {
641            label: name.to_string(),
642            kind: CompletionItemKind::Function,
643            detail: function_detail(db, file, name, schema.as_ref(), position),
644            insert_text: None,
645            insert_text_format: None,
646            trigger_completion_after_insert: false,
647            sort_text: None,
648        }));
649    } else {
650        let functions = binder.all_symbols_by_kind(SymbolKind::Function, None);
651        completions.extend(functions.into_iter().map(|name| CompletionItem {
652            label: format!("{name}()"),
653            kind: CompletionItemKind::Function,
654            detail: function_detail(db, file, name, schema.as_ref(), position),
655            insert_text: None,
656            insert_text_format: None,
657            trigger_completion_after_insert: false,
658            sort_text: None,
659        }));
660
661        completions.push(CompletionItem {
662            label: delete_table_name.to_string(),
663            kind: CompletionItemKind::Table,
664            detail: None,
665            insert_text: None,
666            insert_text_format: None,
667            trigger_completion_after_insert: false,
668            sort_text: None,
669        });
670    }
671
672    let schema = name::schema_name(&path);
673    if let Some(table_ptr) = binder.lookup_with(
674        &delete_table_name,
675        SymbolKind::Table,
676        position,
677        schema.as_ref(),
678    ) && let Some(create_table) = table_ptr
679        .to_node(source_file.syntax())
680        .ancestors()
681        .find_map(ast::CreateTableLike::cast)
682    {
683        let columns = collect::table_columns(db, file, &create_table);
684        completions.extend(columns.into_iter().map(|(name, ty)| CompletionItem {
685            label: name.to_string(),
686            kind: CompletionItemKind::Column,
687            detail: ty.map(|t| t.to_string()),
688            insert_text: None,
689            insert_text_format: None,
690            trigger_completion_after_insert: false,
691            sort_text: None,
692        }));
693    }
694
695    completions
696}
697
698fn table_name_from_from_item(from_item: &ast::FromItem) -> Option<Name> {
699    if let Some(alias) = from_item.alias()
700        && let Some(alias_name) = alias.name()
701    {
702        return Some(Name::from_node(&alias_name));
703    }
704    if let Some(name_ref) = from_item.name_ref() {
705        return Some(Name::from_node(&name_ref));
706    }
707    None
708}
709
710fn qualifier_at_token(token: &SyntaxToken) -> Option<Name> {
711    let qualifier_token = if token.kind() == SyntaxKind::DOT {
712        token.prev_token()
713    } else if token.kind() == SyntaxKind::IDENT
714        && let Some(prev) = token.prev_token()
715        && prev.kind() == SyntaxKind::DOT
716    {
717        prev.prev_token()
718    } else {
719        None
720    };
721
722    qualifier_token
723        .filter(|tk| tk.kind() == SyntaxKind::IDENT)
724        .map(|tk| Name::from_string(tk.text().to_string()))
725}
726
727#[derive(Debug)]
728enum CompletionContext {
729    TableOnly,
730    Default,
731    SelectClause(ast::SelectClause),
732    SelectClauses(ast::Select),
733    SelectExpr(ast::Select),
734    LimitClause,
735    OffsetClause,
736    DeleteClauses(ast::Delete),
737    DeleteExpr(ast::Delete),
738}
739
740fn completion_context(token: &SyntaxToken) -> CompletionContext {
741    if let Some(node) = token.parent() {
742        let mut inside_delete_clause = false;
743        let mut inside_from_item = false;
744        let mut inside_paren_expr = false;
745        let mut inside_select_expr_clause = false;
746        let mut inside_limit_clause = false;
747        let mut inside_offset_clause = false;
748        for a in node.ancestors() {
749            if ast::Truncate::can_cast(a.kind()) || ast::Table::can_cast(a.kind()) {
750                return CompletionContext::TableOnly;
751            }
752            if ast::WhereClause::can_cast(a.kind())
753                || ast::UsingClause::can_cast(a.kind())
754                || ast::ReturningClause::can_cast(a.kind())
755            {
756                inside_delete_clause = true;
757            }
758            if ast::LimitClause::can_cast(a.kind()) {
759                inside_limit_clause = true;
760            }
761            if ast::OffsetClause::can_cast(a.kind()) {
762                inside_offset_clause = true;
763            }
764            if ast::WhereClause::can_cast(a.kind())
765                || ast::GroupByClause::can_cast(a.kind())
766                || ast::HavingClause::can_cast(a.kind())
767                || ast::OrderByClause::can_cast(a.kind())
768            {
769                inside_select_expr_clause = true;
770            }
771            if ast::FromItem::can_cast(a.kind()) {
772                inside_from_item = true;
773            }
774            if ast::ParenExpr::can_cast(a.kind()) {
775                inside_paren_expr = true;
776            }
777            if let Some(delete) = ast::Delete::cast(a.clone()) {
778                if inside_delete_clause {
779                    return CompletionContext::DeleteExpr(delete);
780                }
781                if delete.relation_name().is_some() {
782                    return CompletionContext::DeleteClauses(delete);
783                }
784                return CompletionContext::TableOnly;
785            }
786            if let Some(select) = ast::Select::cast(a.clone()) {
787                if inside_limit_clause {
788                    return CompletionContext::LimitClause;
789                }
790                if inside_offset_clause {
791                    return CompletionContext::OffsetClause;
792                }
793                if inside_select_expr_clause {
794                    return CompletionContext::SelectExpr(select);
795                }
796                if inside_from_item && !inside_paren_expr && select.from_clause().is_some() {
797                    return CompletionContext::SelectClauses(select);
798                }
799            }
800            if let Some(select_clause) = ast::SelectClause::cast(a.clone()) {
801                return CompletionContext::SelectClause(select_clause);
802            }
803        }
804    }
805    CompletionContext::Default
806}
807
808fn token_at_offset(file: &ast::SourceFile, offset: TextSize) -> Option<SyntaxToken> {
809    let Some(mut token) = file.syntax().token_at_offset(offset).left_biased() else {
810        // empty file - definitely at top level
811        return None;
812    };
813    while token.kind() == SyntaxKind::WHITESPACE {
814        if let Some(tk) = token.prev_token() {
815            token = tk;
816        }
817    }
818    Some(token)
819}
820
821// In order to make completions, we do something similar to rust analyzer by
822// inserting an ident to make the parse tree parse in more cases.
823// Rust analyzer does fancier things for this, which we can investigate later.
824//
825// This helps us support `select t. from t`, which parses as `select t.from t`.
826// If we insert the ident we get, `select t.c from t`.
827fn file_with_completion_marker(file: &ast::SourceFile, offset: TextSize) -> ast::SourceFile {
828    let mut sql = file.syntax().text().to_string();
829    let offset = u32::from(offset) as usize;
830    let offset = offset.min(sql.len());
831    sql.insert_str(offset, COMPLETION_MARKER);
832    ast::SourceFile::parse(&sql).tree()
833}
834
835fn schema_qualifier_at_token(token: &SyntaxToken) -> Option<Schema> {
836    qualifier_at_token(token).map(Schema)
837}
838
839fn function_detail(
840    db: &dyn Db,
841    file: File,
842    function_name: &Name,
843    schema: Option<&Schema>,
844    position: TextSize,
845) -> Option<String> {
846    let binder = bind(db, file);
847    let source_file = parse(db, file).tree();
848    let create_function = binder
849        .lookup_with(function_name, SymbolKind::Function, position, schema)?
850        .to_node(source_file.syntax())
851        .ancestors()
852        .find_map(ast::CreateFunction::cast)?;
853    let path = create_function.path()?;
854    let (schema, function_name) = resolve::resolve_function_info(db, file, &path)?;
855
856    let param_list = create_function.param_list()?;
857    let params = param_list.syntax().text().to_string();
858
859    let ret_type = create_function.ret_type()?;
860    let return_type = ret_type.syntax().text().to_string();
861
862    Some(format!(
863        "{}.{}{} {}",
864        schema, function_name, params, return_type
865    ))
866}
867
868fn default_completions() -> Vec<CompletionItem> {
869    ["delete from", "select", "table", "truncate"]
870        .map(|stmt| CompletionItem {
871            label: stmt.to_owned(),
872            kind: CompletionItemKind::Snippet,
873            detail: None,
874            insert_text: Some(format!("{stmt} $0;")),
875            insert_text_format: Some(CompletionInsertTextFormat::Snippet),
876            trigger_completion_after_insert: true,
877            sort_text: None,
878        })
879        .into_iter()
880        .collect()
881}
882
883#[derive(Debug, Clone, Copy, PartialEq, Eq)]
884pub enum CompletionItemKind {
885    Keyword,
886    Table,
887    Column,
888    Function,
889    Schema,
890    Type,
891    Snippet,
892    Operator,
893}
894
895impl CompletionItemKind {
896    fn sort_prefix(self) -> &'static str {
897        match self {
898            Self::Column => "0",
899            Self::Keyword => "1",
900            Self::Table => "1",
901            Self::Type => "1",
902            Self::Snippet => "1",
903            Self::Function => "2",
904            Self::Operator => "8",
905            Self::Schema => "9",
906        }
907    }
908}
909
910impl CompletionItem {
911    pub fn sort_text(&self) -> String {
912        let prefix = self.kind.sort_prefix();
913        let suffix = self.sort_text.as_ref().unwrap_or(&self.label);
914        format!("{prefix}_{suffix}")
915    }
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq)]
919pub enum CompletionInsertTextFormat {
920    PlainText,
921    Snippet,
922}
923
924#[derive(Debug, Clone, PartialEq, Eq)]
925pub struct CompletionItem {
926    pub label: String,
927    pub kind: CompletionItemKind,
928    pub detail: Option<String>,
929    pub insert_text: Option<String>,
930    pub insert_text_format: Option<CompletionInsertTextFormat>,
931    pub trigger_completion_after_insert: bool,
932    pub sort_text: Option<String>,
933}
934
935#[cfg(test)]
936mod tests {
937    use super::completion;
938    use crate::db::{Database, File};
939    use crate::test_utils::Fixture;
940    use insta::assert_snapshot;
941    use tabled::builder::Builder;
942    use tabled::settings::Style;
943
944    #[must_use]
945    fn completions(sql: &str) -> String {
946        let fixture = Fixture::new(sql);
947        let offset = fixture.marker().offset();
948        let sql = fixture.sql();
949        let db = Database::default();
950        let file = File::new(&db, sql.into());
951        let items = completion(&db, file, offset);
952        assert!(
953            !items.is_empty(),
954            "No completions found. If this was intended, use `completions_not_found` instead."
955        );
956        format_items(items)
957    }
958
959    fn completions_not_found(sql: &str) {
960        let fixture = Fixture::new(sql);
961        let offset = fixture.marker().offset();
962        let sql = fixture.sql();
963        let db = Database::default();
964        let file = File::new(&db, sql.into());
965        let items = completion(&db, file, offset);
966        assert_eq!(
967            items,
968            vec![],
969            "Completions found. If this was unintended, use `completions` instead."
970        )
971    }
972
973    fn format_items(mut items: Vec<super::CompletionItem>) -> String {
974        items.sort_by_key(|a| a.sort_text());
975
976        let rows: Vec<Vec<String>> = items
977            .into_iter()
978            .map(|item| {
979                vec![
980                    item.label,
981                    format!("{:?}", item.kind),
982                    item.detail.unwrap_or_default(),
983                ]
984            })
985            .collect();
986
987        let mut builder = Builder::default();
988        builder.push_record(["label", "kind", "detail"]);
989        for row in rows {
990            builder.push_record(row);
991        }
992
993        let mut table = builder.build();
994        table.with(Style::psql());
995        table.to_string()
996    }
997
998    #[test]
999    fn completion_at_start() {
1000        assert_snapshot!(completions("$0"), @r"
1001         label       | kind    | detail 
1002        -------------+---------+--------
1003         delete from | Snippet |        
1004         select      | Snippet |        
1005         table       | Snippet |        
1006         truncate    | Snippet |
1007        ");
1008    }
1009
1010    #[test]
1011    fn completion_at_top_level() {
1012        assert_snapshot!(completions("
1013create table t(a int);
1014$0
1015"), @r"
1016         label       | kind    | detail 
1017        -------------+---------+--------
1018         delete from | Snippet |        
1019         select      | Snippet |        
1020         table       | Snippet |        
1021         truncate    | Snippet |
1022        ");
1023    }
1024
1025    #[test]
1026    fn completion_in_string() {
1027        completions_not_found("select '$0';");
1028    }
1029
1030    #[test]
1031    fn completion_in_comment() {
1032        completions_not_found("-- $0 ");
1033    }
1034
1035    #[test]
1036    fn completion_after_truncate() {
1037        assert_snapshot!(completions("
1038create table users (id int);
1039truncate $0;
1040"), @r"
1041         label              | kind   | detail 
1042        --------------------+--------+--------
1043         users              | Table  |        
1044         public             | Schema |        
1045         pg_catalog         | Schema |        
1046         pg_temp            | Schema |        
1047         pg_toast           | Schema |        
1048         information_schema | Schema |
1049        ");
1050    }
1051
1052    #[test]
1053    fn completion_table_at_top_level() {
1054        assert_snapshot!(completions("$0"), @r"
1055         label       | kind    | detail 
1056        -------------+---------+--------
1057         delete from | Snippet |        
1058         select      | Snippet |        
1059         table       | Snippet |        
1060         truncate    | Snippet |
1061        ");
1062    }
1063
1064    #[test]
1065    fn completion_table_nested() {
1066        assert_snapshot!(completions("select * from ($0)"), @r"
1067         label       | kind    | detail 
1068        -------------+---------+--------
1069         delete from | Snippet |        
1070         select      | Snippet |        
1071         table       | Snippet |        
1072         truncate    | Snippet |
1073        ");
1074    }
1075
1076    #[test]
1077    fn completion_after_table() {
1078        assert_snapshot!(completions("
1079create table users (id int);
1080table $0;
1081"), @r"
1082         label              | kind   | detail 
1083        --------------------+--------+--------
1084         users              | Table  |        
1085         public             | Schema |        
1086         pg_catalog         | Schema |        
1087         pg_temp            | Schema |        
1088         pg_toast           | Schema |        
1089         information_schema | Schema |
1090        ");
1091    }
1092
1093    #[test]
1094    fn completion_select_without_from() {
1095        assert_snapshot!(completions("
1096create table t (a int);
1097select $0;
1098"), @r"
1099         label              | kind    | detail 
1100        --------------------+---------+--------
1101         except             | Snippet |        
1102         fetch              | Snippet |        
1103         for                | Snippet |        
1104         from               | Snippet |        
1105         group by           | Snippet |        
1106         having             | Snippet |        
1107         intersect          | Snippet |        
1108         limit              | Snippet |        
1109         offset             | Snippet |        
1110         order by           | Snippet |        
1111         t                  | Table   |        
1112         union              | Snippet |        
1113         where              | Snippet |        
1114         window             | Snippet |        
1115         public             | Schema  |        
1116         pg_catalog         | Schema  |        
1117         pg_temp            | Schema  |        
1118         pg_toast           | Schema  |        
1119         information_schema | Schema  |
1120        ");
1121    }
1122
1123    #[test]
1124    fn completion_after_select() {
1125        assert_snapshot!(completions("
1126create table t(a text, b int);
1127create function f() returns text as 'select 1::text' language sql;
1128select $0 from t;
1129"), @r"
1130         label              | kind     | detail                  
1131        --------------------+----------+-------------------------
1132         a                  | Column   | text                    
1133         b                  | Column   | int                     
1134         t                  | Table    |                         
1135         f()                | Function | public.f() returns text 
1136         *                  | Operator |                         
1137         public             | Schema   |                         
1138         pg_catalog         | Schema   |                         
1139         pg_temp            | Schema   |                         
1140         pg_toast           | Schema   |                         
1141         information_schema | Schema   |
1142        ");
1143    }
1144
1145    #[test]
1146    fn completion_after_select_create_table_inherits_builtin() {
1147        assert_snapshot!(completions("
1148create table t ()
1149inherits (information_schema.sql_features);
1150select $0 from t;
1151"), @"
1152         label              | kind     | detail         
1153        --------------------+----------+----------------
1154         comments           | Column   | character_data 
1155         feature_id         | Column   | character_data 
1156         feature_name       | Column   | character_data 
1157         is_supported       | Column   | yes_or_no      
1158         is_verified_by     | Column   | character_data 
1159         sub_feature_id     | Column   | character_data 
1160         sub_feature_name   | Column   | character_data 
1161         t                  | Table    |                
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_create_table_inherits_create_table_as() {
1173        assert_snapshot!(completions("
1174create table parent as select 1 a, 'x'::text b;
1175create table child (c int) inherits (parent);
1176select $0 from child;
1177"), @"
1178         label              | kind     | detail  
1179        --------------------+----------+---------
1180         a                  | Column   | integer 
1181         b                  | Column   | text    
1182         c                  | Column   | int     
1183         child              | Table    |         
1184         parent             | Table    |         
1185         *                  | Operator |         
1186         public             | Schema   |         
1187         pg_catalog         | Schema   |         
1188         pg_temp            | Schema   |         
1189         pg_toast           | Schema   |         
1190         information_schema | Schema   |
1191        ");
1192    }
1193
1194    #[test]
1195    fn completion_after_select_create_table_like_select_into() {
1196        assert_snapshot!(completions("
1197select 1 a, 'x'::text b into parent;
1198create table child (like parent);
1199select $0 from child;
1200"), @"
1201         label              | kind     | detail  
1202        --------------------+----------+---------
1203         a                  | Column   | integer 
1204         b                  | Column   | text    
1205         child              | Table    |         
1206         parent             | Table    |         
1207         *                  | Operator |         
1208         public             | Schema   |         
1209         pg_catalog         | Schema   |         
1210         pg_temp            | Schema   |         
1211         pg_toast           | Schema   |         
1212         information_schema | Schema   |
1213        ");
1214    }
1215
1216    #[test]
1217    fn completion_select_table_qualified() {
1218        assert_snapshot!(completions("
1219create table t (c int);
1220select t.$0 from t;
1221"), @r"
1222         label | kind     | detail 
1223        -------+----------+--------
1224         c     | Column   | int    
1225         *     | Operator |
1226        ");
1227    }
1228
1229    #[test]
1230    fn completion_after_select_with_cte() {
1231        assert_snapshot!(completions("
1232with t as (select 1 a)
1233select $0 from t;
1234"), @r"
1235         label              | kind     | detail  
1236        --------------------+----------+---------
1237         a                  | Column   | integer 
1238         *                  | Operator |         
1239         public             | Schema   |         
1240         pg_catalog         | Schema   |         
1241         pg_temp            | Schema   |         
1242         pg_toast           | Schema   |         
1243         information_schema | Schema   |
1244        ");
1245    }
1246
1247    #[test]
1248    fn completion_after_select_with_cte_alias_column_list() {
1249        assert_snapshot!(completions("
1250with t as (select 1 a, 2 b, 3 c)
1251select $0 from t as u(x, y);
1252"), @r"
1253         label              | kind     | detail  
1254        --------------------+----------+---------
1255         x                  | Column   | integer 
1256         y                  | Column   | integer 
1257         c                  | Column   | integer 
1258         *                  | Operator |         
1259         public             | Schema   |         
1260         pg_catalog         | Schema   |         
1261         pg_temp            | Schema   |         
1262         pg_toast           | Schema   |         
1263         information_schema | Schema   |
1264        ");
1265    }
1266
1267    #[test]
1268    fn completion_values_cte() {
1269        assert_snapshot!(completions("
1270with t as (values (1, 'foo', false))
1271select $0 from t;
1272"), @r"
1273         label              | kind     | detail  
1274        --------------------+----------+---------
1275         column1            | Column   | integer 
1276         column2            | Column   | text    
1277         column3            | Column   | boolean 
1278         *                  | Operator |         
1279         public             | Schema   |         
1280         pg_catalog         | Schema   |         
1281         pg_temp            | Schema   |         
1282         pg_toast           | Schema   |         
1283         information_schema | Schema   |
1284        ");
1285    }
1286
1287    #[test]
1288    fn completion_values_subquery() {
1289        assert_snapshot!(completions("
1290select $0 from (values (1, 'foo', 1.5, false));
1291"), @r"
1292         label              | kind     | detail  
1293        --------------------+----------+---------
1294         column1            | Column   | integer 
1295         column2            | Column   | text    
1296         column3            | Column   | numeric 
1297         column4            | Column   | boolean 
1298         *                  | Operator |         
1299         public             | Schema   |         
1300         pg_catalog         | Schema   |         
1301         pg_temp            | Schema   |         
1302         pg_toast           | Schema   |         
1303         information_schema | Schema   |
1304        ");
1305    }
1306
1307    #[test]
1308    fn completion_with_schema_qualifier() {
1309        assert_snapshot!(completions("
1310create function f() returns int8 as 'select 1' language sql;
1311create function foo.b() returns int8 as 'select 2' language sql;
1312select public.$0;
1313"), @r"
1314         label | kind     | detail                  
1315        -------+----------+-------------------------
1316         f()   | Function | public.f() returns int8
1317        ");
1318    }
1319
1320    #[test]
1321    fn completion_truncate_with_schema_qualifier() {
1322        assert_snapshot!(completions("
1323create table users (id int);
1324truncate public.$0;
1325"), @r"
1326         label | kind  | detail 
1327        -------+-------+--------
1328         users | Table |
1329        ");
1330    }
1331
1332    #[test]
1333    fn completion_after_delete_from() {
1334        assert_snapshot!(completions("
1335create table users (id int);
1336delete from $0;
1337"), @r"
1338         label              | kind   | detail 
1339        --------------------+--------+--------
1340         users              | Table  |        
1341         public             | Schema |        
1342         pg_catalog         | Schema |        
1343         pg_temp            | Schema |        
1344         pg_toast           | Schema |        
1345         information_schema | Schema |
1346        ");
1347    }
1348
1349    #[test]
1350    fn completion_delete_clauses() {
1351        assert_snapshot!(completions("
1352create table t (id int);
1353delete from t $0;
1354"), @r"
1355         label     | kind    | detail 
1356        -----------+---------+--------
1357         returning | Snippet |        
1358         using     | Snippet |        
1359         where     | Snippet |
1360        ");
1361    }
1362
1363    #[test]
1364    fn completion_delete_where_expr() {
1365        assert_snapshot!(completions("
1366create table t (id int, name text);
1367create function is_active() returns bool as 'select true' language sql;
1368delete from t where $0;
1369"), @r"
1370         label       | kind     | detail                          
1371        -------------+----------+---------------------------------
1372         id          | Column   | int                             
1373         name        | Column   | text                            
1374         t           | Table    |                                 
1375         is_active() | Function | public.is_active() returns bool
1376        ")
1377    }
1378
1379    #[test]
1380    fn completion_delete_returning_expr() {
1381        assert_snapshot!(completions("
1382create table t (id int, name text);
1383delete from t returning $0;
1384"), @r"
1385         label | kind   | detail 
1386        -------+--------+--------
1387         id    | Column | int    
1388         name  | Column | text   
1389         t     | Table  |
1390        ");
1391    }
1392
1393    #[test]
1394    fn completion_delete_where_qualified() {
1395        assert_snapshot!(completions("
1396-- different type than the table, so we shouldn't show this
1397create function b(diff_type) returns int8
1398  as 'select 1'
1399  language sql;
1400create function f(t) returns int8
1401  as 'select 1'
1402  language sql;
1403create table t (a int, b text);
1404delete from t where t.$0;
1405"), @r"
1406         label | kind     | detail 
1407        -------+----------+--------
1408         a     | Column   | int    
1409         b     | Column   | text   
1410         f     | Function |
1411        ");
1412    }
1413
1414    #[test]
1415    fn completion_select_clauses() {
1416        assert_snapshot!(completions("
1417with t as (select 1 a)
1418select a from t $0;
1419"), @r"
1420         label     | kind    | detail 
1421        -----------+---------+--------
1422         except    | Snippet |        
1423         fetch     | Snippet |        
1424         for       | Snippet |        
1425         group by  | Snippet |        
1426         having    | Snippet |        
1427         intersect | Snippet |        
1428         limit     | Snippet |        
1429         offset    | Snippet |        
1430         order by  | Snippet |        
1431         union     | Snippet |        
1432         where     | Snippet |        
1433         window    | Snippet |
1434        ");
1435    }
1436
1437    #[test]
1438    fn completion_select_clauses_simple() {
1439        assert_snapshot!(completions("
1440select 1 from t $0;
1441"), @r"
1442         label     | kind    | detail 
1443        -----------+---------+--------
1444         except    | Snippet |        
1445         fetch     | Snippet |        
1446         for       | Snippet |        
1447         group by  | Snippet |        
1448         having    | Snippet |        
1449         intersect | Snippet |        
1450         limit     | Snippet |        
1451         offset    | Snippet |        
1452         order by  | Snippet |        
1453         union     | Snippet |        
1454         where     | Snippet |        
1455         window    | Snippet |
1456        ");
1457    }
1458
1459    #[test]
1460    fn completion_select_group_by_expr() {
1461        assert_snapshot!(completions("
1462with t as (select 1 a)
1463select a from t group by $0;
1464"), @r"
1465         label | kind   | detail  
1466        -------+--------+---------
1467         a     | Column | integer 
1468         t     | Table  |
1469        ");
1470    }
1471
1472    #[test]
1473    fn completion_select_where_expr() {
1474        assert_snapshot!(completions("
1475create table t (id int, name text);
1476select * from t where $0;
1477"), @r"
1478         label | kind   | detail 
1479        -------+--------+--------
1480         id    | Column | int    
1481         name  | Column | text   
1482         t     | Table  |
1483        ");
1484    }
1485
1486    #[test]
1487    fn completion_select_limit() {
1488        assert_snapshot!(completions("
1489create function get_limit() returns int as 'select 10' language sql;
1490select 1 from t limit $0;
1491"), @r"
1492         label       | kind     | detail                         
1493        -------------+----------+--------------------------------
1494         all         | Keyword  |                                
1495         get_limit() | Function | public.get_limit() returns int
1496        ");
1497    }
1498
1499    #[test]
1500    fn completion_select_offset() {
1501        assert_snapshot!(completions("
1502create function get_offset() returns int as 'select 10' language sql;
1503select 1 from t offset $0;
1504"), @r"
1505         label        | kind     | detail                          
1506        --------------+----------+---------------------------------
1507         get_offset() | Function | public.get_offset() returns int
1508        ");
1509    }
1510}