aboutsummaryrefslogtreecommitdiff
path: root/rprt-engine
diff options
context:
space:
mode:
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:?}"),
+ }
+}