typstyle_core/
attr.rs

1use rustc_hash::FxHashMap;
2use typst_syntax::{ast, Span, SyntaxKind, SyntaxNode};
3
4use crate::ext::StrExt;
5
6#[derive(Debug, Clone, Default)]
7pub struct Attributes {
8    /// Indicates whether formatting is explicitly disabled (`@typstyle off`) or always ignored.
9    pub(self) is_format_disabled: bool,
10
11    /// Indicates whether any child node contains a comment.
12    pub(self) has_comment: bool,
13
14    /// Indicates whether any descendant has a multiline string or raw.
15    pub(self) has_multiline_str: bool,
16
17    /// Indicates whether any descendant has a `MathAlignPoint`.
18    pub(self) has_math_align_point: bool,
19
20    /// Indicates whether the node text contains a linebreak.
21    /// Currently, it is only used for equations.
22    pub(self) is_multiline: bool,
23}
24
25/// A storage structure that manages formatting attributes for syntax nodes.
26#[derive(Debug, Default)]
27pub struct AttrStore {
28    /// A mapping between syntax node spans and their associated attributes.
29    attr_map: FxHashMap<Span, Attributes>,
30}
31
32impl AttrStore {
33    /// Creates a new `AttrStore` by computing formatting-related attributes
34    /// for all descendants of the given syntax node.
35    pub fn new(node: &SyntaxNode) -> AttrStore {
36        if node.erroneous() {
37            return Default::default(); // No attributes for erroneous nodes
38        }
39        let mut store = AttrStore::default();
40        store.compute_no_format(node);
41        store.compute_multiline(node);
42        store.compute_math_align_point(node);
43        store
44    }
45
46    /// Checks if a given syntax node contains a comment.
47    pub fn has_comment(&self, node: &SyntaxNode) -> bool {
48        self.check_node_attr(node, |attr| attr.has_comment)
49    }
50
51    pub fn has_multiline_str(&self, node: &SyntaxNode) -> bool {
52        self.check_node_attr(node, |attr| attr.has_multiline_str)
53    }
54
55    pub fn has_math_align_point(&self, node: &SyntaxNode) -> bool {
56        self.check_node_attr(node, |attr| attr.has_math_align_point)
57    }
58
59    pub fn can_align_in_math(&self, node: &SyntaxNode) -> bool {
60        self.check_node_attr(node, |attr| {
61            attr.has_math_align_point && !attr.has_multiline_str
62        })
63    }
64
65    /// Checks if a given syntax node or any of its descendants contains a linebreak.
66    pub fn is_multiline(&self, node: &SyntaxNode) -> bool {
67        self.check_node_attr(node, |attr| attr.is_multiline)
68    }
69
70    /// Checks if formatting is explicitly disabled for a given syntax node.
71    pub fn is_format_disabled(&self, node: &SyntaxNode) -> bool {
72        self.check_node_attr(node, |attr| attr.is_format_disabled)
73    }
74
75    fn check_node_attr(&self, node: &SyntaxNode, pred: impl FnOnce(&Attributes) -> bool) -> bool {
76        self.attr_map.get(&node.span()).is_some_and(pred)
77    }
78}
79
80impl AttrStore {
81    fn compute_multiline(&mut self, root: &SyntaxNode) {
82        self.compute_multiline_impl(root);
83    }
84
85    fn compute_multiline_impl(&mut self, node: &SyntaxNode) -> (bool, bool) {
86        let mut is_multiline = false;
87        let mut has_multiline_str = false;
88        for child in node.children() {
89            match child.kind() {
90                SyntaxKind::Space => {
91                    if child.text().has_linebreak() {
92                        is_multiline = true;
93                    }
94                }
95                SyntaxKind::BlockComment => {
96                    is_multiline |= child.text().has_linebreak();
97                }
98                SyntaxKind::Str => {
99                    has_multiline_str |= child.text().has_linebreak();
100                }
101                SyntaxKind::Raw => {
102                    let raw = child.cast::<ast::Raw>().expect("raw");
103                    has_multiline_str |= !raw.block() && raw.lines().nth(1).is_some();
104                }
105                _ => {}
106            }
107            let res = self.compute_multiline_impl(child);
108            is_multiline |= res.0;
109            has_multiline_str |= res.1;
110        }
111        if is_multiline {
112            self.attrs_mut_of(node).is_multiline = true;
113        }
114        if has_multiline_str {
115            self.attrs_mut_of(node).has_multiline_str = true;
116        }
117        (is_multiline, has_multiline_str)
118    }
119
120    fn compute_no_format(&mut self, root: &SyntaxNode) {
121        self.compute_no_format_impl(root);
122    }
123
124    fn compute_no_format_impl(&mut self, node: &SyntaxNode) {
125        let mut disable_next = false;
126        let mut commented = false;
127        for child in node.children() {
128            match child.kind() {
129                SyntaxKind::LineComment | SyntaxKind::BlockComment => {
130                    commented = true;
131                    // @typstyle off affects the whole next block
132                    disable_next = child.text().contains("@typstyle off");
133                }
134                SyntaxKind::Space | SyntaxKind::Hash => {}
135                SyntaxKind::Code | SyntaxKind::Math if disable_next => {
136                    // no format nodes with @typstyle off
137                    self.disable_first_nontrivial_child(child);
138                    disable_next = false;
139                }
140                _ if disable_next => {
141                    // no format nodes with @typstyle off
142                    if !child.kind().is_trivia() {
143                        self.attrs_mut_of(child).is_format_disabled = true;
144                    }
145                    disable_next = false;
146                }
147                _ => {
148                    if !child.kind().is_trivia() {
149                        self.compute_no_format_impl(child);
150                    }
151                }
152            }
153        }
154        if commented {
155            self.attrs_mut_of(node).has_comment = true;
156        }
157    }
158
159    fn disable_first_nontrivial_child(&mut self, node: &SyntaxNode) {
160        node.children()
161            .find(|it| !matches!(it.kind(), SyntaxKind::Space | SyntaxKind::Hash))
162            .inspect(|it| self.attrs_mut_of(it).is_format_disabled = true);
163    }
164
165    fn compute_math_align_point(&mut self, root: &SyntaxNode) {
166        self.compute_math_align_point_impl(root);
167    }
168
169    fn compute_math_align_point_impl(&mut self, node: &SyntaxNode) -> bool {
170        let node_kind = node.kind();
171        if node_kind == SyntaxKind::MathAlignPoint {
172            return true;
173        }
174        if node_kind.is_trivia() {
175            return false;
176        }
177        let mut has_math_align_point = false;
178        for child in node.children() {
179            has_math_align_point |= self.compute_math_align_point_impl(child);
180        }
181        if has_math_align_point && matches!(node_kind, SyntaxKind::Math | SyntaxKind::MathDelimited)
182        {
183            self.attrs_mut_of(node).has_math_align_point = true;
184            true
185        } else {
186            false
187        }
188    }
189
190    fn attrs_mut_of(&mut self, node: &SyntaxNode) -> &mut Attributes {
191        self.attr_map.entry(node.span()).or_default()
192    }
193}