aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortslil <tslil@posteo.de>2025-10-17 20:35:53 +0100
committertslil <tslil@posteo.de>2025-10-17 23:48:42 +0100
commita775b8f56e5422e47c40927328baf3045c500a32 (patch)
treefc003854fb4bc7e3b0cb815034540536a5fd286e
parent1376a23bda5e6fe94148cb57ca1447907c6bb21e (diff)
Starting on RIIR, first steps have been whelming
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock61
-rw-r--r--Cargo.toml12
-rw-r--r--python/src/types/selection.py24
-rw-r--r--rprt-engine/Cargo.toml11
-rw-r--r--rprt-engine/src/buffer.rs51
-rw-r--r--rprt-engine/src/lib.rs7
-rw-r--r--rprt-engine/src/selection.rs288
8 files changed, 440 insertions, 16 deletions
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<Interval>,
+ },
+ MultiRanges {
+ multi_ranges: HashMap<BufferID, Vec<Interval>>,
+ },
+}
+
+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::<Vec<_>>();
+ 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<Self, SelectionError> {
+ 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<BufferID> {
+ 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<Self>) -> Result<Self, SelectionError> {
+ if selections.is_empty() {
+ return Ok(Self::empty());
+ }
+
+ let all_buffers: HashSet<BufferID> = 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<Vec<Self>, 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<BufferID, Vec<Interval>> =
+ 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<E>(
+ &self,
+ fn_rank_zero: &impl Fn(&BufferID, &usize) -> Result<Self, E>,
+ fn_rank_one: &impl Fn(&BufferID, &Interval) -> Result<Self, E>,
+ ) -> Result<Self, Either<E, SelectionError>> {
+ let do_rank_one = |b: &usize, rs: &Vec<Interval>| {
+ rs.iter()
+ .map(|int| fn_rank_one(b, int).map_err(Either::Left))
+ .collect::<Result<_, _>>()
+ };
+
+ 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<Vec<Self>> = multi_ranges
+ .iter()
+ .map(|(buffer, ranges)| do_rank_one(buffer, 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(Either::Right)
+ }
+ }
+ }
+}