-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Text Reflowing/Justification #220
Changes from all commits
1ec8501
6264898
5d125f6
dd497c9
74ee5fc
680fdfe
93b125f
4dee3fb
a1dc7e0
d6f8172
bcda5be
4152494
5ef56ac
34ef570
b4c9861
296bd77
2b87ece
e1a68ba
060539a
14057c9
fe2ad07
ccd53b2
8fad821
00c701f
c5811a3
2b51ff9
fe68b08
2b54b67
8cdd072
03dff12
9dc7eb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -6,32 +6,47 @@ use crate::commands::{self, Result}; | |||||
use crate::util; | ||||||
|
||||||
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 { | ||||||
if app.workspace.current_buffer().is_none() { | ||||||
bail!(BUFFER_MISSING); | ||||||
} | ||||||
match app.mode { | ||||||
Mode::Select(_) | Mode::SelectLine(_) | Mode::Search(_) => { | ||||||
let delete_range = range_from(app)?; | ||||||
let buffer = app.workspace.current_buffer().unwrap(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
buffer.delete_range(delete_range.clone()); | ||||||
buffer.cursor.move_to(delete_range.start()); | ||||||
} | ||||||
_ => bail!("Can't delete selections outside of select mode"), | ||||||
}; | ||||||
|
||||||
Ok(()) | ||||||
} | ||||||
|
||||||
pub fn justify(app: &mut Application) -> Result { | ||||||
if app.workspace.current_buffer().is_none() { | ||||||
bail!(BUFFER_MISSING); | ||||||
} | ||||||
|
||||||
let range = range_from(app)?; | ||||||
|
||||||
// delete and save the range, then justify that range | ||||||
let path = &app.workspace.path.clone(); | ||||||
let buffer = app.workspace.current_buffer().unwrap(); | ||||||
|
||||||
if let Some(text) = buffer.read(&range.clone()) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unneeded
Suggested change
|
||||||
buffer.delete_range(range.clone()); | ||||||
buffer.cursor.move_to(range.start()); | ||||||
|
||||||
buffer.insert( | ||||||
&justify_string( | ||||||
&text, | ||||||
app.preferences.borrow().line_length_guide().unwrap_or(80), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already specify a default value in the preferences file itself. If someone has explicitly disabled the line length guide, let's raise an error here:
Suggested change
|
||||||
&app.preferences.borrow().line_comment_prefix(path).unwrap_or("".to_string()), | ||||||
) | ||||||
); | ||||||
} | ||||||
|
||||||
Ok(()) | ||||||
oldaccountdeadname marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
|
@@ -100,6 +115,80 @@ fn copy_to_clipboard(app: &mut Application) -> Result { | |||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Get the selected range from an application in a selection mode. *Requires* | ||||||
/// that the application has a buffer and is in mode Select, SelectLine, or | ||||||
/// Search. | ||||||
fn range_from(app: &mut Application) -> std::result::Result<Range, Error> { | ||||||
let buffer = app.workspace.current_buffer().ok_or(BUFFER_MISSING)?; | ||||||
|
||||||
Ok(match app.mode { | ||||||
Mode::Select(ref select_mode) => { | ||||||
Range::new(*buffer.cursor.clone(), select_mode.anchor) | ||||||
} | ||||||
Mode::SelectLine(ref select_line_mode) => { | ||||||
select_line_mode.to_range(&*buffer.cursor) | ||||||
} | ||||||
Mode::Search(ref mode) => { | ||||||
mode.results | ||||||
.as_ref() | ||||||
.and_then(|r| r.selection()) | ||||||
.ok_or("Cannot get selection outside of select mode.") | ||||||
.unwrap() | ||||||
.clone() | ||||||
} | ||||||
_ => bail!("Cannot get selection outside of select mode."), | ||||||
}) | ||||||
} | ||||||
|
||||||
/// Wrap a string at a given maximum length (generally 80 characters). If the | ||||||
/// line begins with a comment (matches potential_prefix), the text is wrapped | ||||||
/// around it. | ||||||
fn justify_string(text: &str, max_len: usize, potential_prefix: &str) -> String { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes more sense to move the default value handling for a prefix into this function, and the
Suggested change
|
||||||
let mut justified = String::with_capacity(text.len()); | ||||||
let mut paragraphs = text.split("\n\n").peekable(); | ||||||
while let Some(paragraph) = paragraphs.next() { | ||||||
let mut paragraph = paragraph.split_whitespace().peekable(); | ||||||
let prefix; | ||||||
let max_len_with_prefix; | ||||||
if paragraph.peek().is_some() | ||||||
&& (Some(&potential_prefix) == paragraph.peek()) | ||||||
{ | ||||||
prefix = paragraph.next().unwrap().to_owned() + " "; | ||||||
max_len_with_prefix = max_len - prefix.len(); | ||||||
justified += &prefix; | ||||||
} else { | ||||||
prefix = String::new(); | ||||||
max_len_with_prefix = max_len; | ||||||
} | ||||||
|
||||||
let mut len = 0; | ||||||
|
||||||
while let Some(word) = paragraph.next() { | ||||||
if word == prefix { | ||||||
continue; | ||||||
} | ||||||
|
||||||
len += word.len() + 1; | ||||||
if len > max_len_with_prefix { | ||||||
len = word.len(); | ||||||
justified.push('\n'); | ||||||
justified += &prefix; | ||||||
} | ||||||
justified += word; | ||||||
|
||||||
if paragraph.peek().is_some() { | ||||||
justified.push(' '); | ||||||
} | ||||||
} | ||||||
|
||||||
if paragraphs.peek().is_some() { | ||||||
justified += "\n\n"; // add the paragraph delim | ||||||
} | ||||||
} | ||||||
|
||||||
justified | ||||||
} | ||||||
|
||||||
#[cfg(test)] | ||||||
mod tests { | ||||||
use crate::commands; | ||||||
|
@@ -219,4 +308,48 @@ mod tests { | |||||
String::from("amp\nitor\nbuffer") | ||||||
) | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn justify_justifies() { | ||||||
let text = String::from( | ||||||
"\nthis is a very \n long line with inconsistent line \nbreaks, even though it should have breaks.\n" | ||||||
); | ||||||
assert_eq!( | ||||||
super::justify_string(&text, 80, "//"), | ||||||
String::from("this is a very long line with inconsistent line breaks, even though it should \nhave breaks.") | ||||||
); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn justify_justifies_with_comment() { | ||||||
let text = String::from( | ||||||
"\n// this is a very \n long line with inconsistent line \nbreaks, even though it should have breaks.\n" | ||||||
); | ||||||
assert_eq!( | ||||||
super::justify_string(&text, 80, "//"), | ||||||
String::from("// this is a very long line with inconsistent line breaks, even though it \n// should have breaks.") | ||||||
); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn justify_justifies_paragraphs() { | ||||||
let text = String::from( | ||||||
"\nthis is a very \n long \n\n line with inconsistent line \nbreaks, even though it should have breaks.\n" | ||||||
); | ||||||
assert_eq!( | ||||||
super::justify_string(&text, 80, "//"), | ||||||
String::from("this is a very long\n\nline with inconsistent line breaks, even though it should have breaks.") | ||||||
); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn justify_justifies_paragraphs_with_comment() { | ||||||
let text = String::from( | ||||||
"// this is a very \n long \n\n line with inconsistent line \nbreaks, even though it should have breaks.\n" | ||||||
); | ||||||
assert_eq!( | ||||||
super::justify_string(&text, 80, "//"), | ||||||
String::from("// this is a very long\n\nline with inconsistent line breaks, even though it should have breaks.") | ||||||
); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -226,6 +226,7 @@ select: | |
ctrl-a: selection::select_all | ||
ctrl-z: application::suspend | ||
ctrl-c: application::exit | ||
a: selection::justify | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably should be added to the |
||
|
||
select_line: | ||
up: cursor::move_up | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,8 +54,13 @@ pub struct Application { | |
} | ||
|
||
impl Application { | ||
/// Initialize an empty application in normal mode with any discovered | ||
/// repository, and default preferences. | ||
pub fn new(args: &Vec<String>) -> Result<Application> { | ||
let preferences = initialize_preferences(); | ||
let preferences = | ||
Rc::new(RefCell::new( | ||
Preferences::load().unwrap_or_else(|_| Preferences::new(None)), | ||
)); | ||
Comment on lines
-58
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should stay as-is. This does not provide any benefits and not having all things crammed into one function is very good for readability and maintainability. The comment is all well and can stay of course, such things are always nice to have. |
||
|
||
let (event_channel, events) = mpsc::channel(); | ||
let mut view = View::new(preferences.clone(), event_channel.clone())?; | ||
|
@@ -218,12 +223,6 @@ impl Application { | |
} | ||
} | ||
|
||
fn initialize_preferences() -> Rc<RefCell<Preferences>> { | ||
Rc::new(RefCell::new( | ||
Preferences::load().unwrap_or_else(|_| Preferences::new(None)), | ||
)) | ||
} | ||
|
||
fn create_workspace(view: &mut View, preferences: &Preferences, args: &Vec<String>) -> Result<Workspace> { | ||
// Discard the executable portion of the argument list. | ||
let mut path_args = args.iter().skip(1).peekable(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,8 +46,14 @@ pub struct View { | |
} | ||
|
||
impl View { | ||
pub fn new(preferences: Rc<RefCell<Preferences>>, event_channel: Sender<Event>) -> Result<View> { | ||
let terminal = build_terminal().chain_err(|| "Failed to initialize terminal")?; | ||
/// Return a new view. This will initialize the terminal, load the theme, | ||
/// and begin to listen for events. | ||
pub fn new( | ||
preferences: Rc<RefCell<Preferences>>, | ||
event_channel: Sender<Event> | ||
) -> Result<View> { | ||
let terminal = build_terminal() | ||
.chain_err(|| "Failed to initialize terminal")?; | ||
Comment on lines
-49
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, completely unrelated change? The comment again is nice and can stay, but the reformatting is somewhat unneeded. Especially since there are more occurences of this sort in this file and elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah sorry, I was playing with the codebase a bit trying to understand it, and I think I accidentally commited this instead of starting from the original codebase when I tried to implement line wrapping. |
||
let theme_path = preferences.borrow().theme_path()?; | ||
let theme_set = ThemeLoader::new(theme_path).load()?; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the
buffer
declaration is only used in a single branch of thematch
statement, let's remove this guard clause and inline the existence check; see below for the suggestion on that.