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