Expand transaction API.

This commit is contained in:
Blaž Hrastnik 2020-09-07 17:08:28 +09:00
parent 4e349add60
commit dd749bb284
4 changed files with 117 additions and 26 deletions

View file

@ -1,4 +1,7 @@
use crate::selection::Range;
use crate::state::{Direction, Granularity, Mode, State};
use crate::transaction::{ChangeSet, Transaction};
use crate::Tendril;
/// A command is a function that takes the current state and a count, and does a side-effect on the
/// state (usually by creating and applying a transaction).
@ -49,22 +52,48 @@ pub fn move_line_down(state: &mut State, count: usize) {
);
}
// avoid select by default by having a visual mode switch that makes movements into selects
// insert mode:
// first we calculate the correct cursors/selections
// then we just append at each cursor
// lastly, if it was append mode we shift cursor by 1?
// inserts at the start of each selection
pub fn insert_mode(state: &mut State, _count: usize) {
state.mode = Mode::Insert;
state.selection = state
.selection
.clone()
.transform(|range| Range::new(range.to(), range.from()))
}
// inserts at the end of each selection
pub fn append_mode(state: &mut State, _count: usize) {
state.mode = Mode::Insert;
// TODO: as transaction
state.selection = state.selection.clone().transform(|range| {
// TODO: to() + next char
Range::new(range.from(), range.to())
})
}
// I inserts at the start of each line with a selection
// A inserts at the end of each line with a selection
// o inserts a new line before each line with a selection
// O inserts a new line after each line with a selection
pub fn normal_mode(state: &mut State, _count: usize) {
state.mode = Mode::Normal;
}
// TODO: insert means add text just before cursor, on exit we should be on the last letter.
pub fn insert(state: &mut State, c: char) {
// TODO: needs to work with multiple cursors
use crate::transaction::ChangeSet;
pub fn insert_char(state: &mut State, c: char) {
let c = Tendril::from_char(c);
let transaction = Transaction::insert(&state, c);
let pos = state.selection.primary().head;
let changes = ChangeSet::insert(&state.doc, pos, c);
// TODO: need to store history
changes.apply(&mut state.doc);
state.selection = state.selection.clone().map(&changes);
transaction.apply(state);
// TODO: need to store into history if successful
}

View file

@ -113,6 +113,11 @@ impl Selection {
self.ranges[self.primary_index]
}
#[must_use]
pub fn cursor(&self) -> usize {
self.primary().head
}
/// Ensure selection containing only the primary selection.
pub fn into_single(self) -> Self {
if self.ranges.len() == 1 {
@ -144,6 +149,10 @@ impl Selection {
)
}
pub fn ranges(&self) -> &[Range] {
&self.ranges
}
#[must_use]
/// Constructs a selection holding a single range.
pub fn single(anchor: usize, head: usize) -> Self {
@ -200,6 +209,14 @@ impl Selection {
// TODO: only normalize if needed (any ranges out of order)
normalize(ranges, primary_index)
}
/// Takes a closure and maps each selection over the closure.
pub fn transform<F>(self, f: F) -> Self
where
F: Fn(Range) -> Range,
{
Self::new(self.ranges.into_iter().map(f).collect(), self.primary_index)
}
}
// TODO: checkSelection -> check if valid for doc length

View file

@ -1,4 +1,4 @@
use crate::{Rope, Selection, Tendril};
use crate::{Rope, Selection, SelectionRange, State, Tendril};
// TODO: divided into three different operations, I sort of like having just
// Splice { extent, Option<text>, distance } better.
@ -47,18 +47,6 @@ impl ChangeSet {
}
}
pub fn insert(doc: &Rope, pos: usize, c: char) -> Self {
let len = doc.len_chars();
Self {
changes: vec![
Change::Retain(pos),
Change::Insert(Tendril::from_char(c)),
Change::Retain(len - pos),
],
len,
}
}
// TODO: from iter
/// Combine two changesets together.
@ -210,8 +198,11 @@ impl ChangeSet {
unimplemented!()
}
pub fn apply(&self, text: &mut Rope) {
// TODO: validate text.chars() == self.len
/// Returns true if applied successfully.
pub fn apply(&self, text: &mut Rope) -> bool {
if text.len_chars() != self.len {
return false;
}
let mut pos = 0;
@ -231,6 +222,7 @@ impl ChangeSet {
}
}
}
true
}
/// `true` when the set is empty.
@ -332,7 +324,60 @@ pub struct Transaction {
// scroll_into_view
}
impl Transaction {}
impl Transaction {
/// Returns true if applied successfully.
pub fn apply(&self, state: &mut State) -> bool {
// apply changes to the document
if !self.changes.apply(&mut state.doc) {
return false;
}
// update the selection: either take the selection specified in the transaction, or map the
// current selection through changes.
state.selection = self
.selection
.clone()
.unwrap_or_else(|| state.selection.clone().map(&self.changes));
true
}
pub fn insert(state: &State, text: Tendril) -> Self {
let len = state.doc.len_chars();
let ranges = state.selection.ranges();
let mut changes = Vec::with_capacity(2 * ranges.len() + 1);
let mut last = 0;
for range in state.selection.ranges() {
let cur = range.head;
changes.push(Change::Retain(cur));
changes.push(Change::Insert(text.clone()));
last = cur;
}
changes.push(Change::Retain(len - last));
Self::from(ChangeSet { changes, len })
}
pub fn change_selection<F>(selection: Selection, f: F) -> Self
where
F: Fn(SelectionRange) -> ChangeSet,
{
selection.ranges().iter().map(|range| true);
// TODO: most idiomatic would be to return a
// Change { from: x, to: y, insert: "str" }
unimplemented!()
}
}
impl From<ChangeSet> for Transaction {
fn from(changes: ChangeSet) -> Self {
Self {
changes,
selection: None,
}
}
}
#[cfg(test)]
mod test {

View file

@ -87,7 +87,7 @@ impl Editor {
};
// render the cursor
let pos = state.selection.primary().head;
let pos = state.selection.cursor();
let coords = coords_at_pos(&state.doc.slice(..), pos);
execute!(
stdout,
@ -126,7 +126,7 @@ impl Editor {
KeyEvent {
code: KeyCode::Char(c),
..
} => helix_core::commands::insert(state, c),
} => helix_core::commands::insert_char(state, c),
_ => (), // skip
}
self.render();