diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29b..00000000 diff --git a/src/commands/selection.rs b/src/commands/selection.rs index 93cfb049..64a56a1c 100644 --- a/src/commands/selection.rs +++ b/src/commands/selection.rs @@ -4,33 +4,13 @@ use super::application; use crate::errors::*; use crate::commands::{self, Result}; use crate::util; +use crate::util::reflow::Reflow; pub fn delete(app: &mut Application) -> Result { - if let Some(buffer) = app.workspace.current_buffer() { - match app.mode { - Mode::Select(ref select_mode) => { - let cursor_position = *buffer.cursor.clone(); - let delete_range = Range::new(cursor_position, select_mode.anchor); - buffer.delete_range(delete_range.clone()); - buffer.cursor.move_to(delete_range.start()); - } - Mode::SelectLine(ref mode) => { - let delete_range = mode.to_range(&*buffer.cursor); - buffer.delete_range(delete_range.clone()); - buffer.cursor.move_to(delete_range.start()); - } - Mode::Search(ref mode) => { - let selection = mode.results - .as_ref() - .and_then(|r| r.selection()) - .ok_or("Can't delete in search mode without a selected result")?; - buffer.delete_range(selection.clone()); - } - _ => bail!("Can't delete selections outside of select mode"), - }; - } else { - bail!(BUFFER_MISSING); - } + let rng = sel_to_range(app)?; + let buf = app.workspace.current_buffer().unwrap(); + buf.delete_range(rng.clone()); + buf.cursor.move_to(rng.start()); Ok(()) } @@ -100,6 +80,49 @@ fn copy_to_clipboard(app: &mut Application) -> Result { Ok(()) } +pub fn justify(app: &mut Application) -> Result { + let range = sel_to_range(app)?; + let mut buffer = app.workspace.current_buffer().unwrap(); + + let limit = match app.preferences.borrow().line_length_guide() { + Some(n) => n, + None => bail!("Justification requires a line_length_guide."), + }; + + Reflow::new(&mut buffer, range, limit)?.apply()?; + + Ok(()) +} + +fn sel_to_range(app: &mut Application) -> std::result::Result { + let buf = app.workspace.current_buffer().ok_or(BUFFER_MISSING)?; + + match app.mode { + Mode::Select(ref sel) => { + let cursor_position = *buf.cursor.clone(); + Ok(Range::new(cursor_position, sel.anchor)) + }, + Mode::SelectLine(ref sel) => { + Ok(util::inclusive_range( + &LineRange::new( + sel.anchor, + buf.cursor.line + ), + buf + )) + }, + Mode::Search(ref search) => { + Ok(search + .results + .as_ref() + .and_then(|r| r.selection()) + .ok_or("A selection is required.")? + .clone()) + } + _ => bail!("A selection is required."), + } +} + #[cfg(test)] mod tests { use crate::commands; diff --git a/src/input/key_map/default.yml b/src/input/key_map/default.yml index 089854d0..a71d00c3 100644 --- a/src/input/key_map/default.yml +++ b/src/input/key_map/default.yml @@ -218,6 +218,7 @@ select: R: git::copy_remote_url m: view::scroll_down f: application::switch_to_second_stage_jump_mode + z: selection::justify "'": application::switch_to_jump_mode ",": view::scroll_up page_up: view::scroll_up @@ -263,6 +264,7 @@ select_line: R: git::copy_remote_url m: view::scroll_down f: application::switch_to_second_stage_jump_mode + z: selection::justify ",": view::scroll_up ">": buffer::indent_line "<": buffer::outdent_line diff --git a/src/util/mod.rs b/src/util/mod.rs index 55767bf4..e26c72bc 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,6 +2,7 @@ pub use self::selectable_vec::SelectableVec; pub mod movement_lexer; mod selectable_vec; +pub mod reflow; pub mod token; use crate::errors::*; diff --git a/src/util/reflow.rs b/src/util/reflow.rs new file mode 100644 index 00000000..e9411d09 --- /dev/null +++ b/src/util/reflow.rs @@ -0,0 +1,238 @@ +use super::*; + +/// Encapsulate reflow logic for buffer manipulation. +pub struct Reflow<'a> { + buf: &'a mut Buffer, + range: Range, + text: String, + limit: usize, +} + +impl<'a> Reflow<'a> { + /// Create a reflow instance, where buffer and range determine the target, + /// and the limit is the maximum length of a line, regardless of prefixes. + pub fn new( + buf: &'a mut Buffer, range: Range, limit: usize + ) -> std::result::Result { + let text = buf.read(&range).ok_or("Selection is invalid.")?; + Ok(Self { buf, range, text, limit }) + } + + pub fn apply(mut self) -> std::result::Result<(), Error> { + let prefix = self.infer_prefix()?; + let jtxt = self.justify_str(&prefix); + self.buf.delete_range(self.range.clone()); + self.buf.cursor.move_to(self.range.start()); + self.buf.insert(jtxt); + + Ok(()) + } + + fn infer_prefix(&self) -> std::result::Result { + match self.text.split_whitespace().next() { + Some(n) => if n.chars().next().unwrap().is_alphanumeric() { + Ok("".to_string()) + } else { + Ok(n.to_string()) + }, + None => bail!("Selection is empty."), + } + } + + + fn justify_str(&mut self, prefix: &str) -> String { + let text = self.buf.read(&self.range).unwrap(); + let mut limit = self.limit; + let mut justified = String::with_capacity(text.len()); + let mut pars = text.split("\n\n").peekable(); + + let mut space_delims = ["".to_string(), " ".to_string(), "\n".to_string()]; + if prefix != "" { + space_delims[0] += prefix; + space_delims[0] += " "; + space_delims[2] += prefix; + space_delims[2] += " "; + limit -= prefix.len() + 1; + } + + while let Some(par) = pars.next() { + let mut words = par.split_whitespace(); + let mut len = 0; + let mut first = true; + + while let Some(word) = words.next() { + if word == prefix { + continue; + } + + len += word.len(); + + let over = len > limit; + let u_over = over as usize; + let idx = (!first as usize) * u_over + !first as usize; + + justified += &space_delims[idx]; + justified += word; + + // if we're over, set the length to 0, otherwise increment it + // properly. This just does that mith multiplication by 0 instead of + // branching. + len = (len + 1) * (1 - u_over) + (word.len() + 1) * u_over; + first = false; + } + + if pars.peek().is_some() { + justified += "\n\n"; // add back the paragraph break. + } + } + + justified + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // as simple as it gets: one character words for easy debugging. + #[test] + fn justify_simple() { + let mut buf = Buffer::new(); + buf.insert("\ +a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\n"); + + Reflow::new( + &mut buf, + Range::new( + scribe::buffer::Position { line: 0, offset: 0 }, + scribe::buffer::Position { line: 1, offset: 0 }, + ), + 80, + ).unwrap().apply().unwrap(); + + assert_eq!( + buf.data(), + "\ +a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a +a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a" + ); + } + + #[test] + fn justify_paragraph() { + let mut buf = Buffer::new(); + buf.insert("\ +these are words to be used as demos for the thing that this is. this is text \ +reflowing and justification over a few lines. this is just filler text in case \ +it wasn't obvious.\n" + ); + + Reflow::new( + &mut buf, + Range::new( + scribe::buffer::Position { line: 0, offset: 0 }, + scribe::buffer::Position { line: 1, offset: 0 }, + ), + 80, + ).unwrap().apply().unwrap(); + assert_eq!( + buf.data(), "\ +these are words to be used as demos for the thing that this is. this is text +reflowing and justification over a few lines. this is just filler text in case +it wasn't obvious." + ); + } + + #[test] + fn justify_multiple_pars() { + let mut buf = Buffer::new(); + buf.insert("\ +Here's more filler text! So fun fact of the day, I was trying to just copy paste \ +some lorem ipsum to annoy my latin student friends, but honestly it broke the \ +M-q 'justify' function in emacs, which makes it a bit difficult to work with. \ +Overall, it's just not that great with code! + +Fun fact of the day number two, writing random paragraphs of text is honestly \ +taking way more effort than I anticipated, and I deeply apologize for the lack \ +of sanity and coherence here! + +Fun fact of the day number three is that I spent three hours getting this to not \ +branch. There is no way that that micro-optimization will actually save three \ +hours worth of time, but I did it anyway for no good reason!\n" + ); + + Reflow::new( + &mut buf, + Range::new( + scribe::buffer::Position { line: 0, offset: 0 }, + scribe::buffer::Position { line: 5, offset: 0 }, + ), + 80, + ).unwrap().apply().unwrap(); + + assert_eq!( + buf.data(), "\ +Here's more filler text! So fun fact of the day, I was trying to just copy paste +some lorem ipsum to annoy my latin student friends, but honestly it broke the +M-q 'justify' function in emacs, which makes it a bit difficult to work with. +Overall, it's just not that great with code! + +Fun fact of the day number two, writing random paragraphs of text is honestly +taking way more effort than I anticipated, and I deeply apologize for the lack +of sanity and coherence here! + +Fun fact of the day number three is that I spent three hours getting this to not +branch. There is no way that that micro-optimization will actually save three +hours worth of time, but I did it anyway for no good reason!" + ); + } + + #[test] + fn justify_simple_prefix() { + let mut buf = Buffer::new(); + buf.insert("\ +# a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\n" + ); + Reflow::new( + &mut buf, + Range::new( + scribe::buffer::Position { line: 0, offset: 0 }, + scribe::buffer::Position { line: 1, offset: 0 }, + ), + 80, + ).unwrap().apply().unwrap(); + + assert_eq!( + buf.data(), "\ +# a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a +# a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a" + ); + } + + #[test] + fn justify_paragraph_prefix() { + let mut buf = Buffer::new(); + buf.insert("\ +// filler text meant +// to do stuff and things that end up with text nicely \ +wrappped around a comment delimiter such as the double slashes in c-style \ +languages.\n" + ); + + Reflow::new( + &mut buf, + Range::new( + scribe::buffer::Position { line: 0, offset: 0 }, + scribe::buffer::Position { line: 2, offset: 0 }, + ), + 80, + ).unwrap().apply().unwrap(); + + assert_eq!( + buf.data(), "\ +// filler text meant to do stuff and things that end up with text nicely +// wrappped around a comment delimiter such as the double slashes in c-style +// languages.", + ); + } +}