diff options
author | Blaž Hrastnik | 2020-05-25 04:02:21 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2020-05-25 04:02:21 +0000 |
commit | 44ff4d3c1f5da05e57ce99ba9d67b80a334def83 (patch) | |
tree | 232b8eebab7f709eaf84b8649791a6c74448bfdb /helix-core | |
parent | 240e5f4e3d27415b792776dd126d15302d53e83b (diff) |
Implement a new core based on CodeMirror.
Diffstat (limited to 'helix-core')
-rw-r--r-- | helix-core/Cargo.toml | 2 | ||||
-rw-r--r-- | helix-core/src/buffer.rs | 18 | ||||
-rw-r--r-- | helix-core/src/lib.rs | 16 | ||||
-rw-r--r-- | helix-core/src/position.rs | 27 | ||||
-rw-r--r-- | helix-core/src/range.rs | 9 | ||||
-rw-r--r-- | helix-core/src/selection.rs | 222 | ||||
-rw-r--r-- | helix-core/src/state.rs | 38 | ||||
-rw-r--r-- | helix-core/src/transaction.rs | 25 |
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 |