1use rowan::TextRange;
23
24use crate::ast::{
25 AstNode, Begin, Environment, command_name, first_group_range, group_command_name, nth_group,
26};
27use crate::syntax::{SyntaxKind, SyntaxNode};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DocKind {
32 Macro,
34 Environment,
36 DescribeMacro,
38 DescribeEnv,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DocAssociation {
46 pub name: String,
49 pub kind: DocKind,
50 pub range: TextRange,
52 pub name_range: TextRange,
54 pub code: Vec<TextRange>,
58}
59
60pub fn doc_associations(root: &SyntaxNode) -> Vec<DocAssociation> {
62 let mut out = Vec::new();
63 collect(root, &mut out);
64 out
65}
66
67fn collect(node: &SyntaxNode, out: &mut Vec<DocAssociation>) {
71 for child in node.children() {
72 match child.kind() {
73 SyntaxKind::COMMAND => collect_command(&child, out),
74 SyntaxKind::ENVIRONMENT => collect_environment(&child, out),
75 _ => collect(&child, out),
76 }
77 }
78}
79
80fn collect_environment(env: &SyntaxNode, out: &mut Vec<DocAssociation>) {
84 let begin = Environment::cast(env.clone()).and_then(|e| e.begin());
86 let kind = begin
87 .as_ref()
88 .and_then(Begin::name)
89 .and_then(|name| match name.as_str() {
90 "macro" => Some(DocKind::Macro),
91 "environment" => Some(DocKind::Environment),
92 _ => None,
93 });
94
95 if let (Some(kind), Some(begin)) = (kind, begin.as_ref())
96 && let Some(group) = nth_group(begin.syntax(), 0)
97 && let Some(name) = documented_name(&group, kind)
98 {
99 let mut code = Vec::new();
100 collect_code(env, &mut code);
101 out.push(DocAssociation {
102 name,
103 kind,
104 range: env.text_range(),
105 name_range: group.text_range(),
106 code,
107 });
108 }
109
110 collect(env, out);
113}
114
115fn collect_command(command: &SyntaxNode, out: &mut Vec<DocAssociation>) {
117 let Some(cmd) = command_name(command) else {
118 return;
119 };
120 let kind = match cmd.as_str() {
121 "DescribeMacro" => DocKind::DescribeMacro,
122 "DescribeEnv" => DocKind::DescribeEnv,
123 _ => return,
124 };
125
126 if let Some(group) = nth_group(command, 0) {
131 if let Some(name) = documented_name(&group, kind) {
132 out.push(DocAssociation {
133 name,
134 kind,
135 range: first_group_range(command),
136 name_range: group.text_range(),
137 code: Vec::new(),
138 });
139 }
140 } else if kind == DocKind::DescribeMacro
141 && let Some(sib) = command.next_sibling()
142 && sib.kind() == SyntaxKind::COMMAND
143 && let Some(name) = command_name(&sib)
144 {
145 out.push(DocAssociation {
146 name: format!("\\{name}"),
147 kind,
148 range: TextRange::new(command.text_range().start(), sib.text_range().end()),
149 name_range: sib.text_range(),
150 code: Vec::new(),
151 });
152 }
153}
154
155fn documented_name(group: &SyntaxNode, kind: DocKind) -> Option<String> {
160 match kind {
161 DocKind::Macro | DocKind::DescribeMacro => {
162 group_command_name(group).map(|name| format!("\\{name}"))
163 }
164 DocKind::Environment | DocKind::DescribeEnv => {
165 let text = group_inner_text(group)?;
169 let text = text.trim();
170 (!text.is_empty()).then(|| text.to_owned())
171 }
172 }
173}
174
175fn group_inner_text(group: &SyntaxNode) -> Option<String> {
180 let mut text = String::new();
181 for element in group.children_with_tokens() {
182 match element {
183 rowan::NodeOrToken::Token(token) => match token.kind() {
184 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
185 SyntaxKind::HASH => return None,
186 _ => text.push_str(token.text()),
187 },
188 rowan::NodeOrToken::Node(_) => return None,
189 }
190 }
191 Some(text)
192}
193
194fn collect_code(node: &SyntaxNode, out: &mut Vec<TextRange>) {
197 for child in node.children() {
198 if child.kind() == SyntaxKind::ENVIRONMENT {
199 let name = Environment::cast(child.clone())
200 .and_then(|e| e.begin())
201 .and_then(|b| b.name());
202 match name.as_deref() {
203 Some("macrocode" | "macrocode*") => out.push(child.text_range()),
204 Some("macro" | "environment") => {}
206 _ => collect_code(&child, out),
207 }
208 } else {
209 collect_code(&child, out);
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::parser::{LatexFlavor, LexConfig, parse_with_flavor};
218
219 fn assoc_of(src: &str) -> Vec<DocAssociation> {
220 let config = LexConfig {
221 flavor: LatexFlavor::Document,
222 dtx: true,
223 };
224 let parsed = parse_with_flavor(src, config);
225 assert_eq!(parsed.syntax().to_string(), src, "losslessness violated");
226 doc_associations(&parsed.syntax())
227 }
228
229 #[test]
230 fn macro_env_documents_its_name() {
231 let items = assoc_of("% \\begin{macro}{\\foo}\n% docs.\n% \\end{macro}\n");
232 assert_eq!(items.len(), 1);
233 assert_eq!(items[0].name, "\\foo");
234 assert_eq!(items[0].kind, DocKind::Macro);
235 assert!(items[0].code.is_empty());
236 }
237
238 #[test]
239 fn nested_macrocode_is_the_code() {
240 let src = "% \\begin{macro}{\\foo}\n% docs.\n% \\begin{macrocode}\n\\def\\foo{x}\n% \\end{macrocode}\n% \\end{macro}\n";
241 let items = assoc_of(src);
242 assert_eq!(items.len(), 1);
243 assert_eq!(items[0].name, "\\foo");
244 assert_eq!(items[0].code.len(), 1);
245 let code = &src[items[0].code[0]];
247 assert!(code.starts_with("\\begin{macrocode}"));
248 assert!(code.contains("\\def\\foo{x}"));
249 }
250
251 #[test]
252 fn environment_env_documents_a_plain_name() {
253 let items = assoc_of("% \\begin{environment}{myenv}\n% docs.\n% \\end{environment}\n");
254 assert_eq!(items.len(), 1);
255 assert_eq!(items[0].name, "myenv");
256 assert_eq!(items[0].kind, DocKind::Environment);
257 }
258
259 #[test]
260 fn describe_macro_braced() {
261 let items = assoc_of("% \\DescribeMacro{\\foo} does foo.\n");
262 assert_eq!(items.len(), 1);
263 assert_eq!(items[0].name, "\\foo");
264 assert_eq!(items[0].kind, DocKind::DescribeMacro);
265 assert!(items[0].code.is_empty());
266 }
267
268 #[test]
269 fn describe_macro_braceless() {
270 let items = assoc_of("% \\DescribeMacro\\foo does foo.\n");
272 assert_eq!(items.len(), 1);
273 assert_eq!(items[0].name, "\\foo");
274 assert_eq!(items[0].kind, DocKind::DescribeMacro);
275 }
276
277 #[test]
278 fn describe_env() {
279 let items = assoc_of("% \\DescribeEnv{myenv} is an env.\n");
280 assert_eq!(items.len(), 1);
281 assert_eq!(items[0].name, "myenv");
282 assert_eq!(items[0].kind, DocKind::DescribeEnv);
283 }
284
285 #[test]
286 fn nested_macro_envs_both_surface_with_own_code() {
287 let src = "% \\begin{macro}{\\outer}\n% \\begin{macrocode}\n\\def\\outer{o}\n% \\end{macrocode}\n% \\begin{macro}{\\inner}\n% \\begin{macrocode}\n\\def\\inner{i}\n% \\end{macrocode}\n% \\end{macro}\n% \\end{macro}\n";
288 let items = assoc_of(src);
289 let names: Vec<_> = items.iter().map(|i| i.name.as_str()).collect();
290 assert_eq!(names, vec!["\\outer", "\\inner"]);
291 let outer = &items[0];
294 assert_eq!(outer.code.len(), 1);
295 assert!(src[outer.code[0]].contains("\\def\\outer{o}"));
296 let inner = &items[1];
297 assert_eq!(inner.code.len(), 1);
298 assert!(src[inner.code[0]].contains("\\def\\inner{i}"));
299 }
300
301 #[test]
302 fn empty_or_nested_macro_name_is_skipped() {
303 assert!(assoc_of("% \\begin{macro}{}\n% \\end{macro}\n").is_empty());
304 assert!(assoc_of("% \\DescribeEnv{\\foo}\n").is_empty());
307 }
308
309 #[test]
310 fn non_ltxdoc_constructs_are_ignored() {
311 let items =
312 assoc_of("% \\section{Intro}\n% \\begin{itemize}\n% \\item x\n% \\end{itemize}\n");
313 assert!(items.is_empty());
314 }
315}