1use smol_str::SmolStr;
10
11use rowan::{TextRange, TextSize};
12
13use crate::ast::{command_name, first_group_range, nth_group_inner};
14use crate::semantic::SemanticModel;
15use crate::semantic::label::{
16 CitationRef, ColorDef, ColorDefKind, GlossaryDef, GlossaryDefKind, LabelDef, LabelRef,
17 RefCommand,
18};
19use crate::semantic::pkgmeta;
20use crate::syntax::{SyntaxKind, SyntaxNode};
21
22pub fn build(root: &SyntaxNode) -> SemanticModel {
23 let mut model = SemanticModel::default();
24
25 for command in root
26 .descendants()
27 .filter(|node| node.kind() == SyntaxKind::COMMAND)
28 {
29 let Some(name) = command_name(&command) else {
30 continue;
31 };
32
33 if name == "label" {
34 if let Some((inner_range, inner)) = nth_group_inner(&command, 0) {
38 for (key, key_range) in key_spans(&inner, inner_range, false) {
39 model.labels.push(LabelDef {
40 name: SmolStr::from(key),
41 range: first_group_range(&command),
42 key_range,
43 referenced: false,
44 });
45 }
46 }
47 } else if let Some(kind) = ref_command(&name)
48 && let Some((inner_range, inner)) = nth_group_inner(&command, 0)
49 {
50 for (key, key_range) in key_spans(&inner, inner_range, kind.is_key_list()) {
51 model.refs.push(LabelRef {
52 name: SmolStr::from(key),
53 command: kind,
54 range: command.text_range(),
55 key_range,
56 resolved: false,
57 });
58 }
59 } else if let Some(kind) = glossary_definer(&name) {
60 if let Some((inner_range, inner)) = nth_group_inner(&command, 0) {
65 for (key, key_range) in key_spans(&inner, inner_range, false) {
66 model.glossary_defs.push(GlossaryDef {
67 key: SmolStr::from(key),
68 kind,
69 range: first_group_range(&command),
70 key_range,
71 });
72 }
73 }
74 } else if let Some(kind) = color_definer(&name) {
75 if let Some((inner_range, inner)) = nth_group_inner(&command, 0) {
79 for (key, key_range) in key_spans(&inner, inner_range, false) {
80 model.color_defs.push(ColorDef {
81 name: SmolStr::from(key),
82 kind,
83 range: first_group_range(&command),
84 key_range,
85 });
86 }
87 }
88 } else if is_cite_command(&name)
89 && let Some((inner_range, inner)) = nth_group_inner(&command, 0)
90 {
91 if name == "nocite" && inner.trim() == "*" {
94 model.nocite_all = true;
95 } else {
96 for (key, key_range) in key_spans(&inner, inner_range, true) {
98 model.citations.push(CitationRef {
99 name: SmolStr::from(key),
100 command: SmolStr::from(name.as_str()),
101 range: command.text_range(),
102 key_range,
103 });
104 }
105 }
106 } else if pkgmeta::provides_kind(&name).is_some() {
107 if model.provides.is_none()
110 && let Some(decl) = pkgmeta::provides_from_command(&command)
111 {
112 model.provides = Some(decl);
113 }
114 } else if name == "NeedsTeXFormat" {
115 if model.needs_format.is_none()
116 && let Some(decl) = pkgmeta::needs_format_from_command(&command)
117 {
118 model.needs_format = Some(decl);
119 }
120 } else if name == "DeclareOption"
121 && let Some(decl) = pkgmeta::option_from_command(&command)
122 {
123 model.options.push(decl);
124 }
125 }
126
127 resolve(&mut model);
128 model
129}
130
131pub fn is_cite_command(name: &str) -> bool {
136 const EXTRA: &[&str] = &[
137 "parencite",
138 "Parencite",
139 "footcite",
140 "footcitetext",
141 "textcite",
142 "Textcite",
143 "smartcite",
144 "Smartcite",
145 "autocite",
146 "Autocite",
147 "supercite",
148 "fullcite",
149 "footfullcite",
150 "nocite",
151 "notecite",
152 "Notecite",
153 "pnotecite",
154 "fnotecite",
155 ];
156 name.starts_with("cite") || name.starts_with("Cite") || EXTRA.contains(&name)
159}
160
161pub fn ref_command(name: &str) -> Option<RefCommand> {
166 Some(match name {
167 "ref" => RefCommand::Ref,
168 "pageref" => RefCommand::PageRef,
169 "eqref" => RefCommand::EqRef,
170 "autoref" => RefCommand::AutoRef,
171 "nameref" => RefCommand::NameRef,
172 "cref" => RefCommand::Cref,
173 "Cref" => RefCommand::CrefUpper,
174 "vref" => RefCommand::Vref,
175 "Vref" => RefCommand::VrefUpper,
176 "cpageref" => RefCommand::CpageRef,
177 _ => return None,
178 })
179}
180
181pub(crate) fn glossary_definer(name: &str) -> Option<GlossaryDefKind> {
185 Some(match name {
186 "newglossaryentry"
187 | "longnewglossaryentry"
188 | "provideglossaryentry"
189 | "longprovideglossaryentry" => GlossaryDefKind::Entry,
190 "newacronym" => GlossaryDefKind::Acronym,
191 "newabbreviation" => GlossaryDefKind::Abbreviation,
192 _ => return None,
193 })
194}
195
196pub(crate) fn color_definer(name: &str) -> Option<ColorDefKind> {
201 Some(match name {
202 "definecolor" => ColorDefKind::DefineColor,
203 "providecolor" => ColorDefKind::ProvideColor,
204 "colorlet" => ColorDefKind::Colorlet,
205 _ => return None,
206 })
207}
208
209pub fn is_glossary_ref_command(name: &str) -> bool {
216 const GLS: &[&str] = &[
219 "gls",
220 "Gls",
221 "GLS",
222 "glspl",
223 "Glspl",
224 "GLSpl",
225 "glstext",
228 "Glstext",
229 "glsfirst",
230 "Glsfirst",
231 "glsplural",
232 "Glsplural",
233 "glsfirstplural",
234 "Glsfirstplural",
235 "glsdesc",
236 "Glsdesc",
237 "glsname",
238 "Glsname",
239 "glssymbol",
240 "Glssymbol",
241 "glslink",
243 "glsdisp",
244 "glsadd",
245 "glsxtrshort",
247 "Glsxtrshort",
248 "glsxtrlong",
249 "Glsxtrlong",
250 "glsxtrfull",
251 "Glsxtrfull",
252 ];
253 if GLS.contains(&name) {
254 return true;
255 }
256 for stem in ["acr", "Acr", "ACR"] {
260 if let Some(rest) = name.strip_prefix(stem) {
261 return matches!(
262 rest,
263 "short" | "shortpl" | "long" | "longpl" | "full" | "fullpl"
264 );
265 }
266 }
267 false
268}
269
270pub fn key_argument_command(name: &str) -> bool {
278 matches!(name, "label" | "tag" | "hyperref")
279 || ref_command(name).is_some()
280 || is_cite_command(name)
281 || is_glossary_ref_command(name)
282 || glossary_definer(name).is_some()
283 || color_definer(name).is_some()
284}
285
286fn key_spans(inner: &str, inner_range: TextRange, split: bool) -> Vec<(&str, TextRange)> {
294 let base = inner_range.start();
295 let mut out = Vec::new();
296 if split {
297 let mut seg_off = 0usize;
300 for segment in inner.split(',') {
301 if let Some((key, lo, hi)) = trimmed_span(segment) {
302 out.push((key, key_range(base, seg_off + lo, seg_off + hi)));
303 }
304 seg_off += segment.len() + 1;
305 }
306 } else if let Some((key, lo, hi)) = trimmed_span(inner) {
307 out.push((key, key_range(base, lo, hi)));
308 }
309 out
310}
311
312fn trimmed_span(segment: &str) -> Option<(&str, usize, usize)> {
315 let key = segment.trim();
316 if key.is_empty() {
317 return None;
318 }
319 let lo = segment.len() - segment.trim_start().len();
320 Some((key, lo, lo + key.len()))
321}
322
323fn key_range(base: TextSize, lo: usize, hi: usize) -> TextRange {
325 TextRange::new(
326 base + TextSize::from(lo as u32),
327 base + TextSize::from(hi as u32),
328 )
329}
330
331fn resolve(model: &mut SemanticModel) {
334 for ref_idx in 0..model.refs.len() {
335 let name = model.refs[ref_idx].name.clone();
336 let mut hit = false;
337 for label in &mut model.labels {
338 if label.name == name {
339 label.referenced = true;
340 hit = true;
341 }
342 }
343 model.refs[ref_idx].resolved = hit;
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use crate::parser::parse;
350 use crate::syntax::SyntaxNode;
351
352 use super::build;
353
354 fn model(src: &str) -> crate::semantic::SemanticModel {
355 build(&SyntaxNode::new_root(parse(src).green))
356 }
357
358 #[test]
359 fn label_key_range_excludes_command_and_braces() {
360 let src = "\\label{ sec:intro }\n";
361 let model = model(src);
362 let def = &model.labels()[0];
363 assert_eq!(def.name, "sec:intro");
364 assert_eq!(&src[def.key_range], "sec:intro");
366 }
367
368 #[test]
369 fn cref_list_keys_get_isolated_ranges() {
370 let src = "\\cref{a,b,c}\n";
371 let model = model(src);
372 let keys: Vec<_> = model
373 .refs()
374 .iter()
375 .map(|r| (r.name.as_str(), &src[r.key_range]))
376 .collect();
377 assert_eq!(
378 keys,
379 vec![("a", "a"), ("b", "b"), ("c", "c")],
380 "each key in a list command isolates its own span"
381 );
382 }
383
384 #[test]
385 fn newglossaryentry_key_scanned_with_range() {
386 let src = "\\newglossaryentry{ex}{name={example},description={an example}}\n";
387 let model = model(src);
388 let def = &model.glossary_defs()[0];
389 assert_eq!(def.key, "ex");
390 assert_eq!(def.kind, crate::semantic::label::GlossaryDefKind::Entry);
391 assert_eq!(&src[def.key_range], "ex");
392 }
393
394 #[test]
395 fn newacronym_optional_arg_does_not_shift_key() {
396 let src = "\\newacronym[longplural={frames}]{fps}{FPS}{frame rate}\n";
397 let model = model(src);
398 let def = &model.glossary_defs()[0];
399 assert_eq!(def.key, "fps");
400 assert_eq!(def.kind, crate::semantic::label::GlossaryDefKind::Acronym);
401 assert_eq!(&src[def.key_range], "fps");
402 }
403
404 #[test]
405 fn glossary_definer_family_scanned() {
406 let src = "\\longnewglossaryentry{a}{name={a}}{desc}\n\\newabbreviation{b}{B}{bee}\n\\provideglossaryentry{c}{name={c}}\n";
407 let model = model(src);
408 let keys: Vec<_> = model
409 .glossary_defs()
410 .iter()
411 .map(|d| d.key.as_str())
412 .collect();
413 assert_eq!(keys, vec!["a", "b", "c"]);
414 }
415
416 #[test]
417 fn glossary_nested_macro_key_skipped() {
418 let model = model("\\newacronym{\\foo}{F}{foo}\n");
420 assert!(model.glossary_defs().is_empty());
421 }
422
423 #[test]
424 fn gls_use_is_not_a_definition() {
425 let model = model("\\gls{ex}\\acrshort{fps}\n");
426 assert!(model.glossary_defs().is_empty());
427 }
428
429 #[test]
430 fn color_definers_scanned_with_ranges() {
431 let src = "\\definecolor{brandblue}{HTML}{0055AA}\n\\colorlet{accent}{brandblue}\n\\providecolor{muted}{gray}{0.5}\n";
432 let model = model(src);
433 let defs: Vec<_> = model
434 .color_defs()
435 .iter()
436 .map(|d| (d.name.as_str(), d.kind, &src[d.key_range]))
437 .collect();
438 use crate::semantic::label::ColorDefKind::*;
439 assert_eq!(
440 defs,
441 vec![
442 ("brandblue", DefineColor, "brandblue"),
443 ("accent", Colorlet, "accent"),
444 ("muted", ProvideColor, "muted"),
445 ]
446 );
447 }
448
449 #[test]
450 fn textcolor_use_is_not_a_color_definition() {
451 let model = model("\\textcolor{red}{x}\\color{blue}\n");
452 assert!(model.color_defs().is_empty());
453 }
454
455 #[test]
456 fn cite_list_keys_get_isolated_ranges() {
457 let src = "\\cite{ foo , bar }\n";
458 let model = model(src);
459 let keys: Vec<_> = model
460 .citations()
461 .iter()
462 .map(|c| (c.name.as_str(), &src[c.key_range]))
463 .collect();
464 assert_eq!(keys, vec![("foo", "foo"), ("bar", "bar")]);
465 }
466}