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