diff options
author | Tim Oram <dev@mitmaro.ca> | 2022-11-22 09:37:30 -0330 |
---|---|---|
committer | Tim Oram <dev@mitmaro.ca> | 2022-11-25 09:51:20 -0330 |
commit | 1557e28b8b89b1f2630a43abadd1908c9739a86a (patch) | |
tree | 88132e47f46cb4c677661d0a288474d5f2fdfc9f | |
parent | 247f7083eda05fb9b02bc48bcfdc40034512a637 (diff) |
Add flag support to todo_file Line
-rw-r--r-- | src/todo_file/src/edit_content.rs | 38 | ||||
-rw-r--r-- | src/todo_file/src/lib.rs | 22 | ||||
-rw-r--r-- | src/todo_file/src/line.rs | 161 | ||||
-rw-r--r-- | src/todo_file/src/line_parser.rs | 191 |
4 files changed, 359 insertions, 53 deletions
diff --git a/src/todo_file/src/edit_content.rs b/src/todo_file/src/edit_content.rs index eb8257c..d62ba2d 100644 --- a/src/todo_file/src/edit_content.rs +++ b/src/todo_file/src/edit_content.rs @@ -5,6 +5,7 @@ use super::action::Action; pub struct EditContext { action: Option<Action>, content: Option<String>, + option: Option<String>, } impl EditContext { @@ -15,6 +16,7 @@ impl EditContext { Self { action: None, content: None, + option: None, } } @@ -34,6 +36,14 @@ impl EditContext { self } + /// Set the option. + #[must_use] + #[inline] + pub fn option(mut self, option: &str) -> Self { + self.option = Some(String::from(option)); + self + } + /// Get the action. #[must_use] #[inline] @@ -47,6 +57,13 @@ impl EditContext { pub fn get_content(&self) -> Option<&str> { self.content.as_deref() } + + /// Get the option. + #[must_use] + #[inline] + pub fn get_option(&self) -> Option<&str> { + self.option.as_deref() + } } #[cfg(test)] @@ -60,13 +77,15 @@ mod tests { let edit_context = EditContext::new(); assert_none!(edit_context.get_action()); assert_none!(edit_context.get_content()); + assert_none!(edit_context.get_option()); } #[test] fn with_action() { let edit_context = EditContext::new().action(Action::Break); assert_some_eq!(edit_context.get_action(), Action::Break); - assert_none!(edit_context.get_content(),); + assert_none!(edit_context.get_content()); + assert_none!(edit_context.get_option()); } #[test] @@ -74,12 +93,25 @@ mod tests { let edit_context = EditContext::new().content("test content"); assert_none!(edit_context.get_action()); assert_some_eq!(edit_context.get_content(), "test content"); + assert_none!(edit_context.get_option()); + } + + #[test] + fn with_option() { + let edit_context = EditContext::new().option("-C"); + assert_none!(edit_context.get_action()); + assert_none!(edit_context.get_content()); + assert_some_eq!(edit_context.get_option(), "-C"); } #[test] - fn with_content_and_action() { - let edit_context = EditContext::new().action(Action::Edit).content("test content"); + fn with_all() { + let edit_context = EditContext::new() + .action(Action::Edit) + .content("test content") + .option("-C"); assert_some_eq!(edit_context.get_action(), Action::Edit); assert_some_eq!(edit_context.get_content(), "test content"); + assert_some_eq!(edit_context.get_option(), "-C"); } } diff --git a/src/todo_file/src/lib.rs b/src/todo_file/src/lib.rs index 9fd8ff0..0dfcb77 100644 --- a/src/todo_file/src/lib.rs +++ b/src/todo_file/src/lib.rs @@ -117,6 +117,7 @@ mod edit_content; pub mod errors; mod history; mod line; +mod line_parser; mod search; #[cfg(not(tarpaulin_include))] pub mod testutil; @@ -351,13 +352,17 @@ impl TodoFile { for index in range { let line = &mut self.lines[index]; lines.push(line.clone()); - if let Some(action) = edit_context.get_action().as_ref() { - line.set_action(*action); + if let Some(action) = edit_context.get_action() { + line.set_action(action); } - if let Some(content) = edit_context.get_content().as_ref() { + if let Some(content) = edit_context.get_content() { line.edit_content(content); } + + if let Some(option) = edit_context.get_option() { + line.toggle_option(option); + } } self.version.increment(); self.history.record(HistoryItem::new_modify(start, end, lines)); @@ -676,7 +681,16 @@ mod tests { } #[test] - fn update_range_edit_action() { + fn update_range_set_option() { + let (mut todo_file, _) = create_and_load_todo_file(&["fixup aaa comment"]); + let old_version = *todo_file.version(); + todo_file.update_range(0, 2, &EditContext::new().option("-c")); + assert_todo_lines!(todo_file, "fixup -c aaa comment"); + assert_ne!(todo_file.version(), &old_version); + } + + #[test] + fn update_range_reverse_indexes() { let (mut todo_file, _) = create_and_load_todo_file(&["pick aaa comment", "drop bbb comment", "edit ccc comment"]); todo_file.update_range(2, 0, &EditContext::new().action(Action::Reword)); diff --git a/src/todo_file/src/line.rs b/src/todo_file/src/line.rs index 6f1abbc..ef15559 100644 --- a/src/todo_file/src/line.rs +++ b/src/todo_file/src/line.rs @@ -1,4 +1,4 @@ -use crate::{errors::ParseError, Action}; +use crate::{errors::ParseError, line_parser::LineParser, Action}; /// Represents a line in the rebase file. #[derive(Clone, Debug, PartialEq, Eq)] @@ -7,6 +7,7 @@ pub struct Line { content: String, hash: String, mutated: bool, + option: Option<String>, } impl Line { @@ -18,6 +19,7 @@ impl Line { content: String::new(), hash: String::new(), mutated: false, + option: None, } } @@ -30,6 +32,7 @@ impl Line { content: String::new(), hash: String::from(hash), mutated: false, + option: None, } } @@ -42,6 +45,7 @@ impl Line { content: String::new(), hash: String::new(), mutated: false, + option: None, } } @@ -54,6 +58,7 @@ impl Line { content: String::from(command), hash: String::new(), mutated: false, + option: None, } } @@ -66,6 +71,7 @@ impl Line { content: String::from(command), hash: String::new(), mutated: false, + option: None, } } @@ -78,6 +84,7 @@ impl Line { content: String::from(label), hash: String::new(), mutated: false, + option: None, } } @@ -90,6 +97,7 @@ impl Line { content: String::from(label), hash: String::new(), mutated: false, + option: None, } } @@ -102,6 +110,7 @@ impl Line { content: String::from(ref_name), hash: String::new(), mutated: false, + option: None, } } @@ -112,51 +121,57 @@ impl Line { /// Returns an error if an invalid line is provided. #[inline] pub fn new(input_line: &str) -> Result<Self, ParseError> { - if input_line.starts_with("noop") { - return Ok(Self::new_noop()); - } - else if input_line.starts_with("break") || input_line.starts_with('b') { - return Ok(Self::new_break()); - } - else if input_line.starts_with("exec") - || input_line.starts_with('x') - || input_line.starts_with("merge") - || input_line.starts_with('m') - || input_line.starts_with("label") - || input_line.starts_with('l') - || input_line.starts_with("reset") - || input_line.starts_with('t') - || input_line.starts_with("update-ref") - || input_line.starts_with('u') - { - let input: Vec<&str> = input_line.splitn(2, ' ').collect(); - if input.len() == 2 { - return Ok(Self { - action: Action::try_from(input[0])?, - hash: String::new(), - content: String::from(input[1]), + let mut line_parser = LineParser::new(input_line); + + let action = Action::try_from(line_parser.next()?)?; + Ok(match action { + Action::Noop => Self::new_noop(), + Action::Break => Self::new_break(), + Action::Pick | Action::Reword | Action::Edit | Action::Squash | Action::Drop => { + let hash = String::from(line_parser.next()?); + Self { + action, + hash, + content: String::from(line_parser.take_remaining()), mutated: false, - }); - } - } - else { - let input: Vec<&str> = input_line.splitn(3, ' ').collect(); - if input.len() >= 2 { - return Ok(Self { - action: Action::try_from(input[0])?, - hash: String::from(input[1]), - content: if input.len() == 3 { - String::from(input[2]) - } - else { - String::new() - }, + option: None, + } + }, + Action::Fixup => { + let mut next = line_parser.next()?; + + let option = if next.starts_with('-') { + let opt = String::from(next); + next = line_parser.next()?; + Some(opt) + } + else { + None + }; + + let hash = String::from(next); + + Self { + action, + hash, + content: String::from(line_parser.take_remaining()), mutated: false, - }); - } - } - - Err(ParseError::InvalidLine(String::from(input_line))) + option, + } + }, + Action::Exec | Action::Merge | Action::Label | Action::Reset | Action::UpdateRef => { + if !line_parser.has_more() { + return Err(line_parser.parse_error()); + } + Self { + action, + hash: String::new(), + content: String::from(line_parser.take_remaining()), + mutated: false, + option: None, + } + }, + }) } /// Set the action of the line. @@ -165,6 +180,7 @@ impl Line { if !self.action.is_static() && self.action != action { self.mutated = true; self.action = action; + self.option = None; } } @@ -176,6 +192,19 @@ impl Line { } } + /// Set the option on the line, toggling if the existing option matches. + #[inline] + pub fn toggle_option(&mut self, option: &str) { + // try toggle off first + if let Some(current) = self.option.as_deref() { + if current == option { + self.option = None; + return; + } + } + self.option = Some(String::from(option)); + } + /// Get the action of the line. #[must_use] #[inline] @@ -197,6 +226,13 @@ impl Line { self.hash.as_str() } + /// Get the commit hash for the line. + #[must_use] + #[inline] + pub fn option(&self) -> Option<&str> { + self.option.as_deref() + } + /// Does this line contain a commit reference. #[must_use] #[inline] @@ -227,7 +263,12 @@ impl Line { pub fn to_text(&self) -> String { match self.action { Action::Drop | Action::Edit | Action::Fixup | Action::Pick | Action::Reword | Action::Squash => { - format!("{} {} {}", self.action, self.hash, self.content) + if let Some(opt) = self.option.as_ref() { + format!("{} {opt} {} {}", self.action, self.hash, self.content) + } + else { + format!("{} {} {}", self.action, self.hash, self.content) + } }, Action::Exec | Action::Label | Action::Reset | Action::Merge | Action::UpdateRef => { format!("{} {}", self.action, self.content) @@ -251,84 +292,105 @@ mod tests { hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, })] #[case::reword_action("reword aaa comment", &Line { action: Action::Reword, hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, })] #[case::edit_action("edit aaa comment", &Line { action: Action::Edit, hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, })] #[case::squash_action("squash aaa comment", &Line { action: Action::Squash, hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, })] #[case::fixup_action("fixup aaa comment", &Line { action: Action::Fixup, hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, + })] + #[case::fixup_with_option_action("fixup -c aaa comment", &Line { + action: Action::Fixup, + hash: String::from("aaa"), + content: String::from("comment"), + mutated: false, + option: Some(String::from("-c")), })] #[case::drop_action("drop aaa comment", &Line { action: Action::Drop, hash: String::from("aaa"), content: String::from("comment"), mutated: false, + option: None, })] #[case::action_without_comment("pick aaa", &Line { action: Action::Pick, hash: String::from("aaa"), content: String::new(), mutated: false, + option: None, })] #[case::exec_action("exec command", &Line { action: Action::Exec, hash: String::new(), content: String::from("command"), mutated: false, + option: None, })] #[case::label_action("label ref", &Line { action: Action::Label, hash: String::new(), content: String::from("ref"), mutated: false, + option: None, })] #[case::reset_action("reset ref", &Line { action: Action::Reset, hash: String::new(), content: String::from("ref"), mutated: false, + option: None, })] #[case::reset_action("merge command", &Line { action: Action::Merge, hash: String::new(), content: String::from("command"), mutated: false, + option: None, })] #[case::update_ref_action("update-ref reference", &Line { action: Action::UpdateRef, hash: String::new(), content: String::from("reference"), mutated: false, + option: None, })] #[case::break_action("break", &Line { action: Action::Break, hash: String::new(), content: String::new(), mutated: false, + option: None, })] #[case::nnop( "noop", &Line { action: Action::Noop, hash: String::new(), content: String::new(), mutated: false, + option: None, })] fn new(#[case] line: &str, #[case] expected: &Line) { assert_ok_eq!(&Line::new(line), expected); @@ -341,6 +403,7 @@ mod tests { hash: String::from("abc123"), content: String::new(), mutated: false, + option: None, }); } @@ -351,6 +414,7 @@ mod tests { hash: String::new(), content: String::new(), mutated: false, + option: None, }); } @@ -361,6 +425,7 @@ mod tests { hash: String::new(), content: String::from("command"), mutated: false, + option: None, }); } @@ -371,6 +436,7 @@ mod tests { hash: String::new(), content: String::from("command"), mutated: false, + option: None, }); } @@ -381,6 +447,7 @@ mod tests { hash: String::new(), content: String::from("label"), mutated: false, + option: None, }); } @@ -391,6 +458,7 @@ mod tests { hash: String::new(), content: String::from("label"), mutated: false, + option: None, }); } @@ -401,6 +469,7 @@ mod tests { hash: String::new(), content: String::from("reference"), mutated: false, + option: None, }); } @@ -413,7 +482,6 @@ mod tests { } #[rstest] - #[case::invalid_line_only("invalid")] #[case::pick_line_only("pick")] #[case::reword_line_only("reword")] #[case::edit_line_only("edit")] @@ -586,6 +654,7 @@ mod tests { #[case::edit("edit aaa comment")] #[case::exec("exec git commit --amend 'foo'")] #[case::fixup("fixup aaa comment")] + #[case::fixup_with_options("fixup -c aaa comment")] #[case::pick("pick aaa comment")] #[case::reword("reword aaa comment")] #[case::squash("squash aaa comment")] diff --git a/src/todo_file/src/line_parser.rs b/src/todo_file/src/line_parser.rs new file mode 100644 index 0000000..f4591d7 --- /dev/null +++ b/src/todo_file/src/line_parser.rs @@ -0,0 +1,191 @@ +use crate::errors::ParseError; + +pub(crate) struct LineParser<'line> { + input: &'line str, + index: usize, +} + +impl<'line> LineParser<'line> { + pub(crate) fn new(input: &'line str) -> Self { + let mut index = 0; + while input.get(index..=index).unwrap_or("") == " " { + index += 1; + } + Self { input, index } + } + + pub(crate) const fn has_more(&self) -> bool { + self.index < self.input.len() + } + + #[allow(clippy::unwrap_in_result)] + fn scan(&mut self) -> Result<(&'line str, usize), ParseError> { + let mut new_index = self.index; + if !self.has_more() { + return Err(self.parse_error()); + } + + loop { + if self.input.get(new_index..=new_index).unwrap_or(" ") == " " { + let slice = self.input.get(self.index..new_index).unwrap(); + // skip whitespace + new_index += 1; + while self.input.get(new_index..=new_index).unwrap_or("") == " " { + new_index += 1; + } + return Ok((slice, new_index)); + } + new_index += 1; + } + } + + pub(crate) fn next(&mut self) -> Result<&'line str, ParseError> { + let (slice, new_index) = self.scan()?; + self.index = new_index; + Ok(slice) + } + + pub(crate) fn take_remaining(self) -> &'line str { + self.input.get(self.index..self.input.len()).unwrap_or("") + } + + pub(crate) fn parse_error(&self) -> ParseError { + ParseError::InvalidLine(String::from(self.input)) + } +} + +#[cfg(test)] +mod tests { + use testutils::assert_err_eq; + + use super::*; + + fn collect_all_tokens<'parser>(parser: &'parser mut LineParser<'parser>) -> Vec<&'parser str> { + let mut tokens = vec![]; + while let Ok(t) = parser.next() { + tokens.push(t); + } + tokens + } + + #[test] + fn has_more_new() { + let parser = LineParser::new("foo"); + assert!(parser.has_more()); + } + + #[test] + fn has_more_after_next() { + let mut parser = LineParser::new("foo"); + let _ = parser.next(); + assert!(!parser.has_more()); + } + + #[test] + fn has_more_after_next_with_trailing_spaces() { + let mut parser = LineParser::new("foo "); + let _ = parser.next(); + assert!(!parser.has_more()); + } + + #[test] + fn next_single_token() { + let mut parser = LineParser::new("foo"); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo"]); + } + + #[test] + fn next_single_token_leading_space() { + let mut parser = LineParser::new(" foo"); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo"]); + } + + #[test] + fn next_single_token_trailing_space() { + let mut parser = LineParser::new("foo "); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo"]); + } + + #[test] + fn next_multiple_tokens() { + let mut parser = LineParser::new("foo bar foobar"); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo", "bar", "foobar"]); + } + + #[test] + fn next_tokens_with_multiple_spaces_between() { + let mut parser = LineParser::new("foo bar foobar"); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo", "bar", "foobar"]); + } + + #[test] + fn next_tokens_trailing_spaces() { + let mut parser = LineParser::new("foo bar foobar "); + assert_eq!(collect_all_tokens(&mut parser), vec!["foo", "bar", "foobar"]); + } + + #[test] + fn next_empty_str() { + let mut parser = LineParser::new(""); + assert_err_eq!(parser.next(), ParseError::InvalidLine(String::new())); + } + + #[test] + fn next_space_only_str() { + let mut parser = LineParser::new(" "); + assert_err_eq!(parser.next(), ParseError::InvalidLine(String::from(" "))); + } + + #[test] + fn next_end_of_tokens() { + let mut parser = LineParser::new("foo"); + let _ = parser.next(); + assert_err_eq!(parser.next(), ParseError::InvalidLine(String::from("foo"))); + } + + #[test] + fn next_end_of_tokens_with_trailing_space() { + let mut parser = LineParser::new("foo "); + let _ = parser.next(); + assert_err_eq!(parser.next(), ParseError::InvalidLine(String::from("foo "))); + } + + #[test] + fn take_remaining_new() { + let parser = LineParser::new("foo"); + assert_eq!(parser.take_remaining(), "foo"); + } + + #[test] + fn take_remaining_empty_str() { + let parser = LineParser::new(""); + assert_eq!(parser.take_remaining(), ""); + } + + #[test] + fn take_remaining_space_str() { + let parser = LineParser::new(" "); + assert_eq!(parser.take_remaining(), ""); + } + + #[test] + fn take_remaining_after_next() { + let mut parser = LineParser::new("foo bar"); + let _ = parser.next(); + assert_eq!(parser.take_remaining(), "bar"); + } + + #[test] + fn take_remaining_end_of_tokens() { + let mut parser = LineParser::new("foo"); + let _ = parser.next(); + assert_eq!(parser.take_remaining(), ""); + } + + #[test] + fn take_remaining_end_of_tokens_trailing_space() { + let mut parser = LineParser::new("foo "); + let _ = parser.next(); + assert_eq!(parser.take_remaining(), ""); + } +} |