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