aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine/src
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2025-10-18 22:54:24 +0100
committertslil <tslil@posteo.de>2025-10-18 23:52:52 +0100
commit5cf06bab8d5a8c6115df35f6cec652bfd0fd0922 (patch)
treeca412a1f1c0b58f464bb9f5583e028d403e9e63b /rprt-engine/src
parent16f0479a4e43a58c04796e9aa4d43d92b2655024 (diff)
Parse hooks, these are left associative
Diffstat (limited to 'rprt-engine/src')
-rw-r--r--rprt-engine/src/explainer.rs93
-rw-r--r--rprt-engine/src/expression.rs2
-rw-r--r--rprt-engine/src/parser.rs612
3 files changed, 562 insertions, 145 deletions
diff --git a/rprt-engine/src/explainer.rs b/rprt-engine/src/explainer.rs
index 9c847f2..38bd41b 100644
--- a/rprt-engine/src/explainer.rs
+++ b/rprt-engine/src/explainer.rs
@@ -1,6 +1,6 @@
use rprt_engine::{expression::*, parser::*, token::*};
-use std::env;
use std::collections::BTreeSet;
+use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
@@ -37,6 +37,8 @@ struct TreeNode {
depth: usize,
children: Vec<usize>,
is_leaf: bool,
+ hook_kind: Option<HookKind>,
+ train_arity: Option<u8>,
}
fn generate_tree(formatted: &str, expr: &Composite) -> String {
@@ -51,11 +53,8 @@ fn generate_tree(formatted: &str, expr: &Composite) -> String {
output.push_str(formatted);
output.push('\n');
- let mut active_bars: BTreeSet<usize> = nodes
- .iter()
- .filter(|n| n.is_leaf)
- .map(|n| n.pos)
- .collect();
+ let mut active_bars: BTreeSet<usize> =
+ nodes.iter().filter(|n| n.is_leaf).map(|n| n.pos).collect();
let mut prev_active = active_bars.clone();
@@ -64,7 +63,11 @@ fn generate_tree(formatted: &str, expr: &Composite) -> String {
.iter()
.filter(|n| n.depth == current_depth && !n.is_leaf)
.collect();
-
+
+ if interior.is_empty() {
+ continue;
+ }
+
let mut next_active = active_bars.clone();
for node in &interior {
for &child in &node.children {
@@ -82,15 +85,40 @@ fn generate_tree(formatted: &str, expr: &Composite) -> String {
}
}
+ let mut spacer = vec![' '; width];
+ for i in 0..width {
+ if prev_active.contains(&i) {
+ spacer[i] = '│';
+ }
+ }
+ output.push_str(&spacer.iter().collect::<String>());
+ output.push('\n');
+
let mut line = vec![' '; width];
for i in 0..width {
let up = prev_active.contains(&i);
let down = next_active.contains(&i);
let in_span = horizontal_spans.iter().any(|(l, r)| *l <= i && i <= *r);
- let left = i > 0 && horizontal_spans.iter().any(|(l, r)| *l <= i - 1 && i - 1 <= *r);
- let right = i + 1 < width && horizontal_spans.iter().any(|(l, r)| *l <= i + 1 && i + 1 <= *r);
-
- if up || down || in_span {
+ let left = i > 0
+ && horizontal_spans
+ .iter()
+ .any(|(l, r)| *l <= i - 1 && i - 1 <= *r);
+ let right = i + 1 < width
+ && horizontal_spans
+ .iter()
+ .any(|(l, r)| *l <= i + 1 && i + 1 <= *r);
+
+ let node_at_pos = interior.iter().find(|n| n.pos == i);
+
+ if let Some(node) = node_at_pos {
+ if let Some(hook_kind) = node.hook_kind {
+ line[i] = hook_kind.to_string().chars().next().unwrap();
+ } else if let Some(arity) = node.train_arity {
+ line[i] = char::from_digit(arity as u32, 10).unwrap();
+ } else if up || down || in_span {
+ line[i] = box_char(up, down, left, right);
+ }
+ } else if up || down || in_span {
line[i] = box_char(up, down, left, right);
}
}
@@ -116,7 +144,16 @@ fn generate_tree(formatted: &str, expr: &Composite) -> String {
output
}
-fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &mut Vec<TreeNode>) -> usize {
+fn format_width(expr: &Composite) -> usize {
+ format!("{}", expr).chars().count()
+}
+
+fn build_tree_from_expr(
+ expr: &Composite,
+ depth: usize,
+ offset: usize,
+ nodes: &mut Vec<TreeNode>,
+) -> usize {
match expr {
Composite::SelectionFunction { .. } | Composite::TextFunction { .. } => {
nodes.push(TreeNode {
@@ -124,15 +161,15 @@ fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &m
depth,
children: vec![],
is_leaf: true,
+ hook_kind: None,
+ train_arity: None,
});
offset
}
Composite::Train2 { f, g } => {
- let f_offset = offset + 1;
+ let f_offset = offset + 1; // Account for opening '('
let f_pos = build_tree_from_expr(f, depth + 1, f_offset, nodes);
-
- let f_str = format!("{}", f);
- let g_offset = f_offset + f_str.chars().count() + 1;
+ let g_offset = f_offset + format_width(f) + 1; // +1 for space
let g_pos = build_tree_from_expr(g, depth + 1, g_offset, nodes);
nodes.push(TreeNode {
@@ -140,19 +177,17 @@ fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &m
depth,
children: vec![f_pos, g_pos],
is_leaf: false,
+ hook_kind: None,
+ train_arity: Some(2),
});
f_pos
}
Composite::Train3 { f, g, h } => {
- let f_offset = offset + 1;
+ let f_offset = offset + 1; // Account for opening '('
let f_pos = build_tree_from_expr(f, depth + 1, f_offset, nodes);
-
- let f_str = format!("{}", f);
- let g_offset = f_offset + f_str.chars().count() + 1;
+ let g_offset = f_offset + format_width(f) + 1; // +1 for space
let g_pos = build_tree_from_expr(g, depth + 1, g_offset, nodes);
-
- let g_str = format!("{}", g);
- let h_offset = g_offset + g_str.chars().count() + 1;
+ let h_offset = g_offset + format_width(g) + 1; // +1 for space
let h_pos = build_tree_from_expr(h, depth + 1, h_offset, nodes);
nodes.push(TreeNode {
@@ -160,15 +195,15 @@ fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &m
depth,
children: vec![f_pos, g_pos, h_pos],
is_leaf: false,
+ hook_kind: None,
+ train_arity: Some(3),
});
g_pos
}
Composite::Hook { kind, left, right } => {
- let left_pos = build_tree_from_expr(left, depth + 1, offset, nodes);
-
- let left_str = format!("{}", left);
- let kind_str = format!("{}", kind);
- let right_offset = offset + left_str.chars().count() + kind_str.chars().count();
+ let left_offset = offset + 1; // Account for opening '('
+ let left_pos = build_tree_from_expr(left, depth + 1, left_offset, nodes);
+ let right_offset = left_offset + format_width(left) + 1; // +1 for hook operator (< or >)
let right_pos = build_tree_from_expr(right, depth + 1, right_offset, nodes);
nodes.push(TreeNode {
@@ -176,6 +211,8 @@ fn build_tree_from_expr(expr: &Composite, depth: usize, offset: usize, nodes: &m
depth,
children: vec![left_pos, right_pos],
is_leaf: false,
+ hook_kind: Some(*kind),
+ train_arity: None,
});
left_pos
}
diff --git a/rprt-engine/src/expression.rs b/rprt-engine/src/expression.rs
index c463fca..ba433cf 100644
--- a/rprt-engine/src/expression.rs
+++ b/rprt-engine/src/expression.rs
@@ -51,7 +51,7 @@ impl Display for Composite {
TextFunction { func, swapped } => {
write!(fmtr, "{}{func}", if *swapped { "@" } else { "" })
}
- Hook { kind, left, right } => write!(fmtr, "{left}{kind}{right}",),
+ Hook { kind, left, right } => write!(fmtr, "({left}{kind}{right})"),
Train2 { f, g } => write!(fmtr, "({f} {g})"),
Train3 { f, g, h } => write!(fmtr, "({f} {g} {h})"),
}
diff --git a/rprt-engine/src/parser.rs b/rprt-engine/src/parser.rs
index 78c28c0..c17913a 100644
--- a/rprt-engine/src/parser.rs
+++ b/rprt-engine/src/parser.rs
@@ -46,12 +46,12 @@ pub fn parse(tokens: Vec<Token>) -> Result<Composite, ParseError> {
tokens: tokens,
pos: 0,
};
- parse_function_in_application(&mut context)
+ parse_composition(&mut context)
}
-fn parse_function_in_application(context: &mut ParseContext) -> Result<Composite, ParseError> {
+fn parse_composition(context: &mut ParseContext) -> Result<Composite, ParseError> {
let mut functions = Vec::new();
- while can_start_selection_function(context, 0) {
+ while can_start_composition(context, 0) {
functions.push(parse_function_atom(context)?);
}
if functions.is_empty() {
@@ -103,7 +103,7 @@ fn build_train(mut functions: Vec<Composite>) -> Composite {
}
}
-fn can_start_selection_function(context: &ParseContext, offset: usize) -> bool {
+fn can_start_composition(context: &ParseContext, offset: usize) -> bool {
matches!(
context.peek(offset),
Some(Token::Empty)
@@ -120,13 +120,22 @@ fn can_start_selection_function(context: &ParseContext, offset: usize) -> bool {
| Some(Token::Complement)
| Some(Token::Conditional)
| Some(Token::LParen)
+ | Some(Token::Dot)
+ | Some(Token::Change)
+ | Some(Token::Insert)
+ | Some(Token::Append)
+ | Some(Token::Delete)
+ | Some(Token::Write)
+ | Some(Token::Pipe)
+ | Some(Token::TextLiteral(_))
+ | Some(Token::Swap)
)
}
-fn parse_function_atom(context: &mut ParseContext) -> Result<Composite, ParseError> {
+fn parse_base_function(context: &mut ParseContext) -> Result<Composite, ParseError> {
if matches!(context.current()?, Token::LParen) {
context.advance(1, true)?;
- let func = parse_function_in_application(context)?;
+ let func = parse_composition(context)?;
if !matches!(context.current()?, Token::RParen) {
return Err(ParseError::UnexpectedToken {
found: context.current()?,
@@ -134,143 +143,230 @@ fn parse_function_atom(context: &mut ParseContext) -> Result<Composite, ParseErr
});
}
context.advance(1, false)?;
- return Ok(func);
+ Ok(func)
+ } else if is_function_t(context)? {
+ parse_builtin_function_t(context)
+ } else if is_function_s(context)? {
+ parse_builtin_function_s(context)
+ } else {
+ Err(ParseError::UnexpectedToken {
+ found: context.current()?,
+ expecting: "function",
+ })
}
- let result_trans = parse_result_trans(context)?;
- let search_mod = parse_search_mod(context)?;
- let func = parse_builtin_function_s(context)?;
- Ok(Composite::SelectionFunction {
- func: func,
- search_mod: search_mod,
- result_transform: result_trans,
- })
}
-fn parse_search_mod(context: &mut ParseContext) -> Result<Option<SearchModifier>, ParseError> {
- let maybe_mod = |token| match token {
- Token::Sequential => Ok(Some(SearchModifier::Sequential)),
- Token::Reverse => Ok(Some(SearchModifier::Reverse)),
- _ => Ok(None),
- };
- let token = context.current()?;
- if let Some(m1) = maybe_mod(token)? {
- context.advance(1, true)?;
- if let Some(m2) = maybe_mod(context.current()?)? {
- context.advance(1, true)?;
- return Ok(Some(match (m1, m2) {
- (SearchModifier::Sequential, SearchModifier::Reverse) => {
- SearchModifier::SequentialReverse
- }
- (SearchModifier::Reverse, SearchModifier::Sequential) => {
- SearchModifier::ReverseSequential
- }
- (_, _) => unreachable!(),
- }));
+fn parse_function_atom(context: &mut ParseContext) -> Result<Composite, ParseError> {
+ let mut result = parse_base_function(context)?;
+
+ loop {
+ match context.current() {
+ Ok(Token::Before) => {
+ context.advance(1, true)?;
+ let right = parse_base_function(context)?;
+ result = Composite::Hook {
+ kind: HookKind::Before,
+ left: Box::new(result),
+ right: Box::new(right),
+ };
+ }
+ Ok(Token::After) => {
+ context.advance(1, true)?;
+ let right = parse_base_function(context)?;
+ result = Composite::Hook {
+ kind: HookKind::After,
+ left: Box::new(result),
+ right: Box::new(right),
+ };
+ }
+ _ => return Ok(result),
}
- return Ok(Some(m1));
- };
- Ok(None)
+ }
}
-fn parse_result_trans(context: &mut ParseContext) -> Result<Option<ResultTransform>, ParseError> {
- let maybe_mod = |token| match token {
- Token::Complement => Ok(Some(ResultTransform::Complement)),
- Token::Conditional => Ok(Some(ResultTransform::Conditional)),
- _ => Ok(None),
- };
- let token = context.current()?;
- if let Some(m1) = maybe_mod(token)? {
- context.advance(1, true)?;
- if let Some(m2) = maybe_mod(context.current()?)? {
- context.advance(1, true)?;
- return Ok(Some(match (m1, m2) {
- (ResultTransform::Complement, ResultTransform::Conditional) => {
- ResultTransform::ComplementConditional
- }
- (ResultTransform::Conditional, ResultTransform::Complement) => {
- ResultTransform::ConditionalComplement
- }
- (_, _) => unreachable!(),
- }));
- }
- return Ok(Some(m1));
- };
- Ok(None)
+fn is_function_t(context: &ParseContext) -> Result<bool, ParseError> {
+ Ok(matches!(
+ context.current()?,
+ Token::Dot
+ | Token::Change
+ | Token::Insert
+ | Token::Append
+ | Token::Delete
+ | Token::Write
+ | Token::Pipe
+ | Token::TextLiteral(_)
+ | Token::Swap
+ ))
}
-fn parse_builtin_function_s(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
+fn parse_builtin_function_t(context: &mut ParseContext) -> Result<Composite, ParseError> {
+ let mut swapped = false;
+ if context.current()? == Token::Swap {
+ swapped = true;
+ context.advance(1, true)?;
+ }
let token = context.current()?;
let result = match token {
- Token::Empty => BuiltinSelectionFn::Empty,
- Token::EndOfBuffer => BuiltinSelectionFn::EndOfBuffer,
- Token::Span => BuiltinSelectionFn::Span,
- Token::CharacterOffset => return parse_char_offset(context),
- Token::Number(n) => BuiltinSelectionFn::Line(n),
- Token::Regex(pattern) => BuiltinSelectionFn::Regex(pattern),
- Token::AllMatches => return parse_all_matches(context),
- Token::LineOffset => return parse_relative_line(context),
- Token::BufferMatch => return parse_buffer_match(context),
+ Token::Dot => BuiltinTextFn::Dot,
+ Token::Change => BuiltinTextFn::Change,
+ Token::Insert => BuiltinTextFn::Insert,
+ Token::Append => BuiltinTextFn::Append,
+ Token::Delete => BuiltinTextFn::Delete,
+ Token::Write => BuiltinTextFn::Write,
+ Token::Pipe => BuiltinTextFn::Pipe,
+ Token::TextLiteral(s) => BuiltinTextFn::Literal(s),
_ => {
return Err(ParseError::UnexpectedToken {
found: token,
- expecting: "selection function",
+ expecting: "text function",
});
}
};
context.advance(1, false)?;
- Ok(result)
+ Ok(Composite::TextFunction {
+ func: result,
+ swapped: swapped,
+ })
}
-fn parse_char_offset(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
- context.advance(1, true)?;
- let token = context.current()?;
- context.advance(1, false)?;
- match token {
- Token::Number(n) => Ok(BuiltinSelectionFn::CharOffset(n)),
- _ => Err(ParseError::UnexpectedToken {
- found: token,
- expecting: "number",
- }),
- }
+fn is_function_s(context: &ParseContext) -> Result<bool, ParseError> {
+ Ok(matches!(
+ context.current()?,
+ Token::Empty
+ | Token::EndOfBuffer
+ | Token::Span
+ | Token::CharacterOffset
+ | Token::Number(_)
+ | Token::Regex(_)
+ | Token::AllMatches
+ | Token::LineOffset
+ | Token::BufferMatch
+ | Token::Reverse
+ | Token::Sequential
+ | Token::Complement
+ | Token::Conditional
+ | Token::LParen
+ ))
}
-fn parse_all_matches(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
- context.advance(1, true)?;
- let token = context.current()?;
- context.advance(1, false)?;
- match token {
- Token::Regex(pattern) => Ok(BuiltinSelectionFn::AllMatches(pattern)),
- _ => Err(ParseError::UnexpectedToken {
- found: token,
- expecting: "regex",
- }),
+fn parse_builtin_function_s(context: &mut ParseContext) -> Result<Composite, ParseError> {
+ fn parse_modifier_pair<T: Copy>(
+ context: &mut ParseContext,
+ parse_single: impl Fn(Token) -> Option<T>,
+ combine: impl Fn(T, T) -> T,
+ ) -> Result<Option<T>, ParseError> {
+ let token = context.current()?;
+ if let Some(m1) = parse_single(token) {
+ context.advance(1, true)?;
+ if let Some(m2) = parse_single(context.current()?) {
+ context.advance(1, true)?;
+ return Ok(Some(combine(m1, m2)));
+ }
+ return Ok(Some(m1));
+ }
+ Ok(None)
}
-}
-fn parse_relative_line(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
- context.advance(1, true)?;
- let token = context.current()?;
- context.advance(1, false)?;
- match token {
- Token::Number(n) => Ok(BuiltinSelectionFn::RelativeLine(n)),
- _ => Err(ParseError::UnexpectedToken {
+ fn parse_prefixed_builtin(
+ context: &mut ParseContext,
+ expected: &'static str,
+ matcher: impl FnOnce(Token) -> Option<BuiltinSelectionFn>,
+ ) -> Result<BuiltinSelectionFn, ParseError> {
+ context.advance(1, true)?;
+ let token = context.current()?;
+ context.advance(1, false)?;
+ matcher(token.clone()).ok_or(ParseError::UnexpectedToken {
found: token,
- expecting: "number",
- }),
+ expecting: expected,
+ })
}
-}
-fn parse_buffer_match(context: &mut ParseContext) -> Result<BuiltinSelectionFn, ParseError> {
- context.advance(1, true)?;
+ let result_trans = parse_modifier_pair(
+ context,
+ |token| match token {
+ Token::Complement => Some(ResultTransform::Complement),
+ Token::Conditional => Some(ResultTransform::Conditional),
+ _ => None,
+ },
+ |m1, m2| match (m1, m2) {
+ (ResultTransform::Complement, ResultTransform::Conditional) => {
+ ResultTransform::ComplementConditional
+ }
+ (ResultTransform::Conditional, ResultTransform::Complement) => {
+ ResultTransform::ConditionalComplement
+ }
+ _ => unreachable!(),
+ },
+ )?;
+
+ let search_mod = parse_modifier_pair(
+ context,
+ |token| match token {
+ Token::Sequential => Some(SearchModifier::Sequential),
+ Token::Reverse => Some(SearchModifier::Reverse),
+ _ => None,
+ },
+ |m1, m2| match (m1, m2) {
+ (SearchModifier::Sequential, SearchModifier::Reverse) => {
+ SearchModifier::SequentialReverse
+ }
+ (SearchModifier::Reverse, SearchModifier::Sequential) => {
+ SearchModifier::ReverseSequential
+ }
+ _ => unreachable!(),
+ },
+ )?;
+
let token = context.current()?;
- context.advance(1, false)?;
- match token {
- Token::Regex(pattern) => Ok(BuiltinSelectionFn::BufferMatch(pattern)),
- _ => Err(ParseError::UnexpectedToken {
- found: token,
- expecting: "regex",
- }),
- }
+ let func = match token {
+ Token::Empty => {
+ context.advance(1, false)?;
+ BuiltinSelectionFn::Empty
+ }
+ Token::EndOfBuffer => {
+ context.advance(1, false)?;
+ BuiltinSelectionFn::EndOfBuffer
+ }
+ Token::Span => {
+ context.advance(1, false)?;
+ BuiltinSelectionFn::Span
+ }
+ Token::CharacterOffset => parse_prefixed_builtin(context, "number", |t| match t {
+ Token::Number(n) => Some(BuiltinSelectionFn::CharOffset(n)),
+ _ => None,
+ })?,
+ Token::Number(n) => {
+ context.advance(1, false)?;
+ BuiltinSelectionFn::Line(n)
+ }
+ Token::Regex(pattern) => {
+ context.advance(1, false)?;
+ BuiltinSelectionFn::Regex(pattern)
+ }
+ Token::AllMatches => parse_prefixed_builtin(context, "regex", |t| match t {
+ Token::Regex(pattern) => Some(BuiltinSelectionFn::AllMatches(pattern)),
+ _ => None,
+ })?,
+ Token::LineOffset => parse_prefixed_builtin(context, "number", |t| match t {
+ Token::Number(n) => Some(BuiltinSelectionFn::RelativeLine(n)),
+ _ => None,
+ })?,
+ Token::BufferMatch => parse_prefixed_builtin(context, "regex", |t| match t {
+ Token::Regex(pattern) => Some(BuiltinSelectionFn::BufferMatch(pattern)),
+ _ => None,
+ })?,
+ _ => {
+ return Err(ParseError::UnexpectedToken {
+ found: token,
+ expecting: "selection function",
+ });
+ }
+ };
+ return Ok(Composite::SelectionFunction {
+ func: func,
+ search_mod: search_mod,
+ result_transform: result_trans,
+ });
}
#[cfg(test)]
@@ -494,4 +590,288 @@ mod tests {
_ => panic!(),
}
}
+
+ #[test]
+ fn test_parse_train_selection_and_text() {
+ let input = r#""replacement" @c /pattern/"#;
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Train3 { f, g, h } => {
+ match *f {
+ Composite::TextFunction {
+ func: BuiltinTextFn::Literal(ref s),
+ swapped: false,
+ } => assert_eq!(s, "replacement"),
+ _ => panic!(),
+ }
+
+ assert!(matches!(
+ *g,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Change,
+ swapped: true,
+ }
+ ));
+
+ match *h {
+ Composite::SelectionFunction {
+ func: BuiltinSelectionFn::Regex(ref pattern),
+ search_mod: None,
+ result_transform: None,
+ } => assert_eq!(pattern, "pattern"),
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_train_with_pipe() {
+ let input = r#"| "date""#;
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ match result {
+ Composite::Train2 { f, g } => {
+ assert!(matches!(
+ *f,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Pipe,
+ swapped: false,
+ }
+ ));
+ match *g {
+ Composite::TextFunction {
+ func: BuiltinTextFn::Literal(ref s),
+ swapped: false,
+ } => assert_eq!(s, "date"),
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_before_hook() {
+ let input = "e>d";
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Hook { kind, left, right } => {
+ assert_eq!(kind, HookKind::Before);
+ assert!(matches!(
+ *left,
+ Composite::SelectionFunction {
+ func: BuiltinSelectionFn::Empty,
+ search_mod: None,
+ result_transform: None,
+ }
+ ));
+ assert!(matches!(
+ *right,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Delete,
+ swapped: false,
+ }
+ ));
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_nested_hooks_left_associative() {
+ let input = r#"c<(|<"sort")"#;
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Hook {
+ kind: outer_kind,
+ left: outer_left,
+ right: outer_right,
+ } => {
+ assert_eq!(outer_kind, HookKind::After);
+ assert!(matches!(
+ *outer_left,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Change,
+ swapped: false,
+ }
+ ));
+ match *outer_right {
+ Composite::Hook {
+ kind: inner_kind,
+ left: inner_left,
+ right: inner_right,
+ } => {
+ assert_eq!(inner_kind, HookKind::After);
+ assert!(matches!(
+ *inner_left,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Pipe,
+ swapped: false,
+ }
+ ));
+ match *inner_right {
+ Composite::TextFunction {
+ func: BuiltinTextFn::Literal(ref s),
+ swapped: false,
+ } => assert_eq!(s, "sort"),
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_hooks_without_parens() {
+ let input = r#"c<|<"sort""#;
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Hook {
+ kind: outer_kind,
+ left: outer_left,
+ right: outer_right,
+ } => {
+ assert_eq!(outer_kind, HookKind::After);
+ match *outer_left {
+ Composite::Hook {
+ kind: inner_kind,
+ left: inner_left,
+ right: inner_right,
+ } => {
+ assert_eq!(inner_kind, HookKind::After);
+ assert!(matches!(
+ *inner_left,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Change,
+ swapped: false,
+ }
+ ));
+ assert!(matches!(
+ *inner_right,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Pipe,
+ swapped: false,
+ }
+ ));
+ }
+ _ => panic!(),
+ }
+ match *outer_right {
+ Composite::TextFunction {
+ func: BuiltinTextFn::Literal(ref s),
+ swapped: false,
+ } => assert_eq!(s, "sort"),
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_triple_nested_hooks() {
+ let input = "e>d<c>i";
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Hook {
+ kind: k1,
+ left: l1,
+ right: r1,
+ } => {
+ assert_eq!(k1, HookKind::Before);
+ assert!(matches!(
+ *r1,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Insert,
+ swapped: false,
+ }
+ ));
+ match *l1 {
+ Composite::Hook {
+ kind: k2,
+ left: l2,
+ right: r2,
+ } => {
+ assert_eq!(k2, HookKind::After);
+ assert!(matches!(
+ *r2,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Change,
+ swapped: false,
+ }
+ ));
+ match *l2 {
+ Composite::Hook {
+ kind: k3,
+ left: l3,
+ right: r3,
+ } => {
+ assert_eq!(k3, HookKind::Before);
+ assert!(matches!(
+ *l3,
+ Composite::SelectionFunction {
+ func: BuiltinSelectionFn::Empty,
+ search_mod: None,
+ result_transform: None,
+ }
+ ));
+ assert!(matches!(
+ *r3,
+ Composite::TextFunction {
+ func: BuiltinTextFn::Delete,
+ swapped: false,
+ }
+ ));
+ }
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ #[test]
+ fn test_parse_hook_in_train() {
+ let input = "e>d $";
+ let tokens = tokenise(input).unwrap();
+ let result = parse(tokens).unwrap();
+ println!("{:?}", result);
+ match result {
+ Composite::Train2 { f, g } => {
+ match *f {
+ Composite::Hook { kind, .. } => {
+ assert_eq!(kind, HookKind::Before);
+ }
+ _ => panic!(),
+ }
+ assert!(matches!(
+ *g,
+ Composite::SelectionFunction {
+ func: BuiltinSelectionFn::EndOfBuffer,
+ search_mod: None,
+ result_transform: None,
+ }
+ ));
+ }
+ _ => panic!(),
+ }
+ }
}