1use rowan::TextRange;
25
26use crate::ast::{
27 AstNode, Begin, Environment, command_name, first_group_range, group_command_name, nth_group,
28};
29use crate::syntax::{SyntaxKind, SyntaxNode};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DocKind {
34 Macro,
36 Environment,
38 DescribeMacro,
40 DescribeEnv,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct DocAssociation {
48 pub name: String,
51 pub kind: DocKind,
52 pub range: TextRange,
54 pub name_range: TextRange,
56 pub code: Vec<TextRange>,
60}
61
62pub fn doc_associations(root: &SyntaxNode) -> Vec<DocAssociation> {
64 let mut out = Vec::new();
65 collect(root, &mut out);
66 out
67}
68
69fn collect(node: &SyntaxNode, out: &mut Vec<DocAssociation>) {
73 for child in node.children() {
74 match child.kind() {
75 SyntaxKind::COMMAND => collect_command(&child, out),
76 SyntaxKind::ENVIRONMENT => collect_environment(&child, out),
77 _ => collect(&child, out),
78 }
79 }
80}
81
82fn collect_environment(env: &SyntaxNode, out: &mut Vec<DocAssociation>) {
86 let begin = Environment::cast(env.clone()).and_then(|e| e.begin());
88 let kind = begin
89 .as_ref()
90 .and_then(Begin::name)
91 .and_then(|name| match name.as_str() {
92 "macro" => Some(DocKind::Macro),
93 "environment" => Some(DocKind::Environment),
94 _ => None,
95 });
96
97 if let (Some(kind), Some(begin)) = (kind, begin.as_ref())
98 && let Some(group) = nth_group(begin.syntax(), 0)
99 && let Some(name) = documented_name(&group, kind)
100 {
101 let mut code = Vec::new();
102 collect_code(env, &mut code);
103 out.push(DocAssociation {
104 name,
105 kind,
106 range: env.text_range(),
107 name_range: group.text_range(),
108 code,
109 });
110 }
111
112 collect(env, out);
115}
116
117fn collect_command(command: &SyntaxNode, out: &mut Vec<DocAssociation>) {
119 let Some(cmd) = command_name(command) else {
120 return;
121 };
122 let kind = match cmd.as_str() {
123 "DescribeMacro" => DocKind::DescribeMacro,
124 "DescribeEnv" => DocKind::DescribeEnv,
125 _ => return,
126 };
127
128 if let Some(group) = nth_group(command, 0) {
133 if let Some(name) = documented_name(&group, kind) {
134 out.push(DocAssociation {
135 name,
136 kind,
137 range: first_group_range(command),
138 name_range: group.text_range(),
139 code: Vec::new(),
140 });
141 }
142 } else if kind == DocKind::DescribeMacro
143 && let Some(sib) = command.next_sibling()
144 && sib.kind() == SyntaxKind::COMMAND
145 && let Some(name) = command_name(&sib)
146 {
147 out.push(DocAssociation {
148 name: format!("\\{name}"),
149 kind,
150 range: TextRange::new(command.text_range().start(), sib.text_range().end()),
151 name_range: sib.text_range(),
152 code: Vec::new(),
153 });
154 }
155}
156
157fn documented_name(group: &SyntaxNode, kind: DocKind) -> Option<String> {
162 match kind {
163 DocKind::Macro | DocKind::DescribeMacro => {
164 group_command_name(group).map(|name| format!("\\{name}"))
165 }
166 DocKind::Environment | DocKind::DescribeEnv => {
167 let text = group_inner_text(group)?;
171 let text = text.trim();
172 (!text.is_empty()).then(|| text.to_owned())
173 }
174 }
175}
176
177fn group_inner_text(group: &SyntaxNode) -> Option<String> {
181 let mut text = String::new();
182 for element in group.children_with_tokens() {
183 match element {
184 rowan::NodeOrToken::Token(token) => match token.kind() {
185 SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
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}