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.contains("\\catcode") && body.contains("12"))
469}
470
471fn called_macros(body: &str) -> Vec<SmolStr> {
476 body.match_indices('\\')
477 .filter_map(|(pos, _)| {
478 let after = &body[pos + 1..];
479 let len: usize = after
480 .chars()
481 .take_while(|c| c.is_ascii_alphabetic() || *c == '@')
482 .map(char::len_utf8)
483 .sum();
484 (len > 0).then(|| SmolStr::new(&after[..len]))
485 })
486 .collect()
487}
488
489const DELEGATING_PRIMITIVES: &[&str] = &["secdef", "@startsection", "@dblarg", "@sect", "@ssect"];
499
500fn keeps_builtin_over_arity0(name: &str, arity: usize, body: &DefBody) -> bool {
511 arity == 0
512 && body
513 .called
514 .iter()
515 .any(|callee| DELEGATING_PRIMITIVES.contains(&callee.as_str()))
516 && crate::semantic::signature::builtin()
517 .command(name)
518 .is_some()
519}
520
521pub fn is_definition_command(name: &str) -> bool {
528 DefKind::of(name).is_some()
529}
530
531enum DefKind {
533 Command,
534 Def,
535 Environment,
536 XparseCommand,
537 XparseEnvironment,
538 VerbatimEnvironment,
543}
544
545impl DefKind {
546 fn of(name: &str) -> Option<Self> {
547 Some(match name {
548 "newcommand" | "renewcommand" | "providecommand" | "DeclareRobustCommand" => {
549 DefKind::Command
550 }
551 "def" | "edef" | "gdef" | "xdef" => DefKind::Def,
554 "newenvironment" | "renewenvironment" => DefKind::Environment,
555 "NewDocumentCommand"
556 | "RenewDocumentCommand"
557 | "ProvideDocumentCommand"
558 | "DeclareDocumentCommand" => DefKind::XparseCommand,
559 "NewDocumentEnvironment"
560 | "RenewDocumentEnvironment"
561 | "ProvideDocumentEnvironment"
562 | "DeclareDocumentEnvironment" => DefKind::XparseEnvironment,
563 "lstnewenvironment" | "DefineVerbatimEnvironment" => DefKind::VerbatimEnvironment,
566 _ => return None,
567 })
568 }
569}
570
571fn scan_newcommand(
577 command: &SyntaxNode,
578 db: &mut SignatureDb,
579 bodies: &mut HashMap<SmolStr, DefBody>,
580 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
581) {
582 let Some(def) = resolve_command_def(command) else {
583 return;
584 };
585 let (arity, first_optional) = newcommand_arity(&def.host);
586 let body = nth_group(&def.host, def.first_arg_group);
589 record_body(bodies, &def.name, body.as_ref());
590 record_env_alias(aliases, &def.name, arity, body.as_ref());
591 if bodies
595 .get(def.name.as_str())
596 .is_some_and(|body| keeps_builtin_over_arity0(&def.name, arity, body))
597 {
598 return;
599 }
600 db.insert_command(
601 def.name,
602 CommandSig {
603 args: latex2e_args(arity, first_optional).into(),
604 sectioning: None,
605 verbatim: false,
606 verbatim_delimited: false,
607 rule: false,
608 inline: false,
609 block: false,
613 },
614 );
615}
616
617fn scan_def(
625 command: &SyntaxNode,
626 db: &mut SignatureDb,
627 bodies: &mut HashMap<SmolStr, DefBody>,
628 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
629) {
630 let Some(name_node) = adjacent_sibling_command(command) else {
631 return;
632 };
633 let Some(name) = command_name(&name_node) else {
634 return;
635 };
636 let (arity, body) = def_params_and_body(&name_node);
637 record_body(bodies, &name, body.as_ref());
638 record_env_alias(aliases, &name, arity, body.as_ref());
639 if bodies
642 .get(name.as_str())
643 .is_some_and(|body| keeps_builtin_over_arity0(&name, arity, body))
644 {
645 return;
646 }
647 db.insert_command(
648 name,
649 CommandSig {
650 args: latex2e_args(arity, false).into(),
654 sectioning: None,
655 verbatim: false,
656 verbatim_delimited: false,
657 rule: false,
658 inline: false,
659 block: false,
663 },
664 );
665}
666
667fn def_params_and_body(name_node: &SyntaxNode) -> (usize, Option<SyntaxNode>) {
680 if let Some(body) = nth_group(name_node, 0) {
682 return (0, Some(body));
683 }
684 let mut arity = 0usize;
686 let mut next = name_node.next_sibling_or_token();
687 while let Some(element) = next {
688 match element {
689 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
690 next = token.next_sibling_or_token();
691 }
692 NodeOrToken::Token(token) if token.kind() == SyntaxKind::HASH => {
693 arity += 1;
694 next = token.next_sibling_or_token();
695 }
696 NodeOrToken::Token(token) if token.kind() == SyntaxKind::WORD => {
698 next = token.next_sibling_or_token();
699 }
700 NodeOrToken::Node(node) if node.kind() == SyntaxKind::GROUP => {
701 return (arity.min(9), Some(node));
702 }
703 _ => return (arity.min(9), None),
704 }
705 }
706 (arity.min(9), None)
707}
708
709fn record_body(bodies: &mut HashMap<SmolStr, DefBody>, name: &str, body: Option<&SyntaxNode>) {
712 let text = body.map(group_inner_source).unwrap_or_default();
713 bodies.insert(
714 SmolStr::new(name),
715 DefBody {
716 signal: catcode_signal(&text),
717 called: called_macros(&text),
718 },
719 );
720}
721
722fn scan_newenvironment(
727 command: &SyntaxNode,
728 db: &mut SignatureDb,
729 env_bodies: &mut HashMap<SmolStr, DefBody>,
730) {
731 let Some(name) = nth_group_text(command, 0) else {
732 return;
733 };
734 let name = name.trim();
735 if name.is_empty() {
736 return;
737 }
738 record_body(env_bodies, name, nth_group(command, 1).as_ref());
739 let (arity, first_optional) = newcommand_arity(command);
740 db.insert_environment(name, environment_sig(latex2e_args(arity, first_optional)));
741}
742
743fn scan_verbatim_environment(defining_command: &str, command: &SyntaxNode, db: &mut SignatureDb) {
753 let Some(name) = nth_group_text(command, 0) else {
754 return;
755 };
756 let name = name.trim();
757 if name.is_empty() {
758 return;
759 }
760 let args = if defining_command == "lstnewenvironment" {
761 let (arity, first_optional) = newcommand_arity(command);
762 latex2e_args(arity, first_optional)
763 } else {
764 latex2e_args(1, true)
766 };
767 let mut sig = environment_sig(args);
768 sig.verbatim_body = true;
769 sig.reflow = false;
770 db.insert_environment(name, sig);
771}
772
773fn scan_xparse_command(
778 command: &SyntaxNode,
779 db: &mut SignatureDb,
780 bodies: &mut HashMap<SmolStr, DefBody>,
781 aliases: &mut HashMap<SmolStr, EnvAliasCandidate>,
782) {
783 let Some(def) = resolve_command_def(command) else {
784 return;
785 };
786 let Some(spec) = nth_group(&def.host, def.first_arg_group) else {
787 return;
788 };
789 let body = nth_group(&def.host, def.first_arg_group + 1);
791 record_body(bodies, &def.name, body.as_ref());
792 let args = xparse::parse_spec(&group_inner_source(&spec));
793 record_env_alias(aliases, &def.name, args.len(), body.as_ref());
794 db.insert_command(
795 def.name,
796 CommandSig {
797 args: args.into(),
798 sectioning: None,
799 verbatim: false,
800 verbatim_delimited: false,
801 rule: false,
802 inline: false,
803 block: false,
807 },
808 );
809}
810
811struct CommandDef {
822 name: String,
823 host: SyntaxNode,
824 first_arg_group: usize,
825}
826
827fn resolve_command_def(command: &SyntaxNode) -> Option<CommandDef> {
831 if command.children().any(|c| c.kind() == SyntaxKind::GROUP) {
834 let name = nth_group(command, 0)
835 .as_ref()
836 .and_then(group_command_name)?;
837 return Some(CommandDef {
838 name,
839 host: command.clone(),
840 first_arg_group: 1,
841 });
842 }
843 let sibling = adjacent_sibling_command(command)?;
847 let name = command_name(&sibling)?;
848 Some(CommandDef {
849 name,
850 host: sibling,
851 first_arg_group: 0,
852 })
853}
854
855fn adjacent_sibling_command(command: &SyntaxNode) -> Option<SyntaxNode> {
861 let mut next = command.next_sibling_or_token();
862 while let Some(element) = next {
863 match element {
864 NodeOrToken::Token(token) if is_trivia(token.kind()) => {
865 next = token.next_sibling_or_token();
866 }
867 NodeOrToken::Node(node) if node.kind() == SyntaxKind::COMMAND => return Some(node),
868 _ => return None,
869 }
870 }
871 None
872}
873
874fn is_trivia(kind: SyntaxKind) -> bool {
877 matches!(
878 kind,
879 SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
880 )
881}
882
883fn scan_xparse_environment(
887 command: &SyntaxNode,
888 db: &mut SignatureDb,
889 env_bodies: &mut HashMap<SmolStr, DefBody>,
890) {
891 let Some(name) = nth_group_text(command, 0) else {
892 return;
893 };
894 let name = name.trim();
895 if name.is_empty() {
896 return;
897 }
898 let Some(spec) = nth_group(command, 1) else {
899 return;
900 };
901 record_body(env_bodies, name, nth_group(command, 2).as_ref());
902 db.insert_environment(
903 name,
904 environment_sig(xparse::parse_spec(&group_inner_source(&spec))),
905 );
906}
907
908fn newcommand_arity(command: &SyntaxNode) -> (usize, bool) {
912 let optionals: Vec<Optional> = children::<Optional>(command).collect();
913 let arity = optionals
914 .first()
915 .map(|o| o.syntax())
916 .and_then(optional_number)
917 .unwrap_or(0)
918 .min(9); (arity, optionals.len() >= 2)
920}
921
922fn optional_number(node: &SyntaxNode) -> Option<usize> {
925 let text = node.text().to_string();
926 let inner = text.strip_prefix('[').unwrap_or(&text);
927 let inner = inner.strip_suffix(']').unwrap_or(inner);
928 inner.trim().parse().ok()
929}
930
931fn latex2e_args(arity: usize, first_optional: bool) -> Vec<ArgSpec> {
934 (0..arity)
935 .map(|i| {
936 if i == 0 && first_optional {
937 ArgSpec {
938 required: false,
939 kind: ArgKind::Bracket,
940 content: ContentKind::Opaque,
941 }
942 } else {
943 ArgSpec {
944 required: true,
945 kind: ArgKind::Brace,
946 content: ContentKind::Opaque,
947 }
948 }
949 })
950 .collect()
951}
952
953fn environment_sig(args: Vec<ArgSpec>) -> EnvironmentSig {
957 EnvironmentSig {
958 args: args.into(),
959 verbatim_body: false,
960 verbatim_arg: false,
963 math: false,
964 code: false,
965 statement_body: false,
968 align: false,
969 reflow: true,
970 no_indent: false,
971 list: false,
974 block: false,
978 outline: None,
981 }
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987 use crate::parser::{parse, reconstruct};
988
989 fn db_of(src: &str) -> SignatureDb {
990 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
992 scan_definitions(&SyntaxNode::new_root(parse(src).green))
993 }
994
995 fn arg_kinds(args: &[ArgSpec]) -> Vec<ArgKind> {
996 args.iter().map(|a| a.kind).collect()
997 }
998
999 #[test]
1000 fn newcommand_counts_mandatory_args() {
1001 let db = db_of("\\newcommand{\\foo}[2]{#1#2}\n");
1002 let sig = db.command("foo").expect("foo defined");
1003 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1004 assert!(sig.args.iter().all(|a| a.required));
1005 }
1006
1007 #[test]
1008 fn newcommand_optional_first_arg() {
1009 let db = db_of("\\newcommand{\\foo}[2][d]{#1#2}\n");
1010 let sig = db.command("foo").expect("foo defined");
1011 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1012 assert!(!sig.args[0].required);
1013 assert!(sig.args[1].required);
1014 }
1015
1016 #[test]
1017 fn newcommand_zero_args() {
1018 let db = db_of("\\newcommand{\\foo}{bar}\n");
1019 assert!(db.command("foo").expect("foo defined").args.is_empty());
1020 }
1021
1022 #[test]
1023 fn renew_and_provide_recognized() {
1024 let db = db_of("\\renewcommand{\\a}[1]{x}\\providecommand{\\b}[1]{y}\n");
1025 assert_eq!(db.command("a").unwrap().args.len(), 1);
1026 assert_eq!(db.command("b").unwrap().args.len(), 1);
1027 }
1028
1029 #[test]
1030 fn secdef_redefinition_keeps_builtin_prose() {
1031 let db = db_of("\\renewcommand{\\section}{\\secdef \\a \\b}\n");
1036 assert!(
1037 db.command("section").is_none(),
1038 "the delegating redefinition must not be recorded as a scanned override"
1039 );
1040 let sigs = crate::semantic::signature::Signatures::new(&db);
1041 let sig = sigs.command("section").expect("built-in section survives");
1042 let last = sig.args.last().expect("section keeps its title argument");
1043 assert_eq!(
1044 last.content,
1045 crate::semantic::signature::ContentKind::Prose,
1046 "the title argument stays prose (reflowable)"
1047 );
1048 }
1049
1050 #[test]
1051 fn genuine_zero_arg_redefinition_downgrades_builtin() {
1052 let db = db_of("\\renewcommand{\\section}{\\textbf{Fixed}}\n");
1056 let sig = db
1057 .command("section")
1058 .expect("genuine 0-arg redefinition is recorded");
1059 assert!(
1060 sig.args.is_empty(),
1061 "no delegation means the scanned 0-arg signature wins"
1062 );
1063 }
1064
1065 #[test]
1066 fn secdef_redefinition_of_unknown_still_records() {
1067 let db = db_of("\\renewcommand{\\mysec}{\\secdef \\a \\b}\n");
1071 let sig = db.command("mysec").expect("unknown name is still recorded");
1072 assert!(sig.args.is_empty());
1073 }
1074
1075 #[test]
1076 fn newenvironment_args() {
1077 let db = db_of("\\newenvironment{thm}[1]{begin #1}{end}\n");
1078 let sig = db.environment("thm").expect("thm defined");
1079 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1080 assert!(sig.reflow);
1081 assert!(!sig.verbatim_body);
1082 assert!(!sig.math);
1083 }
1084
1085 #[test]
1086 fn xparse_command_spec() {
1087 let db = db_of("\\NewDocumentCommand{\\foo}{m O{d} m}{x}\n");
1088 let sig = db.command("foo").expect("foo defined");
1089 assert_eq!(
1090 arg_kinds(&sig.args),
1091 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1092 );
1093 }
1094
1095 #[test]
1096 fn xparse_environment_spec() {
1097 let db = db_of("\\NewDocumentEnvironment{env}{O{x} m}{a}{b}\n");
1098 let sig = db.environment("env").expect("env defined");
1099 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1100 }
1101
1102 #[test]
1103 fn unbraced_newcommand_extracted() {
1104 let db = db_of("\\newcommand\\foo[2]{#1#2}\n");
1107 let sig = db.command("foo").expect("foo defined");
1108 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1109 assert!(sig.args.iter().all(|a| a.required));
1110 }
1111
1112 #[test]
1113 fn unbraced_optional_first_arg() {
1114 let db = db_of("\\newcommand\\foo[2][d]{#1#2}\n");
1115 let sig = db.command("foo").expect("foo defined");
1116 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket, ArgKind::Brace]);
1117 assert!(!sig.args[0].required);
1118 assert!(sig.args[1].required);
1119 }
1120
1121 #[test]
1122 fn unbraced_zero_args() {
1123 let db = db_of("\\newcommand\\foo{x}\n");
1124 assert!(db.command("foo").expect("foo defined").args.is_empty());
1125 }
1126
1127 #[test]
1128 fn unbraced_spaced_binds() {
1129 let db = db_of("\\newcommand \\foo[1]{x}\n");
1131 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1132 }
1133
1134 #[test]
1135 fn unbraced_renewcommand() {
1136 let db = db_of("\\renewcommand\\foo[1]{x}\n");
1137 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1138 }
1139
1140 #[test]
1141 fn unbraced_xparse_command() {
1142 let db = db_of("\\NewDocumentCommand\\foo{m O{d} m}{x}\n");
1143 let sig = db.command("foo").expect("foo defined");
1144 assert_eq!(
1145 arg_kinds(&sig.args),
1146 vec![ArgKind::Brace, ArgKind::Bracket, ArgKind::Brace]
1147 );
1148 }
1149
1150 #[test]
1151 fn unbraced_stray_text_not_bound() {
1152 let db = db_of("\\newcommand foo \\bar{x}\n");
1155 assert!(db.command("foo").is_none());
1156 assert!(db.command("bar").is_none());
1157 }
1158
1159 #[test]
1160 fn redefinition_last_wins() {
1161 let db = db_of("\\newcommand{\\foo}[1]{x}\\renewcommand{\\foo}[3]{y}\n");
1162 assert_eq!(db.command("foo").unwrap().args.len(), 3);
1163 }
1164
1165 #[test]
1166 fn garbage_definition_degrades_to_no_insert() {
1167 let db = db_of("\\newcommand\n");
1169 assert!(db.command("foo").is_none());
1170 }
1171
1172 #[test]
1173 fn nested_definition_collected() {
1174 let db = db_of("\\begin{document}\n\\newcommand{\\foo}[1]{x}\n\\end{document}\n");
1175 assert_eq!(db.command("foo").unwrap().args.len(), 1);
1176 }
1177
1178 #[test]
1179 fn commented_definition_ignored() {
1180 let db = db_of("% \\newcommand{\\foo}[1]{x}\n");
1181 assert!(db.command("foo").is_none());
1182 }
1183
1184 #[test]
1185 fn verbatim_makeother_flagged() {
1186 let db = db_of("\\newcommand\\shellcmd[1]{\\@makeother\\$#1}\n");
1189 let sig = db.command("shellcmd").expect("shellcmd defined");
1190 assert!(sig.verbatim);
1191 assert!(sig.args.is_empty());
1192 }
1193
1194 #[test]
1195 fn verbatim_catcode_flagged() {
1196 let db = db_of("\\newcommand\\shellcmd[1]{\\catcode 36=12 #1}\n");
1198 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1199 }
1200
1201 #[test]
1202 fn verbatim_dospecials_flagged() {
1203 let db = db_of("\\newcommand\\shellcmd[1]{\\let\\do\\@makeother\\dospecials #1}\n");
1205 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1206 }
1207
1208 #[test]
1209 fn verbatim_keeps_leading_args() {
1210 let db = db_of("\\newcommand\\mycode[2]{\\@makeother\\$#1#2}\n");
1213 let sig = db.command("mycode").expect("mycode defined");
1214 assert!(sig.verbatim);
1215 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1216 }
1217
1218 #[test]
1219 fn verbatim_via_chained_helper() {
1220 let db =
1223 db_of("\\newcommand\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1224 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1225 assert!(!db.command("setup").expect("setup defined").verbatim);
1227 }
1228
1229 #[test]
1230 fn verbatim_chain_cycle_terminates() {
1231 let db = db_of("\\newcommand\\a[1]{\\b#1}\\newcommand\\b[1]{\\a#1}\n");
1234 assert!(!db.command("a").expect("a defined").verbatim);
1235 assert!(!db.command("b").expect("b defined").verbatim);
1236 }
1237
1238 #[test]
1239 fn ordinary_command_not_verbatim() {
1240 let db = db_of("\\newcommand\\foo[1]{\\emph{#1}}\n");
1241 assert!(!db.command("foo").expect("foo defined").verbatim);
1242 }
1243
1244 #[test]
1245 fn verbatim_needs_an_argument() {
1246 let db = db_of("\\newcommand\\setup{\\@makeother\\$}\n");
1249 assert!(!db.command("setup").expect("setup defined").verbatim);
1250 }
1251
1252 #[test]
1253 fn def_helper_chain_followed() {
1254 let db = db_of("\\def\\setup{\\@makeother\\$}\\newcommand\\shellcmd[1]{\\setup#1}\n");
1257 assert!(db.command("shellcmd").expect("shellcmd defined").verbatim);
1258 assert!(!db.command("setup").expect("setup defined").verbatim);
1260 }
1261
1262 #[test]
1263 fn def_direct_verbatim_flagged() {
1264 let db = db_of("\\def\\shellcmd#1{\\@makeother\\$#1}\n");
1267 let sig = db.command("shellcmd").expect("shellcmd defined");
1268 assert!(sig.verbatim);
1269 assert!(sig.args.is_empty());
1270 }
1271
1272 #[test]
1273 fn def_zero_params() {
1274 let db = db_of("\\def\\foo{x}\n");
1276 let sig = db.command("foo").expect("foo defined");
1277 assert!(sig.args.is_empty());
1278 assert!(!sig.verbatim);
1279 }
1280
1281 #[test]
1282 fn def_counts_params() {
1283 let db = db_of("\\def\\foo#1#2{#1#2}\n");
1285 let sig = db.command("foo").expect("foo defined");
1286 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace, ArgKind::Brace]);
1287 }
1288
1289 #[test]
1290 fn def_variants_scanned() {
1291 let db = db_of("\\edef\\a#1{x}\\gdef\\b{y}\\xdef\\c#1{\\@makeother\\$#1}\n");
1293 assert_eq!(db.command("a").expect("a defined").args.len(), 1);
1294 assert!(db.command("b").expect("b defined").args.is_empty());
1295 let c = db.command("c").expect("c defined");
1296 assert!(c.verbatim);
1297 assert!(c.args.is_empty());
1298 }
1299
1300 #[test]
1301 fn def_chain_through_def_helpers() {
1302 let db = db_of(
1304 "\\def\\inner{\\@makeother\\$}\\def\\outer{\\inner}\\newcommand\\cmd[1]{\\outer#1}\n",
1305 );
1306 assert!(db.command("cmd").expect("cmd defined").verbatim);
1307 }
1308
1309 #[test]
1310 fn verbatim_xparse_flagged() {
1311 let db = db_of("\\NewDocumentCommand\\shellcmd{m}{\\@makeother\\$#1}\n");
1312 let sig = db.command("shellcmd").expect("shellcmd defined");
1313 assert!(sig.verbatim);
1314 assert!(sig.args.is_empty());
1315 }
1316
1317 #[test]
1318 fn env_makeother_flagged() {
1319 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");
1331 let sig = db.environment("shellenv").expect("shellenv defined");
1332 assert!(sig.verbatim_body);
1333 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Brace]);
1335 }
1336
1337 #[test]
1338 fn env_via_chained_helper() {
1339 let db =
1342 db_of("\\newcommand\\setup{\\@makeother\\$}\\newenvironment{shellenv}{\\setup}{}\n");
1343 assert!(
1344 db.environment("shellenv")
1345 .expect("shellenv defined")
1346 .verbatim_body
1347 );
1348 }
1349
1350 #[test]
1351 fn env_without_signal_not_flagged() {
1352 let db = db_of("\\newenvironment{remark}{\\par\\noindent\\textbf{Remark.}}{\\par}\n");
1354 let sig = db.environment("remark").expect("remark defined");
1355 assert!(!sig.verbatim_body);
1356 assert!(sig.reflow);
1357 }
1358
1359 #[test]
1360 fn lstnewenvironment_flagged_verbatim() {
1361 let db = db_of("\\lstnewenvironment{demo}[1][code]{\\lstset{#1}}{}\n");
1365 let sig = db.environment("demo").expect("demo defined");
1366 assert!(sig.verbatim_body);
1367 assert!(!sig.reflow);
1368 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1369 }
1370
1371 #[test]
1372 fn lstnewenvironment_no_args_flagged_verbatim() {
1373 let db = db_of("\\lstnewenvironment{demo}{}{}\n");
1374 let sig = db.environment("demo").expect("demo defined");
1375 assert!(sig.verbatim_body);
1376 assert!(sig.args.is_empty());
1377 }
1378
1379 #[test]
1380 fn defineverbatimenvironment_flagged_verbatim() {
1381 let db = db_of("\\DefineVerbatimEnvironment{code}{Verbatim}{fontsize=\\small}\n");
1383 let sig = db.environment("code").expect("code defined");
1384 assert!(sig.verbatim_body);
1385 assert!(!sig.reflow);
1386 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1387 }
1388
1389 #[test]
1390 fn xparse_env_makeother_flagged() {
1391 let db = db_of("\\NewDocumentEnvironment{shellenv}{O{x}}{\\dospecials}{}\n");
1393 let sig = db.environment("shellenv").expect("shellenv defined");
1394 assert!(sig.verbatim_body);
1395 assert_eq!(arg_kinds(&sig.args), vec![ArgKind::Bracket]);
1396 }
1397
1398 fn sites_of(src: &str) -> Vec<DefSite> {
1399 assert_eq!(reconstruct(src), src, "reconstruct must round-trip");
1400 scan_definition_sites(&SyntaxNode::new_root(parse(src).green))
1401 }
1402
1403 #[test]
1404 fn def_site_newcommand_braced_name_span() {
1405 let src = "\\newcommand{\\foo}[1]{#1}\n";
1406 let sites = sites_of(src);
1407 assert_eq!(sites.len(), 1);
1408 let site = &sites[0];
1409 assert_eq!(site.name, "foo");
1410 assert_eq!(site.kind, DefSiteKind::Command);
1411 assert_eq!(&src[site.name_range], "\\foo");
1412 assert_eq!(&src[site.range], "\\newcommand{\\foo}[1]{#1}");
1413 }
1414
1415 #[test]
1416 fn def_site_newcommand_unbraced_name_span() {
1417 let src = "\\newcommand\\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!(&src[sites[0].name_range], "\\foo");
1422 assert_eq!(&src[sites[0].range], "\\newcommand\\foo[1]{#1}");
1423 }
1424
1425 #[test]
1426 fn def_site_def_sibling_name_span() {
1427 let src = "\\def\\foo#1{#1}\n";
1428 let sites = sites_of(src);
1429 assert_eq!(sites.len(), 1);
1430 assert_eq!(sites[0].name, "foo");
1431 assert_eq!(sites[0].kind, DefSiteKind::Command);
1432 assert_eq!(&src[sites[0].name_range], "\\foo");
1433 }
1434
1435 #[test]
1436 fn def_site_xparse_command_name_span() {
1437 let src = "\\NewDocumentCommand{\\foo}{m O{d}}{x}\n";
1438 let sites = sites_of(src);
1439 assert_eq!(sites.len(), 1);
1440 assert_eq!(sites[0].name, "foo");
1441 assert_eq!(&src[sites[0].name_range], "\\foo");
1442 }
1443
1444 #[test]
1445 fn def_site_newenvironment_name_span() {
1446 let src = "\\newenvironment{myenv}{begin}{end}\n";
1447 let sites = sites_of(src);
1448 assert_eq!(sites.len(), 1);
1449 let site = &sites[0];
1450 assert_eq!(site.name, "myenv");
1451 assert_eq!(site.kind, DefSiteKind::Environment);
1452 assert_eq!(&src[site.name_range], "myenv");
1453 }
1454
1455 #[test]
1456 fn def_site_xparse_environment_name_span() {
1457 let src = "\\NewDocumentEnvironment{myenv}{m}{a}{b}\n";
1458 let sites = sites_of(src);
1459 assert_eq!(sites.len(), 1);
1460 assert_eq!(sites[0].name, "myenv");
1461 assert_eq!(sites[0].kind, DefSiteKind::Environment);
1462 assert_eq!(&src[sites[0].name_range], "myenv");
1463 }
1464
1465 #[test]
1466 fn def_site_keeps_every_redefinition() {
1467 let src = "\\newcommand{\\foo}{a}\n\\renewcommand{\\foo}{b}\n";
1469 let sites = sites_of(src);
1470 assert_eq!(sites.len(), 2);
1471 assert!(sites.iter().all(|s| s.name == "foo"));
1472 assert!(sites[0].name_range.start() < sites[1].name_range.start());
1473 }
1474
1475 #[test]
1476 fn def_site_none_for_malformed() {
1477 assert!(sites_of("\\newcommand\n").is_empty());
1478 assert!(sites_of("\\newenvironment{}{a}{b}\n").is_empty());
1479 }
1480
1481 const EQNARRAY_PAIR: &str =
1485 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n";
1486
1487 #[test]
1488 fn newcommand_env_alias_pair_is_recorded() {
1489 let db = db_of(EQNARRAY_PAIR);
1490 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1491 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1492 assert_eq!(db.env_begin_alias("eea"), None);
1494 assert_eq!(db.env_end_alias("bea"), None);
1495 }
1496
1497 #[test]
1498 fn def_env_alias_pair_is_recorded() {
1499 let db = db_of("\\def\\bea{\\begin{eqnarray}}\n\\def\\eea{\\end{eqnarray}}\n");
1500 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1501 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1502 }
1503
1504 #[test]
1505 fn xparse_env_alias_pair_is_recorded() {
1506 let db = db_of(
1507 "\\NewDocumentCommand{\\bea}{}{\\begin{eqnarray}}\n\
1508 \\NewDocumentCommand{\\eea}{}{\\end{eqnarray}}\n",
1509 );
1510 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1511 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1512 }
1513
1514 #[test]
1515 fn env_alias_body_tolerates_trivia() {
1516 let db = db_of(
1520 "\\newcommand{\\bea}{ \\begin{eqnarray} }\n\\newcommand{\\eea}{% why\n\\end{eqnarray}}\n",
1521 );
1522 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1523 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1524 }
1525
1526 #[test]
1532 fn a_lone_env_alias_half_is_recorded() {
1533 let db = db_of("\\newcommand{\\bea}{\\begin{eqnarray}}\n");
1534 assert_eq!(db.env_begin_alias("bea"), Some("eqnarray"));
1535 assert_eq!(db.env_end_alias("bea"), None);
1536
1537 let db = db_of("\\newcommand{\\eea}{\\end{eqnarray}}\n");
1538 assert_eq!(db.env_end_alias("eea"), Some("eqnarray"));
1539 assert_eq!(db.env_begin_alias("eea"), None);
1540 }
1541
1542 #[test]
1546 fn a_lone_half_still_obeys_every_target_rule() {
1547 assert_eq!(
1548 db_of("\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n").env_begin_alias("bmy"),
1549 None
1550 );
1551 assert_eq!(
1552 db_of("\\newcommand{\\ev}{\\end{verbatim}}\n").env_end_alias("ev"),
1553 None
1554 );
1555 assert_eq!(
1556 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n").env_begin_alias("bt"),
1557 None
1558 );
1559 assert_eq!(
1560 db_of("\\newcommand{\\bea}[1]{\\begin{eqnarray}}\n").env_begin_alias("bea"),
1561 None
1562 );
1563 }
1564
1565 #[test]
1566 fn env_alias_rejects_uncurated_target() {
1567 let db = db_of(
1568 "\\newcommand{\\bmy}{\\begin{notacuratedenv}}\n\
1569 \\newcommand{\\emy}{\\end{notacuratedenv}}\n",
1570 );
1571 assert_eq!(db.env_begin_alias("bmy"), None);
1572 assert_eq!(db.env_end_alias("emy"), None);
1573 }
1574
1575 #[test]
1576 fn env_alias_rejects_verbatim_target() {
1577 let db =
1581 db_of("\\newcommand{\\bv}{\\begin{verbatim}}\n\\newcommand{\\ev}{\\end{verbatim}}\n");
1582 assert_eq!(db.env_begin_alias("bv"), None);
1583 }
1584
1585 #[test]
1586 fn env_alias_rejects_argument_taking_target() {
1587 let db =
1590 db_of("\\newcommand{\\bt}{\\begin{tabular}}\n\\newcommand{\\et}{\\end{tabular}}\n");
1591 assert_eq!(db.env_begin_alias("bt"), None);
1592 }
1593
1594 #[test]
1595 fn env_alias_rejects_parameterized_definition() {
1596 let db = db_of(
1597 "\\newcommand{\\bt}[1]{\\begin{tabular}{#1}}\n\\newcommand{\\et}{\\end{tabular}}\n",
1598 );
1599 assert_eq!(db.env_begin_alias("bt"), None);
1600 }
1601
1602 #[test]
1603 fn env_alias_rejects_body_with_extra_content() {
1604 let db = db_of(
1605 "\\newcommand{\\bea}{\\begin{eqnarray}\\label{x}}\n\\newcommand{\\eea}{\\end{eqnarray}}\n",
1606 );
1607 assert_eq!(db.env_begin_alias("bea"), None);
1608 }
1609
1610 #[test]
1611 fn redefinition_retracts_an_earlier_env_alias() {
1612 let db = db_of(
1616 "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1617 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1618 \\renewcommand{\\bea}{\\textbf}\n",
1619 );
1620 assert_eq!(db.env_begin_alias("bea"), None);
1621 }
1622
1623 #[test]
1624 fn env_alias_does_not_pollute_the_environment_namespace() {
1625 let db = db_of(EQNARRAY_PAIR);
1628 assert!(db.environment("bea").is_none());
1629 assert!(!db.environment_names().any(|n| n == "bea"));
1630 }
1631
1632 fn environment_node(src: &str) -> SyntaxNode {
1634 SyntaxNode::new_root(parse(src).green)
1635 .descendants()
1636 .find(|n| n.kind() == SyntaxKind::ENVIRONMENT)
1637 .expect("an environment")
1638 }
1639
1640 #[test]
1641 fn signatures_resolves_an_env_alias_to_the_curated_target() {
1642 use crate::semantic::signature::Signatures;
1643 let src = format!("{EQNARRAY_PAIR}\\bea a \\eea\n");
1644 let db = db_of(&src);
1645 let sigs = Signatures::new(&db);
1646 let sig = sigs
1647 .environment_at(&environment_node(&src))
1648 .expect("alias resolves");
1649 let target = builtin().environment("eqnarray").expect("curated target");
1650 assert_eq!(sig, target);
1651 assert!(sig.math && sig.align);
1652 assert!(sigs.environment("bea").is_none());
1654 }
1655
1656 #[test]
1657 fn a_literal_begin_does_not_inherit_the_alias_target() {
1658 use crate::semantic::signature::Signatures;
1662 let src = format!("{EQNARRAY_PAIR}\\begin{{bea}} a \\end{{bea}}\n");
1663 let db = db_of(&src);
1664 let sigs = Signatures::new(&db);
1665 assert!(sigs.environment_at(&environment_node(&src)).is_none());
1666 }
1667
1668 #[test]
1669 fn a_real_environment_wins_over_an_alias_of_the_same_name() {
1670 use crate::semantic::signature::Signatures;
1673 let defs = "\\newcommand{\\bea}{\\begin{eqnarray}}\n\
1674 \\newcommand{\\eea}{\\end{eqnarray}}\n\
1675 \\newenvironment{bea}{x}{y}\n";
1676 let db = db_of(&format!("{defs}\\begin{{bea}} a \\end{{bea}}\n"));
1677 let sigs = Signatures::new(&db);
1678 let sig = sigs
1679 .environment_at(&environment_node(&format!(
1680 "{defs}\\begin{{bea}} a \\end{{bea}}\n"
1681 )))
1682 .expect("real environment resolves");
1683 assert!(
1684 !sig.math,
1685 "the scanned \\newenvironment must win for a literal `\\begin{{bea}}`"
1686 );
1687 assert!(
1688 sigs.environment_at(&environment_node(&format!("{defs}\\bea a \\eea\n")))
1689 .is_some_and(|sig| sig.math),
1690 "while the alias delimiters still resolve to eqnarray"
1691 );
1692 }
1693
1694 #[test]
1695 fn merge_from_carries_env_aliases() {
1696 let mut target = SignatureDb::default();
1697 target.merge_from(&db_of(EQNARRAY_PAIR));
1698 assert_eq!(target.env_begin_alias("bea"), Some("eqnarray"));
1699 assert_eq!(target.env_end_alias("eea"), Some("eqnarray"));
1700 }
1701}