tinymist_query/analysis/
link_expr.rs1use std::borrow::Cow;
4use std::str::FromStr;
5
6use lsp_types::Url;
7use tinymist_world::package::PackageSpec;
8use tinymist_world::vfs::PathResolution;
9
10use super::prelude::*;
11
12#[typst_macros::time(span = src.root().span())]
14#[comemo::memoize]
15pub fn get_link_exprs(src: &Source) -> Arc<LinkInfo> {
16 let root = LinkedNode::new(src.root());
17 Arc::new(get_link_exprs_in(&root))
18}
19
20pub fn get_link_exprs_in(node: &LinkedNode) -> LinkInfo {
22 let mut worker = LinkStrWorker {
23 info: LinkInfo::default(),
24 };
25 worker.collect_links(node);
26 worker.info
27}
28
29#[derive(Debug, Default)]
31pub struct LinkInfo {
32 pub objects: Vec<LinkObject>,
34}
35
36#[derive(Debug)]
38pub struct LinkObject {
39 pub range: Range<usize>,
41 pub span: Span,
43 pub target: LinkTarget,
45}
46
47#[derive(Debug)]
49pub enum LinkTarget {
50 Package(Box<PackageSpec>),
52 Url(Box<Url>),
54 Path(TypstFileId, EcoString),
62}
63
64impl LinkTarget {
65 pub(crate) fn resolve(&self, ctx: &mut LocalContext) -> Option<Url> {
66 match self {
67 LinkTarget::Package(..) => None,
68 LinkTarget::Url(url) => Some(url.as_ref().clone()),
69 LinkTarget::Path(id, path) => {
70 let resolved = resolve_path_from_id(*id, path.as_str()).ok()?;
71 let path = match ctx.world().vfs().resolve_root(*id).ok()? {
72 Some(root) => PathResolution::Resolved(resolved.vpath().realize(&root).ok()?),
73 None => PathResolution::Rootless(Cow::Owned(resolved.vpath().clone())),
74 };
75 crate::path_res_to_url(path).ok()
76 }
77 }
78 }
79}
80
81struct LinkStrWorker {
82 info: LinkInfo,
83}
84
85impl LinkStrWorker {
86 fn collect_links(&mut self, node: &LinkedNode) {
87 match node.kind() {
88 SyntaxKind::FuncCall => {
90 let fc = self.analyze_call(node);
91 if fc.is_some() {
92 return;
93 }
94 }
95 SyntaxKind::ModuleInclude => {
96 let inc = node.cast::<ast::ModuleInclude>().expect("checked cast");
97 let path = inc.source();
98 self.analyze_path_expr(node, path);
99 }
100 kind if kind.is_trivia() || kind.is_keyword() || kind.is_error() => return,
102 _ => {}
103 };
104
105 for child in node.children() {
106 self.collect_links(&child);
107 }
108 }
109
110 fn analyze_call(&mut self, node: &LinkedNode) -> Option<()> {
111 let call = node.cast::<ast::FuncCall>()?;
112 let mut callee = call.callee();
113 'check_link_fn: loop {
114 match callee {
115 ast::Expr::FieldAccess(fa) => {
116 let target = fa.target();
117 let ast::Expr::Ident(ident) = target else {
118 return None;
119 };
120 if ident.get().as_str() != "std" {
121 return None;
122 }
123 callee = ast::Expr::Ident(fa.field());
124 continue 'check_link_fn;
125 }
126 ast::Expr::Ident(ident) => match ident.get().as_str() {
127 "raw" => {
128 self.analyze_reader(node, call, "theme", false);
129 self.analyze_reader(node, call, "syntaxes", false);
130 }
131 "bibliography" => {
132 self.analyze_reader(node, call, "cite", false);
133 self.analyze_bibliography_style(node, call);
134 self.analyze_reader(node, call, "path", true);
135 }
136 "cbor" | "csv" | "image" | "read" | "json" | "yaml" | "xml" => {
137 self.analyze_reader(node, call, "path", true);
138 }
139 _ => return None,
140 },
141 _ => return None,
142 }
143 return None;
144 }
145 }
146
147 fn analyze_bibliography_style(&mut self, node: &LinkedNode, call: ast::FuncCall) -> Option<()> {
148 for item in call.args().items() {
149 match item {
150 ast::Arg::Named(named) if named.name().get().as_str() == "style" => {
151 if let ast::Expr::Str(style) = named.expr()
152 && hayagriva::archive::ArchivedStyle::by_name(&style.get()).is_some()
153 {
154 return Some(());
155 }
156 self.analyze_path_expr(node, named.expr());
157 return Some(());
158 }
159 _ => {}
160 }
161 }
162 Some(())
163 }
164
165 fn analyze_reader(
166 &mut self,
167 node: &LinkedNode,
168 call: ast::FuncCall,
169 key: &str,
170 pos: bool,
171 ) -> Option<()> {
172 let arg = call.args().items().next()?;
173 match arg {
174 ast::Arg::Pos(s) if pos => {
175 self.analyze_path_expr(node, s);
176 }
177 _ => {}
178 }
179 for item in call.args().items() {
180 match item {
181 ast::Arg::Named(named) if named.name().get().as_str() == key => {
182 self.analyze_path_expr(node, named.expr());
183 }
184 _ => {}
185 }
186 }
187 Some(())
188 }
189
190 fn analyze_path_expr(&mut self, node: &LinkedNode, path_expr: ast::Expr) -> Option<()> {
191 match path_expr {
192 ast::Expr::Str(s) => self.analyze_path_str(node, s),
193 ast::Expr::Array(a) => {
194 for item in a.items() {
195 if let ast::ArrayItem::Pos(ast::Expr::Str(s)) = item {
196 self.analyze_path_str(node, s);
197 }
198 }
199 Some(())
200 }
201 _ => None,
202 }
203 }
204
205 fn analyze_path_str(&mut self, node: &LinkedNode, s: ast::Str<'_>) -> Option<()> {
206 let str_node = node.find(s.span())?;
207 let str_range = str_node.range();
208 let range = str_range.start + 1..str_range.end - 1;
209 if range.is_empty() {
210 return None;
211 }
212
213 let content = s.get();
214 if content.starts_with('@') {
215 let pkg_spec = PackageSpec::from_str(&content).ok()?;
216 self.info.objects.push(LinkObject {
217 range,
218 span: s.span(),
219 target: LinkTarget::Package(Box::new(pkg_spec)),
220 });
221 return Some(());
222 }
223
224 let id = node.span().id()?;
225 self.info.objects.push(LinkObject {
226 range,
227 span: s.span(),
228 target: LinkTarget::Path(id, content),
229 });
230 Some(())
231 }
232}