diff --git a/Cargo.lock b/Cargo.lock index 08e1021..dbdf482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arrayvec" @@ -185,7 +185,7 @@ dependencies = [ [[package]] name = "fzf-make" -version = "0.37.0" +version = "0.38.0" dependencies = [ "anyhow", "colored", diff --git a/Cargo.toml b/Cargo.toml index e7d645c..d4c8d9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fzf-make" -version = "0.37.0" +version = "0.38.0" edition = "2021" authors = ["kyu08"] description = "A command line tool that executes make target using fuzzy finder with preview window." diff --git a/src/err/any_to_string.rs b/src/err/any_to_string.rs new file mode 100644 index 0000000..66db679 --- /dev/null +++ b/src/err/any_to_string.rs @@ -0,0 +1,10 @@ +// ref: https://qiita.com/kgtkr/items/a17827c4bb704f39c854 +pub fn any_to_string(any: &dyn std::any::Any) -> String { + if let Some(s) = any.downcast_ref::() { + s.clone() + } else if let Some(s) = any.downcast_ref::<&str>() { + s.to_string() + } else { + "Any".to_string() + } +} diff --git a/src/err/mod.rs b/src/err/mod.rs new file mode 100644 index 0000000..e7ec8d0 --- /dev/null +++ b/src/err/mod.rs @@ -0,0 +1 @@ +pub(crate) mod any_to_string; diff --git a/src/file/mod.rs b/src/file/mod.rs index 3a2562c..9ab6739 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod path_to_content; pub(crate) mod toml; +mod toml_old; diff --git a/src/file/path_to_content.rs b/src/file/path_to_content.rs index e6d1f2b..a1d5f42 100644 --- a/src/file/path_to_content.rs +++ b/src/file/path_to_content.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; - use std::{fs::read_to_string, path::PathBuf}; pub fn path_to_content(path: PathBuf) -> Result { diff --git a/src/file/toml.rs b/src/file/toml.rs index f7659ff..455b040 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -1,4 +1,4 @@ -use super::path_to_content; +use super::{path_to_content, toml_old}; use crate::model::{ histories::{self}, runner_type, @@ -23,18 +23,27 @@ impl Histories { match history_file_path() { Some((history_file_dir, history_file_name)) => { match path_to_content::path_to_content(history_file_dir.join(history_file_name)) { - // TODO: Show error message on message pane if parsing history file failed. https://github.com/kyu08/fzf-make/issues/152 - Ok(c) => match parse_history(c.to_string()) { - Ok(h) => h, - Err(_) => Histories { histories: vec![] }, - }, - Err(_) => Histories { histories: vec![] }, + Ok(c) => Histories::parse_history_in_considering_history_file_format_version(c), + Err(_) => Histories::default(), } } - None => Histories { histories: vec![] }, + None => Histories::default(), } } + fn parse_history_in_considering_history_file_format_version(content: String) -> Histories { + // NOTE: The history file format has changed after https://github.com/kyu08/fzf-make/pull/324. + // So at first we try to parse it as the new format, and then try to parse it as the old format. + match parse_history(content.to_string()) { + Ok(h) => h, + Err(_) => toml_old::parse_history(content.to_string()).unwrap_or_default(), + } + } + + pub fn new(histories: Vec) -> Self { + Self { histories } + } + fn from(histories: histories::Histories) -> Self { let mut result: Vec = vec![]; for h in histories.histories { @@ -53,7 +62,7 @@ impl Histories { } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -struct History { +pub struct History { path: PathBuf, commands: Vec, } @@ -82,17 +91,25 @@ impl History { commands, } } + + pub fn new(path: PathBuf, commands: Vec) -> Self { + Self { path, commands } + } } /// toml representation of histories::HistoryCommand. #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] -struct HistoryCommand { +pub struct HistoryCommand { runner_type: runner_type::RunnerType, name: String, } impl HistoryCommand { + pub fn new(runner_type: runner_type::RunnerType, name: String) -> Self { + Self { runner_type, name } + } + fn from(command: histories::HistoryCommand) -> Self { Self { runner_type: command.runner_type, @@ -260,4 +277,143 @@ name = "echo1" } } } + + #[test] + fn parse_history_in_considering_history_file_format_version_test() { + struct Case { + title: &'static str, + content: String, + expect: Histories, + } + let cases = vec![ + Case { + title: "Success(new format)", + content: r#" +[[histories]] +path = "/Users/user/code/fzf-make" + +[[histories.commands]] +runner-type = "make" +name = "test" + +[[histories.commands]] +runner-type = "make" +name = "check" + +[[histories.commands]] +runner-type = "make" +name = "spell-check" + +[[histories]] +path = "/Users/user/code/golang/go-playground" + +[[histories.commands]] +runner-type = "make" +name = "run" + +[[histories.commands]] +runner-type = "make" +name = "echo1" + "# + .to_string(), + expect: Histories { + histories: vec![ + History { + path: PathBuf::from("/Users/user/code/fzf-make"), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "test".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "check".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "spell-check".to_string(), + }, + ], + }, + History { + path: PathBuf::from("/Users/user/code/golang/go-playground"), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "run".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "echo1".to_string(), + }, + ], + }, + ], + }, + }, + Case { + title: "Success(old format)", + content: r#" +[[history]] +path = "/Users/user/code/fzf-make/Makefile" +executed-targets = ["test", "check", "spell-check"] + +[[history]] +path = "/Users/user/code/golang/go-playground/Makefile" +executed-targets = ["run", "echo1"] + "# + .to_string(), + expect: Histories { + histories: vec![ + History { + path: PathBuf::from("/Users/user/code/fzf-make"), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "test".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "check".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "spell-check".to_string(), + }, + ], + }, + History { + path: PathBuf::from("/Users/user/code/golang/go-playground"), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "run".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "echo1".to_string(), + }, + ], + }, + ], + }, + }, + Case { + title: "Error", + content: r#" + "# + .to_string(), + expect: Histories::default(), + }, + ]; + + for case in cases { + assert_eq!( + case.expect, + Histories::parse_history_in_considering_history_file_format_version(case.content), + "\nFailed: 🚨{:?}🚨\n", + case.title, + ) + } + } } diff --git a/src/file/toml_old.rs b/src/file/toml_old.rs new file mode 100644 index 0000000..8a124e3 --- /dev/null +++ b/src/file/toml_old.rs @@ -0,0 +1,173 @@ +use super::toml as fzf_make_toml; +use crate::model; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +struct Histories { + history: Vec, +} + +impl Histories { + fn into_histories(self) -> fzf_make_toml::Histories { + let mut result: Vec = vec![]; + for h in self.history.clone() { + let mut commands: Vec = vec![]; + for c in h.executed_targets { + commands.push(fzf_make_toml::HistoryCommand::new( + model::runner_type::RunnerType::Make, + c, + )); + } + // NOTE: In old format, the path includes the file name but new format does not. + let mut makefile_path = PathBuf::from(h.path); + makefile_path.pop(); + result.push(fzf_make_toml::History::new(makefile_path, commands)); + } + + fzf_make_toml::Histories::new(result) + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename(deserialize = "history"))] +#[serde(rename_all = "kebab-case")] +struct History { + path: String, + executed_targets: Vec, +} + +pub fn parse_history(content: String) -> Result { + toml::from_str(&content) + .map(|h: Histories| h.into_histories()) + .map_err(|e| e.into()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::file::toml as fzf_make_toml; + use crate::model::runner_type; + use pretty_assertions::assert_eq; + + #[test] + fn into_histories_test() { + struct Case { + title: &'static str, + before: Histories, + after: fzf_make_toml::Histories, + } + let cases = vec![Case { + title: "Success", + before: Histories { + history: vec![History { + path: "path/Makefile".to_string(), + executed_targets: vec!["command1".to_string(), "command2".to_string()], + }], + }, + after: fzf_make_toml::Histories::new(vec![fzf_make_toml::History::new( + PathBuf::from("path"), + vec![ + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "command1".to_string(), + ), + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "command2".to_string(), + ), + ], + )]), + }]; + + for case in cases { + assert_eq!( + case.after, + case.before.into_histories(), + "\nFailed: 🚨{:?}🚨\n", + case.title, + ) + } + } + + #[test] + fn parse_history_test() { + struct Case { + title: &'static str, + content: String, + expect: Result, + } + let cases = vec![ + Case { + title: "Success", + content: r#" +[[history]] +path = "/Users/user/code/fzf-make/Makefile" +executed-targets = ["test", "check", "spell-check"] + +[[history]] +path = "/Users/user/code/golang/go-playground/Makefile" +executed-targets = ["run", "echo1"] + "# + .to_string(), + expect: Ok(fzf_make_toml::Histories::new(vec![ + fzf_make_toml::History::new( + PathBuf::from("/Users/user/code/fzf-make"), + vec![ + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "test".to_string(), + ), + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "check".to_string(), + ), + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "spell-check".to_string(), + ), + ], + ), + fzf_make_toml::History::new( + PathBuf::from("/Users/user/code/golang/go-playground"), + vec![ + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "run".to_string(), + ), + fzf_make_toml::HistoryCommand::new( + runner_type::RunnerType::Make, + "echo1".to_string(), + ), + ], + ), + ])), + }, + Case { + title: "Error", + content: r#" + "# + .to_string(), + expect: Err(anyhow::anyhow!("TOML parse error at line 1, column 1\n |\n1 | \n | ^\nmissing field `history`\n")), + }, + ]; + + for case in cases { + match case.expect { + Ok(e) => assert_eq!( + e, + parse_history(case.content).unwrap(), + "\nFailed: 🚨{:?}🚨\n", + case.title, + ), + Err(err) => assert_eq!( + err.to_string(), + parse_history(case.content).unwrap_err().to_string(), + "\nFailed: 🚨{:?}🚨\n", + case.title, + ), + } + } + } +} diff --git a/src/main.rs b/src/main.rs index b080db2..aa52881 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod controller; +mod err; mod file; mod model; mod usecase; @@ -8,11 +9,8 @@ fn main() { let result = panic::catch_unwind(|| { controller::controller_main::run(); }); - match result { - Ok(_) => {} - Err(e) => { - eprintln!("Error: {:?}", e); - std::process::exit(1); - } + if let Err(e) = result { + println!("{}", err::any_to_string::any_to_string(&*e)); + std::process::exit(1); } } diff --git a/src/usecase/tui/app.rs b/src/usecase/tui/app.rs index 6a2ceb2..5950469 100644 --- a/src/usecase/tui/app.rs +++ b/src/usecase/tui/app.rs @@ -1,5 +1,6 @@ use super::{config, ui::ui}; use crate::{ + err::any_to_string, file::toml, model::{ command, @@ -198,7 +199,7 @@ pub fn main(config: config::Config) -> Result<()> { Err(e) => { disable_raw_mode()?; execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - println!("panic: {:?}", e); + println!("{}", any_to_string::any_to_string(&*e)); process::exit(1); } } @@ -527,9 +528,12 @@ impl SelectCommandState<'_> { } fn handle_key_input(&mut self, key_event: KeyEvent) { - if let KeyCode::Char(_) = key_event.code { - self.reset_selection(); - }; + match key_event.code { + KeyCode::Char(_) | KeyCode::Backspace => { + self.reset_selection(); + } + _ => {} + } self.search_text_area.0.input(key_event); } @@ -733,6 +737,30 @@ mod test { }), }, }, + Case { + title: "when BackSpace is inputted, the selection should be reset", + model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(1), + ), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::SearchTextAreaKeyInput(KeyEvent::from( + KeyCode::Backspace, + ))), + expect_model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), + ..SelectCommandState::new_for_test() + }), + }, + }, Case { title: "Next(0 -> 1)", model: Model { @@ -924,7 +952,8 @@ mod test { }, }, Case { - title: "PreviousCommand when there is no commands to select, panic should not occur", + title: "PreviousCommand when there is no commands to select, + panic should not occur", model: { let mut m = Model { app_state: AppState::SelectCommand(SelectCommandState { @@ -994,14 +1023,12 @@ mod test { }, }, Case { - title: "When the last history is selected and NextHistory is received, it returns to the beginning.", + title: "When the last history is selected and NextHistory is received, + it returns to the beginning.", model: Model { app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - history_list_state: ListState::with_selected( - ListState::default(), - Some(2), - ), + history_list_state: ListState::with_selected(ListState::default(), Some(2)), ..SelectCommandState::new_for_test() }), }, @@ -1009,23 +1036,18 @@ mod test { expect_model: Model { app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - history_list_state: ListState::with_selected( - ListState::default(), - Some(0), - ), + history_list_state: ListState::with_selected(ListState::default(), Some(0)), ..SelectCommandState::new_for_test() }), }, }, Case { - title: "When the first history is selected and PreviousHistory is received, it moves to the last history.", + title: "When the first history is selected and PreviousHistory is received, + it moves to the last history.", model: Model { app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - history_list_state: ListState::with_selected( - ListState::default(), - Some(0), - ), + history_list_state: ListState::with_selected(ListState::default(), Some(0)), ..SelectCommandState::new_for_test() }), }, @@ -1033,10 +1055,7 @@ mod test { expect_model: Model { app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - history_list_state: ListState::with_selected( - ListState::default(), - Some(2), - ), + history_list_state: ListState::with_selected(ListState::default(), Some(2)), ..SelectCommandState::new_for_test() }), },