Skip to main content

badness_parser/semantic/
mode.rs

1//! Effective text/math mode over a lossless syntax tree.
2
3use rowan::{TextRange, TextSize};
4
5use crate::ast::command_name;
6use crate::semantic::define::scan_definitions;
7use crate::semantic::signature::{
8    ArgKind, ArgumentDomain, Signatures, match_arg_slot, match_verbatim_arg_slot,
9};
10use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
11
12/// The effective mode at a source position.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Mode {
15    Math,
16    Text,
17    Unknown,
18}
19
20impl From<ArgumentDomain> for Mode {
21    fn from(domain: ArgumentDomain) -> Self {
22        match domain {
23            ArgumentDomain::Math => Self::Math,
24            ArgumentDomain::Text => Self::Text,
25            ArgumentDomain::Unknown => Self::Unknown,
26        }
27    }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31struct ModeRange {
32    range: TextRange,
33    mode: Mode,
34}
35
36/// A sorted, coalesced partition of the document's token ranges by effective
37/// mode.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ModeIndex {
40    ranges: Vec<ModeRange>,
41}
42
43/// Return the curated positional domain of an attached argument group.
44/// Unowned, unmatched, over-attached, and unknown-owner groups are unknown.
45pub fn argument_domain(group: &SyntaxNode) -> ArgumentDomain {
46    if !matches!(group.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) {
47        return ArgumentDomain::Unknown;
48    }
49    let Some(owner) = group.parent() else {
50        return ArgumentDomain::Unknown;
51    };
52    let root = owner.ancestors().last().unwrap_or_else(|| owner.clone());
53    let user = scan_definitions(&root);
54    let signatures = Signatures::new(&user);
55    let args = match owner.kind() {
56        SyntaxKind::COMMAND => command_name(&owner)
57            .and_then(|name| signatures.command(&name))
58            .map(|sig| sig.args.as_ref()),
59        SyntaxKind::BEGIN => environment_name(&owner)
60            .as_deref()
61            .and_then(|name| signatures.environment(name))
62            .map(|sig| sig.args.as_ref()),
63        _ => None,
64    };
65    let mut slot = 0usize;
66    for candidate in owner.children_with_tokens() {
67        match candidate {
68            SyntaxElement::Token(token) if token.kind() == SyntaxKind::VERB => {
69                if let Some(args) = args
70                    && token.text().starts_with('{')
71                {
72                    match_verbatim_arg_slot(args, &mut slot);
73                }
74            }
75            SyntaxElement::Node(candidate)
76                if matches!(candidate.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) =>
77            {
78                let kind = if candidate.kind() == SyntaxKind::OPTIONAL {
79                    ArgKind::Bracket
80                } else {
81                    ArgKind::Brace
82                };
83                let domain = args
84                    .and_then(|args| match_arg_slot(args, &mut slot, kind))
85                    .map_or(ArgumentDomain::Unknown, |spec| spec.domain);
86                if candidate == *group {
87                    return domain;
88                }
89            }
90            _ => {}
91        }
92    }
93    ArgumentDomain::Unknown
94}
95
96impl ModeIndex {
97    pub fn build(root: &SyntaxNode) -> Self {
98        let mut ranges = Vec::new();
99        let user = scan_definitions(root);
100        let signatures = Signatures::new(&user);
101        walk(root, Mode::Text, &signatures, &mut ranges);
102        Self { ranges }
103    }
104
105    /// Return the mode of the token containing `offset`.
106    ///
107    /// Offsets outside the document, including EOF, have no token and are
108    /// conservatively unknown.
109    pub fn mode_at(&self, offset: usize) -> Mode {
110        let offset = TextSize::from(offset as u32);
111        match self
112            .ranges
113            .binary_search_by(|entry| entry.range.start().cmp(&offset))
114        {
115            Ok(i) => self.ranges[i].mode,
116            Err(0) => Mode::Unknown,
117            Err(i) => {
118                if self.ranges[i - 1].range.contains(offset) {
119                    self.ranges[i - 1].mode
120                } else {
121                    Mode::Unknown
122                }
123            }
124        }
125    }
126}
127
128fn walk(
129    node: &SyntaxNode,
130    inherited: Mode,
131    signatures: &Signatures<'_>,
132    ranges: &mut Vec<ModeRange>,
133) {
134    let mode = match node.kind() {
135        SyntaxKind::MATH => Mode::Math,
136        SyntaxKind::NAME_GROUP => Mode::Unknown,
137        _ => inherited,
138    };
139
140    let argument_modes = match node.kind() {
141        SyntaxKind::COMMAND => command_argument_modes(node, signatures),
142        SyntaxKind::BEGIN => environment_argument_modes(node, signatures),
143        _ => Vec::new(),
144    };
145    let mut argument = 0usize;
146
147    for element in node.children_with_tokens() {
148        match element {
149            SyntaxElement::Token(token) => push_range(ranges, token.text_range(), mode),
150            SyntaxElement::Node(child) => {
151                let child_mode = if matches!(child.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL)
152                    && matches!(node.kind(), SyntaxKind::COMMAND | SyntaxKind::BEGIN)
153                {
154                    let mode = argument_modes
155                        .get(argument)
156                        .copied()
157                        .unwrap_or(Mode::Unknown);
158                    argument += 1;
159                    mode
160                } else {
161                    mode
162                };
163                walk(&child, child_mode, signatures, ranges);
164            }
165        }
166    }
167}
168
169fn command_argument_modes(node: &SyntaxNode, signatures: &Signatures<'_>) -> Vec<Mode> {
170    let args = command_name(node)
171        .and_then(|name| signatures.command(&name))
172        .map(|sig| sig.args.as_ref());
173    match_groups(node, args, false)
174}
175
176fn environment_argument_modes(begin: &SyntaxNode, signatures: &Signatures<'_>) -> Vec<Mode> {
177    let name = environment_name(begin);
178    let args = name
179        .as_deref()
180        .and_then(|name| signatures.environment(name))
181        .map(|sig| sig.args.as_ref());
182    match_groups(begin, args, false)
183}
184
185fn environment_name(begin: &SyntaxNode) -> Option<String> {
186    begin
187        .children()
188        .find(|child| child.kind() == SyntaxKind::NAME_GROUP)
189        .map(|group| group.text().to_string())
190        .and_then(|text| text.strip_prefix('{')?.strip_suffix('}').map(str::to_owned))
191}
192
193fn match_groups(
194    node: &SyntaxNode,
195    args: Option<&[crate::semantic::ArgSpec]>,
196    skip_first: bool,
197) -> Vec<Mode> {
198    let mut slot = 0usize;
199    let mut first = skip_first;
200    let mut modes = Vec::new();
201    for child in node.children_with_tokens() {
202        match child {
203            SyntaxElement::Token(token) if token.kind() == SyntaxKind::VERB => {
204                if let Some(args) = args
205                    && token.text().starts_with('{')
206                {
207                    match_verbatim_arg_slot(args, &mut slot);
208                }
209            }
210            SyntaxElement::Node(group)
211                if matches!(group.kind(), SyntaxKind::GROUP | SyntaxKind::OPTIONAL) =>
212            {
213                if first {
214                    first = false;
215                    modes.push(Mode::Unknown);
216                    continue;
217                }
218                let kind = if group.kind() == SyntaxKind::OPTIONAL {
219                    ArgKind::Bracket
220                } else {
221                    ArgKind::Brace
222                };
223                modes.push(
224                    args.and_then(|args| match_arg_slot(args, &mut slot, kind))
225                        .map_or(Mode::Unknown, |spec| spec.domain.into()),
226                );
227            }
228            _ => {}
229        }
230    }
231    modes
232}
233
234fn push_range(ranges: &mut Vec<ModeRange>, range: TextRange, mode: Mode) {
235    if range.is_empty() {
236        return;
237    }
238    if let Some(last) = ranges.last_mut()
239        && last.mode == mode
240        && last.range.end() == range.start()
241    {
242        last.range = TextRange::new(last.range.start(), range.end());
243    } else {
244        ranges.push(ModeRange { range, mode });
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::parser::parse;
252
253    fn modes(source: &str, needles: &[&str]) -> Vec<Mode> {
254        let parsed = parse(source);
255        needles
256            .iter()
257            .scan(0usize, |from, needle| {
258                let offset = source[*from..].find(needle).unwrap() + *from;
259                *from = offset + needle.len();
260                Some(ModeIndex::build(&parsed.syntax()).mode_at(offset))
261            })
262            .collect()
263    }
264
265    #[test]
266    fn nested_domains_override_ambient_mode() {
267        assert_eq!(
268            modes(
269                r"$a \frac{b \text{c $d$}}{e} \unknown{f}$",
270                &["a", "b", "c", "d", "e", "f"]
271            ),
272            vec![
273                Mode::Math,
274                Mode::Math,
275                Mode::Text,
276                Mode::Math,
277                Mode::Math,
278                Mode::Unknown
279            ]
280        );
281    }
282
283    #[test]
284    fn omitted_optional_slots_keep_their_positions() {
285        assert_eq!(
286            modes(r"\sqrt{x}\sqrt[n]{y}", &["x", "n", "y"]),
287            vec![Mode::Math; 3]
288        );
289    }
290
291    #[test]
292    fn known_math_arguments_work_in_text_and_unknown_arguments_override_math() {
293        assert_eq!(
294            modes(
295                r"before \ensuremath{x_i} $\foo{x_i}$",
296                &["before", "x_i", "x_i"]
297            ),
298            vec![Mode::Text, Mode::Math, Mode::Unknown]
299        );
300    }
301
302    #[test]
303    fn redefined_builtin_arguments_are_unknown() {
304        assert_eq!(
305            modes(
306                r"\renewcommand{\text}[1]{\ensuremath{#1}} $\text{5-10}$",
307                &["5-10"]
308            ),
309            vec![Mode::Unknown]
310        );
311    }
312
313    #[test]
314    fn prose_arguments_establish_text_mode() {
315        assert_eq!(
316            modes(
317                r"$\textbf{pages 5-10} \section[short 5-10]{pages 5-10} \footnote{pages 5-10}$",
318                &["pages 5-10", "short 5-10", "pages 5-10", "pages 5-10"]
319            ),
320            vec![Mode::Text; 4]
321        );
322    }
323
324    #[test]
325    fn environment_name_and_header_are_not_body_math() {
326        assert_eq!(
327            modes(r"\begin{align}[t]x\end{align}", &["align", "t", "x"]),
328            vec![Mode::Unknown, Mode::Unknown, Mode::Math]
329        );
330    }
331
332    #[test]
333    fn boundaries_belong_to_the_token_starting_there() {
334        let parsed = parse(r"a\ensuremath{b}c");
335        let index = ModeIndex::build(&parsed.syntax());
336        assert_eq!(index.mode_at(0), Mode::Text);
337        assert_eq!(index.mode_at(13), Mode::Math);
338        assert_eq!(index.mode_at(15), Mode::Text);
339        assert_eq!(index.mode_at(16), Mode::Unknown);
340    }
341}