1use std::collections::{HashMap, HashSet};
28
29use crate::ast::{
30 AstNode, Command, Optional, child, children, command_name, control_word_range,
31 group_command_name, group_inner_source, nth_group, nth_group_inner, nth_group_text,
32};
33use crate::semantic::signature::{
34 ArgKind, ArgSpec, CommandSig, ContentKind, EnvironmentSig, SignatureDb, builtin,
35};
36use crate::semantic::xparse;
37use crate::syntax::{SyntaxKind, SyntaxNode, is_collapsible_trivia, is_trivia};
38use rowan::{NodeOrToken, TextRange, TextSize};
39use smol_str::SmolStr;
40
41pub fn scan_definitions(root: &SyntaxNode) -> SignatureDb {
46 let mut db = SignatureDb::default();
47 let mut bodies: HashMap<SmolStr, DefBody> = HashMap::new();
51 let mut env_bodies: HashMap<SmolStr, DefBody> = HashMap::new();
56 let mut alias_candidates: HashMap<SmolStr, EnvAliasCandidate> = HashMap::new();
61
62 for command in root
63 .descendants()
64 .filter(|node| node.kind() == SyntaxKind::COMMAND)
65 {
66 let Some(name) = command_name(&command) else {
67 continue;
68 };
69 match DefKind::of(&name) {
70 Some(DefKind::Command) => {
71 scan_newcommand(&command, &mut db, &mut bodies, &mut alias_candidates)
72 }
73 Some(DefKind::Def) => scan_def(&command, &mut db, &mut bodies, &mut alias_candidates),
74 Some(DefKind::Environment) => scan_newenvironment(&command, &mut db, &mut env_bodies),
75 Some(DefKind::XparseCommand) => {
76 scan_xparse_command(&command, &mut db, &mut bodies, &mut alias_candidates)
77 }
78 Some(DefKind::XparseEnvironment) => {
79 scan_xparse_environment(&command, &mut db, &mut env_bodies)
80 }
81 Some(DefKind::VerbatimEnvironment) => {
82 scan_verbatim_environment(&name, &command, &mut db)
83 }
84 None => {}
85 }
86 }
87
88 apply_verbatim_flags(&mut db, &bodies);
89 apply_verbatim_env_flags(&mut db, &env_bodies, &bodies);
90 apply_env_aliases(&mut db, &alias_candidates);
91 db
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96enum AliasSide {
97 Begin,
98 End,
99}
100
101#[derive(Debug, Clone)]
105struct EnvAliasCandidate {
106 side: AliasSide,
107 target: SmolStr,
108}
109
110fn env_alias_body(body: &SyntaxNode) -> Option<EnvAliasCandidate> {
122 let mut sole: Option<SyntaxNode> = None;
123 for child in body.children_with_tokens() {
124 match child {
125 NodeOrToken::Token(t) => match t.kind() {
126 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE | SyntaxKind::COMMENT => {}
128 k if is_collapsible_trivia(k) => {}
129 _ => return None,
130 },
131 NodeOrToken::Node(n) => {
133 if sole.replace(n).is_some() {
134 return None;
135 }
136 }
137 }
138 }
139 let cmd = sole?;
140 if cmd.kind() != SyntaxKind::COMMAND {
141 return None;
142 }
143 if cmd.children().count() != 1 {
146 return None;
147 }
148 let side = match command_name(&cmd)?.as_str() {
149 "begin" => AliasSide::Begin,
150 "end" => AliasSide::End,
151 _ => return None,
152 };
153 let target = nth_group_text(&cmd, 0)?;
154 let target = target.trim();
155 if target.is_empty() {
156 return None;
157 }
158 Some(EnvAliasCandidate {
159 side,
160 target: SmolStr::new(target),
161 })
162}
163
164fn record_env_alias(
169 candidates: &mut HashMap<SmolStr, EnvAliasCandidate>,
170 name: &str,
171 arity: usize,
172 body: Option<&SyntaxNode>,
173) {
174 candidates.remove(name);
178 if arity != 0 {
179 return;
180 }
181 if let Some(candidate) = body.and_then(env_alias_body) {
182 candidates.insert(SmolStr::new(name), candidate);
183 }
184}
185
186fn apply_env_aliases(db: &mut SignatureDb, candidates: &HashMap<SmolStr, EnvAliasCandidate>) {
212 let admissible = |target: &str| {
213 builtin()
214 .environment(target)
215 .is_some_and(|sig| !sig.verbatim_body && sig.args.is_empty())
216 };
217 for (name, candidate) in candidates {
218 let target = candidate.target.as_str();
219 if !admissible(target) {
220 continue;
221 }
222 match candidate.side {
223 AliasSide::Begin => {
224 db.insert_env_begin_alias(name.clone(), candidate.target.clone());
225 }
226 AliasSide::End => {
227 db.insert_env_end_alias(name.clone(), candidate.target.clone());
228 }
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum DefSiteKind {
237 Command,
238 Environment,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct DefSite {
248 pub name: SmolStr,
250 pub kind: DefSiteKind,
251 pub name_range: TextRange,
258 pub range: TextRange,
261}
262
263pub fn scan_definition_sites(root: &SyntaxNode) -> Vec<DefSite> {
269 let mut sites = Vec::new();
270 for command in root
271 .descendants()
272 .filter(|node| node.kind() == SyntaxKind::COMMAND)
273 {
274 let Some(name) = command_name(&command) else {
275 continue;
276 };
277 let site = match DefKind::of(&name) {
278 Some(DefKind::Command | DefKind::XparseCommand) => command_def_site(&command),
279 Some(DefKind::Def) => def_def_site(&command),
280 Some(
281 DefKind::Environment | DefKind::XparseEnvironment | DefKind::VerbatimEnvironment,
282 ) => environment_def_site(&command),
283 None => None,
284 };
285 sites.extend(site);
286 }
287 sites
288}
289
290fn command_def_site(command: &SyntaxNode) -> Option<DefSite> {
295 let def = resolve_command_def(command)?;
296 let name_range = if def.first_arg_group == 1 {
297 let group = nth_group(command, 0)?;
298 child::<Command>(&group)?.control_word_range()?
299 } else {
300 control_word_range(&def.host)?
301 };
302 Some(DefSite {
303 name: SmolStr::new(&def.name),
304 kind: DefSiteKind::Command,
305 name_range,
306 range: TextRange::new(
307 command.text_range().start(),
308 command.text_range().end().max(def.host.text_range().end()),
309 ),
310 })
311}
312
313fn def_def_site(command: &SyntaxNode) -> Option<DefSite> {
316 let name_node = adjacent_sibling_command(command)?;
317 let name = command_name(&name_node)?;
318 let name_range = control_word_range(&name_node)?;
319 Some(DefSite {
320 name: SmolStr::new(&name),
321 kind: DefSiteKind::Command,
322 name_range,
323 range: TextRange::new(command.text_range().start(), name_node.text_range().end()),
324 })
325}
326
327fn environment_def_site(command: &SyntaxNode) -> Option<DefSite> {
331 let (inner_range, text) = nth_group_inner(command, 0)?;
332 let trimmed = text.trim();
333 if trimmed.is_empty() {
334 return None;
335 }
336 let leading = text.len() - text.trim_start().len();
337 let name_range = TextRange::at(
338 inner_range.start() + TextSize::new(leading as u32),
339 TextSize::new(trimmed.len() as u32),
340 );
341 Some(DefSite {
342 name: SmolStr::new(trimmed),
343 kind: DefSiteKind::Environment,
344 name_range,
345 range: command.text_range(),
346 })
347}
348
349struct DefBody {
353 signal: bool,
356 called: Vec<SmolStr>,
359}
360
361fn apply_verbatim_flags(db: &mut SignatureDb, bodies: &HashMap<SmolStr, DefBody>) {
373 let verbatim: Vec<SmolStr> = bodies
374 .keys()
375 .filter(|name| {
376 db.command(name).is_some_and(|sig| !sig.args.is_empty())
378 && reaches_signal(name, bodies, &mut HashSet::new())
379 })
380 .cloned()
381 .collect();
382
383 for name in verbatim {
384 if let Some(mut sig) = db.command(&name).cloned() {
385 sig.args.to_mut().pop(); sig.verbatim = true;
387 db.insert_command(name, sig);
388 }
389 }
390}
391
392fn apply_verbatim_env_flags(
401 db: &mut SignatureDb,
402 env_bodies: &HashMap<SmolStr, DefBody>,
403 bodies: &HashMap<SmolStr, DefBody>,
404) {
405 let verbatim: Vec<SmolStr> = env_bodies
406 .iter()
407 .filter(|(name, body)| {
408 db.environment(name).is_some() && reaches_signal_body(body, bodies, &mut HashSet::new())
409 })
410 .map(|(name, _)| name.clone())
411 .collect();
412
413 for name in verbatim {
414 if let Some(mut sig) = db.environment(&name).cloned() {
415 sig.verbatim_body = true;
416 db.insert_environment(name, sig);
417 }
418 }
419}
420
421fn reaches_signal(
427 name: &str,
428 bodies: &HashMap<SmolStr, DefBody>,
429 visited: &mut HashSet<SmolStr>,
430) -> bool {
431 if !visited.insert(SmolStr::new(name)) {
432 return false;
433 }
434 let Some(body) = bodies.get(name) else {
435 return false;
436 };
437 reaches_signal_body(body, bodies, visited)
438}
439
440fn reaches_signal_body(
446 body: &DefBody,
447 bodies: &HashMap<SmolStr, DefBody>,
448 visited: &mut HashSet<SmolStr>,
449) -> bool {
450 body.signal
451 || body
452 .called
453 .iter()
454 .any(|callee| reaches_signal(callee, bodies, visited))
455}
456
457fn catcode_signal(body: &str) -> bool {
462 body.contains("\\@makeother")
463 || body.contains("\\@sanitize")
464 || body.contains("\\dospecials")
465 || body
466 .match_indices("\\catcode")
467 .any(|(start, _)| catcode_assigns_other(&body[start + "\\catcode".len()..]))
468}
469
470fn catcode_assigns_other(tail: &str) -> bool {
476 let mut chars = tail.chars();
477 if chars
478 .clone()
479 .next()
480 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '@' | '_' | ':'))
481 {
482 return false;
483 }
484
485 let mut after_equals = chars
486 .by_ref()
487 .take(64)
488 .skip_while(|&c| c != '=')
489 .skip(1)
490 .skip_while(|c| c.is_whitespace());
491 after_equals.next() == Some('1')
492 && after_equals.next() == Some('2')
493 && !after_equals.next().is_some_and(|c| c.is_ascii_digit())
494}
495
496fn called_macros(body: &str) -> Vec<SmolStr> {
501 body.match_indices('\\')
502 .filter_map(|(pos, _)| {
503 let after = &body[pos + 1..];
504 let len: usize = after
505 .chars()
506 .take_while(|c| c.is_ascii_alphabetic() || *c == '@')
507 .map(char::len_utf8)
508 .sum();
509 (len > 0).then(|| SmolStr::new(&after[..len]))
510 })
511 .collect()
512}
513
514const DELEGATING_PRIMITIVES: &[&str] = &["secdef", "@startsection", "@dblarg", "@sect", "@ssect"];
524
525fn keeps_builtin_over_arity0(name: &str, arity: usize, body: &DefBody) -> bool {
536 arity == 0
537 && body
538 .called
539 .iter()
540 .any(|callee| DELEGATING_PRIMITIVES.contains(&callee.as_str()))
541 && crate::semantic::signature::builtin()
542 .command(name)
543 .is_some()
544}
545
546pub fn is_definition_command(name: &str) -> bool {
553 DefKind::of(name).is_some()
554}
555
556enum DefKind {
558 Command,
559 Def,
560 Environment,
561 XparseCommand,
562 XparseEnvironment,
563 VerbatimEnvironment,
568}
569
570impl DefKind {
571 fn of(name: &str) -> Option<Self> {
572 Some(match name {
573 "newcommand" | "renewcommand" | "providecommand" | "DeclareRobustCommand" => {
574 DefKind::Command
575 }
576 "def" | "edef" | "gdef" | "xdef" => DefKind::Def,
579 "newenvironment" | "renewenvironment" => DefKind::Environment,
580 "NewDocumentCommand"
581 | "RenewDocumentCommand"
582 | "ProvideDocumentCommand"
583 | "DeclareDocumentCommand" => DefKind::XparseCommand,
584 "NewDocumentEnvironment"
585 | "RenewDocumentEnvironment"
586 | "ProvideDocumentEnvironment"
587 | "DeclareDocumentEnvironment" => DefKind::XparseEnvironment,
588 "lstnewenvironment" | "DefineVerbatimEnvironment" => DefKind::VerbatimEnvironment,
591 _ => return None,
592 })
593 }
594}
595
596fn scan_newcommand(
602 command: &SyntaxNode,
603 db: &mut SignatureDb,
604 bodies: &mut HashMap<SmolStr, DefBody>,
605 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
606) {
607 let Some(def) = resolve_command_def(command) else {
608 return;
609 };
610 let (arity, first_optional) = newcommand_arity(&def.host);
611 let body = nth_group(&def.host, def.first_arg_group);
614 record_body(bodies, &def.name, body.as_ref());
615 record_env_alias(aliases, &def.name, arity, body.as_ref());
616 if bodies
620 .get(def.name.as_str())
621 .is_some_and(|body| keeps_builtin_over_arity0(&def.name, arity, body))
622 {
623 return;
624 }
625 db.insert_command(
626 def.name,
627 CommandSig {
628 args: latex2e_args(arity, first_optional).into(),
629 sectioning: None,
630 verbatim: false,
631 verbatim_delimited: false,
632 rule: false,
633 inline: false,
634 block: false,
638 },
639 );
640}
641
642fn scan_def(
650 command: &SyntaxNode,
651 db: &mut SignatureDb,
652 bodies: &mut HashMap<SmolStr, DefBody>,
653 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
654) {
655 let Some(name_node) = adjacent_sibling_command(command) else {
656 return;
657 };
658 let Some(name) = command_name(&name_node) else {
659 return;
660 };
661 let (arity, body) = def_params_and_body(&name_node);
662 record_body(bodies, &name, body.as_ref());
663 record_env_alias(aliases, &name, arity, body.as_ref());
664 if bodies
667 .get(name.as_str())
668 .is_some_and(|body| keeps_builtin_over_arity0(&name, arity, body))
669 {
670 return;
671 }
672 db.insert_command(
673 name,
674 CommandSig {
675 args: latex2e_args(arity, false).into(),
679 sectioning: None,
680 verbatim: false,
681 verbatim_delimited: false,
682 rule: false,
683 inline: false,
684 block: false,
688 },
689 );
690}
691
692fn def_params_and_body(name_node: &SyntaxNode) -> (usize, Option<SyntaxNode>) {
705 if let Some(body) = nth_group(name_node, 0) {
707 return (0, Some(body));
708 }
709 let mut arity = 0usize;
711 let mut next = name_node.next_sibling_or_token();
712 while let Some(element) = next {
713 match element {
714 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
715 next = token.next_sibling_or_token();
716 }
717 NodeOrToken::Token(token) if token.kind() == SyntaxKind::HASH => {
718 arity += 1;
719 next = token.next_sibling_or_token();
720 }
721 NodeOrToken::Token(token) if token.kind() == SyntaxKind::WORD => {
723 next = token.next_sibling_or_token();
724 }
725 NodeOrToken::Node(node) if node.kind() == SyntaxKind::GROUP => {
726 return (arity.min(9), Some(node));
727 }
728 _ => return (arity.min(9), None),
729 }
730 }
731 (arity.min(9), None)
732}
733
734fn record_body(bodies: &mut HashMap<SmolStr, DefBody>, name: &str, body: Option<&SyntaxNode>) {
737 let text = body.map(group_inner_source).unwrap_or_default();
738 bodies.insert(
739 SmolStr::new(name),
740 DefBody {
741 signal: catcode_signal(&text),
742 called: called_macros(&text),
743 },
744 );
745}
746
747fn scan_newenvironment(
752 command: &SyntaxNode,
753 db: &mut SignatureDb,
754 env_bodies: &mut HashMap<SmolStr, DefBody>,
755) {
756 let Some(name) = nth_group_text(command, 0) else {
757 return;
758 };
759 let name = name.trim();
760 if name.is_empty() {
761 return;
762 }
763 record_body(env_bodies, name, nth_group(command, 1).as_ref());
764 let (arity, first_optional) = newcommand_arity(command);
765 db.insert_environment(name, environment_sig(latex2e_args(arity, first_optional)));
766}
767
768fn scan_verbatim_environment(defining_command: &str, command: &SyntaxNode, db: &mut SignatureDb) {
778 let Some(name) = nth_group_text(command, 0) else {
779 return;
780 };
781 let name = name.trim();
782 if name.is_empty() {
783 return;
784 }
785 let args = if defining_command == "lstnewenvironment" {
786 let (arity, first_optional) = newcommand_arity(command);
787 latex2e_args(arity, first_optional)
788 } else {
789 latex2e_args(1, true)
791 };
792 let mut sig = environment_sig(args);
793 sig.verbatim_body = true;
794 db.insert_environment(name, sig);
795}
796
797fn scan_xparse_command(
802 command: &SyntaxNode,
803 db: &mut SignatureDb,
804 bodies: &mut HashMap<SmolStr, DefBody>,
805 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
806) {
807 let Some(def) = resolve_command_def(command) else {
808 return;
809 };
810 let Some(spec) = nth_group(&def.host, def.first_arg_group) else {
811 return;
812 };
813 let body = nth_group(&def.host, def.first_arg_group + 1);
815 record_body(bodies, &def.name, body.as_ref());
816 let args = xparse::parse_spec(&group_inner_source(&spec));
817 record_env_alias(aliases, &def.name, args.len(), body.as_ref());
818 db.insert_command(
819 def.name,
820 CommandSig {
821 args: args.into(),
822 sectioning: None,
823 verbatim: false,
824 verbatim_delimited: false,
825 rule: false,
826 inline: false,
827 block: false,
831 },
832 );
833}
834
835struct CommandDef {
846 name: SmolStr,
847 host: SyntaxNode,
848 first_arg_group: usize,
849}
850
851fn resolve_command_def(command: &SyntaxNode) -> Option<CommandDef> {
855 if command.children().any(|c| c.kind() == SyntaxKind::GROUP) {
858 let name = nth_group(command, 0)
859 .as_ref()
860 .and_then(group_command_name)?;
861 return Some(CommandDef {
862 name,
863 host: command.clone(),
864 first_arg_group: 1,
865 });
866 }
867 let sibling = adjacent_sibling_command(command)?;
871 let name = command_name(&sibling)?;
872 Some(CommandDef {
873 name,
874 host: sibling,
875 first_arg_group: 0,
876 })
877}
878
879fn adjacent_sibling_command(command: &SyntaxNode) -> Option<SyntaxNode> {
885 let mut next = command.next_sibling_or_token();
886 while let Some(element) = next {
887 match element {
888 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
889 next = token.next_sibling_or_token();
890 }
891 NodeOrToken::Node(node) if node.kind() == SyntaxKind::COMMAND => return Some(node),
892 _ => return None,
893 }
894 }
895 None
896}
897
898fn scan_xparse_environment(
902 command: &SyntaxNode,
903 db: &mut SignatureDb,
904 env_bodies: &mut HashMap<SmolStr, DefBody>,
905) {
906 let Some(name) = nth_group_text(command, 0) else {
907 return;
908 };
909 let name = name.trim();
910 if name.is_empty() {
911 return;
912 }
913 let Some(spec) = nth_group(command, 1) else {
914 return;
915 };
916 record_body(env_bodies, name, nth_group(command, 2).as_ref());
917 db.insert_environment(
918 name,
919 environment_sig(xparse::parse_spec(&group_inner_source(&spec))),
920 );
921}
922
923fn newcommand_arity(command: &SyntaxNode) -> (usize, bool) {
927 let optionals: Vec<Optional> = children::<Optional>(command).collect();
928 let arity = optionals
929 .first()
930 .map(|o| o.syntax())
931 .and_then(optional_number)
932 .unwrap_or(0)
933 .min(9); (arity, optionals.len() >= 2)
935}
936
937fn optional_number(node: &SyntaxNode) -> Option<usize> {
940 let text = node.text().to_string();
941 let inner = text.strip_prefix('[').unwrap_or(&text);
942 let inner = inner.strip_suffix(']').unwrap_or(inner);
943 inner.trim().parse().ok()
944}
945
946fn latex2e_args(arity: usize, first_optional: bool) -> Vec<ArgSpec> {
949 (0..arity)
950 .map(|i| {
951 if i == 0 && first_optional {
952 ArgSpec {
953 required: false,
954 kind: ArgKind::Bracket,
955 content: ContentKind::Opaque,
956 domain: crate::semantic::ArgumentDomain::Unknown,
957 verbatim: false,
958 }
959 } else {
960 ArgSpec {
961 required: true,
962 kind: ArgKind::Brace,
963 content: ContentKind::Opaque,
964 domain: crate::semantic::ArgumentDomain::Unknown,
965 verbatim: false,
966 }
967 }
968 })
969 .collect()
970}
971
972fn environment_sig(args: Vec<ArgSpec>) -> EnvironmentSig {
976 EnvironmentSig {
977 args: args.into(),
978 verbatim_body: false,
979 verbatim_arg: false,
982 math: false,
983 code: false,
984 statement_body: false,
987 label_key: false,
989 align: false,
990 no_indent: false,
991 list: false,
994 block_explicit: false,
998 outline: None,
1001 }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007 use crate::parser::{parse, reconstruct};
1008
1009 fn db_of(src: &str) -> SignatureDb {
1010 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1011 scan_definitions(&SyntaxNode::new_root(parse(src).green))
1012 }
1013
1014 fn arg_kinds(args: &[ArgSpec]) -> Vec<ArgKind> {
1015 args.iter().map(|a| a.kind).collect()
1016 }
1017
1018 #[test]
1019 fn newcommand_counts_mandatory_args() {
1020 let db = db_of("\\newcommand{\\foo}[2]{#1#2}\n");
1021 let sig = db.command("foo").expect("foo defined");
1022 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1023 assert!(sig.args.iter().all(|a| a.required));
1024 assert!(
1025 sig.args
1026 .iter()
1027 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1028 );
1029 }
1030
1031 #[test]
1032 fn newcommand_optional_first_arg() {
1033 let db = db_of("\\newcommand{\\foo}[2][d]{#1#2}\n");
1034 let sig = db.command("foo").expect("foo defined");
1035 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1036 assert!(!sig.args[0].required);
1037 assert!(sig.args[1].required);
1038 }
1039
1040 #[test]
1041 fn newcommand_zero_args() {
1042 let db = db_of("\\newcommand{\\foo}{bar}\n");
1043 assert!(db.command("foo").expect("foo defined").args.is_empty());
1044 }
1045
1046 #[test]
1047 fn renew_and_provide_recognized() {
1048 let db = db_of("\\renewcommand{\\a}[1]{x}\\providecommand{\\b}[1]{y}\n");
1049 assert_eq!(db.command("a").unwrap().args.len(), 1);
1050 assert_eq!(db.command("b").unwrap().args.len(), 1);
1051 }
1052
1053 #[test]
1054 fn secdef_redefinition_keeps_builtin_prose() {
1055 let db = db_of("\\renewcommand{\\section}{\\secdef \\a \\b}\n");
1056 assert!(
1057 db.command("section").is_none(),
1058 "the delegating redefinition must not be recorded as a scanned override"
1059 );
1060 let sigs = crate::semantic::signature::Signatures::new(&db);
1061 let sig = sigs.command("section").expect("built-in section survives");
1062 let last = sig.args.last().expect("section keeps its title argument");
1063 assert_eq!(
1064 last.content,
1065 crate::semantic::signature::ContentKind::Prose,
1066 "the title argument stays prose (reflowable)"
1067 );
1068 }
1069
1070 #[test]
1071 fn genuine_zero_arg_redefinition_downgrades_builtin() {
1072 let db = db_of("\\renewcommand{\\section}{\\textbf{Fixed}}\n");
1073 let sig = db
1074 .command("section")
1075 .expect("genuine 0-arg redefinition is recorded");
1076 assert!(
1077 sig.args.is_empty(),
1078 "no delegation means the scanned 0-arg signature wins"
1079 );
1080 }
1081
1082 #[test]
1083 fn secdef_redefinition_of_unknown_still_records() {
1084 let db = db_of("\\renewcommand{\\mysec}{\\secdef \\a \\b}\n");
1085 let sig = db.command("mysec").expect("unknown name is still recorded");
1086 assert!(sig.args.is_empty());
1087 }
1088
1089 #[test]
1090 fn newenvironment_args() {
1091 let db = db_of("\\newenvironment{thm}[1]{begin #1}{end}\n");
1092 let sig = db.environment("thm").expect("thm defined");
1093 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1094 assert!(sig.reflow());
1095 assert!(!sig.verbatim_body);
1096 assert!(!sig.math);
1097 }
1098
1099 #[test]
1100 fn xparse_command_spec() {
1101 let db = db_of("\\NewDocumentCommand{\\foo}{m O{d} m}{x}\n");
1102 let sig = db.command("foo").expect("foo defined");
1103 assert_eq!(
1104 arg_kinds(&sig.args),
1105 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1106 );
1107 assert!(
1108 sig.args
1109 .iter()
1110 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1111 );
1112 }
1113
1114 #[test]
1115 fn xparse_environment_spec() {
1116 let db = db_of("\\NewDocumentEnvironment{env}{O{x} m}{a}{b}\n");
1117 let sig = db.environment("env").expect("env defined");
1118 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1119 assert!(
1120 sig.args
1121 .iter()
1122 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1123 );
1124 }
1125
1126 #[test]
1127 fn unbraced_newcommand_extracted() {
1128 let db = db_of("\\newcommand\\foo[2]{#1#2}\n");
1129 let sig = db.command("foo").expect("foo defined");
1130 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1131 assert!(sig.args.iter().all(|a| a.required));
1132 }
1133
1134 #[test]
1135 fn unbraced_optional_first_arg() {
1136 let db = db_of("\\newcommand\\foo[2][d]{#1#2}\n");
1137 let sig = db.command("foo").expect("foo defined");
1138 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1139 assert!(!sig.args[0].required);
1140 assert!(sig.args[1].required);
1141 }
1142
1143 #[test]
1144 fn unbraced_zero_args() {
1145 let db = db_of("\\newcommand\\foo{x}\n");
1146 assert!(db.command("foo").expect("foo defined").args.is_empty());
1147 }
1148
1149 #[test]
1150 fn unbraced_spaced_binds() {
1151 let db = db_of("\\newcommand \\foo[1]{x}\n");
1152 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1153 }
1154
1155 #[test]
1156 fn unbraced_renewcommand() {
1157 let db = db_of("\\renewcommand\\foo[1]{x}\n");
1158 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1159 }
1160
1161 #[test]
1162 fn unbraced_xparse_command() {
1163 let db = db_of("\\NewDocumentCommand\\foo{m O{d} m}{x}\n");
1164 let sig = db.command("foo").expect("foo defined");
1165 assert_eq!(
1166 arg_kinds(&sig.args),
1167 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1168 );
1169 }
1170
1171 #[test]
1172 fn unbraced_stray_text_not_bound() {
1173 let db = db_of("\\newcommand foo \\bar{x}\n");
1174 assert!(db.command("foo").is_none());
1175 assert!(db.command("bar").is_none());
1176 }
1177
1178 #[test]
1179 fn redefinition_last_wins() {
1180 let db = db_of("\\newcommand{\\foo}[1]{x}\\renewcommand{\\foo}[3]{y}\n");
1181 assert_eq!(db.command("foo").unwrap().args.len(), 3);
1182 }
1183
1184 #[test]
1185 fn garbage_definition_degrades_to_no_insert() {
1186 let db = db_of("\\newcommand\n");
1187 assert!(db.command("foo").is_none());
1188 }
1189
1190 #[test]
1191 fn nested_definition_collected() {
1192 let db = db_of("\\begin{document}\n\\newcommand{\\foo}[1]{x}\n\\end{document}\n");
1193 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1194 }
1195
1196 #[test]
1197 fn commented_definition_ignored() {
1198 let db = db_of("% \\newcommand{\\foo}[1]{x}\n");
1199 assert!(db.command("foo").is_none());
1200 }
1201
1202 #[test]
1203 fn verbatim_makeother_flagged() {
1204 let db = db_of("\\newcommand\\shellcmd[1]{\\@makeother\\$#1}\n");
1205 let sig = db.command("shellcmd").expect("shellcmd defined");
1206 assert!(sig.verbatim);
1207 assert!(sig.args.is_empty());
1208 }
1209
1210 #[test]
1211 fn verbatim_catcode_flagged() {
1212 let db = db_of("\\newcommand\\shellcmd[1]{\\catcode 36=12 #1}\n");
1213 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1214 }
1215
1216 #[test]
1217 fn unrelated_twelve_does_not_flag_catcode_assignment() {
1218 let db = db_of("\\newcommand\\ordinary[1]{\\catcode 36=\\active \\hspace{12pt}#1}\n");
1219 assert!(!db.command("ordinary").expect("ordinary defined").verbatim);
1220 }
1221
1222 #[test]
1223 fn verbatim_dospecials_flagged() {
1224 let db = db_of("\\newcommand\\shellcmd[1]{\\let\\do\\@makeother\\dospecials #1}\n");
1225 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1226 }
1227
1228 #[test]
1229 fn verbatim_keeps_leading_args() {
1230 let db = db_of("\\newcommand\\mycode[2]{\\@makeother\\$#1#2}\n");
1231 let sig = db.command("mycode").expect("mycode defined");
1232 assert!(sig.verbatim);
1233 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1234 }
1235
1236 #[test]
1237 fn verbatim_via_chained_helper() {
1238 let db =
1239 db_of("\\newcommand\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1240 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1241 assert!(!db.command("setup").expect("setup defined").verbatim);
1242 }
1243
1244 #[test]
1245 fn verbatim_chain_cycle_terminates() {
1246 let db = db_of("\\newcommand\\a[1]{\\b#1}\\newcommand\\b[1]{\\a#1}\n");
1247 assert!(!db.command("a").expect("a defined").verbatim);
1248 assert!(!db.command("b").expect("b defined").verbatim);
1249 }
1250
1251 #[test]
1252 fn ordinary_command_not_verbatim() {
1253 let db = db_of("\\newcommand\\foo[1]{\\emph{#1}}\n");
1254 assert!(!db.command("foo").expect("foo defined").verbatim);
1255 }
1256
1257 #[test]
1258 fn verbatim_needs_an_argument() {
1259 let db = db_of("\\newcommand\\setup{\\@makeother\\$}\n");
1260 assert!(!db.command("setup").expect("setup defined").verbatim);
1261 }
1262
1263 #[test]
1264 fn def_helper_chain_followed() {
1265 let db = db_of("\\def\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1266 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1267 assert!(!db.command("setup").expect("setup defined").verbatim);
1268 }
1269
1270 #[test]
1271 fn def_direct_verbatim_flagged() {
1272 let db = db_of("\\def\\shellcmd#1{\\@makeother\\$#1}\n");
1273 let sig = db.command("shellcmd").expect("shellcmd defined");
1274 assert!(sig.verbatim);
1275 assert!(sig.args.is_empty());
1276 }
1277
1278 #[test]
1279 fn def_zero_params() {
1280 let db = db_of("\\def\\foo{x}\n");
1281 let sig = db.command("foo").expect("foo defined");
1282 assert!(sig.args.is_empty());
1283 assert!(!sig.verbatim);
1284 }
1285
1286 #[test]
1287 fn def_counts_params() {
1288 let db = db_of("\\def\\foo#1#2{#1#2}\n");
1289 let sig = db.command("foo").expect("foo defined");
1290 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1291 }
1292
1293 #[test]
1294 fn def_variants_scanned() {
1295 let db = db_of("\\edef\\a#1{x}\\gdef\\b{y}\\xdef\\c#1{\\@makeother\\$#1}\n");
1296 assert_eq!(db.command("a").expect("a defined").args.len(), 1);
1297 assert!(db.command("b").expect("b defined").args.is_empty());
1298 let c = db.command("c").expect("c defined");
1299 assert!(c.verbatim);
1300 assert!(c.args.is_empty());
1301 }
1302
1303 #[test]
1304 fn def_chain_through_def_helpers() {
1305 let db = db_of(
1306 "\\def\\inner{\\@makeother\\$}\\def\\outer{\\inner}\\newcommand\\cmd[1]{\\outer#1}\n",
1307 );
1308 assert!(db.command("cmd").expect("cmd defined").verbatim);
1309 }
1310
1311 #[test]
1312 fn verbatim_xparse_flagged() {
1313 let db = db_of("\\NewDocumentCommand\\shellcmd{m}{\\@makeother\\$#1}\n");
1314 let sig = db.command("shellcmd").expect("shellcmd defined");
1315 assert!(sig.verbatim);
1316 assert!(sig.args.is_empty());
1317 }
1318
1319 #[test]
1320 fn env_makeother_flagged() {
1321 let db = db_of("\\newenvironment{shellenv}{\\@makeother\\$}{}\n");
1322 let sig = db.environment("shellenv").expect("shellenv defined");
1323 assert!(sig.verbatim_body);
1324 assert!(!sig.reflow()); }
1326
1327 #[test]
1328 fn env_catcode_flagged() {
1329 let db = db_of("\\newenvironment{shellenv}[1]{\\catcode 36=12 }{}\n");
1330 let sig = db.environment("shellenv").expect("shellenv defined");
1331 assert!(sig.verbatim_body);
1332 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1333 }
1334
1335 #[test]
1336 fn env_via_chained_helper() {
1337 let db =
1338 db_of("\\newcommand\\setup{\\@makeother\\$}\\newenvironment{shellenv}{\\setup}{}\n");
1339 assert!(
1340 db.environment("shellenv")
1341 .expect("shellenv defined")
1342 .verbatim_body
1343 );
1344 }
1345
1346 #[test]
1347 fn env_without_signal_not_flagged() {
1348 let db = db_of("\\newenvironment{remark}{\\par\\noindent\\textbf{Remark.}}{\\par}\n");
1349 let sig = db.environment("remark").expect("remark defined");
1350 assert!(!sig.verbatim_body);
1351 assert!(sig.reflow());
1352 }
1353
1354 #[test]
1355 fn lstnewenvironment_flagged_verbatim() {
1356 let db = db_of("\\lstnewenvironment{demo}[1][code]{\\lstset{#1}}{}\n");
1357 let sig = db.environment("demo").expect("demo defined");
1358 assert!(sig.verbatim_body);
1359 assert!(!sig.reflow());
1360 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1361 }
1362
1363 #[test]
1364 fn lstnewenvironment_no_args_flagged_verbatim() {
1365 let db = db_of("\\lstnewenvironment{demo}{}{}\n");
1366 let sig = db.environment("demo").expect("demo defined");
1367 assert!(sig.verbatim_body);
1368 assert!(sig.args.is_empty());
1369 }
1370
1371 #[test]
1372 fn defineverbatimenvironment_flagged_verbatim() {
1373 let db = db_of("\\DefineVerbatimEnvironment{code}{Verbatim}{fontsize=\\small}\n");
1374 let sig = db.environment("code").expect("code defined");
1375 assert!(sig.verbatim_body);
1376 assert!(!sig.reflow());
1377 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1378 }
1379
1380 #[test]
1381 fn xparse_env_makeother_flagged() {
1382 let db = db_of("\\NewDocumentEnvironment{shellenv}{O{x}}{\\dospecials}{}\n");
1383 let sig = db.environment("shellenv").expect("shellenv defined");
1384 assert!(sig.verbatim_body);
1385 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1386 }
1387
1388 fn sites_of(src: &str) -> Vec<DefSite> {
1389 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1390 scan_definition_sites(&SyntaxNode::new_root(parse(src).green))
1391 }
1392
1393 #[test]
1394 fn def_site_newcommand_braced_name_span() {
1395 let src = "\\newcommand{\\foo}[1]{#1}\n";
1396 let sites = sites_of(src);
1397 assert_eq!(sites.len(), 1);
1398 let site = &sites[0];
1399 assert_eq!(site.name, "foo");
1400 assert_eq!(site.kind, DefSiteKind::Command);
1401 assert_eq!(&src[site.name_range], "\\foo");
1402 assert_eq!(&src[site.range], "\\newcommand{\\foo}[1]{#1}");
1403 }
1404
1405 #[test]
1406 fn def_site_newcommand_unbraced_name_span() {
1407 let src = "\\newcommand\\foo[1]{#1}\n";
1408 let sites = sites_of(src);
1409 assert_eq!(sites.len(), 1);
1410 assert_eq!(sites[0].name, "foo");
1411 assert_eq!(&src[sites[0].name_range], "\\foo");
1412 assert_eq!(&src[sites[0].range], "\\newcommand\\foo[1]{#1}");
1413 }
1414
1415 #[test]
1416 fn def_site_def_sibling_name_span() {
1417 let src = "\\def\\foo#1{#1}\n";
1418 let sites = sites_of(src);
1419 assert_eq!(sites.len(), 1);
1420 assert_eq!(sites[0].name, "foo");
1421 assert_eq!(sites[0].kind, DefSiteKind::Command);
1422 assert_eq!(&src[sites[0].name_range], "\\foo");
1423 }
1424
1425 #[test]
1426 fn def_site_xparse_command_name_span() {
1427 let src = "\\NewDocumentCommand{\\foo}{m O{d}}{x}\n";
1428 let sites = sites_of(src);
1429 assert_eq!(sites.len(), 1);
1430 assert_eq!(sites[0].name, "foo");
1431 assert_eq!(&src[sites[0].name_range], "\\foo");
1432 }
1433
1434 #[test]
1435 fn def_site_newenvironment_name_span() {
1436 let src = "\\newenvironment{myenv}{begin}{end}\n";
1437 let sites = sites_of(src);
1438 assert_eq!(sites.len(), 1);
1439 let site = &sites[0];
1440 assert_eq!(site.name, "myenv");
1441 assert_eq!(site.kind, DefSiteKind::Environment);
1442 assert_eq!(&src[site.name_range], "myenv");
1443 }
1444
1445 #[test]
1446 fn def_site_xparse_environment_name_span() {
1447 let src = "\\NewDocumentEnvironment{myenv}{m}{a}{b}\n";
1448 let sites = sites_of(src);
1449 assert_eq!(sites.len(), 1);
1450 assert_eq!(sites[0].name, "myenv");
1451 assert_eq!(sites[0].kind, DefSiteKind::Environment);
1452 assert_eq!(&src[sites[0].name_range], "myenv");
1453 }
1454
1455 #[test]
1456 fn def_site_keeps_every_redefinition() {
1457 let src = "\\newcommand{\\foo}{a}\n\\renewcommand{\\foo}{b}\n";
1458 let sites = sites_of(src);
1459 assert_eq!(sites.len(), 2);
1460 assert!(sites.iter().all(|s| s.name == "foo"));
1461 assert!(sites[0].name_range.start() < sites[1].name_range.start());
1462 }
1463
1464 #[test]
1465 fn def_site_none_for_malformed() {
1466 assert!(sites_of("\\newcommand\n").is_empty());
1467 assert!(sites_of("\\newenvironment{}{a}{b}\n").is_empty());
1468 }
1469
1470 const EQNARRAY_PAIR: &str =
1471 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n";
1472
1473 #[test]
1474 fn newcommand_env_alias_pair_is_recorded() {
1475 let db = db_of(EQNARRAY_PAIR);
1476 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1477 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1478 assert_eq!(db.env_begin_alias("eea"), None);
1479 assert_eq!(db.env_end_alias("bea"), None);
1480 }
1481
1482 #[test]
1483 fn def_env_alias_pair_is_recorded() {
1484 let db = db_of("\\def\\bea{\\begin{eqnarray}}\n\\def\\eea{\\end{eqnarray}}\n");
1485 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1486 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1487 }
1488
1489 #[test]
1490 fn xparse_env_alias_pair_is_recorded() {
1491 let db = db_of(
1492 "\\NewDocumentCommand{\\bea}{}{\\begin{eqnarray}}\n\
1493 \\NewDocumentCommand{\\eea}{}{\\end{eqnarray}}\n",
1494 );
1495 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1496 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1497 }
1498
1499 #[test]
1500 fn env_alias_body_tolerates_trivia() {
1501 let db = db_of(
1502 "\\newcommand{\\bea}{ \\begin{eqnarray} }\n\\newcommand{\\eea}{% why\n\\end{eqnarray}}\n",
1503 );
1504 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1505 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1506 }
1507
1508 #[test]
1509 fn a_lone_env_alias_half_is_recorded() {
1510 let db = db_of("\\newcommand{\\bea}{\\begin{eqnarray}}\n");
1511 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1512 assert_eq!(db.env_end_alias("bea"), None);
1513
1514 let db = db_of("\\newcommand{\\eea}{\\end{eqnarray}}\n");
1515 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1516 assert_eq!(db.env_begin_alias("eea"), None);
1517 }
1518
1519 #[test]
1520 fn a_lone_half_still_obeys_every_target_rule() {
1521 assert_eq!(
1522 db_of("\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n").env_begin_alias("bmy"),
1523 None
1524 );
1525 assert_eq!(
1526 db_of("\\newcommand{\\ev}{\\end{verbatim}}\n").env_end_alias("ev"),
1527 None
1528 );
1529 assert_eq!(
1530 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n").env_begin_alias("bt"),
1531 None
1532 );
1533 assert_eq!(
1534 db_of("\\newcommand{\\bea}[1]{\\begin{eqnarray}}\n").env_begin_alias("bea"),
1535 None
1536 );
1537 }
1538
1539 #[test]
1540 fn env_alias_rejects_uncurated_target() {
1541 let db = db_of(
1542 "\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n\
1543 \\newcommand{\\emy}{\\end{notacuratedenv}}\n",
1544 );
1545 assert_eq!(db.env_begin_alias("bmy"), None);
1546 assert_eq!(db.env_end_alias("emy"), None);
1547 }
1548
1549 #[test]
1550 fn env_alias_rejects_verbatim_target() {
1551 let db =
1552 db_of("\\newcommand{\\bv}{\\begin{verbatim}}\n\\newcommand{\\ev}{\\end{verbatim}}\n");
1553 assert_eq!(db.env_begin_alias("bv"), None);
1554 }
1555
1556 #[test]
1557 fn env_alias_rejects_argument_taking_target() {
1558 let db =
1559 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n\\newcommand{\\et}{\\end{tabular}}\n");
1560 assert_eq!(db.env_begin_alias("bt"), None);
1561 }
1562
1563 #[test]
1564 fn env_alias_rejects_parameterized_definition() {
1565 let db = db_of(
1566 "\\newcommand{\\bt}[1]{\\begin{tabular}{#1}}\n\\newcommand{\\et}{\\end{tabular}}\n",
1567 );
1568 assert_eq!(db.env_begin_alias("bt"), None);
1569 }
1570
1571 #[test]
1572 fn env_alias_rejects_body_with_extra_content() {
1573 let db = db_of(
1574 "\\newcommand{\\bea}{\\begin{eqnarray}\\label{x}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n",
1575 );
1576 assert_eq!(db.env_begin_alias("bea"), None);
1577 }
1578
1579 #[test]
1580 fn redefinition_retracts_an_earlier_env_alias() {
1581 let db = db_of(
1582 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1583 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1584 \\renewcommand{\\bea}{\\textbf}\n",
1585 );
1586 assert_eq!(db.env_begin_alias("bea"), None);
1587 }
1588
1589 #[test]
1590 fn env_alias_does_not_pollute_the_environment_namespace() {
1591 let db = db_of(EQNARRAY_PAIR);
1592 assert!(db.environment("bea").is_none());
1593 assert!(!db.environment_names().any(|n| n == "bea"));
1594 }
1595
1596 fn environment_node(src: &str) -> SyntaxNode {
1597 SyntaxNode::new_root(parse(src).green)
1598 .descendants()
1599 .find(|n| n.kind() == SyntaxKind::ENVIRONMENT)
1600 .expect("an environment")
1601 }
1602
1603 #[test]
1604 fn signatures_resolves_an_env_alias_to_the_curated_target() {
1605 use crate::semantic::signature::Signatures;
1606 let src = format!("{EQNARRAY_PAIR}\\bea a \\eea\n");
1607 let db = db_of(&src);
1608 let sigs = Signatures::new(&db);
1609 let sig = sigs
1610 .environment_at(&environment_node(&src))
1611 .expect("alias resolves");
1612 let target = builtin().environment("eqnarray").expect("curated target");
1613 assert_eq!(sig, target);
1614 assert!(sig.math && sig.align);
1615 assert!(sigs.environment("bea").is_none());
1616 }
1617
1618 #[test]
1619 fn a_literal_begin_does_not_inherit_the_alias_target() {
1620 use crate::semantic::signature::Signatures;
1621 let src = format!("{EQNARRAY_PAIR}\\begin{{bea}} a \\end{{bea}}\n");
1622 let db = db_of(&src);
1623 let sigs = Signatures::new(&db);
1624 assert!(sigs.environment_at(&environment_node(&src)).is_none());
1625 }
1626
1627 #[test]
1628 fn a_real_environment_wins_over_an_alias_of_the_same_name() {
1629 use crate::semantic::signature::Signatures;
1630 let defs = "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1631 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1632 \\newenvironment{bea}{x}{y}\n";
1633 let db = db_of(&format!("{defs}\\begin{{bea}} a \\end{{bea}}\n"));
1634 let sigs = Signatures::new(&db);
1635 let sig = sigs
1636 .environment_at(&environment_node(&format!(
1637 "{defs}\\begin{{bea}} a \\end{{bea}}\n"
1638 )))
1639 .expect("real environment resolves");
1640 assert!(
1641 !sig.math,
1642 "the scanned \\newenvironment must win for a literal `\\begin{{bea}}`"
1643 );
1644 assert!(
1645 sigs.environment_at(&environment_node(&format!("{defs}\\bea a \\eea\n")))
1646 .is_some_and(|sig| sig.math),
1647 "while the alias delimiters still resolve to eqnarray"
1648 );
1649 }
1650
1651 #[test]
1652 fn merge_from_carries_env_aliases() {
1653 let mut target = SignatureDb::default();
1654 target.merge_from(&db_of(EQNARRAY_PAIR), None);
1655 assert_eq!(target.env_begin_alias("bea"), Some("eqnarray"));
1656 assert_eq!(target.env_end_alias("eea"), Some("eqnarray"));
1657 }
1658}