Skip to main content

squawk_ide/
completion.rs

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