aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2026-08-24 20:39:57 +0100
committertslil <tslil@posteo.de>2026-08-24 21:39:25 +0100
commitf3acb5cc0faf739f09112607bb98b6594184bd8b (patch)
tree9f60aeda21a775f9d34ed8a70fffd9be638f27ce /rprt-engine
parent72a5e0fc968e2bea9f7d348c7189ebc15a5b15d3 (diff)
The big transposition-ing: move from functions as data to data of functionsHEADmain
Basically Rust will fight you all the way if you treat it like Haskell, instead work with traits and impls on ZSTs.
Diffstat (limited to 'rprt-engine')
-rw-r--r--rprt-engine/src/evaluate.rs10
-rw-r--r--rprt-engine/src/selection.rs138
-rw-r--r--rprt-engine/src/selection_functions/function_character.rs298
-rw-r--r--rprt-engine/src/selection_functions/function_empty.rs175
-rw-r--r--rprt-engine/src/selection_functions/function_end.rs204
-rw-r--r--rprt-engine/src/selection_functions/mod.rs31
-rw-r--r--rprt-engine/src/selection_functions/result_transformation.rs47
-rw-r--r--rprt-engine/src/selection_functions/types.rs237
-rw-r--r--rprt-engine/tests/selection_functions.rs245
9 files changed, 1086 insertions, 299 deletions
diff --git a/rprt-engine/src/evaluate.rs b/rprt-engine/src/evaluate.rs
index 4f567fb..eeb1415 100644
--- a/rprt-engine/src/evaluate.rs
+++ b/rprt-engine/src/evaluate.rs
@@ -1,7 +1,7 @@
use crate::{
expression::{Composite, HookKind},
- selection::{Selection, SelectionError, VectoriseError},
- selection_functions::{evaluate_selection_function, SFError},
+ selection::Selection,
+ selection_functions::{SelectionFunctionError, evaluate_selection_function},
state::{EditorState, GroupedChangeError, StateChange, StateResult},
text::Text,
};
@@ -11,9 +11,7 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum EvaluationError {
#[error("{0}")]
- SelectionFunctionError(VectoriseError<SFError>),
- #[error("{0}")]
- SelectionUnionError(SelectionError),
+ SelectionFunctionError(SelectionFunctionError),
#[error("Error in processing group {0}")]
GroupError(GroupedChangeError),
#[error("Invalid right-only application")]
@@ -152,7 +150,7 @@ fn _eval(
}
.map_err(EvaluationError::GroupError)?;
- let sel = Selection::union(selections).map_err(EvaluationError::SelectionUnionError)?;
+ let sel = Selection::union(selections);
Ok((sel, states))
}
}
diff --git a/rprt-engine/src/selection.rs b/rprt-engine/src/selection.rs
index a3259b8..45a895b 100644
--- a/rprt-engine/src/selection.rs
+++ b/rprt-engine/src/selection.rs
@@ -1,4 +1,5 @@
use crate::buffer::BufferID;
+use crate::state::EditorState;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Display;
@@ -69,26 +70,55 @@ pub enum Selection {
},
}
-// rust is ... sigh. I have to make a wrapper class and worse still _other
-// people_ have to deal with my wrapper class!
-#[derive(Error, Debug)]
-pub enum VectoriseError<E>
-where
- E: std::error::Error + 'static,
-{
- #[error("An error occurred during processing: {0}")]
- ProcessingError(E),
-
- #[error("A selection error occurred: {0}")]
- SelectionError(SelectionError),
-}
+/// A type that knows how to act on the rank-0 (scalar) and rank-1 (interval)
+/// elements of a [`Selection`], and so can be vectorised over one.
+pub trait Vectorisable<P, E> {
+ /// Action on a rank-0 element.
+ fn rank0(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, E>;
-impl<E> From<E> for VectoriseError<E>
-where
- E: std::error::Error + 'static,
-{
- fn from(err: E) -> Self {
- VectoriseError::ProcessingError(err)
+ /// Action on a rank-1 element.
+ fn rank1(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, E>;
+
+ /// Vectorise `sel`: apply `rank0`/`rank1` to every element and union
+ /// the results.
+ fn vectorise(&self, param: &P, es: &EditorState, sel: &Selection) -> Result<Selection, E> {
+ // Single rank-0/rank-1 elements pass through as-is (no rank
+ // promotion); multi-element selections are unioned.
+ match sel {
+ Selection::Scalar { buffer_id, pos } => Self::rank0(self, param, es, *buffer_id, *pos),
+ Selection::Vector {
+ buffer_id,
+ interval,
+ } => Self::rank1(self, param, es, *buffer_id, interval),
+ Selection::Vectors { buffer_id, ranges } => {
+ let mut selections = Vec::with_capacity(ranges.len());
+ for interval in ranges {
+ selections.push(Self::rank1(self, param, es, *buffer_id, interval)?);
+ }
+ Ok(Selection::union(selections))
+ }
+ Selection::MultiVectors { multi_ranges } => {
+ let mut selections = Vec::new();
+ for (buffer_id, ranges) in multi_ranges {
+ for interval in ranges {
+ selections.push(Self::rank1(self, param, es, *buffer_id, interval)?);
+ }
+ }
+ Ok(Selection::union(selections))
+ }
+ }
}
}
@@ -210,15 +240,20 @@ impl Selection {
}
}
- pub fn union(selections: Vec<Self>) -> Result<Self, SelectionError> {
+ /// Union the given selections into a single selection.
+ ///
+ /// The target rank is the maximum rank needed to hold all inputs (rank 3
+ /// if they span multiple buffers), to which every input can be promoted,
+ /// so this operation cannot fail.
+ pub fn union(selections: Vec<Self>) -> Self {
if selections.is_empty() {
- return Ok(Self::empty());
+ return Self::empty();
}
let all_buffers: HashSet<BufferID> = selections.iter().flat_map(|s| s.buffers()).collect();
if all_buffers.is_empty() {
- return Ok(Self::empty());
+ return Self::empty();
}
let max_rank = if all_buffers.len() > 1 {
@@ -232,9 +267,13 @@ impl Selection {
.max(Rank::Two) // At least rank 2
};
- let promoted: Result<Vec<Self>, SelectionError> =
- selections.iter().map(|s| s.promote(max_rank)).collect();
- let promoted = promoted?;
+ let promoted: Vec<Self> = selections
+ .iter()
+ .map(|s| {
+ s.promote(max_rank)
+ .expect("union promotes to the max rank present, which cannot fail")
+ })
+ .collect();
match max_rank {
Rank::Two => {
@@ -248,10 +287,10 @@ impl Selection {
}
}
- Ok(Self::Vectors {
+ Self::Vectors {
ranges: all_intervals,
buffer_id,
- })
+ }
}
Rank::Three => {
let mut buffer_intervals: HashMap<BufferID, Vec<Interval>> =
@@ -273,52 +312,11 @@ impl Selection {
}
}
- Ok(Self::MultiVectors {
+ Self::MultiVectors {
multi_ranges: buffer_intervals,
- })
+ }
}
_ => unreachable!(),
}
}
-
- pub fn vectorise<S, E>(
- &self,
- state: &S,
- fn_rank_zero: impl Fn(&S, BufferID, usize) -> Result<Selection, E> + 'static,
- fn_rank_one: impl Fn(&S, BufferID, &Interval) -> Result<Selection, E> + 'static,
- ) -> Result<Selection, VectoriseError<E>>
- where
- E: std::error::Error + 'static,
- {
- // look at this mess!
- use VectoriseError::{ProcessingError, SelectionError};
- let do_rank_one = |b: usize, rs: &Vec<Interval>| {
- rs.iter()
- .map(|int| fn_rank_one(state, b, int).map_err(ProcessingError))
- .collect::<Result<_, _>>()
- };
-
- match self {
- &Self::Scalar { buffer_id, pos } => {
- fn_rank_zero(state, buffer_id, pos).map_err(ProcessingError)
- }
- Self::Vector {
- interval,
- buffer_id,
- } => fn_rank_one(state, *buffer_id, interval).map_err(VectoriseError::ProcessingError),
- Self::Vectors { buffer_id, ranges } => {
- let results = do_rank_one(*buffer_id, ranges)?;
- Self::union(results).map_err(SelectionError)
- }
- Self::MultiVectors { multi_ranges } => {
- let all_ok: Vec<Vec<Self>> = multi_ranges
- .iter()
- .map(|(buffer_id, ranges)| do_rank_one(*buffer_id, ranges))
- .collect::<Result<_, _>>()?;
- // It would seem that Rust has no built in monadic flatten, or in general cannot lift things to operate on Result... :(
- let results: Vec<Self> = all_ok.into_iter().flatten().collect();
- Self::union(results).map_err(SelectionError)
- }
- }
- }
}
diff --git a/rprt-engine/src/selection_functions/function_character.rs b/rprt-engine/src/selection_functions/function_character.rs
index b4bbf0f..4b41297 100644
--- a/rprt-engine/src/selection_functions/function_character.rs
+++ b/rprt-engine/src/selection_functions/function_character.rs
@@ -1,91 +1,249 @@
use crate::{
- dispatch_selection_function,
- selection::Selection,
- selection_function,
- selection_functions::types::{SFArguments, SFError, SFResult, get_buffer},
+ buffer::BufferID,
+ expression::SearchModifier,
+ selection::{Interval, Selection},
+ selection_functions::types::{SelectionFunction, SelectionFunctionError, get_buffer},
state::EditorState,
};
-selection_function! {
- name: character,
- param_type: usize,
- niladic: |(param, es): (usize, &EditorState)| {
+pub struct Character;
+
+impl SelectionFunction<usize> for Character {
+ const NAME: &'static str = "character";
+
+ fn niladic(
+ &self,
+ param: &usize,
+ es: &EditorState,
+ ) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, es.current_buffer_id)?;
Ok(Selection::Scalar {
buffer_id: es.current_buffer_id,
- pos: usize::min(param, buf.max_pos())
+ pos: usize::min(*param, buf.max_pos()),
+ })
+ }
- }) },
- monadic_rank0: |_, buffer_id, pos| Ok(Selection::Scalar {buffer_id, pos}),
- monadic_rank1: |(param, _), buffer_id, int| Ok(
- Selection::Scalar {buffer_id, pos: usize::min(int.end, *param)}
- )
+ fn monadic_rank0(
+ &self,
+ _param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar { buffer_id, pos })
+ }
+
+ fn monadic_rank1(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: usize::min(interval.start + *param, interval.end),
+ })
+ }
}
-selection_function!(
- name: sequential_character,
- param_type: usize,
- niladic: |_| Err(SFError::NoNiladicForm(";n").into()),
- monadic_rank0: |(param, es), buffer_id, pos| {
+pub struct SequentialCharacter;
+
+impl SelectionFunction<usize> for SequentialCharacter {
+ const NAME: &'static str = "sequential_character";
+
+ fn niladic(
+ &self,
+ _param: &usize,
+ _es: &EditorState,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";n"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ param: &usize,
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, buffer_id)?;
- Ok(Selection::Scalar {buffer_id, pos: usize::min(pos+*param, buf.max_pos())})
- },
- monadic_rank1: |(param, es), buffer_id, int| {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: usize::min(pos + *param, buf.max_pos()),
+ })
+ }
+
+ fn monadic_rank1(
+ &self,
+ param: &usize,
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, buffer_id)?;
- Ok(Selection::Scalar {buffer_id, pos: usize::min(int.end+*param, buf.max_pos())})
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: usize::min(interval.end + *param, buf.max_pos()),
+ })
}
-);
+}
+
+pub struct ReverseCharacter;
+
+impl SelectionFunction<usize> for ReverseCharacter {
+ const NAME: &'static str = "reverse_character";
-selection_function!(
- name: reverse_character,
- param_type: usize,
- niladic: |(param, es): (usize, &EditorState)| {
+ fn niladic(
+ &self,
+ param: &usize,
+ es: &EditorState,
+ ) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, es.current_buffer_id)?;
Ok(Selection::Scalar {
buffer_id: es.current_buffer_id,
- pos: if buf.max_pos() < param {
+ pos: if buf.max_pos() < *param {
+ 0
+ } else {
+ buf.max_pos() - *param
+ },
+ })
+ }
+
+ fn monadic_rank0(
+ &self,
+ _param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar { buffer_id, pos })
+ }
+
+ fn monadic_rank1(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: if *param < interval.end {
+ interval.end - *param
+ } else {
+ 0
+ },
+ })
+ }
+}
+
+pub struct SequentialReverseCharacter;
+
+impl SelectionFunction<usize> for SequentialReverseCharacter {
+ const NAME: &'static str = "sequential_reverse_character";
+
+ fn niladic(
+ &self,
+ _param: &usize,
+ _es: &EditorState,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";'n"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: if *param < pos { pos - *param } else { 0 },
+ })
+ }
+
+ fn monadic_rank1(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: if *param < interval.end {
+ interval.end - *param
+ } else {
0
+ },
+ })
+ }
+}
+
+pub struct ReverseSequentialCharacter;
+
+impl SelectionFunction<usize> for ReverseSequentialCharacter {
+ const NAME: &'static str = "reverse_sequential_character";
+
+ fn niladic(
+ &self,
+ _param: &usize,
+ _es: &EditorState,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm("';n"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: if *param < pos { pos - *param } else { 0 },
+ })
+ }
+
+ fn monadic_rank1(
+ &self,
+ param: &usize,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: if *param < interval.start {
+ interval.start - *param
} else {
- buf.max_pos() - param
+ 0
},
})
- },
- monadic_rank0: |_, buffer_id, pos| Ok(Selection::Scalar {buffer_id, pos}),
- monadic_rank1: |(param, _), buffer_id, int| Ok(
- Selection::Scalar {buffer_id, pos: usize::min(int.end, *param)}
- )
-);
-
-selection_function!(
- name: sequential_reverse_character,
- param_type: usize,
- niladic: |_| Err(SFError::NoNiladicForm(";'n").into()),
- monadic_rank0: |(param, _), buffer_id, pos| {
- Ok(Selection::Scalar {buffer_id, pos: if *param<pos {pos - *param} else {0}})
- },
- monadic_rank1: |(param, _), buffer_id, int| {
- Ok(Selection::Scalar {buffer_id, pos: if *param<int.end {int.end - *param} else {0}})
- }
-);
-
-selection_function!(
- name: reverse_sequential_character,
- param_type: usize,
- niladic: |_| Err(SFError::NoNiladicForm("';n").into()),
- monadic_rank0: |(param, _), buffer_id, pos| {
- Ok(Selection::Scalar {buffer_id, pos: if *param<pos {pos - *param} else {0}})
- },
- monadic_rank1: |(param, _), buffer_id, int| {
- Ok(Selection::Scalar {buffer_id, pos: if *param<int.start {int.start - *param} else {0}})
- }
-);
-
-dispatch_selection_function!(
- name: selection_function_character,
- param_type: usize,
- base: character,
- sequential: sequential_character,
- reverse: reverse_character,
- sequential_reverse: sequential_reverse_character,
- reverse_sequential: reverse_sequential_character,
-);
+ }
+}
+
+/// Dispatch on the search modifier and apply the matching character-family
+/// function to the supplied selections.
+pub fn selection_function_character(
+ offset: usize,
+ es: &EditorState,
+ left: Option<&Selection>,
+ right: Option<&Selection>,
+ search_mod: Option<SearchModifier>,
+) -> Result<Selection, SelectionFunctionError> {
+ match search_mod {
+ None => Character.apply(&offset, es, left, right),
+ Some(SearchModifier::Sequential) => SequentialCharacter.apply(&offset, es, left, right),
+ Some(SearchModifier::Reverse) => ReverseCharacter.apply(&offset, es, left, right),
+ Some(SearchModifier::SequentialReverse) => {
+ SequentialReverseCharacter.apply(&offset, es, left, right)
+ }
+ Some(SearchModifier::ReverseSequential) => {
+ ReverseSequentialCharacter.apply(&offset, es, left, right)
+ }
+ }
+}
diff --git a/rprt-engine/src/selection_functions/function_empty.rs b/rprt-engine/src/selection_functions/function_empty.rs
index bf0a45c..4b69dcf 100644
--- a/rprt-engine/src/selection_functions/function_empty.rs
+++ b/rprt-engine/src/selection_functions/function_empty.rs
@@ -1,12 +1,175 @@
use crate::{
- selection::Selection,
- selection_functions::types::{SFArguments, SFResult},
+ buffer::BufferID,
+ expression::SearchModifier,
+ selection::{Interval, Selection},
+ selection_functions::types::{
+ SelectionFunction, SelectionFunctionError, UnparameterisedSelectionFunction,
+ },
+ state::EditorState,
};
-pub fn empty_sequential(_: SFArguments) -> SFResult {
- Ok(Selection::empty())
+pub struct Empty;
+
+impl UnparameterisedSelectionFunction for Empty {
+ const NAME: &'static str = "empty";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::empty())
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ _buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::empty())
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ _buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::empty())
+ }
+}
+
+pub struct ReverseEmpty;
+
+impl UnparameterisedSelectionFunction for ReverseEmpty {
+ const NAME: &'static str = "reverse_empty";
+
+ fn niladic(&self, es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(es.current_buffer_id))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+}
+
+pub struct SequentialEmpty;
+
+impl UnparameterisedSelectionFunction for SequentialEmpty {
+ const NAME: &'static str = "sequential_empty";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";e"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+}
+
+pub struct SequentialReverseEmpty;
+
+impl UnparameterisedSelectionFunction for SequentialReverseEmpty {
+ const NAME: &'static str = "sequential_reverse_empty";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";'e"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+}
+
+pub struct ReverseSequentialEmpty;
+
+impl UnparameterisedSelectionFunction for ReverseSequentialEmpty {
+ const NAME: &'static str = "reverse_sequential_empty";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm("';e"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(empty_in_buffer(buffer_id))
+ }
+}
+
+fn empty_in_buffer(buffer_id: BufferID) -> Selection {
+ Selection::Vectors {
+ buffer_id,
+ ranges: Vec::new(),
+ }
}
-pub fn empty_structural(_: SFArguments) -> SFResult {
- Ok(Selection::empty())
+pub fn selection_function_empty(
+ es: &EditorState,
+ left: Option<&Selection>,
+ right: Option<&Selection>,
+ search_mod: Option<SearchModifier>,
+) -> Result<Selection, SelectionFunctionError> {
+ match search_mod {
+ None => Empty.apply(&(), es, left, right),
+ Some(SearchModifier::Sequential) => SequentialEmpty.apply(&(), es, left, right),
+ Some(SearchModifier::Reverse) => ReverseEmpty.apply(&(), es, left, right),
+ Some(SearchModifier::SequentialReverse) => {
+ SequentialReverseEmpty.apply(&(), es, left, right)
+ }
+ Some(SearchModifier::ReverseSequential) => {
+ ReverseSequentialEmpty.apply(&(), es, left, right)
+ }
+ }
}
diff --git a/rprt-engine/src/selection_functions/function_end.rs b/rprt-engine/src/selection_functions/function_end.rs
index 75b4c96..ad161d2 100644
--- a/rprt-engine/src/selection_functions/function_end.rs
+++ b/rprt-engine/src/selection_functions/function_end.rs
@@ -1,57 +1,142 @@
use crate::{
- dispatch_selection_function,
- selection::Selection,
- selection_function,
- selection_functions::types::{SFArguments, SFError, SFResult, get_buffer},
+ buffer::BufferID,
+ expression::SearchModifier,
+ selection::{Interval, Selection},
+ selection_functions::types::{
+ SelectionFunction, SelectionFunctionError, UnparameterisedSelectionFunction, get_buffer,
+ },
state::EditorState,
};
-selection_function! {
- name: end,
- param_type: (),
- niladic: |(_, es): ((), &EditorState)| {
+pub struct End;
+
+impl UnparameterisedSelectionFunction for End {
+ const NAME: &'static str = "end";
+
+ fn niladic(&self, es: &EditorState) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, es.current_buffer_id)?;
Ok(Selection::Scalar {
buffer_id: es.current_buffer_id,
pos: buf.max_pos(),
})
- },
- monadic_rank0: |_, buffer_id, pos | Ok(Selection::Scalar { buffer_id, pos }),
- monadic_rank1: |_, buffer_id, int | Ok(Selection::Scalar { buffer_id, pos: int.end })
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar { buffer_id, pos })
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: interval.end,
+ })
+ }
}
-selection_function! {
- name: reverse_end,
- param_type: (),
- niladic: |(_, es): ((), &EditorState)| {
+pub struct ReverseEnd;
+
+impl UnparameterisedSelectionFunction for ReverseEnd {
+ const NAME: &'static str = "reverse_end";
+
+ fn niladic(&self, es: &EditorState) -> Result<Selection, SelectionFunctionError> {
Ok(Selection::Scalar {
buffer_id: es.current_buffer_id,
pos: 0,
})
- },
- monadic_rank0: |_, buffer_id, pos | Ok(Selection::Scalar { buffer_id, pos }),
- monadic_rank1: |_, buffer_id, int | Ok(Selection::Scalar { buffer_id, pos: int.start })
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar { buffer_id, pos })
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: interval.start,
+ })
+ }
}
-selection_function! {
- name: sequential_end,
- param_type: (),
- niladic: |_| Err(SFError::NoNiladicForm(";$").into()),
- monadic_rank0: |_, buffer_id, pos| Ok(Selection::Scalar { buffer_id, pos }),
- monadic_rank1: |(_, es), buffer_id, _| {
+pub struct SequentialEnd;
+
+impl UnparameterisedSelectionFunction for SequentialEnd {
+ const NAME: &'static str = "sequential_end";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";$"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ es: &EditorState,
+ buffer_id: BufferID,
+ _pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ let buf = get_buffer(es, buffer_id)?;
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: buf.max_pos(),
+ })
+ }
+
+ fn monadic_rank1(
+ &self,
+ es: &EditorState,
+ buffer_id: BufferID,
+ _interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
let buf = get_buffer(es, buffer_id)?;
- Ok(Selection::Scalar { buffer_id, pos: buf.max_pos() })
+ Ok(Selection::Scalar {
+ buffer_id,
+ pos: buf.max_pos(),
+ })
}
}
-selection_function! {
- name: sequential_reverse_end,
- param_type: (),
- niladic: |_| Err(SFError::NoNiladicForm(";'$").into()),
- monadic_rank0: |_, buffer_id, pos| {
+pub struct SequentialReverseEnd;
+
+impl UnparameterisedSelectionFunction for SequentialReverseEnd {
+ const NAME: &'static str = "sequential_reverse_end";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(";'$"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
Ok(Selection::Scalar { buffer_id, pos })
- },
- monadic_rank1: |_, buffer_id, interval| {
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
Ok(Selection::Scalar {
buffer_id,
pos: interval.end,
@@ -59,14 +144,30 @@ selection_function! {
}
}
-selection_function! {
- name: reverse_sequential_end,
- param_type: (),
- niladic: |_| Err(SFError::NoNiladicForm("';$").into()),
- monadic_rank0: |_, buffer_id, pos| {
+pub struct ReverseSequentialEnd;
+
+impl UnparameterisedSelectionFunction for ReverseSequentialEnd {
+ const NAME: &'static str = "reverse_sequential_end";
+
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm("';$"))
+ }
+
+ fn monadic_rank0(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
Ok(Selection::Scalar { buffer_id, pos })
- },
- monadic_rank1: |_, buffer_id, interval| {
+ }
+
+ fn monadic_rank1(
+ &self,
+ _es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
Ok(Selection::Scalar {
buffer_id,
pos: interval.end,
@@ -74,12 +175,19 @@ selection_function! {
}
}
-dispatch_selection_function!(
- name: selection_function_end,
- param_type: (),
- base: end,
- sequential: sequential_end,
- reverse: reverse_end,
- sequential_reverse: sequential_reverse_end,
- reverse_sequential: reverse_sequential_end,
-);
+/// Dispatch on the search modifier and apply the matching end-family
+/// function to the supplied selections.
+pub fn selection_function_end(
+ es: &EditorState,
+ left: Option<&Selection>,
+ right: Option<&Selection>,
+ search_mod: Option<SearchModifier>,
+) -> Result<Selection, SelectionFunctionError> {
+ match search_mod {
+ None => End.apply(&(), es, left, right),
+ Some(SearchModifier::Sequential) => SequentialEnd.apply(&(), es, left, right),
+ Some(SearchModifier::Reverse) => ReverseEnd.apply(&(), es, left, right),
+ Some(SearchModifier::SequentialReverse) => SequentialReverseEnd.apply(&(), es, left, right),
+ Some(SearchModifier::ReverseSequential) => ReverseSequentialEnd.apply(&(), es, left, right),
+ }
+}
diff --git a/rprt-engine/src/selection_functions/mod.rs b/rprt-engine/src/selection_functions/mod.rs
index b753200..85c333e 100644
--- a/rprt-engine/src/selection_functions/mod.rs
+++ b/rprt-engine/src/selection_functions/mod.rs
@@ -3,9 +3,9 @@ use crate::{
selection::Selection,
selection_functions::{
function_character::selection_function_character,
+ function_empty::selection_function_empty,
function_end::selection_function_end,
result_transformation::{complement_transform, conditional_transform},
- types::SFArguments,
},
state::EditorState,
};
@@ -16,7 +16,9 @@ mod function_end;
mod result_transformation;
mod types;
-pub use crate::selection_functions::types::{SFError, SFResult};
+pub use crate::selection_functions::types::{
+ SelectionFunction, SelectionFunctionError, UnparameterisedSelectionFunction,
+};
pub fn evaluate_selection_function(
es: &EditorState,
@@ -25,31 +27,30 @@ pub fn evaluate_selection_function(
result_transform: Option<ResultTransform>,
left: Option<Selection>,
right: Option<Selection>,
-) -> SFResult {
- let sf_args = match (left, right) {
- (None, None) => Ok(SFArguments::Niladic { es }),
- (Some(left), None) => Ok(SFArguments::Monadic { es, left }),
- (Some(left), Some(right)) => Ok(SFArguments::Dyadic { es, left, right }),
- _ => Err(SFError::NoLeftArgument),
- }?;
+) -> Result<Selection, SelectionFunctionError> {
let res = match sf {
BuiltinSelectionFn::CharOffset(off) => {
- selection_function_character(off, &sf_args, search_mod)
+ selection_function_character(off, es, left.as_ref(), right.as_ref(), search_mod)
+ }
+ BuiltinSelectionFn::Empty => {
+ selection_function_empty(es, left.as_ref(), right.as_ref(), search_mod)
+ }
+ BuiltinSelectionFn::EndOfBuffer => {
+ selection_function_end(es, left.as_ref(), right.as_ref(), search_mod)
}
- BuiltinSelectionFn::EndOfBuffer => selection_function_end((), &sf_args, search_mod),
- x => Err(SFError::NotImplemented(x).into()),
+ x => Err(SelectionFunctionError::NotImplemented(x)),
}?;
if let Some(result_transform) = result_transform {
match result_transform {
ResultTransform::Complement => complement_transform(es, res),
- ResultTransform::Conditional => conditional_transform(res, &sf_args),
+ ResultTransform::Conditional => conditional_transform(res, left.as_ref()),
ResultTransform::ConditionalComplement => {
let res = complement_transform(es, res)?;
- conditional_transform(res, &sf_args)
+ conditional_transform(res, left.as_ref())
}
ResultTransform::ComplementConditional => {
- let res = conditional_transform(res, &sf_args)?;
+ let res = conditional_transform(res, left.as_ref())?;
complement_transform(es, res)
}
}
diff --git a/rprt-engine/src/selection_functions/result_transformation.rs b/rprt-engine/src/selection_functions/result_transformation.rs
index c150916..b437afd 100644
--- a/rprt-engine/src/selection_functions/result_transformation.rs
+++ b/rprt-engine/src/selection_functions/result_transformation.rs
@@ -1,16 +1,20 @@
use crate::{
buffer::BufferID,
- selection::{Interval, Selection},
- selection_functions::types::{SFArguments, SFError, SFResult},
+ selection::{Interval, Selection, Vectorisable},
+ selection_functions::types::SelectionFunctionError,
state::EditorState,
};
-pub fn complement_transform(es: &EditorState, result: Selection) -> SFResult {
- fn complement_rank0(
+struct Complement;
+
+impl Vectorisable<(), SelectionFunctionError> for Complement {
+ fn rank0(
+ &self,
+ _param: &(),
es: &EditorState,
buffer_id: BufferID,
pos: usize,
- ) -> Result<Selection, SFError> {
+ ) -> Result<Selection, SelectionFunctionError> {
if let Some(buf) = es.get_buffer(buffer_id) {
let mut ranges = Vec::new();
if pos > 0 {
@@ -22,15 +26,17 @@ pub fn complement_transform(es: &EditorState, result: Selection) -> SFResult {
}
Ok(Selection::Vectors { buffer_id, ranges })
} else {
- Err(SFError::BufferNotFound(buffer_id))
+ Err(SelectionFunctionError::BufferNotFound(buffer_id))
}
}
- fn complement_rank1(
+ fn rank1(
+ &self,
+ _param: &(),
es: &EditorState,
buffer_id: BufferID,
interval: &Interval,
- ) -> Result<Selection, SFError> {
+ ) -> Result<Selection, SelectionFunctionError> {
if let Some(buf) = es.get_buffer(buffer_id) {
let max_pos = buf.max_pos();
if interval.start == 0 && interval.end >= max_pos {
@@ -50,25 +56,24 @@ pub fn complement_transform(es: &EditorState, result: Selection) -> SFResult {
}
Ok(Selection::Vectors { buffer_id, ranges })
} else {
- Err(SFError::BufferNotFound(buffer_id))
+ Err(SelectionFunctionError::BufferNotFound(buffer_id))
}
}
+}
- result.vectorise(es, complement_rank0, complement_rank1)
+pub fn complement_transform(
+ es: &EditorState,
+ result: Selection,
+) -> Result<Selection, SelectionFunctionError> {
+ Complement.vectorise(&(), es, &result)
}
-pub fn conditional_transform(mut result: Selection, arg: &SFArguments) -> SFResult {
+pub fn conditional_transform(
+ mut result: Selection,
+ left: Option<&Selection>,
+) -> Result<Selection, SelectionFunctionError> {
if !result.is_empty() {
- Ok(match arg {
- // TODO: think about this niladic case?
- SFArguments::Niladic { .. } => Selection::empty(),
- SFArguments::Monadic { es: _, left } => left.clone(),
- SFArguments::Dyadic {
- es: _,
- left,
- right: _,
- } => left.clone(),
- })
+ Ok(left.cloned().unwrap_or_else(Selection::empty))
} else {
Ok(result)
}
diff --git a/rprt-engine/src/selection_functions/types.rs b/rprt-engine/src/selection_functions/types.rs
index 6bd94bf..4cd76d3 100644
--- a/rprt-engine/src/selection_functions/types.rs
+++ b/rprt-engine/src/selection_functions/types.rs
@@ -1,13 +1,13 @@
use crate::{
buffer::{Buffer, BufferID},
expression::BuiltinSelectionFn,
- selection::{Selection, VectoriseError},
+ selection::{Interval, Selection, Vectorisable},
state::EditorState,
};
use thiserror::Error;
#[derive(Error, Debug)]
-pub enum SFError {
+pub enum SelectionFunctionError {
#[error("Could not find buffer {0}")]
BufferNotFound(BufferID),
#[error("Function {0} has no dyadic form")]
@@ -20,75 +20,186 @@ pub enum SFError {
NotImplemented(BuiltinSelectionFn),
}
-pub enum SFArguments<'a> {
- Niladic {
- es: &'a EditorState,
- },
- Monadic {
- es: &'a EditorState,
- left: Selection,
- },
- Dyadic {
- es: &'a EditorState,
- left: Selection,
- right: Selection,
- },
-}
+/// A selection function parameterised by `P` (the type of its parameter,
+/// e.g. `usize` for character offsets).
+///
+/// Each concrete function is a zero-sized struct implementing this trait;
+/// the parameter value is passed at call time. The arity of the call
+/// (niladic/monadic/dyadic) is routed by [`SelectionFunction::apply`].
+pub trait SelectionFunction<P>: Vectorisable<P, SelectionFunctionError> {
+ /// Name of the function, used in error messages.
+ const NAME: &'static str;
+
+ /// The monadic form applied to a rank-0 (scalar) element.
+ fn monadic_rank0(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError>;
+
+ /// The monadic form applied to a rank-1 (interval) element.
+ fn monadic_rank1(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError>;
+
+ /// The niladic form.
+ fn niladic(&self, _param: &P, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(Self::NAME))
+ }
+
+ /// The monadic form: vectorise over `left`.
+ fn monadic(
+ &self,
+ param: &P,
+ es: &EditorState,
+ left: &Selection,
+ ) -> Result<Selection, SelectionFunctionError> {
+ self.vectorise(param, es, left)
+ }
+
+ /// The dyadic form.
+ fn dyadic(
+ &self,
+ _param: &P,
+ _es: &EditorState,
+ _left: &Selection,
+ _right: &Selection,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoDyadicForm(Self::NAME))
+ }
-pub type SFResult = Result<Selection, VectoriseError<SFError>>;
-
-#[macro_export]
-macro_rules! selection_function {
- (
- name: $name:ident,
- param_type: $P:ty,
- niladic: $niladic_body:expr,
- monadic_rank0: $monadic_rank0_body:expr,
- monadic_rank1: $monadic_rank1_body:expr
- ) => {
- fn $name(param: $P, arg: &SFArguments) -> SFResult {
- match arg {
- SFArguments::Niladic { es } => ($niladic_body)((param, es)),
- SFArguments::Monadic { es, left } => {
- left.vectorise(&(param, es), $monadic_rank0_body, $monadic_rank1_body)
- }
- SFArguments::Dyadic { .. } => Err(SFError::NoDyadicForm(stringify!($name)).into()),
- }
+ /// Route to the arity form matching the selections supplied.
+ fn apply(
+ &self,
+ param: &P,
+ es: &EditorState,
+ left: Option<&Selection>,
+ right: Option<&Selection>,
+ ) -> Result<Selection, SelectionFunctionError> {
+ match (left, right) {
+ (None, None) => self.niladic(param, es),
+ (Some(left), None) => self.monadic(param, es, left),
+ (Some(left), Some(right)) => self.dyadic(param, es, left, right),
+ (None, Some(_)) => Err(SelectionFunctionError::NoLeftArgument),
}
- };
+ }
}
-#[macro_export]
-macro_rules! dispatch_selection_function {
- (
- name: $name:ident,
- param_type: $P:ty,
- base: $bs:ident,
- sequential: $sq:ident,
- reverse: $rv:ident,
- sequential_reverse: $sr:ident,
- reverse_sequential: $rs:ident,
- ) => {
- pub fn $name(
- param: $P,
- arg: &SFArguments,
- search_mod: Option<crate::expression::SearchModifier>,
- ) -> SFResult {
- match search_mod {
- None => ($bs)(param, arg),
- Some(crate::expression::SearchModifier::Sequential) => ($sq)(param, arg),
- Some(crate::expression::SearchModifier::Reverse) => ($rv)(param, arg),
- Some(crate::expression::SearchModifier::SequentialReverse) => ($sr)(param, arg),
- Some(crate::expression::SearchModifier::ReverseSequential) => ($rs)(param, arg),
- }
- }
- };
+/// Every selection function can vectorise a selection by applying its
+/// monadic rank-0/rank-1 forms to each element.
+impl<P, F: SelectionFunction<P> + ?Sized> Vectorisable<P, SelectionFunctionError> for F {
+ fn rank0(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ self.monadic_rank0(param, es, buffer_id, pos)
+ }
+
+ fn rank1(
+ &self,
+ param: &P,
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ self.monadic_rank1(param, es, buffer_id, interval)
+ }
+}
+
+/// A selection function with no parameter.
+///
+/// Blanket-implements [`SelectionFunction`] with `P = ()`, so
+/// unparameterised functions are callable through the same interface.
+pub trait UnparameterisedSelectionFunction {
+ /// Name of the function, used in error messages.
+ const NAME: &'static str;
+
+ /// The monadic form applied to a rank-0 (scalar) element.
+ fn monadic_rank0(
+ &self,
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError>;
+
+ /// The monadic form applied to a rank-1 (interval) element.
+ fn monadic_rank1(
+ &self,
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError>;
+
+ /// The niladic form.
+ fn niladic(&self, _es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoNiladicForm(Self::NAME))
+ }
+
+ /// The dyadic form.
+ fn dyadic(
+ &self,
+ _es: &EditorState,
+ _left: &Selection,
+ _right: &Selection,
+ ) -> Result<Selection, SelectionFunctionError> {
+ Err(SelectionFunctionError::NoDyadicForm(Self::NAME))
+ }
+}
+
+impl<F: UnparameterisedSelectionFunction + ?Sized> SelectionFunction<()> for F {
+ const NAME: &'static str = <F as UnparameterisedSelectionFunction>::NAME;
+
+ fn monadic_rank0(
+ &self,
+ _param: &(),
+ es: &EditorState,
+ buffer_id: BufferID,
+ pos: usize,
+ ) -> Result<Selection, SelectionFunctionError> {
+ UnparameterisedSelectionFunction::monadic_rank0(self, es, buffer_id, pos)
+ }
+
+ fn monadic_rank1(
+ &self,
+ _param: &(),
+ es: &EditorState,
+ buffer_id: BufferID,
+ interval: &Interval,
+ ) -> Result<Selection, SelectionFunctionError> {
+ UnparameterisedSelectionFunction::monadic_rank1(self, es, buffer_id, interval)
+ }
+
+ fn niladic(&self, _param: &(), es: &EditorState) -> Result<Selection, SelectionFunctionError> {
+ UnparameterisedSelectionFunction::niladic(self, es)
+ }
+
+ fn dyadic(
+ &self,
+ _param: &(),
+ es: &EditorState,
+ left: &Selection,
+ right: &Selection,
+ ) -> Result<Selection, SelectionFunctionError> {
+ UnparameterisedSelectionFunction::dyadic(self, es, left, right)
+ }
}
-pub fn get_buffer(es: &EditorState, buffer_id: BufferID) -> Result<&Buffer, SFError> {
+pub fn get_buffer(
+ es: &EditorState,
+ buffer_id: BufferID,
+) -> Result<&Buffer, SelectionFunctionError> {
if let Some(buf) = es.get_buffer(buffer_id) {
Ok(buf)
} else {
- Err(SFError::BufferNotFound(es.current_buffer_id))
+ Err(SelectionFunctionError::BufferNotFound(es.current_buffer_id))
}
}
diff --git a/rprt-engine/tests/selection_functions.rs b/rprt-engine/tests/selection_functions.rs
new file mode 100644
index 0000000..c326375
--- /dev/null
+++ b/rprt-engine/tests/selection_functions.rs
@@ -0,0 +1,245 @@
+use rprt_engine::{
+ evaluate::evaluate,
+ expression::{BuiltinSelectionFn, ResultTransform, SearchModifier},
+ parser::parse,
+ selection::{Interval, Selection},
+ selection_functions::{SelectionFunctionError, evaluate_selection_function},
+ state::EditorState,
+ token::tokenise,
+};
+
+fn state_with_content(content: &str) -> EditorState {
+ let mut es = EditorState::new();
+ es.create_buffer("test".to_string(), content.to_string(), None);
+ es
+}
+
+/// Parse `input` and evaluate it against a fresh state holding `content`.
+fn eval_str(content: &str, input: &str) -> Result<Selection, String> {
+ let mut es = state_with_content(content);
+ let tokens = tokenise(input).map_err(|e| format!("tokenise: {e:?}"))?;
+ let comp = parse(tokens).map_err(|e| format!("parse: {e:?}"))?;
+ evaluate(&mut es, comp).map_err(|e| format!("evaluate: {e:?}"))
+}
+
+fn assert_scalar(res: Selection, pos: usize) {
+ match res {
+ Selection::Scalar { buffer_id, pos: p } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(p, pos, "expected scalar at {pos}");
+ }
+ other => panic!("expected scalar at {pos}, got {other:?}"),
+ }
+}
+
+#[test]
+fn niladic_character() {
+ assert_scalar(eval_str("hello world", "#5").unwrap(), 5);
+ // clamped to buffer end
+ assert_scalar(eval_str("hello world", "#100").unwrap(), 11);
+}
+
+#[test]
+fn niladic_end_unparameterised() {
+ // These go through UnparameterisedSelectionFunction + the
+ // SelectionFunction<()> blanket impl.
+ assert_scalar(eval_str("hello world", "$").unwrap(), 11);
+ assert_scalar(eval_str("hello world", "'$").unwrap(), 0);
+ assert!(eval_str("hello world", ";$").is_err());
+ assert!(eval_str("hello world", "';$").is_err());
+ assert!(eval_str("hello world", ";'$").is_err());
+}
+
+#[test]
+fn monadic_over_scalar() {
+ // Train: f = #5 (niladic), g = $ applied to f's result (rank-0 vectorise).
+ assert_scalar(eval_str("hello world", "#5 $").unwrap(), 5);
+ // "α ;$" = "End of buffer"
+ assert_scalar(eval_str("hello world", "#5 ;$").unwrap(), 11);
+ assert_scalar(eval_str("hello world", "#5 '$").unwrap(), 5);
+}
+
+#[test]
+fn character_within_interval() {
+ // "α #n" rank 1: "Char n within α", offset from the interval's start,
+ // clamped at its end.
+ let es = state_with_content("hello world");
+ let interval = || Selection::Vector {
+ buffer_id: 0,
+ interval: Interval::new(2, 9),
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(3),
+ None,
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 5); // 2 + 3
+
+ // clamped at the interval's end
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(100),
+ None,
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 9);
+}
+
+#[test]
+fn search_modifiers_character() {
+ // ;#1: min(pos + 1, max)
+ assert_scalar(eval_str("hello world", "#2 ;#1").unwrap(), 3);
+ // ';#1: pos - 1
+ assert_scalar(eval_str("hello world", "#2 ';#1").unwrap(), 1);
+}
+
+#[test]
+fn reverse_character() {
+ // '#3 niladic: "Character 3 from end of buffer"
+ assert_scalar(eval_str("hello world", "'#3").unwrap(), 8);
+
+ // "α '#n" rank 1: "Char n from end of α" --- offset back from the
+ // interval's end boundary, clamped at 0.
+ let es = state_with_content("hello world");
+ let interval = || Selection::Vector {
+ buffer_id: 0,
+ interval: Interval::new(2, 9),
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(3),
+ Some(SearchModifier::Reverse),
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 6); // 9 - 3
+
+ // n larger than the interval's length clamps to 0.
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(10),
+ Some(SearchModifier::Reverse),
+ None,
+ Some(interval()),
+ None,
+ )
+ .unwrap();
+ assert_scalar(res, 0);
+}
+
+#[test]
+fn monadic_over_vectors() {
+ // #100 applied to a rank-2 selection: min(interval.end, 100) per range.
+ let es = state_with_content("hello world");
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(100),
+ None,
+ None,
+ Some(Selection::Vectors {
+ buffer_id: 0,
+ ranges: vec![Interval::new(2, 4), Interval::new(6, 9)],
+ }),
+ None,
+ )
+ .unwrap();
+ match res {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(ranges.len(), 2);
+ assert_eq!((ranges[0].start, ranges[0].end), (4, 5));
+ assert_eq!((ranges[1].start, ranges[1].end), (9, 10));
+ }
+ other => panic!("expected Vectors, got {other:?}"),
+ }
+}
+
+#[test]
+fn complement_transform() {
+ // ~#5 on "hello world": complement of pos 5 is [0..5) and (6..11].
+ let es = state_with_content("hello world");
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::CharOffset(5),
+ None,
+ Some(ResultTransform::Complement),
+ None,
+ None,
+ )
+ .unwrap();
+ match res {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert_eq!(ranges.len(), 2);
+ assert_eq!((ranges[0].start, ranges[0].end), (0, 5));
+ assert_eq!((ranges[1].start, ranges[1].end), (6, 11));
+ }
+ other => panic!("expected Vectors, got {other:?}"),
+ }
+}
+
+#[test]
+fn empty_selection() {
+ // `e` and `α e`: empty selection of rank 3.
+ match eval_str("hello world", "e").unwrap() {
+ Selection::MultiVectors { multi_ranges } => assert!(multi_ranges.is_empty()),
+ other => panic!("expected rank-3 empty, got {other:?}"),
+ }
+ match eval_str("hello world", "#5 e").unwrap() {
+ Selection::MultiVectors { multi_ranges } => assert!(multi_ranges.is_empty()),
+ other => panic!("expected rank-3 empty, got {other:?}"),
+ }
+
+ // `'e` and `;e`: empty selection of rank 2, anchored to the buffer.
+ match eval_str("hello world", "#5 'e").unwrap() {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert!(ranges.is_empty());
+ }
+ other => panic!("expected rank-2 empty, got {other:?}"),
+ }
+ match eval_str("hello world", "#5 ;e").unwrap() {
+ Selection::Vectors { buffer_id, ranges } => {
+ assert_eq!(buffer_id, 0);
+ assert!(ranges.is_empty());
+ }
+ other => panic!("expected rank-2 empty, got {other:?}"),
+ }
+
+ // `;e` and friends have no niladic form.
+ assert!(eval_str("hello world", ";e").is_err());
+ assert!(eval_str("hello world", ";'e").is_err());
+ assert!(eval_str("hello world", "';e").is_err());
+}
+
+#[test]
+fn dyadic_is_rejected() {
+ let es = state_with_content("hello world");
+ let scalar = || Selection::Scalar {
+ buffer_id: 0,
+ pos: 0,
+ };
+ let res = evaluate_selection_function(
+ &es,
+ BuiltinSelectionFn::EndOfBuffer,
+ None,
+ None,
+ Some(scalar()),
+ Some(scalar()),
+ );
+ match res {
+ Err(SelectionFunctionError::NoDyadicForm(name)) => {
+ assert_eq!(name, "end");
+ }
+ other => panic!("expected NoDyadicForm, got {other:?}"),
+ }
+}