tinymist_query/analysis/
link_expr.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! Analyze link expressions in a source file.

use std::str::FromStr;

use lsp_types::Url;
use tinymist_world::package::PackageSpec;

use super::prelude::*;

/// Get link expressions from a source.
#[comemo::memoize]
pub fn get_link_exprs(src: &Source) -> Arc<LinkInfo> {
    let root = LinkedNode::new(src.root());
    Arc::new(get_link_exprs_in(&root).unwrap_or_default())
}

/// Get link expressions in a source node.
pub fn get_link_exprs_in(node: &LinkedNode) -> Option<LinkInfo> {
    let mut worker = LinkStrWorker {
        info: LinkInfo::default(),
    };
    worker.collect_links(node)?;
    Some(worker.info)
}

/// Link information in a source file.
#[derive(Debug, Default)]
pub struct LinkInfo {
    /// The link objects in a source file.
    pub objects: Vec<LinkObject>,
}

/// A link object in a source file.
#[derive(Debug)]
pub struct LinkObject {
    /// The range of the link expression.
    pub range: Range<usize>,
    /// The span of the link expression.
    pub span: Span,
    /// The target of the link.
    pub target: LinkTarget,
}

/// A valid link target.
#[derive(Debug)]
pub enum LinkTarget {
    /// A package specification.
    Package(Box<PackageSpec>),
    /// A URL.
    Url(Box<Url>),
    /// A file path.
    Path(TypstFileId, EcoString),
}

impl LinkTarget {
    pub(crate) fn resolve(&self, ctx: &mut LocalContext) -> Option<Url> {
        match self {
            LinkTarget::Package(..) => None,
            LinkTarget::Url(url) => Some(url.as_ref().clone()),
            LinkTarget::Path(id, path) => {
                // Avoid creating new ids here.
                let root = ctx.path_for_id(id.join("")).ok()?;
                crate::path_res_to_url(root.join(path).ok()?).ok()
            }
        }
    }
}

struct LinkStrWorker {
    info: LinkInfo,
}

impl LinkStrWorker {
    fn collect_links(&mut self, node: &LinkedNode) -> Option<()> {
        match node.kind() {
            // SyntaxKind::Link => { }
            SyntaxKind::FuncCall => {
                let fc = self.analyze_call(node);
                if fc.is_some() {
                    return Some(());
                }
            }
            SyntaxKind::Include => {
                let inc = node.cast::<ast::ModuleInclude>()?;
                let path = inc.source();
                self.analyze_path_expr(node, path);
            }
            // early exit
            kind if kind.is_trivia() || kind.is_keyword() || kind.is_error() => return Some(()),
            _ => {}
        };

        for child in node.children() {
            self.collect_links(&child);
        }

        Some(())
    }

    fn analyze_call(&mut self, node: &LinkedNode) -> Option<()> {
        let call = node.cast::<ast::FuncCall>()?;
        let mut callee = call.callee();
        'check_link_fn: loop {
            match callee {
                ast::Expr::FieldAccess(fa) => {
                    let target = fa.target();
                    let ast::Expr::Ident(ident) = target else {
                        return None;
                    };
                    if ident.get().as_str() != "std" {
                        return None;
                    }
                    callee = ast::Expr::Ident(fa.field());
                    continue 'check_link_fn;
                }
                ast::Expr::Ident(ident) => match ident.get().as_str() {
                    "raw" => {
                        self.analyze_reader(node, call, "theme", false);
                        self.analyze_reader(node, call, "syntaxes", false);
                    }
                    "bibliography" => {
                        self.analyze_reader(node, call, "cite", false);
                        self.analyze_reader(node, call, "style", false);
                        self.analyze_reader(node, call, "path", true);
                    }
                    "cbor" | "csv" | "image" | "read" | "json" | "yaml" | "xml" => {
                        self.analyze_reader(node, call, "path", true);
                    }
                    _ => return None,
                },
                _ => return None,
            }
            return None;
        }
    }

    fn analyze_reader(
        &mut self,
        node: &LinkedNode,
        call: ast::FuncCall,
        key: &str,
        pos: bool,
    ) -> Option<()> {
        let arg = call.args().items().next()?;
        match arg {
            ast::Arg::Pos(s) if pos => {
                self.analyze_path_expr(node, s);
            }
            _ => {}
        }
        for item in call.args().items() {
            match item {
                ast::Arg::Named(named) if named.name().get().as_str() == key => {
                    self.analyze_path_expr(node, named.expr());
                }
                _ => {}
            }
        }
        Some(())
    }

    fn analyze_path_expr(&mut self, node: &LinkedNode, path_expr: ast::Expr) -> Option<()> {
        match path_expr {
            ast::Expr::Str(s) => self.analyze_path_str(node, s),
            ast::Expr::Array(a) => {
                for item in a.items() {
                    if let ast::ArrayItem::Pos(ast::Expr::Str(s)) = item {
                        self.analyze_path_str(node, s);
                    }
                }
                Some(())
            }
            _ => None,
        }
    }

    fn analyze_path_str(&mut self, node: &LinkedNode, s: ast::Str<'_>) -> Option<()> {
        let str_node = node.find(s.span())?;
        let str_range = str_node.range();
        let range = str_range.start + 1..str_range.end - 1;
        if range.is_empty() {
            return None;
        }

        let content = s.get();
        if content.starts_with('@') {
            let pkg_spec = PackageSpec::from_str(&content).ok()?;
            self.info.objects.push(LinkObject {
                range,
                span: s.span(),
                target: LinkTarget::Package(Box::new(pkg_spec)),
            });
            return Some(());
        }

        let id = node.span().id()?;
        self.info.objects.push(LinkObject {
            range,
            span: s.span(),
            target: LinkTarget::Path(id, content),
        });
        Some(())
    }
}