aboutsummaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/Cargo.toml2
-rw-r--r--helix-core/src/buffer.rs18
-rw-r--r--helix-core/src/lib.rs16
-rw-r--r--helix-core/src/position.rs27
-rw-r--r--helix-core/src/range.rs9
-rw-r--r--helix-core/src/selection.rs222
-rw-r--r--helix-core/src/state.rs38
-rw-r--r--helix-core/src/transaction.rs25
8 files changed, 317 insertions, 40 deletions
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 446e8e42..f1f6264f 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -8,4 +8,6 @@ edition = "2018"
[dependencies]
ropey = "1.1.0"
+anyhow = "1.0.31"
+smallvec = "1.4.0"
# slab = "0.4.2"
diff --git a/helix-core/src/buffer.rs b/helix-core/src/buffer.rs
new file mode 100644
index 00000000..9dd22773
--- /dev/null
+++ b/helix-core/src/buffer.rs
@@ -0,0 +1,18 @@
+use anyhow::Error;
+use ropey::Rope;
+use std::{env, fs::File, io::BufReader, path::PathBuf};
+
+pub struct Buffer {
+ pub contents: Rope,
+}
+
+impl Buffer {
+ pub fn load(path: PathBuf) -> Result<Self, Error> {
+ let current_dir = env::current_dir()?;
+
+ let contents = Rope::from_reader(BufReader::new(File::open(path)?))?;
+
+ // TODO: create if not found
+ Ok(Buffer { contents })
+ }
+}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 71a66030..ceed961f 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -1,5 +1,13 @@
-mod position;
-mod range;
+mod buffer;
+mod selection;
+mod state;
+mod transaction;
-use position::Position;
-use range::Range;
+pub use buffer::Buffer;
+
+pub use selection::Range as SelectionRange;
+pub use selection::Selection;
+
+pub use state::State;
+
+pub use transaction::{Change, Transaction};
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
deleted file mode 100644
index 8c82b83b..00000000
--- a/helix-core/src/position.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-/// Represents a single point in a text buffer. Zero indexed.
-#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Position {
- pub row: usize,
- pub col: usize,
-}
-
-impl Position {
- pub fn new(row: usize, col: usize) -> Self {
- Self { row, col }
- }
-
- pub fn is_zero(self) -> bool {
- self.row == 0 && self.col == 0
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_ordering() {
- // (0, 5) is less than (1, 0 w v f)
- assert!(Position::new(0, 5) < Position::new(1, 0));
- }
-}
diff --git a/helix-core/src/range.rs b/helix-core/src/range.rs
deleted file mode 100644
index 46411664..00000000
--- a/helix-core/src/range.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use crate::Position;
-
-#[derive(Clone, Copy, PartialEq, Eq)]
-pub struct Range {
- pub start: Position,
- pub end: Position,
-}
-
-// range traversal iters
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
new file mode 100644
index 00000000..24a8be46
--- /dev/null
+++ b/helix-core/src/selection.rs
@@ -0,0 +1,222 @@
+//! Selections are the primary editing construct. Even a single cursor is defined as an empty
+//! single selection range.
+//!
+//! All positioning is done via `char` offsets into the buffer.
+use smallvec::{smallvec, SmallVec};
+
+#[inline]
+fn abs_difference(x: usize, y: usize) -> usize {
+ if x < y {
+ y - x
+ } else {
+ x - y
+ }
+}
+
+/// A single selection range. Anchor-inclusive, head-exclusive.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Range {
+ // TODO: optimize into u32
+ /// The anchor of the range: the side that doesn't move when extending.
+ pub anchor: usize,
+ /// The head of the range, moved when extending.
+ pub head: usize,
+}
+
+impl Range {
+ pub fn new(anchor: usize, head: usize) -> Self {
+ Self { anchor, head }
+ }
+
+ /// Start of the range.
+ #[inline]
+ pub fn from(&self) -> usize {
+ std::cmp::min(self.anchor, self.head)
+ }
+
+ /// End of the range.
+ #[inline]
+ pub fn to(&self) -> usize {
+ std::cmp::max(self.anchor, self.head)
+ }
+
+ /// `true` when head and anchor are at the same position.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.anchor == self.head
+ }
+
+ /// Check two ranges for overlap.
+ pub fn overlaps(&self, other: &Self) -> bool {
+ // cursor overlap is checked differently
+ if self.is_empty() {
+ self.from() <= other.to()
+ } else {
+ self.from() < other.to()
+ }
+ }
+
+ // TODO: map
+
+ /// Extend the range to cover at least `from` `to`.
+ pub fn extend(&self, from: usize, to: usize) -> Self {
+ if from <= self.anchor && to >= self.anchor {
+ return Range {
+ anchor: from,
+ head: to,
+ };
+ }
+
+ Range {
+ anchor: self.anchor,
+ head: if abs_difference(from, self.anchor) > abs_difference(to, self.anchor) {
+ from
+ } else {
+ to
+ },
+ }
+ }
+
+ // groupAt
+}
+
+/// A selection consists of one or more selection ranges.
+pub struct Selection {
+ // TODO: decide how many ranges to inline SmallVec<[Range; 1]>
+ ranges: Vec<Range>,
+ primary_index: usize,
+}
+
+impl Selection {
+ // map
+ // eq
+ pub fn primary(&self) -> Range {
+ self.ranges[self.primary_index]
+ }
+
+ /// Ensure selection containing only the primary selection.
+ pub fn as_single(self) -> Self {
+ if self.ranges.len() == 1 {
+ self
+ } else {
+ Self {
+ ranges: vec![self.ranges[self.primary_index]],
+ primary_index: 0,
+ }
+ }
+ }
+
+ // add_range // push
+ // replace_range
+
+ /// Constructs a selection holding a single range.
+ pub fn single(anchor: usize, head: usize) -> Self {
+ Self {
+ ranges: vec![Range { anchor, head }],
+ primary_index: 0,
+ }
+ }
+
+ pub fn new(ranges: Vec<Range>, primary_index: usize) -> Self {
+ fn normalize(mut ranges: Vec<Range>, primary_index: usize) -> Selection {
+ let primary = ranges[primary_index];
+ ranges.sort_unstable_by_key(|range| range.from());
+ let mut primary_index = ranges.iter().position(|&range| range == primary).unwrap();
+
+ let mut result: Vec<Range> = Vec::new();
+
+ // TODO: we could do with one vec by removing elements as we mutate
+
+ for (i, range) in ranges.into_iter().enumerate() {
+ // if previous value exists
+ if let Some(prev) = result.last_mut() {
+ // and we overlap it
+ if range.overlaps(prev) {
+ let from = prev.from();
+ let to = std::cmp::max(range.to(), prev.to());
+
+ if i <= primary_index {
+ primary_index -= 1
+ }
+
+ // merge into previous
+ if range.anchor > range.head {
+ prev.anchor = to;
+ prev.head = from;
+ } else {
+ prev.anchor = from;
+ prev.head = to;
+ }
+ continue;
+ }
+ }
+
+ result.push(range)
+ }
+
+ Selection {
+ ranges: result,
+ primary_index,
+ }
+ }
+
+ // TODO: only normalize if needed (any ranges out of order)
+ normalize(ranges, primary_index)
+ }
+}
+
+// TODO: checkSelection -> check if valid for doc length
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_create_normalizes_and_merges() {
+ let sel = Selection::new(
+ vec![
+ Range::new(10, 12),
+ Range::new(6, 7),
+ Range::new(4, 5),
+ Range::new(3, 4),
+ Range::new(0, 6),
+ Range::new(7, 8),
+ Range::new(9, 13),
+ Range::new(13, 14),
+ ],
+ 0,
+ );
+
+ let res = sel
+ .ranges
+ .into_iter()
+ .map(|range| format!("{}/{}", range.anchor, range.head))
+ .collect::<Vec<String>>()
+ .join(",");
+
+ assert_eq!(res, "0/6,6/7,7/8,9/13,13/14");
+ }
+
+ #[test]
+ fn test_create_merges_adjacent_points() {
+ let sel = Selection::new(
+ vec![
+ Range::new(10, 12),
+ Range::new(12, 12),
+ Range::new(12, 12),
+ Range::new(10, 10),
+ Range::new(8, 10),
+ ],
+ 0,
+ );
+
+ let res = sel
+ .ranges
+ .into_iter()
+ .map(|range| format!("{}/{}", range.anchor, range.head))
+ .collect::<Vec<String>>()
+ .join(",");
+
+ assert_eq!(res, "8/10,10/12");
+ }
+}
diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs
new file mode 100644
index 00000000..81b8e108
--- /dev/null
+++ b/helix-core/src/state.rs
@@ -0,0 +1,38 @@
+use crate::{Buffer, Selection};
+
+/// A state represents the current editor state of a single buffer.
+pub struct State {
+ // TODO: maybe doc: ?
+ buffer: Buffer,
+ selection: Selection,
+}
+
+impl State {
+ pub fn new(buffer: Buffer) -> Self {
+ Self {
+ buffer,
+ selection: Selection::single(0, 0),
+ }
+ }
+
+ // TODO: buf/selection accessors
+
+ // update/transact
+ // replaceSelection (transaction that replaces selection)
+ // changeByRange
+ // changes
+ // slice
+ //
+ // getters:
+ // tabSize
+ // indentUnit
+ // languageDataAt()
+ //
+ // config:
+ // indentation
+ // tabSize
+ // lineUnit
+ // syntax
+ // foldable
+ // changeFilter/transactionFilter
+}
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
new file mode 100644
index 00000000..ecbe0c50
--- /dev/null
+++ b/helix-core/src/transaction.rs
@@ -0,0 +1,25 @@
+pub struct Change {
+ from: usize,
+ to: usize,
+ insert: Option<String>,
+}
+
+impl Change {
+ pub fn new(from: usize, to: usize, insert: Option<String>) {
+ // old_extent, new_extent, insert
+ }
+}
+
+pub struct Transaction {}
+
+// ChangeSpec = Change | ChangeSet | Vec<Change>
+// ChangeDesc as a ChangeSet without text: can't be applied, cheaper to store.
+// ChangeSet = ChangeDesc with Text
+pub struct ChangeSet {
+ // basically Vec<ChangeDesc> where ChangeDesc = (current len, replacement len?)
+ // (0, n>0) for insertion, (n>0, 0) for deletion, (>0, >0) for replacement
+ sections: Vec<(usize, isize)>,
+}
+//
+// trait Transaction
+// trait StrictTransaction