From a775b8f56e5422e47c40927328baf3045c500a32 Mon Sep 17 00:00:00 2001 From: tslil Date: Fri, 17 Oct 2025 20:35:53 +0100 Subject: Starting on RIIR, first steps have been whelming --- .gitignore | 2 + Cargo.lock | 61 +++++++++ Cargo.toml | 12 ++ python/src/types/selection.py | 24 ++-- rprt-engine/Cargo.toml | 11 ++ rprt-engine/src/buffer.rs | 51 ++++++++ rprt-engine/src/lib.rs | 7 + rprt-engine/src/selection.rs | 288 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 440 insertions(+), 16 deletions(-) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rprt-engine/Cargo.toml create mode 100644 rprt-engine/src/buffer.rs create mode 100644 rprt-engine/src/lib.rs create mode 100644 rprt-engine/src/selection.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b45c6e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/target/* +**/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..016131a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rprt-engine" +version = "0.1.0" +dependencies = [ + "either", + "regex", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2316588 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +resolver = "2" +members = [ + "rprt-engine", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-or-later" +authors = ["tslil clingman"] + diff --git a/python/src/types/selection.py b/python/src/types/selection.py index 478b5b9..d75c9f0 100644 --- a/python/src/types/selection.py +++ b/python/src/types/selection.py @@ -38,15 +38,13 @@ class Selection: @classmethod def empty(cls) -> "Selection": - return cls(selection=Position(0), buffers=set()) + return cls(selection=Ranges(ranges=[]), buffers=set()) def promote(self, to_rank: int) -> "Selection": if to_rank < 0 or to_rank > 3: raise ValueError(f"Invalid rank: {to_rank}") if self.rank > to_rank: - raise ValueError( - f"Cannot coerce selection of rank {self.rank} to rank {to_rank}" - ) + raise ValueError(f"Cannot coerce selection of rank {self.rank} to rank {to_rank}") if self.rank == to_rank: return self match self.selection: @@ -61,22 +59,16 @@ class Selection: if to_rank == 2: return Selection(selection=as_ranges, buffers=self.buffers) if to_rank == 3: - return Selection( - MultiRanges({buffer: as_ranges for buffer in self.buffers}) - ) + return Selection(MultiRanges({buffer: as_ranges for buffer in self.buffers})) case Range(_, _): as_ranges = Ranges(ranges=[self.selection]) if to_rank == 2: return Selection(selection=as_ranges, buffers=self.buffers) if to_rank == 3: - return Selection( - MultiRanges({buffer: as_ranges for buffer in self.buffers}) - ) + return Selection(MultiRanges({buffer: as_ranges for buffer in self.buffers})) case Ranges(_): - return Selection( - MultiRanges({buffer: self.selection for buffer in self.buffers}) - ) - + return Selection(MultiRanges({buffer: self.selection for buffer in self.buffers})) + raise RuntimeError("Unexpected promotion issue") @classmethod @@ -101,7 +93,7 @@ class Selection: if max_rank == 2: all_ranges = [] for sel in promoted: - if not type(sel.selection) is Ranges: + if type(sel.selection) is not Ranges: raise ValueError( f"Selection of rank {sel.rank} is not a range. Should have been promoted to rank 2." ) @@ -111,7 +103,7 @@ class Selection: if max_rank == 3: buffer_ranges: dict[Buffer, list[Range]] = {buf: [] for buf in all_buffers} for sel in promoted: - if not type(sel.selection) is MultiRanges: + if type(sel.selection) is not MultiRanges: raise ValueError( f"Selection of rank {sel.rank} is not a multi-range. Should have been promoted to rank 3." ) diff --git a/rprt-engine/Cargo.toml b/rprt-engine/Cargo.toml new file mode 100644 index 0000000..96aa5b1 --- /dev/null +++ b/rprt-engine/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rprt-engine" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +either = "1.15.0" +regex = "1.12.2" + diff --git a/rprt-engine/src/buffer.rs b/rprt-engine/src/buffer.rs new file mode 100644 index 0000000..f536054 --- /dev/null +++ b/rprt-engine/src/buffer.rs @@ -0,0 +1,51 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub type BufferID = usize; +pub struct Buffer { + pub content: String, + pub name: String, +} + +impl Buffer { + pub fn new(name: String, content: String) -> Self { + Self { + name, + content, + } + } + + fn generate_shell_name(command: &str) -> String { + let mut hasher = DefaultHasher::new(); + command.hash(&mut hasher); + let command_hash = hasher.finish(); + + let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(x) => x.as_secs(), + Err(_) => 0, + }; + + format!("shell_{:x}_{}", command_hash, timestamp) + } + + pub fn from_shell_command(command: &str) -> Self { + let name = Self::generate_shell_name(command); + + let output = Command::new("sh").arg("-c").arg(command).output(); + + let content = match output { + Ok(output) => match String::from_utf8(output.stdout) { + Ok(s) => s, + Err(e) => format!("UTF-8 decode error: {}", e), + }, + Err(e) => format!("Command failed: {}", e), + }; + + Self { + name, + content, + } + } +} diff --git a/rprt-engine/src/lib.rs b/rprt-engine/src/lib.rs new file mode 100644 index 0000000..416b65f --- /dev/null +++ b/rprt-engine/src/lib.rs @@ -0,0 +1,7 @@ +// RPRT Engine - Core text editing functionality + +pub mod buffer; +pub mod selection; + +pub use buffer::*; +pub use selection::*; diff --git a/rprt-engine/src/selection.rs b/rprt-engine/src/selection.rs new file mode 100644 index 0000000..9494544 --- /dev/null +++ b/rprt-engine/src/selection.rs @@ -0,0 +1,288 @@ +use crate::buffer::BufferID; +use either::Either; +use std::collections::HashMap; +use std::collections::HashSet; +use std::fmt::Display; + +#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)] +pub enum Rank { + Zero, + One, + Two, + Three, +} + +impl Display for Rank { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Rank::Zero => write!(f, "Rank 0"), + Rank::One => write!(f, "Rank 1"), + Rank::Two => write!(f, "Rank 2"), + Rank::Three => write!(f, "Rank 3"), + } + } +} + +pub enum SelectionError { + InvalidPromotion { from: Rank, to: Rank }, +} + +impl std::fmt::Display for SelectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SelectionError::InvalidPromotion { from, to } => { + write!(f, "Cannot change selection of {} to {}", from, to) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct Interval { + pub start: usize, + pub end: usize, + capture_groups: Vec<(usize, usize)>, +} + +impl Interval { + pub fn is_disjoint(&self, other: &Interval) -> bool { + self.start >= other.end || self.end <= other.start + } +} + +#[derive(Debug, Clone)] +pub enum Selection { + Position { + pos: usize, + buffer: BufferID, + }, + Range { + buffer: BufferID, + interval: Interval, + }, + Ranges { + buffer: BufferID, + ranges: Vec, + }, + MultiRanges { + multi_ranges: HashMap>, + }, +} + +impl Selection { + pub fn empty() -> Self { + Self::MultiRanges { + multi_ranges: HashMap::new(), + } + } + + pub fn ranges_disjoint(ranges: &[Interval]) -> bool { + if ranges.len() < 1 { + return true; + } + + let mut sorted_by_start: Vec<&Interval> = ranges.iter().collect::>(); + sorted_by_start.sort_by_key(|i| i.start); + sorted_by_start.windows(2).all(|w| w[0].is_disjoint(&w[1])) + } + + pub fn is_disjoint(&self) -> bool { + match self { + Self::Position { .. } => true, + Self::Range { .. } => true, + Self::Ranges { ranges, .. } => Self::ranges_disjoint(ranges), + Self::MultiRanges { multi_ranges } => { + for (_, intervals) in multi_ranges { + if !Self::ranges_disjoint(intervals) { + return false; + } + } + true + } + } + } + + pub fn rank(&self) -> Rank { + match self { + Self::Position { .. } => Rank::Zero, + Self::Range { .. } => Rank::One, + Self::Ranges { .. } => Rank::Two, + Self::MultiRanges { .. } => Rank::Three, + } + } + + pub fn promote(&self, to_rank: Rank) -> Result { + use Rank::*; + + match (self, to_rank) { + (Self::Position { .. }, Zero) => Ok(self.clone()), + (Self::Range { .. }, One) => Ok(self.clone()), + (Self::Ranges { .. }, Two) => Ok(self.clone()), + (Self::MultiRanges { .. }, Three) => Ok(self.clone()), + + // Position + (Self::Position { pos, buffer }, One) => Ok(Self::Range { + interval: Interval { + start: pos + 0, + end: pos + 1, + capture_groups: Vec::new(), + }, + buffer: *buffer, + }), + (Self::Position { pos, buffer }, Two) => Ok(Self::Ranges { + ranges: vec![Interval { + start: pos + 0, + end: pos + 1, + capture_groups: Vec::new(), + }], + buffer: *buffer, + }), + (Self::Position { pos, buffer }, Three) => { + let mut map = HashMap::new(); + map.insert( + *buffer, + vec![Interval { + start: pos + 0, + end: pos + 1, + capture_groups: Vec::new(), + }], + ); + Ok(Self::MultiRanges { multi_ranges: map }) + } + + // Range promotions + (Self::Range { interval, buffer }, Two) => Ok(Self::Ranges { + ranges: vec![interval.clone()], + buffer: *buffer, + }), + (Self::Range { interval, buffer }, Three) => { + let mut map = HashMap::new(); + map.insert(*buffer, vec![interval.clone()]); + Ok(Self::MultiRanges { multi_ranges: map }) + } + + // Ranges promotion + (Self::Ranges { ranges, buffer }, Three) => { + let mut map = HashMap::new(); + map.insert(*buffer, ranges.clone()); + Ok(Self::MultiRanges { multi_ranges: map }) + } + + // all other cases + (sel, target) => Err(SelectionError::InvalidPromotion { + from: sel.rank(), + to: target, + }), + } + } + + pub fn buffers(&self) -> Vec { + match self { + Self::Position { buffer, .. } => vec![*buffer], + Self::Range { buffer, .. } => vec![*buffer], + Self::Ranges { buffer, .. } => vec![*buffer], + Self::MultiRanges { multi_ranges } => multi_ranges.keys().copied().collect(), + } + } + + pub fn union(selections: Vec) -> Result { + if selections.is_empty() { + return Ok(Self::empty()); + } + + let all_buffers: HashSet = selections.iter().flat_map(|s| s.buffers()).collect(); + + if all_buffers.is_empty() { + return Ok(Self::empty()); + } + + let max_rank = if all_buffers.len() > 1 { + Rank::Three + } else { + selections + .iter() + .map(|s| s.rank()) + .max() + .unwrap_or(Rank::Two) + .max(Rank::Two) // At least rank 2 + }; + + let promoted: Result, SelectionError> = + selections.iter().map(|s| s.promote(max_rank)).collect(); + let promoted = promoted?; + + match max_rank { + Rank::Two => { + let mut all_intervals = Vec::new(); + let buffer = *all_buffers.iter().next().unwrap(); + + for sel in &promoted { + match sel { + Self::Ranges { ranges, .. } => all_intervals.extend(ranges.clone()), + _ => unreachable!("promote() to Rank::Two should always produce Ranges"), + } + } + + Ok(Self::Ranges { + ranges: all_intervals, + buffer, + }) + } + Rank::Three => { + let mut buffer_intervals: HashMap> = + all_buffers.iter().map(|&b| (b, Vec::new())).collect(); + + for sel in &promoted { + match sel { + Self::MultiRanges { multi_ranges } => { + for (&buffer, intervals) in multi_ranges { + buffer_intervals + .entry(buffer) + .or_insert_with(Vec::new) + .extend(intervals.clone()); + } + } + _ => unreachable!( + "promote() to Rank::Three should always produce MultiRanges" + ), + } + } + + Ok(Self::MultiRanges { + multi_ranges: buffer_intervals, + }) + } + _ => unreachable!(), + } + } + + pub fn broadcast( + &self, + fn_rank_zero: &impl Fn(&BufferID, &usize) -> Result, + fn_rank_one: &impl Fn(&BufferID, &Interval) -> Result, + ) -> Result> { + let do_rank_one = |b: &usize, rs: &Vec| { + rs.iter() + .map(|int| fn_rank_one(b, int).map_err(Either::Left)) + .collect::>() + }; + + match self { + Self::Position { buffer, pos } => fn_rank_zero(buffer, pos).map_err(Either::Left), + Self::Range { buffer, interval } => fn_rank_one(buffer, interval).map_err(Either::Left), + Self::Ranges { buffer, ranges } => { + let results = do_rank_one(buffer, ranges)?; + Self::union(results).map_err(Either::Right) + } + Self::MultiRanges { multi_ranges } => { + let all_ok: Vec> = multi_ranges + .iter() + .map(|(buffer, ranges)| do_rank_one(buffer, ranges)) + .collect::>()?; + // It would seem that Rust has no built in monadic flatten, or in general cannot lift things to operate on Result... :( + let results: Vec = all_ok.into_iter().flatten().collect(); + Self::union(results).map_err(Either::Right) + } + } + } +} -- cgit v1.2.3