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