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};
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 sig.reflow = false; db.insert_environment(name, sig);
418 }
419 }
420}
421
422fn reaches_signal(
428 name: &str,
429 bodies: &HashMap<SmolStr, DefBody>,
430 visited: &mut HashSet<SmolStr>,
431) -> bool {
432 if !visited.insert(SmolStr::new(name)) {
433 return false;
434 }
435 let Some(body) = bodies.get(name) else {
436 return false;
437 };
438 reaches_signal_body(body, bodies, visited)
439}
440
441fn reaches_signal_body(
447 body: &DefBody,
448 bodies: &HashMap<SmolStr, DefBody>,
449 visited: &mut HashSet<SmolStr>,
450) -> bool {
451 body.signal
452 || body
453 .called
454 .iter()
455 .any(|callee| reaches_signal(callee, bodies, visited))
456}
457
458fn catcode_signal(body: &str) -> bool {
463 body.contains("\\@makeother")
464 || body.contains("\\@sanitize")
465 || body.contains("\\dospecials")
466 || body
467 .match_indices("\\catcode")
468 .any(|(start, _)| catcode_assigns_other(&body[start + "\\catcode".len()..]))
469}
470
471fn catcode_assigns_other(tail: &str) -> bool {
477 let mut chars = tail.chars();
478 if chars
479 .clone()
480 .next()
481 .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '@' | '_' | ':'))
482 {
483 return false;
484 }
485
486 let mut after_equals = chars
487 .by_ref()
488 .take(64)
489 .skip_while(|&c| c != '=')
490 .skip(1)
491 .skip_while(|c| c.is_whitespace());
492 after_equals.next() == Some('1')
493 && after_equals.next() == Some('2')
494 && !after_equals.next().is_some_and(|c| c.is_ascii_digit())
495}
496
497fn called_macros(body: &str) -> Vec<SmolStr> {
502 body.match_indices('\\')
503 .filter_map(|(pos, _)| {
504 let after = &body[pos + 1..];
505 let len: usize = after
506 .chars()
507 .take_while(|c| c.is_ascii_alphabetic() || *c == '@')
508 .map(char::len_utf8)
509 .sum();
510 (len > 0).then(|| SmolStr::new(&after[..len]))
511 })
512 .collect()
513}
514
515const DELEGATING_PRIMITIVES: &[&str] = &["secdef", "@startsection", "@dblarg", "@sect", "@ssect"];
525
526fn keeps_builtin_over_arity0(name: &str, arity: usize, body: &DefBody) -> bool {
537 arity == 0
538 && body
539 .called
540 .iter()
541 .any(|callee| DELEGATING_PRIMITIVES.contains(&callee.as_str()))
542 && crate::semantic::signature::builtin()
543 .command(name)
544 .is_some()
545}
546
547pub fn is_definition_command(name: &str) -> bool {
554 DefKind::of(name).is_some()
555}
556
557enum DefKind {
559 Command,
560 Def,
561 Environment,
562 XparseCommand,
563 XparseEnvironment,
564 VerbatimEnvironment,
569}
570
571impl DefKind {
572 fn of(name: &str) -> Option<Self> {
573 Some(match name {
574 "newcommand" | "renewcommand" | "providecommand" | "DeclareRobustCommand" => {
575 DefKind::Command
576 }
577 "def" | "edef" | "gdef" | "xdef" => DefKind::Def,
580 "newenvironment" | "renewenvironment" => DefKind::Environment,
581 "NewDocumentCommand"
582 | "RenewDocumentCommand"
583 | "ProvideDocumentCommand"
584 | "DeclareDocumentCommand" => DefKind::XparseCommand,
585 "NewDocumentEnvironment"
586 | "RenewDocumentEnvironment"
587 | "ProvideDocumentEnvironment"
588 | "DeclareDocumentEnvironment" => DefKind::XparseEnvironment,
589 "lstnewenvironment" | "DefineVerbatimEnvironment" => DefKind::VerbatimEnvironment,
592 _ => return None,
593 })
594 }
595}
596
597fn scan_newcommand(
603 command: &SyntaxNode,
604 db: &mut SignatureDb,
605 bodies: &mut HashMap<SmolStr, DefBody>,
606 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
607) {
608 let Some(def) = resolve_command_def(command) else {
609 return;
610 };
611 let (arity, first_optional) = newcommand_arity(&def.host);
612 let body = nth_group(&def.host, def.first_arg_group);
615 record_body(bodies, &def.name, body.as_ref());
616 record_env_alias(aliases, &def.name, arity, body.as_ref());
617 if bodies
621 .get(def.name.as_str())
622 .is_some_and(|body| keeps_builtin_over_arity0(&def.name, arity, body))
623 {
624 return;
625 }
626 db.insert_command(
627 def.name,
628 CommandSig {
629 args: latex2e_args(arity, first_optional).into(),
630 sectioning: None,
631 verbatim: false,
632 verbatim_delimited: false,
633 rule: false,
634 inline: false,
635 block: false,
639 },
640 );
641}
642
643fn scan_def(
651 command: &SyntaxNode,
652 db: &mut SignatureDb,
653 bodies: &mut HashMap<SmolStr, DefBody>,
654 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
655) {
656 let Some(name_node) = adjacent_sibling_command(command) else {
657 return;
658 };
659 let Some(name) = command_name(&name_node) else {
660 return;
661 };
662 let (arity, body) = def_params_and_body(&name_node);
663 record_body(bodies, &name, body.as_ref());
664 record_env_alias(aliases, &name, arity, body.as_ref());
665 if bodies
668 .get(name.as_str())
669 .is_some_and(|body| keeps_builtin_over_arity0(&name, arity, body))
670 {
671 return;
672 }
673 db.insert_command(
674 name,
675 CommandSig {
676 args: latex2e_args(arity, false).into(),
680 sectioning: None,
681 verbatim: false,
682 verbatim_delimited: false,
683 rule: false,
684 inline: false,
685 block: false,
689 },
690 );
691}
692
693fn def_params_and_body(name_node: &SyntaxNode) -> (usize, Option<SyntaxNode>) {
706 if let Some(body) = nth_group(name_node, 0) {
708 return (0, Some(body));
709 }
710 let mut arity = 0usize;
712 let mut next = name_node.next_sibling_or_token();
713 while let Some(element) = next {
714 match element {
715 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
716 next = token.next_sibling_or_token();
717 }
718 NodeOrToken::Token(token) if token.kind() == SyntaxKind::HASH => {
719 arity += 1;
720 next = token.next_sibling_or_token();
721 }
722 NodeOrToken::Token(token) if token.kind() == SyntaxKind::WORD => {
724 next = token.next_sibling_or_token();
725 }
726 NodeOrToken::Node(node) if node.kind() == SyntaxKind::GROUP => {
727 return (arity.min(9), Some(node));
728 }
729 _ => return (arity.min(9), None),
730 }
731 }
732 (arity.min(9), None)
733}
734
735fn record_body(bodies: &mut HashMap<SmolStr, DefBody>, name: &str, body: Option<&SyntaxNode>) {
738 let text = body.map(group_inner_source).unwrap_or_default();
739 bodies.insert(
740 SmolStr::new(name),
741 DefBody {
742 signal: catcode_signal(&text),
743 called: called_macros(&text),
744 },
745 );
746}
747
748fn scan_newenvironment(
753 command: &SyntaxNode,
754 db: &mut SignatureDb,
755 env_bodies: &mut HashMap<SmolStr, DefBody>,
756) {
757 let Some(name) = nth_group_text(command, 0) else {
758 return;
759 };
760 let name = name.trim();
761 if name.is_empty() {
762 return;
763 }
764 record_body(env_bodies, name, nth_group(command, 1).as_ref());
765 let (arity, first_optional) = newcommand_arity(command);
766 db.insert_environment(name, environment_sig(latex2e_args(arity, first_optional)));
767}
768
769fn scan_verbatim_environment(defining_command: &str, command: &SyntaxNode, db: &mut SignatureDb) {
779 let Some(name) = nth_group_text(command, 0) else {
780 return;
781 };
782 let name = name.trim();
783 if name.is_empty() {
784 return;
785 }
786 let args = if defining_command == "lstnewenvironment" {
787 let (arity, first_optional) = newcommand_arity(command);
788 latex2e_args(arity, first_optional)
789 } else {
790 latex2e_args(1, true)
792 };
793 let mut sig = environment_sig(args);
794 sig.verbatim_body = true;
795 sig.reflow = false;
796 db.insert_environment(name, sig);
797}
798
799fn scan_xparse_command(
804 command: &SyntaxNode,
805 db: &mut SignatureDb,
806 bodies: &mut HashMap<SmolStr, DefBody>,
807 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
808) {
809 let Some(def) = resolve_command_def(command) else {
810 return;
811 };
812 let Some(spec) = nth_group(&def.host, def.first_arg_group) else {
813 return;
814 };
815 let body = nth_group(&def.host, def.first_arg_group + 1);
817 record_body(bodies, &def.name, body.as_ref());
818 let args = xparse::parse_spec(&group_inner_source(&spec));
819 record_env_alias(aliases, &def.name, args.len(), body.as_ref());
820 db.insert_command(
821 def.name,
822 CommandSig {
823 args: args.into(),
824 sectioning: None,
825 verbatim: false,
826 verbatim_delimited: false,
827 rule: false,
828 inline: false,
829 block: false,
833 },
834 );
835}
836
837struct CommandDef {
848 name: String,
849 host: SyntaxNode,
850 first_arg_group: usize,
851}
852
853fn resolve_command_def(command: &SyntaxNode) -> Option<CommandDef> {
857 if command.children().any(|c| c.kind() == SyntaxKind::GROUP) {
860 let name = nth_group(command, 0)
861 .as_ref()
862 .and_then(group_command_name)?;
863 return Some(CommandDef {
864 name,
865 host: command.clone(),
866 first_arg_group: 1,
867 });
868 }
869 let sibling = adjacent_sibling_command(command)?;
873 let name = command_name(&sibling)?;
874 Some(CommandDef {
875 name,
876 host: sibling,
877 first_arg_group: 0,
878 })
879}
880
881fn adjacent_sibling_command(command: &SyntaxNode) -> Option<SyntaxNode> {
887 let mut next = command.next_sibling_or_token();
888 while let Some(element) = next {
889 match element {
890 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
891 next = token.next_sibling_or_token();
892 }
893 NodeOrToken::Node(node) if node.kind() == SyntaxKind::COMMAND => return Some(node),
894 _ => return None,
895 }
896 }
897 None
898}
899
900fn is_trivia(kind: SyntaxKind) -> bool {
903 matches!(
904 kind,
905 SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
906 )
907}
908
909fn scan_xparse_environment(
913 command: &SyntaxNode,
914 db: &mut SignatureDb,
915 env_bodies: &mut HashMap<SmolStr, DefBody>,
916) {
917 let Some(name) = nth_group_text(command, 0) else {
918 return;
919 };
920 let name = name.trim();
921 if name.is_empty() {
922 return;
923 }
924 let Some(spec) = nth_group(command, 1) else {
925 return;
926 };
927 record_body(env_bodies, name, nth_group(command, 2).as_ref());
928 db.insert_environment(
929 name,
930 environment_sig(xparse::parse_spec(&group_inner_source(&spec))),
931 );
932}
933
934fn newcommand_arity(command: &SyntaxNode) -> (usize, bool) {
938 let optionals: Vec<Optional> = children::<Optional>(command).collect();
939 let arity = optionals
940 .first()
941 .map(|o| o.syntax())
942 .and_then(optional_number)
943 .unwrap_or(0)
944 .min(9); (arity, optionals.len() >= 2)
946}
947
948fn optional_number(node: &SyntaxNode) -> Option<usize> {
951 let text = node.text().to_string();
952 let inner = text.strip_prefix('[').unwrap_or(&text);
953 let inner = inner.strip_suffix(']').unwrap_or(inner);
954 inner.trim().parse().ok()
955}
956
957fn latex2e_args(arity: usize, first_optional: bool) -> Vec<ArgSpec> {
960 (0..arity)
961 .map(|i| {
962 if i == 0 && first_optional {
963 ArgSpec {
964 required: false,
965 kind: ArgKind::Bracket,
966 content: ContentKind::Opaque,
967 domain: crate::semantic::ArgumentDomain::Unknown,
968 verbatim: false,
969 }
970 } else {
971 ArgSpec {
972 required: true,
973 kind: ArgKind::Brace,
974 content: ContentKind::Opaque,
975 domain: crate::semantic::ArgumentDomain::Unknown,
976 verbatim: false,
977 }
978 }
979 })
980 .collect()
981}
982
983fn environment_sig(args: Vec<ArgSpec>) -> EnvironmentSig {
987 EnvironmentSig {
988 args: args.into(),
989 verbatim_body: false,
990 verbatim_arg: false,
993 math: false,
994 code: false,
995 statement_body: false,
998 align: false,
999 reflow: true,
1000 no_indent: false,
1001 list: false,
1004 block: false,
1008 outline: None,
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017 use crate::parser::{parse, reconstruct};
1018
1019 fn db_of(src: &str) -> SignatureDb {
1020 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1022 scan_definitions(&SyntaxNode::new_root(parse(src).green))
1023 }
1024
1025 fn arg_kinds(args: &[ArgSpec]) -> Vec<ArgKind> {
1026 args.iter().map(|a| a.kind).collect()
1027 }
1028
1029 #[test]
1030 fn newcommand_counts_mandatory_args() {
1031 let db = db_of("\\newcommand{\\foo}[2]{#1#2}\n");
1032 let sig = db.command("foo").expect("foo defined");
1033 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1034 assert!(sig.args.iter().all(|a| a.required));
1035 assert!(
1036 sig.args
1037 .iter()
1038 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1039 );
1040 }
1041
1042 #[test]
1043 fn newcommand_optional_first_arg() {
1044 let db = db_of("\\newcommand{\\foo}[2][d]{#1#2}\n");
1045 let sig = db.command("foo").expect("foo defined");
1046 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1047 assert!(!sig.args[0].required);
1048 assert!(sig.args[1].required);
1049 }
1050
1051 #[test]
1052 fn newcommand_zero_args() {
1053 let db = db_of("\\newcommand{\\foo}{bar}\n");
1054 assert!(db.command("foo").expect("foo defined").args.is_empty());
1055 }
1056
1057 #[test]
1058 fn renew_and_provide_recognized() {
1059 let db = db_of("\\renewcommand{\\a}[1]{x}\\providecommand{\\b}[1]{y}\n");
1060 assert_eq!(db.command("a").unwrap().args.len(), 1);
1061 assert_eq!(db.command("b").unwrap().args.len(), 1);
1062 }
1063
1064 #[test]
1065 fn secdef_redefinition_keeps_builtin_prose() {
1066 let db = db_of("\\renewcommand{\\section}{\\secdef \\a \\b}\n");
1071 assert!(
1072 db.command("section").is_none(),
1073 "the delegating redefinition must not be recorded as a scanned override"
1074 );
1075 let sigs = crate::semantic::signature::Signatures::new(&db);
1076 let sig = sigs.command("section").expect("built-in section survives");
1077 let last = sig.args.last().expect("section keeps its title argument");
1078 assert_eq!(
1079 last.content,
1080 crate::semantic::signature::ContentKind::Prose,
1081 "the title argument stays prose (reflowable)"
1082 );
1083 }
1084
1085 #[test]
1086 fn genuine_zero_arg_redefinition_downgrades_builtin() {
1087 let db = db_of("\\renewcommand{\\section}{\\textbf{Fixed}}\n");
1091 let sig = db
1092 .command("section")
1093 .expect("genuine 0-arg redefinition is recorded");
1094 assert!(
1095 sig.args.is_empty(),
1096 "no delegation means the scanned 0-arg signature wins"
1097 );
1098 }
1099
1100 #[test]
1101 fn secdef_redefinition_of_unknown_still_records() {
1102 let db = db_of("\\renewcommand{\\mysec}{\\secdef \\a \\b}\n");
1106 let sig = db.command("mysec").expect("unknown name is still recorded");
1107 assert!(sig.args.is_empty());
1108 }
1109
1110 #[test]
1111 fn newenvironment_args() {
1112 let db = db_of("\\newenvironment{thm}[1]{begin #1}{end}\n");
1113 let sig = db.environment("thm").expect("thm defined");
1114 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1115 assert!(sig.reflow);
1116 assert!(!sig.verbatim_body);
1117 assert!(!sig.math);
1118 }
1119
1120 #[test]
1121 fn xparse_command_spec() {
1122 let db = db_of("\\NewDocumentCommand{\\foo}{m O{d} m}{x}\n");
1123 let sig = db.command("foo").expect("foo defined");
1124 assert_eq!(
1125 arg_kinds(&sig.args),
1126 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1127 );
1128 assert!(
1129 sig.args
1130 .iter()
1131 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1132 );
1133 }
1134
1135 #[test]
1136 fn xparse_environment_spec() {
1137 let db = db_of("\\NewDocumentEnvironment{env}{O{x} m}{a}{b}\n");
1138 let sig = db.environment("env").expect("env defined");
1139 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1140 assert!(
1141 sig.args
1142 .iter()
1143 .all(|arg| arg.domain == crate::semantic::ArgumentDomain::Unknown)
1144 );
1145 }
1146
1147 #[test]
1148 fn unbraced_newcommand_extracted() {
1149 let db = db_of("\\newcommand\\foo[2]{#1#2}\n");
1152 let sig = db.command("foo").expect("foo defined");
1153 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1154 assert!(sig.args.iter().all(|a| a.required));
1155 }
1156
1157 #[test]
1158 fn unbraced_optional_first_arg() {
1159 let db = db_of("\\newcommand\\foo[2][d]{#1#2}\n");
1160 let sig = db.command("foo").expect("foo defined");
1161 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1162 assert!(!sig.args[0].required);
1163 assert!(sig.args[1].required);
1164 }
1165
1166 #[test]
1167 fn unbraced_zero_args() {
1168 let db = db_of("\\newcommand\\foo{x}\n");
1169 assert!(db.command("foo").expect("foo defined").args.is_empty());
1170 }
1171
1172 #[test]
1173 fn unbraced_spaced_binds() {
1174 let db = db_of("\\newcommand \\foo[1]{x}\n");
1176 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1177 }
1178
1179 #[test]
1180 fn unbraced_renewcommand() {
1181 let db = db_of("\\renewcommand\\foo[1]{x}\n");
1182 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1183 }
1184
1185 #[test]
1186 fn unbraced_xparse_command() {
1187 let db = db_of("\\NewDocumentCommand\\foo{m O{d} m}{x}\n");
1188 let sig = db.command("foo").expect("foo defined");
1189 assert_eq!(
1190 arg_kinds(&sig.args),
1191 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1192 );
1193 }
1194
1195 #[test]
1196 fn unbraced_stray_text_not_bound() {
1197 let db = db_of("\\newcommand foo \\bar{x}\n");
1200 assert!(db.command("foo").is_none());
1201 assert!(db.command("bar").is_none());
1202 }
1203
1204 #[test]
1205 fn redefinition_last_wins() {
1206 let db = db_of("\\newcommand{\\foo}[1]{x}\\renewcommand{\\foo}[3]{y}\n");
1207 assert_eq!(db.command("foo").unwrap().args.len(), 3);
1208 }
1209
1210 #[test]
1211 fn garbage_definition_degrades_to_no_insert() {
1212 let db = db_of("\\newcommand\n");
1214 assert!(db.command("foo").is_none());
1215 }
1216
1217 #[test]
1218 fn nested_definition_collected() {
1219 let db = db_of("\\begin{document}\n\\newcommand{\\foo}[1]{x}\n\\end{document}\n");
1220 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1221 }
1222
1223 #[test]
1224 fn commented_definition_ignored() {
1225 let db = db_of("% \\newcommand{\\foo}[1]{x}\n");
1226 assert!(db.command("foo").is_none());
1227 }
1228
1229 #[test]
1230 fn verbatim_makeother_flagged() {
1231 let db = db_of("\\newcommand\\shellcmd[1]{\\@makeother\\$#1}\n");
1234 let sig = db.command("shellcmd").expect("shellcmd defined");
1235 assert!(sig.verbatim);
1236 assert!(sig.args.is_empty());
1237 }
1238
1239 #[test]
1240 fn verbatim_catcode_flagged() {
1241 let db = db_of("\\newcommand\\shellcmd[1]{\\catcode 36=12 #1}\n");
1243 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1244 }
1245
1246 #[test]
1247 fn unrelated_twelve_does_not_flag_catcode_assignment() {
1248 let db = db_of("\\newcommand\\ordinary[1]{\\catcode 36=\\active \\hspace{12pt}#1}\n");
1251 assert!(!db.command("ordinary").expect("ordinary defined").verbatim);
1252 }
1253
1254 #[test]
1255 fn verbatim_dospecials_flagged() {
1256 let db = db_of("\\newcommand\\shellcmd[1]{\\let\\do\\@makeother\\dospecials #1}\n");
1258 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1259 }
1260
1261 #[test]
1262 fn verbatim_keeps_leading_args() {
1263 let db = db_of("\\newcommand\\mycode[2]{\\@makeother\\$#1#2}\n");
1266 let sig = db.command("mycode").expect("mycode defined");
1267 assert!(sig.verbatim);
1268 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1269 }
1270
1271 #[test]
1272 fn verbatim_via_chained_helper() {
1273 let db =
1276 db_of("\\newcommand\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1277 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1278 assert!(!db.command("setup").expect("setup defined").verbatim);
1280 }
1281
1282 #[test]
1283 fn verbatim_chain_cycle_terminates() {
1284 let db = db_of("\\newcommand\\a[1]{\\b#1}\\newcommand\\b[1]{\\a#1}\n");
1287 assert!(!db.command("a").expect("a defined").verbatim);
1288 assert!(!db.command("b").expect("b defined").verbatim);
1289 }
1290
1291 #[test]
1292 fn ordinary_command_not_verbatim() {
1293 let db = db_of("\\newcommand\\foo[1]{\\emph{#1}}\n");
1294 assert!(!db.command("foo").expect("foo defined").verbatim);
1295 }
1296
1297 #[test]
1298 fn verbatim_needs_an_argument() {
1299 let db = db_of("\\newcommand\\setup{\\@makeother\\$}\n");
1302 assert!(!db.command("setup").expect("setup defined").verbatim);
1303 }
1304
1305 #[test]
1306 fn def_helper_chain_followed() {
1307 let db = db_of("\\def\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1310 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1311 assert!(!db.command("setup").expect("setup defined").verbatim);
1313 }
1314
1315 #[test]
1316 fn def_direct_verbatim_flagged() {
1317 let db = db_of("\\def\\shellcmd#1{\\@makeother\\$#1}\n");
1320 let sig = db.command("shellcmd").expect("shellcmd defined");
1321 assert!(sig.verbatim);
1322 assert!(sig.args.is_empty());
1323 }
1324
1325 #[test]
1326 fn def_zero_params() {
1327 let db = db_of("\\def\\foo{x}\n");
1329 let sig = db.command("foo").expect("foo defined");
1330 assert!(sig.args.is_empty());
1331 assert!(!sig.verbatim);
1332 }
1333
1334 #[test]
1335 fn def_counts_params() {
1336 let db = db_of("\\def\\foo#1#2{#1#2}\n");
1338 let sig = db.command("foo").expect("foo defined");
1339 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1340 }
1341
1342 #[test]
1343 fn def_variants_scanned() {
1344 let db = db_of("\\edef\\a#1{x}\\gdef\\b{y}\\xdef\\c#1{\\@makeother\\$#1}\n");
1346 assert_eq!(db.command("a").expect("a defined").args.len(), 1);
1347 assert!(db.command("b").expect("b defined").args.is_empty());
1348 let c = db.command("c").expect("c defined");
1349 assert!(c.verbatim);
1350 assert!(c.args.is_empty());
1351 }
1352
1353 #[test]
1354 fn def_chain_through_def_helpers() {
1355 let db = db_of(
1357 "\\def\\inner{\\@makeother\\$}\\def\\outer{\\inner}\\newcommand\\cmd[1]{\\outer#1}\n",
1358 );
1359 assert!(db.command("cmd").expect("cmd defined").verbatim);
1360 }
1361
1362 #[test]
1363 fn verbatim_xparse_flagged() {
1364 let db = db_of("\\NewDocumentCommand\\shellcmd{m}{\\@makeother\\$#1}\n");
1365 let sig = db.command("shellcmd").expect("shellcmd defined");
1366 assert!(sig.verbatim);
1367 assert!(sig.args.is_empty());
1368 }
1369
1370 #[test]
1371 fn env_makeother_flagged() {
1372 let db = db_of("\\newenvironment{shellenv}{\\@makeother\\$}{}\n");
1375 let sig = db.environment("shellenv").expect("shellenv defined");
1376 assert!(sig.verbatim_body);
1377 assert!(!sig.reflow); }
1379
1380 #[test]
1381 fn env_catcode_flagged() {
1382 let db = db_of("\\newenvironment{shellenv}[1]{\\catcode 36=12 }{}\n");
1384 let sig = db.environment("shellenv").expect("shellenv defined");
1385 assert!(sig.verbatim_body);
1386 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1388 }
1389
1390 #[test]
1391 fn env_via_chained_helper() {
1392 let db =
1395 db_of("\\newcommand\\setup{\\@makeother\\$}\\newenvironment{shellenv}{\\setup}{}\n");
1396 assert!(
1397 db.environment("shellenv")
1398 .expect("shellenv defined")
1399 .verbatim_body
1400 );
1401 }
1402
1403 #[test]
1404 fn env_without_signal_not_flagged() {
1405 let db = db_of("\\newenvironment{remark}{\\par\\noindent\\textbf{Remark.}}{\\par}\n");
1407 let sig = db.environment("remark").expect("remark defined");
1408 assert!(!sig.verbatim_body);
1409 assert!(sig.reflow);
1410 }
1411
1412 #[test]
1413 fn lstnewenvironment_flagged_verbatim() {
1414 let db = db_of("\\lstnewenvironment{demo}[1][code]{\\lstset{#1}}{}\n");
1418 let sig = db.environment("demo").expect("demo defined");
1419 assert!(sig.verbatim_body);
1420 assert!(!sig.reflow);
1421 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1422 }
1423
1424 #[test]
1425 fn lstnewenvironment_no_args_flagged_verbatim() {
1426 let db = db_of("\\lstnewenvironment{demo}{}{}\n");
1427 let sig = db.environment("demo").expect("demo defined");
1428 assert!(sig.verbatim_body);
1429 assert!(sig.args.is_empty());
1430 }
1431
1432 #[test]
1433 fn defineverbatimenvironment_flagged_verbatim() {
1434 let db = db_of("\\DefineVerbatimEnvironment{code}{Verbatim}{fontsize=\\small}\n");
1436 let sig = db.environment("code").expect("code defined");
1437 assert!(sig.verbatim_body);
1438 assert!(!sig.reflow);
1439 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1440 }
1441
1442 #[test]
1443 fn xparse_env_makeother_flagged() {
1444 let db = db_of("\\NewDocumentEnvironment{shellenv}{O{x}}{\\dospecials}{}\n");
1446 let sig = db.environment("shellenv").expect("shellenv defined");
1447 assert!(sig.verbatim_body);
1448 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1449 }
1450
1451 fn sites_of(src: &str) -> Vec<DefSite> {
1452 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1453 scan_definition_sites(&SyntaxNode::new_root(parse(src).green))
1454 }
1455
1456 #[test]
1457 fn def_site_newcommand_braced_name_span() {
1458 let src = "\\newcommand{\\foo}[1]{#1}\n";
1459 let sites = sites_of(src);
1460 assert_eq!(sites.len(), 1);
1461 let site = &sites[0];
1462 assert_eq!(site.name, "foo");
1463 assert_eq!(site.kind, DefSiteKind::Command);
1464 assert_eq!(&src[site.name_range], "\\foo");
1465 assert_eq!(&src[site.range], "\\newcommand{\\foo}[1]{#1}");
1466 }
1467
1468 #[test]
1469 fn def_site_newcommand_unbraced_name_span() {
1470 let src = "\\newcommand\\foo[1]{#1}\n";
1471 let sites = sites_of(src);
1472 assert_eq!(sites.len(), 1);
1473 assert_eq!(sites[0].name, "foo");
1474 assert_eq!(&src[sites[0].name_range], "\\foo");
1475 assert_eq!(&src[sites[0].range], "\\newcommand\\foo[1]{#1}");
1476 }
1477
1478 #[test]
1479 fn def_site_def_sibling_name_span() {
1480 let src = "\\def\\foo#1{#1}\n";
1481 let sites = sites_of(src);
1482 assert_eq!(sites.len(), 1);
1483 assert_eq!(sites[0].name, "foo");
1484 assert_eq!(sites[0].kind, DefSiteKind::Command);
1485 assert_eq!(&src[sites[0].name_range], "\\foo");
1486 }
1487
1488 #[test]
1489 fn def_site_xparse_command_name_span() {
1490 let src = "\\NewDocumentCommand{\\foo}{m O{d}}{x}\n";
1491 let sites = sites_of(src);
1492 assert_eq!(sites.len(), 1);
1493 assert_eq!(sites[0].name, "foo");
1494 assert_eq!(&src[sites[0].name_range], "\\foo");
1495 }
1496
1497 #[test]
1498 fn def_site_newenvironment_name_span() {
1499 let src = "\\newenvironment{myenv}{begin}{end}\n";
1500 let sites = sites_of(src);
1501 assert_eq!(sites.len(), 1);
1502 let site = &sites[0];
1503 assert_eq!(site.name, "myenv");
1504 assert_eq!(site.kind, DefSiteKind::Environment);
1505 assert_eq!(&src[site.name_range], "myenv");
1506 }
1507
1508 #[test]
1509 fn def_site_xparse_environment_name_span() {
1510 let src = "\\NewDocumentEnvironment{myenv}{m}{a}{b}\n";
1511 let sites = sites_of(src);
1512 assert_eq!(sites.len(), 1);
1513 assert_eq!(sites[0].name, "myenv");
1514 assert_eq!(sites[0].kind, DefSiteKind::Environment);
1515 assert_eq!(&src[sites[0].name_range], "myenv");
1516 }
1517
1518 #[test]
1519 fn def_site_keeps_every_redefinition() {
1520 let src = "\\newcommand{\\foo}{a}\n\\renewcommand{\\foo}{b}\n";
1522 let sites = sites_of(src);
1523 assert_eq!(sites.len(), 2);
1524 assert!(sites.iter().all(|s| s.name == "foo"));
1525 assert!(sites[0].name_range.start() < sites[1].name_range.start());
1526 }
1527
1528 #[test]
1529 fn def_site_none_for_malformed() {
1530 assert!(sites_of("\\newcommand\n").is_empty());
1531 assert!(sites_of("\\newenvironment{}{a}{b}\n").is_empty());
1532 }
1533
1534 const EQNARRAY_PAIR: &str =
1538 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n";
1539
1540 #[test]
1541 fn newcommand_env_alias_pair_is_recorded() {
1542 let db = db_of(EQNARRAY_PAIR);
1543 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1544 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1545 assert_eq!(db.env_begin_alias("eea"), None);
1547 assert_eq!(db.env_end_alias("bea"), None);
1548 }
1549
1550 #[test]
1551 fn def_env_alias_pair_is_recorded() {
1552 let db = db_of("\\def\\bea{\\begin{eqnarray}}\n\\def\\eea{\\end{eqnarray}}\n");
1553 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1554 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1555 }
1556
1557 #[test]
1558 fn xparse_env_alias_pair_is_recorded() {
1559 let db = db_of(
1560 "\\NewDocumentCommand{\\bea}{}{\\begin{eqnarray}}\n\
1561 \\NewDocumentCommand{\\eea}{}{\\end{eqnarray}}\n",
1562 );
1563 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1564 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1565 }
1566
1567 #[test]
1568 fn env_alias_body_tolerates_trivia() {
1569 let db = db_of(
1573 "\\newcommand{\\bea}{ \\begin{eqnarray} }\n\\newcommand{\\eea}{% why\n\\end{eqnarray}}\n",
1574 );
1575 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1576 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1577 }
1578
1579 #[test]
1585 fn a_lone_env_alias_half_is_recorded() {
1586 let db = db_of("\\newcommand{\\bea}{\\begin{eqnarray}}\n");
1587 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1588 assert_eq!(db.env_end_alias("bea"), None);
1589
1590 let db = db_of("\\newcommand{\\eea}{\\end{eqnarray}}\n");
1591 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1592 assert_eq!(db.env_begin_alias("eea"), None);
1593 }
1594
1595 #[test]
1599 fn a_lone_half_still_obeys_every_target_rule() {
1600 assert_eq!(
1601 db_of("\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n").env_begin_alias("bmy"),
1602 None
1603 );
1604 assert_eq!(
1605 db_of("\\newcommand{\\ev}{\\end{verbatim}}\n").env_end_alias("ev"),
1606 None
1607 );
1608 assert_eq!(
1609 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n").env_begin_alias("bt"),
1610 None
1611 );
1612 assert_eq!(
1613 db_of("\\newcommand{\\bea}[1]{\\begin{eqnarray}}\n").env_begin_alias("bea"),
1614 None
1615 );
1616 }
1617
1618 #[test]
1619 fn env_alias_rejects_uncurated_target() {
1620 let db = db_of(
1621 "\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n\
1622 \\newcommand{\\emy}{\\end{notacuratedenv}}\n",
1623 );
1624 assert_eq!(db.env_begin_alias("bmy"), None);
1625 assert_eq!(db.env_end_alias("emy"), None);
1626 }
1627
1628 #[test]
1629 fn env_alias_rejects_verbatim_target() {
1630 let db =
1634 db_of("\\newcommand{\\bv}{\\begin{verbatim}}\n\\newcommand{\\ev}{\\end{verbatim}}\n");
1635 assert_eq!(db.env_begin_alias("bv"), None);
1636 }
1637
1638 #[test]
1639 fn env_alias_rejects_argument_taking_target() {
1640 let db =
1643 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n\\newcommand{\\et}{\\end{tabular}}\n");
1644 assert_eq!(db.env_begin_alias("bt"), None);
1645 }
1646
1647 #[test]
1648 fn env_alias_rejects_parameterized_definition() {
1649 let db = db_of(
1650 "\\newcommand{\\bt}[1]{\\begin{tabular}{#1}}\n\\newcommand{\\et}{\\end{tabular}}\n",
1651 );
1652 assert_eq!(db.env_begin_alias("bt"), None);
1653 }
1654
1655 #[test]
1656 fn env_alias_rejects_body_with_extra_content() {
1657 let db = db_of(
1658 "\\newcommand{\\bea}{\\begin{eqnarray}\\label{x}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n",
1659 );
1660 assert_eq!(db.env_begin_alias("bea"), None);
1661 }
1662
1663 #[test]
1664 fn redefinition_retracts_an_earlier_env_alias() {
1665 let db = db_of(
1669 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1670 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1671 \\renewcommand{\\bea}{\\textbf}\n",
1672 );
1673 assert_eq!(db.env_begin_alias("bea"), None);
1674 }
1675
1676 #[test]
1677 fn env_alias_does_not_pollute_the_environment_namespace() {
1678 let db = db_of(EQNARRAY_PAIR);
1681 assert!(db.environment("bea").is_none());
1682 assert!(!db.environment_names().any(|n| n == "bea"));
1683 }
1684
1685 fn environment_node(src: &str) -> SyntaxNode {
1687 SyntaxNode::new_root(parse(src).green)
1688 .descendants()
1689 .find(|n| n.kind() == SyntaxKind::ENVIRONMENT)
1690 .expect("an environment")
1691 }
1692
1693 #[test]
1694 fn signatures_resolves_an_env_alias_to_the_curated_target() {
1695 use crate::semantic::signature::Signatures;
1696 let src = format!("{EQNARRAY_PAIR}\\bea a \\eea\n");
1697 let db = db_of(&src);
1698 let sigs = Signatures::new(&db);
1699 let sig = sigs
1700 .environment_at(&environment_node(&src))
1701 .expect("alias resolves");
1702 let target = builtin().environment("eqnarray").expect("curated target");
1703 assert_eq!(sig, target);
1704 assert!(sig.math && sig.align);
1705 assert!(sigs.environment("bea").is_none());
1707 }
1708
1709 #[test]
1710 fn a_literal_begin_does_not_inherit_the_alias_target() {
1711 use crate::semantic::signature::Signatures;
1715 let src = format!("{EQNARRAY_PAIR}\\begin{{bea}} a \\end{{bea}}\n");
1716 let db = db_of(&src);
1717 let sigs = Signatures::new(&db);
1718 assert!(sigs.environment_at(&environment_node(&src)).is_none());
1719 }
1720
1721 #[test]
1722 fn a_real_environment_wins_over_an_alias_of_the_same_name() {
1723 use crate::semantic::signature::Signatures;
1726 let defs = "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1727 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1728 \\newenvironment{bea}{x}{y}\n";
1729 let db = db_of(&format!("{defs}\\begin{{bea}} a \\end{{bea}}\n"));
1730 let sigs = Signatures::new(&db);
1731 let sig = sigs
1732 .environment_at(&environment_node(&format!(
1733 "{defs}\\begin{{bea}} a \\end{{bea}}\n"
1734 )))
1735 .expect("real environment resolves");
1736 assert!(
1737 !sig.math,
1738 "the scanned \\newenvironment must win for a literal `\\begin{{bea}}`"
1739 );
1740 assert!(
1741 sigs.environment_at(&environment_node(&format!("{defs}\\bea a \\eea\n")))
1742 .is_some_and(|sig| sig.math),
1743 "while the alias delimiters still resolve to eqnarray"
1744 );
1745 }
1746
1747 #[test]
1748 fn merge_from_carries_env_aliases() {
1749 let mut target = SignatureDb::default();
1750 target.merge_from(&db_of(EQNARRAY_PAIR));
1751 assert_eq!(target.env_begin_alias("bea"), Some("eqnarray"));
1752 assert_eq!(target.env_end_alias("eea"), Some("eqnarray"));
1753 }
1754}