Skip to content

Commit

Permalink
Merge pull request #148 from HKalbasi/selection-undo
Browse files Browse the repository at this point in the history
Add selection - part 2
  • Loading branch information
curlpipe authored Sep 26, 2024
2 parents adad1f8 + c321d74 commit 296c063
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 535 deletions.
346 changes: 9 additions & 337 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ assets = [

[dependencies]
alinio = "0.2.1"
copypasta-ext = "0.4.4"
base64 = "0.22.1"
crossterm = "0.28.1"
jargon-args = "0.2.7"
kaolinite = { path = "./kaolinite" }
Expand Down
5 changes: 4 additions & 1 deletion config/.oxrc
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ event_mapping = {
["ctrl_a"] = function()
editor:select_all()
end,
["ctrl_x"] = function()
editor:cut()
end,
["ctrl_c"] = function()
editor:copy()
end,
["ctrl_v"] = function()
editor:paste()
editor:display_info("Use ctrl+shift+v for paste or set your terminal emulator to do paste on ctrl+v")
end,
-- Undo & Redo
["ctrl_z"] = function()
Expand Down
68 changes: 50 additions & 18 deletions kaolinite/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// document.rs - has Document, for opening, editing and saving documents
use crate::event::{Error, Event, EventMgmt, Result, Status};
use crate::event::{Error, Event, Result, Status, UndoMgmt};
use crate::map::{form_map, CharMap};
use crate::searching::{Match, Searcher};
use crate::utils::{
Expand Down Expand Up @@ -38,7 +38,7 @@ pub struct Document {
/// Keeps track of where the character pointer is
pub char_ptr: usize,
/// Manages events, for the purpose of undo and redo
pub event_mgmt: EventMgmt,
pub undo_mgmt: UndoMgmt,
/// true if the file has been modified since saving, false otherwise
pub modified: bool,
/// The number of spaces a tab should be rendered as
Expand All @@ -57,7 +57,7 @@ impl Document {
/// Creates a new, empty document with no file name.
#[cfg(not(tarpaulin_include))]
pub fn new(size: Size) -> Self {
Self {
let mut this = Self {
file: Rope::from_str("\n"),
lines: vec!["".to_string()],
dbl_map: CharMap::default(),
Expand All @@ -68,14 +68,17 @@ impl Document {
offset: Loc::default(),
size,
char_ptr: 0,
event_mgmt: EventMgmt::default(),
undo_mgmt: UndoMgmt::default(),
modified: false,
tab_width: 4,
read_only: false,
old_cursor: 0,
in_redo: false,
eol: false,
}
};
this.undo_mgmt.undo.push(this.take_snapshot());
this.undo_mgmt.saved();
this
}

/// Open a document from a file name.
Expand All @@ -87,7 +90,7 @@ impl Document {
pub fn open<S: Into<String>>(size: Size, file_name: S) -> Result<Self> {
let file_name = file_name.into();
let file = Rope::from_reader(BufReader::new(File::open(&file_name)?))?;
Ok(Self {
let mut this = Self {
eol: !file
.line(file.len_lines().saturating_sub(1))
.to_string()
Expand All @@ -102,13 +105,16 @@ impl Document {
offset: Loc::default(),
size,
char_ptr: 0,
event_mgmt: EventMgmt::default(),
undo_mgmt: UndoMgmt::default(),
modified: false,
tab_width: 4,
read_only: false,
old_cursor: 0,
in_redo: false,
})
};
this.undo_mgmt.undo.push(this.take_snapshot());
this.undo_mgmt.saved();
Ok(this)
}

/// Sets the tab display width measured in spaces, default being 4
Expand All @@ -125,6 +131,7 @@ impl Document {
if let Some(file_name) = &self.file_name {
self.file
.write_to(BufWriter::new(File::create(file_name)?))?;
self.undo_mgmt.saved();
self.modified = false;
Ok(())
} else {
Expand Down Expand Up @@ -155,7 +162,7 @@ impl Document {
/// Will return an error if the event was unable to be completed.
pub fn exe(&mut self, ev: Event) -> Result<()> {
if !self.read_only {
self.event_mgmt.register(ev.clone());
self.undo_mgmt.set_dirty();
self.forth(ev)?;
}
self.cancel_selection();
Expand All @@ -166,23 +173,27 @@ impl Document {
/// # Errors
/// Will return an error if any of the events failed to be reversed.
pub fn undo(&mut self) -> Result<()> {
for ev in self.event_mgmt.undo().unwrap_or_default() {
self.forth(ev.reverse())?;
if let Some(s) = self.undo_mgmt.undo(self.take_snapshot()) {
self.apply_snapshot(s);
self.modified = true;
}
if self.undo_mgmt.at_file() {
self.modified = false;
}
self.modified = !self.event_mgmt.is_undo_empty();
Ok(())
}

/// Redo the last patch in the document.
/// # Errors
/// Will return an error if any of the events failed to be re-executed.
pub fn redo(&mut self) -> Result<()> {
self.in_redo = true;
for ev in self.event_mgmt.redo().unwrap_or_default() {
self.forth(ev)?;
if let Some(s) = self.undo_mgmt.redo() {
self.apply_snapshot(s);
self.modified = true;
}
if self.undo_mgmt.at_file() {
self.modified = false;
}
self.modified = true;
self.in_redo = false;
Ok(())
}

Expand Down Expand Up @@ -1052,10 +1063,31 @@ impl Document {
pub fn selection_text(&self) -> String {
self.file.slice(self.selection_range()).to_string()
}

pub fn commit(&mut self) {
let s = self.take_snapshot();
self.undo_mgmt.commit(s);
}

pub fn reload_lines(&mut self) {
let to = std::mem::take(&mut self.loaded_to);
self.lines.clear();
self.load_to(to);
}

pub fn remove_selection(&mut self) {
self.file.remove(self.selection_range());
self.reload_lines();
self.cursor.loc = self.selection_loc_bound().0;
self.char_ptr = self.character_idx(&self.cursor.loc);
self.cancel_selection();
self.bring_cursor_in_viewport();
self.modified = true;
}
}

/// Defines a cursor's position and any selection it may be covering
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Cursor {
pub loc: Loc,
pub selection_end: Loc,
Expand Down
113 changes: 61 additions & 52 deletions kaolinite/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/// event.rs - manages editing events and provides tools for error handling
use crate::utils::Loc;
use crate::{document::Cursor, utils::Loc, Document};
use quick_error::quick_error;
use ropey::Rope;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
content: Rope,
cursor: Cursor,
}

/// Represents an editing event.
/// All possible editing events can be made up of a combination these events.
Expand Down Expand Up @@ -77,78 +84,80 @@ quick_error! {

/// For managing events for purposes of undo and redo
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct EventMgmt {
/// The patch is the current sequence of editing actions
pub patch: Vec<Event>,
pub struct UndoMgmt {
/// Whether the file touched since the latest commit
pub is_dirty: bool,
/// Undo contains all the patches that have been applied
pub undo: Vec<Vec<Event>>,
pub undo: Vec<Snapshot>,
/// Redo contains all the patches that have been undone
pub redo: Vec<Vec<Event>>,
pub redo: Vec<Snapshot>,
/// Store where the file on the disk is currently at
pub on_disk: usize,
}

impl EventMgmt {
/// Register that an event has occurred with the event manager
pub fn register(&mut self, ev: Event) {
impl Document {
pub fn take_snapshot(&self) -> Snapshot {
Snapshot {
content: self.file.clone(),
cursor: self.cursor,
}
}

pub fn apply_snapshot(&mut self, snapshot: Snapshot) {
self.file = snapshot.content;
self.cursor = snapshot.cursor;
self.char_ptr = self.character_idx(&snapshot.cursor.loc);
self.reload_lines();
self.bring_cursor_in_viewport();
}
}

impl UndoMgmt {
/// Register that an event has occurred and the last snapshot is not update
pub fn set_dirty(&mut self) {
self.redo.clear();
self.patch.push(ev);
self.is_dirty = true;
}

/// This will commit the current patch to the undo stack, ready to be undone.
/// This will commit take a snapshot and add it to the undo stack, ready to be undone.
/// You can call this after every space character, for example, which would
/// make it so that every undo action would remove the previous word the user typed.
pub fn commit(&mut self) {
if !self.patch.is_empty() {
let mut patch = vec![];
std::mem::swap(&mut self.patch, &mut patch);
self.undo.push(patch);
pub fn commit(&mut self, current_snapshot: Snapshot) {
if self.is_dirty {
self.is_dirty = false;
self.undo.push(current_snapshot);
}
}

/// Provide a list of actions to perform in order of when they should be applied for purposes
/// of undoing (you'll need to reverse the events themselves manually)
pub fn undo(&mut self) -> Option<Vec<Event>> {
self.commit();
let mut ev = self.undo.pop()?;
self.redo.push(ev.clone());
ev.reverse();
Some(ev)
/// Provide a snapshot of the desired state of the document for purposes
/// of undoing
pub fn undo(&mut self, current_snapshot: Snapshot) -> Option<Snapshot> {
self.commit(current_snapshot);
if self.undo.len() < 2 {
return None;
}
let snapshot_to_remove = self.undo.pop()?;
let snapshot_to_apply = self.undo.last()?.clone();
self.redo.push(snapshot_to_remove);

Some(snapshot_to_apply)
}

/// Provide a list of events to execute in order of when they should be applied for purposes of
/// Provide a snapshot of the desired state of the document for purposes of
/// redoing
pub fn redo(&mut self) -> Option<Vec<Event>> {
self.commit();
pub fn redo(&mut self) -> Option<Snapshot> {
let ev = self.redo.pop()?;
self.undo.push(ev.clone());
Some(ev)
}

/// Returns true if the undo stack is empty, meaning no patches have been applied
#[must_use]
pub fn is_undo_empty(&self) -> bool {
self.undo.is_empty()
}

/// Returns true if the redo stack is empty, meaning no patches have been undone
#[must_use]
pub fn is_redo_empty(&self) -> bool {
self.redo.is_empty()
}

/// Returns true if the current patch is empty, meaning no edits have been done since the last
/// commit
#[must_use]
pub fn is_patch_empty(&self) -> bool {
self.patch.is_empty()
/// On file save, mark where the document is to match it on the disk
pub fn saved(&mut self) {
self.on_disk = self.undo.len()
}

/// Get the last event that was committed
#[must_use]
pub fn last(&self) -> Option<&Event> {
if self.patch.is_empty() {
self.undo.last().and_then(|u| u.last())
} else {
self.patch.last()
}
/// Determine if the state of the document is currently that of what is on the disk
pub fn at_file(&self) -> bool {
self.undo.len() == self.on_disk
}
}
Loading

0 comments on commit 296c063

Please sign in to comment.