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))
16}
17
18pub fn get_link_exprs_in(node: &LinkedNode) -> LinkInfo {
20 let mut worker = LinkStrWorker {
21 info: LinkInfo::default(),
22 };
23 worker.collect_links(node);
24 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),
60}
61
62impl LinkTarget {
63 pub(crate) fn resolve(&self, ctx: &mut LocalContext) -> Option<Url> {
64 match self {
65 LinkTarget::Package(..) => None,
66 LinkTarget::Url(url) => Some(url.as_ref().clone()),
67 LinkTarget::Path(id, path) => {
68 let root = ctx.path_for_id(id.join("/")).ok()?;
70 let path_in_workspace = id.vpath().join(Path::new(path.as_str()));
71 let path = root.resolve_to(&path_in_workspace)?;
72 crate::path_res_to_url(path).ok()
73 }
74 }
75 }
76}
77
78struct LinkStrWorker {
79 info: LinkInfo,
80}
81
82impl LinkStrWorker {
83 fn collect_links(&mut self, node: &LinkedNode) {
84 match node.kind() {
85 SyntaxKind::FuncCall => {
87 let fc = self.analyze_call(node);
88 if fc.is_some() {
89 return;
90 }
91 }
92 SyntaxKind::ModuleInclude => {
93 let inc = node.cast::<ast::ModuleInclude>().expect("checked cast");
94 let path = inc.source();
95 self.analyze_path_expr(node, path);
96 }
97 kind if kind.is_trivia() || kind.is_keyword() || kind.is_error() => return,
99 _ => {}
100 };
101
102 for child in node.children() {
103 self.collect_links(&child);
104 }
105 }
106
107 fn analyze_call(&mut self, node: &LinkedNode) -> Option<()> {
108 let call = node.cast::<ast::FuncCall>()?;
109 let mut callee = call.callee();
110 'check_link_fn: loop {
111 match callee {
112 ast::Expr::FieldAccess(fa) => {
113 let target = fa.target();
114 let ast::Expr::Ident(ident) = target else {
115 return None;
116 };
117 if ident.get().as_str() != "std" {
118 return None;
119 }
120 callee = ast::Expr::Ident(fa.field());
121 continue 'check_link_fn;
122 }
123 ast::Expr::Ident(ident) => match ident.get().as_str() {
124 "raw" => {
125 self.analyze_reader(node, call, "theme", false);
126 self.analyze_reader(node, call, "syntaxes", false);
127 }
128 "bibliography" => {
129 self.analyze_reader(node, call, "cite", false);
130 self.analyze_bibliography_style(node, call);
131 self.analyze_reader(node, call, "path", true);
132 }
133 "cbor" | "csv" | "image" | "read" | "json" | "yaml" | "xml" => {
134 self.analyze_reader(node, call, "path", true);
135 }
136 _ => return None,
137 },
138 _ => return None,
139 }
140 return None;
141 }
142 }
143
144 fn analyze_bibliography_style(&mut self, node: &LinkedNode, call: ast::FuncCall) -> Option<()> {
145 for item in call.args().items() {
146 match item {
147 ast::Arg::Named(named) if named.name().get().as_str() == "style" => {
148 if let ast::Expr::Str(style) = named.expr()
149 && hayagriva::archive::ArchivedStyle::by_name(&style.get()).is_some()
150 {
151 return Some(());
152 }
153 self.analyze_path_expr(node, named.expr());
154 return Some(());
155 }
156 _ => {}
157 }
158 }
159 Some(())
160 }
161
162 fn analyze_reader(
163 &mut self,
164 node: &LinkedNode,
165 call: ast::FuncCall,
166 key: &str,
167 pos: bool,
168 ) -> Option<()> {
169 let arg = call.args().items().next()?;
170 match arg {
171 ast::Arg::Pos(s) if pos => {
172 self.analyze_path_expr(node, s);
173 }
174 _ => {}
175 }
176 for item in call.args().items() {
177 match item {
178 ast::Arg::Named(named) if named.name().get().as_str() == key => {
179 self.analyze_path_expr(node, named.expr());
180 }
181 _ => {}
182 }
183 }
184 Some(())
185 }
186
187 fn analyze_path_expr(&mut self, node: &LinkedNode, path_expr: ast::Expr) -> Option<()> {
188 match path_expr {
189 ast::Expr::Str(s) => self.analyze_path_str(node, s),
190 ast::Expr::Array(a) => {
191 for item in a.items() {
192 if let ast::ArrayItem::Pos(ast::Expr::Str(s)) = item {
193 self.analyze_path_str(node, s);
194 }
195 }
196 Some(())
197 }
198 _ => None,
199 }
200 }
201
202 fn analyze_path_str(&mut self, node: &LinkedNode, s: ast::Str<'_>) -> Option<()> {
203 let str_node = node.find(s.span())?;
204 let str_range = str_node.range();
205 let range = str_range.start + 1..str_range.end - 1;
206 if range.is_empty() {
207 return None;
208 }
209
210 let content = s.get();
211 if content.starts_with('@') {
212 let pkg_spec = PackageSpec::from_str(&content).ok()?;
213 self.info.objects.push(LinkObject {
214 range,
215 span: s.span(),
216 target: LinkTarget::Package(Box::new(pkg_spec)),
217 });
218 return Some(());
219 }
220
221 let id = node.span().id()?;
222 self.info.objects.push(LinkObject {
223 range,
224 span: s.span(),
225 target: LinkTarget::Path(id, content),
226 });
227 Some(())
228 }
229}