tinymist_query/analysis/completion/
import.rs

1//! Completion for import items.
2
3use super::*;
4impl CompletionPair<'_, '_, '_> {
5    /// Complete imports.
6    pub fn complete_imports(&mut self) -> bool {
7        // On the colon marker of an import list:
8        // "#import "path.typ":|"
9        if matches!(self.cursor.leaf.kind(), SyntaxKind::Colon)
10            && let Some(parent) = self.cursor.leaf.clone().parent()
11            && let Some(ast::Expr::Import(import)) = parent.get().cast()
12            && !matches!(import.imports(), Some(ast::Imports::Wildcard))
13            && let Some(source) = parent.children().find(|child| child.is::<ast::Expr>())
14        {
15            let items = match import.imports() {
16                Some(ast::Imports::Items(items)) => items,
17                _ => Default::default(),
18            };
19
20            self.cursor.from = self.cursor.cursor;
21
22            self.import_item_completions(items, vec![], &source);
23            if items.iter().next().is_some() {
24                self.worker.enrich("", ", ");
25            }
26            return true;
27        }
28
29        // Behind an import list:
30        // "#import "path.typ": |",
31        // "#import "path.typ": a, b, |".
32
33        if let Some(prev) = self.cursor.leaf.prev_sibling()
34            && let Some(ast::Expr::Import(import)) = prev.get().cast()
35            && !self.cursor.text[prev.offset()..self.cursor.cursor].contains('\n')
36            && let Some(ast::Imports::Items(items)) = import.imports()
37            && let Some(source) = prev.children().find(|child| child.is::<ast::Expr>())
38        {
39            self.cursor.from = self.cursor.cursor;
40            self.import_item_completions(items, vec![], &source);
41            return true;
42        }
43
44        // Behind a comma in an import list:
45        // "#import "path.typ": this,|".
46        if matches!(self.cursor.leaf.kind(), SyntaxKind::Comma)
47            && let Some(parent) = self.cursor.leaf.clone().parent()
48            && parent.kind() == SyntaxKind::ImportItems
49            && let Some(grand) = parent.parent()
50            && let Some(ast::Expr::Import(import)) = grand.get().cast()
51            && let Some(ast::Imports::Items(items)) = import.imports()
52            && let Some(source) = grand.children().find(|child| child.is::<ast::Expr>())
53        {
54            self.import_item_completions(items, vec![], &source);
55            self.worker.enrich(" ", "");
56            return true;
57        }
58
59        // Behind a half-started identifier in an import list:
60        // "#import "path.typ": th|".
61        if matches!(self.cursor.leaf.kind(), SyntaxKind::Ident | SyntaxKind::Dot)
62            && let Some(path_ctx) = self.cursor.leaf.clone().parent()
63            && path_ctx.kind() == SyntaxKind::ImportItemPath
64            && let Some(parent) = path_ctx.parent()
65            && parent.kind() == SyntaxKind::ImportItems
66            && let Some(grand) = parent.parent()
67            && let Some(ast::Expr::Import(import)) = grand.get().cast()
68            && let Some(ast::Imports::Items(items)) = import.imports()
69            && let Some(source) = grand.children().find(|child| child.is::<ast::Expr>())
70        {
71            if self.cursor.leaf.kind() == SyntaxKind::Ident {
72                self.cursor.from = self.cursor.leaf.offset();
73            }
74            let path = path_ctx.cast::<ast::ImportItemPath>().map(|path| {
75                path.iter()
76                    .take_while(|ident| ident.span() != self.cursor.leaf.span())
77                    .collect()
78            });
79            self.import_item_completions(items, path.unwrap_or_default(), &source);
80            return true;
81        }
82
83        false
84    }
85
86    /// Add completions for all exports of a module.
87    pub fn import_item_completions(
88        &mut self,
89        existing: ast::ImportItems,
90        comps: Vec<ast::Ident>,
91        source: &LinkedNode,
92    ) {
93        // Select the source by `comps`
94        let value = self.worker.ctx.module_by_syntax(source);
95        let value = comps.iter().fold(value.as_ref(), |value, comp| {
96            value?.scope()?.get(comp)?.read().into()
97        });
98        let Some(scope) = value.and_then(|v| v.scope()) else {
99            return;
100        };
101
102        // Check imported items in the scope
103        let seen = existing
104            .iter()
105            .flat_map(|item| {
106                let item_comps = item.path().iter().collect::<Vec<_>>();
107                if item_comps.len() == comps.len() + 1
108                    && item_comps
109                        .iter()
110                        .zip(comps.as_slice())
111                        .all(|(l, r)| l.as_str() == r.as_str())
112                {
113                    // item_comps.len() >= 1
114                    item_comps.last().cloned()
115                } else {
116                    None
117                }
118            })
119            .collect::<Vec<_>>();
120
121        if existing.iter().next().is_none() {
122            self.snippet_completion("*", "*", "Import everything.");
123        }
124
125        for (name, bind) in scope.iter() {
126            if seen.iter().all(|item| item.as_str() != name) {
127                self.value_completion(Some(name.clone()), bind.read(), false, None);
128            }
129        }
130    }
131}