summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Oram <dev@mitmaro.ca>2022-11-22 09:37:30 -0330
committerTim Oram <dev@mitmaro.ca>2022-11-25 09:51:20 -0330
commit1557e28b8b89b1f2630a43abadd1908c9739a86a (patch)
tree88132e47f46cb4c677661d0a288474d5f2fdfc9f
parent247f7083eda05fb9b02bc48bcfdc40034512a637 (diff)
Add flag support to todo_file Line
-rw-r--r--src/todo_file/src/edit_content.rs38
-rw-r--r--src/todo_file/src/lib.rs22
-rw-r--r--src/todo_file/src/line.rs161
-rw-r--r--src/todo_file/src/line_parser.rs191
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(), "");
+ }
+}