Skip to main content

comfit_core/query/
predicate.rs

1//! Compile custom query predicates and adjust capture byte ranges.
2
3use crate::{Error, backend::Match};
4
5use super::Argument;
6
7/// A compiled custom predicate or capture-offset directive.
8pub enum Predicate {
9    /// Require captured text to contain at least one literal.
10    Contains {
11        /// Capture index to inspect or adjust.
12        capture: u32,
13        /// Literal substrings to search for.
14        values: Vec<String>,
15    },
16    /// Test parent or ancestor node kinds, optionally negating the result.
17    Ancestor {
18        /// Capture index to inspect or adjust.
19        capture: u32,
20        /// Node kinds that satisfy the structural test.
21        kinds: Vec<String>,
22        /// Inspect only the immediate parent when true.
23        parent: bool,
24        /// Accept when no inspected node matches a kind.
25        negative: bool,
26    },
27    /// Adjust capture boundaries by row and byte-column offsets.
28    Offset {
29        /// Capture index to inspect or adjust.
30        capture: u32,
31        /// Start row/column and end row/column adjustments, in that order.
32        offsets: [isize; 4],
33    },
34}
35
36impl Predicate {
37    /// Compile a custom-predicate operator and literal arguments.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`Error::Query`] for unsupported operators or malformed arguments.
42    pub fn compile(operator: &str, arguments: Vec<Argument>) -> Result<Self, Error> {
43        let mut arguments = arguments.into_iter();
44        let Some(Argument::Capture(capture)) = arguments.next() else {
45            return Err(Error::Query(format!("#{operator} requires a capture")));
46        };
47
48        let values = arguments
49            .map(|argument| match argument {
50                Argument::String(value) => Ok(value),
51                _ => Err(Error::Query(format!(
52                    "#{operator} requires literal arguments"
53                ))),
54            })
55            .collect::<Result<Vec<_>, _>>()?;
56
57        match operator {
58            "contains?" if !values.is_empty() => Ok(Self::Contains { capture, values }),
59            "has-parent?" | "not-has-parent?" | "has-ancestor?" | "not-has-ancestor?"
60                if !values.is_empty() =>
61            {
62                Ok(Self::Ancestor {
63                    capture,
64                    kinds: values,
65                    parent: operator.ends_with("parent?"),
66                    negative: operator.starts_with("not-"),
67                })
68            }
69            "offset!" if values.len() == 4 => {
70                let mut offsets = [0; 4];
71
72                for (offset, value) in offsets.iter_mut().zip(values) {
73                    *offset = value
74                        .parse()
75                        .map_err(|_| Error::Query("invalid #offset! argument".into()))?;
76                }
77
78                Ok(Self::Offset { capture, offsets })
79            }
80            _ => Err(Error::Query(format!(
81                "unsupported or malformed predicate: #{operator}"
82            ))),
83        }
84    }
85
86    /// Test all matching captures before applying offsets.
87    ///
88    /// A predicate with no matching captures is accepted. Offset directives do not
89    /// filter matches. [`Self::apply`] adjusts their ranges.
90    pub fn accepts(&self, matched: &Match, source: &str) -> bool {
91        let capture_index = match self {
92            Self::Contains { capture, .. }
93            | Self::Ancestor { capture, .. }
94            | Self::Offset { capture, .. } => *capture,
95        };
96        matched
97            .captures
98            .iter()
99            .filter(|capture| capture.index == capture_index)
100            .all(|capture| {
101                let text = source.get(capture.node.range.clone()).unwrap_or_default();
102
103                match self {
104                    Self::Contains { values, .. } => {
105                        values.iter().any(|value| text.contains(value))
106                    }
107
108                    Self::Ancestor {
109                        kinds,
110                        parent,
111                        negative,
112                        ..
113                    } => {
114                        let count = if *parent {
115                            1
116                        } else {
117                            capture.node.ancestors.len()
118                        };
119
120                        let found = capture
121                            .node
122                            .ancestors
123                            .iter()
124                            .take(count)
125                            .any(|kind| kinds.contains(kind));
126                        found != *negative
127                    }
128
129                    Self::Offset { .. } => true,
130                }
131            })
132    }
133
134    /// Apply offset directives to matching captures. Other predicates do nothing.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`Error::InvalidRange`] when an offset leaves the source or splits
139    /// a UTF-8 character.
140    pub fn apply(&self, matched: &mut Match, source: &str) -> Result<(), Error> {
141        let Self::Offset { capture, offsets } = self else {
142            return Ok(());
143        };
144
145        for node in matched
146            .captures
147            .iter_mut()
148            .filter(|item| item.index == *capture)
149            .map(|item| &mut item.node)
150        {
151            let start = offset(source, node.range.start, offsets[0], offsets[1]);
152            let end = offset(source, node.range.end, offsets[2], offsets[3]);
153            let range = start
154                .zip(end)
155                .map(|(start, end)| start..end)
156                .filter(|range| source.get(range.clone()).is_some())
157                .ok_or_else(|| Error::InvalidRange(node.range.clone()))?;
158            node.range = range;
159        }
160
161        Ok(())
162    }
163}
164
165fn offset(source: &str, byte: usize, rows: isize, columns: isize) -> Option<usize> {
166    let prefix = source.get(..byte)?;
167    let mut start = prefix.rfind('\n').map_or(0, |newline| newline + 1);
168    let column = (byte - start).checked_add_signed(columns)?;
169
170    for _ in 0..rows.unsigned_abs() {
171        start = if rows > 0 {
172            start + source[start..].find('\n')? + 1
173        } else {
174            let end = start.checked_sub(1)?;
175            source[..end].rfind('\n').map_or(0, |newline| newline + 1)
176        };
177    }
178
179    let length = source[start..].find('\n').unwrap_or(source.len() - start);
180
181    if column > length {
182        return None;
183    }
184
185    let byte = start.checked_add(column)?;
186
187    source.is_char_boundary(byte).then_some(byte)
188}