diff --git a/CREDITS b/CREDITS index 20799a8c..506b6e37 100644 --- a/CREDITS +++ b/CREDITS @@ -1093,3 +1093,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================ + +rust-pretty-assertions +https://github.com/rust-pretty-assertions/rust-pretty-assertions/tree/main +---------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2016 rust-derive-builder contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Cargo.lock b/Cargo.lock index 9906afee..08e10213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "downcast-rs" version = "1.2.0" @@ -186,6 +192,7 @@ dependencies = [ "crossterm", "fuzzy-matcher", "portable-pty", + "pretty_assertions", "ratatui", "regex", "serde", @@ -433,6 +440,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.76" @@ -1078,6 +1095,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.26" diff --git a/Cargo.toml b/Cargo.toml index 57c87ff3..e7d645cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ tui-textarea = "0.6.0" toml = "0.8.19" serde = {version = "1.0.204", features = ["derive"]} simple-home-dir = "0.4.0" +pretty_assertions = "1.4.1" diff --git a/src/controller/controller_main.rs b/src/controller/controller_main.rs index 3fafdabc..cea82e77 100644 --- a/src/controller/controller_main.rs +++ b/src/controller/controller_main.rs @@ -1,10 +1,9 @@ +use crate::usecase::fzf_make::FzfMake; +use crate::usecase::{fzf_make, help, history, invalid_arg, repeat, usecase_main, version}; use colored::Colorize; use std::sync::Arc; use std::{collections::HashMap, env}; -use crate::usecase::fzf_make::FzfMake; -use crate::usecase::{fzf_make, help, history, invalid_arg, repeat, usecase_main, version}; - pub fn run() { let command_line_args = env::args().collect(); let usecase = args_to_usecase(command_line_args); diff --git a/src/file/toml.rs b/src/file/toml.rs index 34419897..f7659ff8 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -1,61 +1,154 @@ +use super::path_to_content; +use crate::model::{ + histories::{self}, + runner_type, +}; use anyhow::Result; use serde::{Deserialize, Serialize}; +use simple_home_dir::home_dir; use std::{ + env, fs::{self, File}, io::Write, path::PathBuf, }; -#[derive(Debug, Serialize, Deserialize)] -struct Histories { - history: Vec, +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub struct Histories { + histories: Vec, } impl Histories { - fn from(histories: Vec<(PathBuf, Vec)>) -> Self { + pub fn get_history() -> 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![] }, + } + } + None => Histories { histories: vec![] }, + } + } + + fn from(histories: histories::Histories) -> Self { let mut result: Vec = vec![]; - for h in histories { - result.push(History { - path: h.0.to_str().unwrap().to_string(), - executed_targets: h.1, - }); + for h in histories.histories { + result.push(History::from(h)); } - Histories { history: result } + Self { histories: result } + } + + pub fn into(self) -> histories::Histories { + let mut result: Vec = vec![]; + for h in self.histories { + result.push(History::into(h)); + } + histories::Histories { histories: result } } } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] struct History { - path: String, - executed_targets: Vec, + path: PathBuf, + commands: Vec, } -pub fn parse_history(content: String) -> Result)>> { - let histories: Histories = toml::from_str(&content)?; +impl History { + fn from(history: histories::History) -> Self { + let mut commands: Vec = vec![]; + for h in history.commands { + commands.push(HistoryCommand::from(h)); + } + + History { + path: history.path, + commands, + } + } - let mut result: Vec<(PathBuf, Vec)> = Vec::new(); + fn into(self) -> histories::History { + let mut commands: Vec = vec![]; + for h in self.commands { + commands.push(HistoryCommand::into(h)); + } - for history in histories.history { - result.push((PathBuf::from(history.path), history.executed_targets)); + histories::History { + path: self.path, + commands, + } } - Ok(result) } -#[allow(dead_code)] // TODO(#321): remove -pub fn store_history( +/// toml representation of histories::HistoryCommand. +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +struct HistoryCommand { + runner_type: runner_type::RunnerType, + name: String, +} + +impl HistoryCommand { + fn from(command: histories::HistoryCommand) -> Self { + Self { + runner_type: command.runner_type, + name: command.name.clone(), + } + } + + fn into(self) -> histories::HistoryCommand { + histories::HistoryCommand { + runner_type: self.runner_type, + name: self.name, + } + } +} + +// TODO: should return Result not Option(returns when it fails to get the home dir) +pub fn history_file_path() -> Option<(PathBuf, String)> { + const HISTORY_FILE_NAME: &str = "history.toml"; + + match env::var("FZF_MAKE_IS_TESTING") { + Ok(_) => { + // When testing + let cwd = env::current_dir().unwrap(); + Some(( + cwd.join(PathBuf::from("test_dir")), + HISTORY_FILE_NAME.to_string(), + )) + } + _ => home_dir().map(|home_dir| { + ( + home_dir.join(PathBuf::from(".config/fzf-make")), + HISTORY_FILE_NAME.to_string(), + ) + }), + } +} + +pub fn parse_history(content: String) -> Result { + let histories = toml::from_str(&content)?; + Ok(histories) +} + +pub fn create_or_update_history_file( history_directory_path: PathBuf, history_file_name: String, - histories_tuple: Vec<(PathBuf, Vec)>, + new_history: histories::Histories, ) -> Result<()> { - let histories = Histories::from(histories_tuple); - if !history_directory_path.is_dir() { fs::create_dir_all(history_directory_path.clone())?; } - let mut history_file = File::create(history_directory_path.join(history_file_name))?; - history_file.write_all(toml::to_string(&histories).unwrap().as_bytes())?; + history_file.write_all( + toml::to_string(&Histories::from(new_history)) + .unwrap() + .as_bytes(), + )?; history_file.flush()?; Ok(()) @@ -64,49 +157,89 @@ pub fn store_history( #[cfg(test)] mod test { use super::*; + use crate::model::runner_type; use anyhow::Result; + use pretty_assertions::assert_eq; #[test] fn parse_history_test() { struct Case { title: &'static str, content: String, - expect: Result)>>, + expect: Result, } let cases = vec![ Case { title: "Success", content: r#" -[[history]] +[[histories]] path = "/Users/user/code/fzf-make" -executed-targets = ["test", "check", "spell-check"] -[[history]] +[[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" -executed-targets = ["run", "echo1"] + +[[histories.commands]] +runner-type = "make" +name = "run" + +[[histories.commands]] +runner-type = "make" +name = "echo1" "# .to_string(), - expect: Ok(vec![ - ( - PathBuf::from("/Users/user/code/fzf-make".to_string()), - vec![ - "test".to_string(), - "check".to_string(), - "spell-check".to_string(), - ], - ), - ( - PathBuf::from("/Users/user/code/golang/go-playground".to_string()), - vec!["run".to_string(), "echo1".to_string()], - ), - ]), + expect: Ok(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: Err(anyhow::anyhow!("TOML parse error at line 1, column 1\n |\n1 | \n | ^\nmissing field `history`\n")), + expect: Err(anyhow::anyhow!("TOML parse error at line 1, column 1\n |\n1 | \n | ^\nmissing field `histories`\n")), }, ]; diff --git a/src/main.rs b/src/main.rs index 58e31e1a..b080db26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,11 @@ mod controller; mod file; mod model; mod usecase; - -use crate::controller::controller_main; use std::panic; fn main() { let result = panic::catch_unwind(|| { - controller_main::run(); + controller::controller_main::run(); }); match result { Ok(_) => {} diff --git a/src/model/command.rs b/src/model/command.rs index 99ed2c9c..e405b776 100644 --- a/src/model/command.rs +++ b/src/model/command.rs @@ -1,8 +1,7 @@ -use std::{fmt, path::PathBuf}; - use super::runner_type; +use std::{fmt, path::PathBuf}; -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Clone, Debug)] pub struct Command { pub runner_type: runner_type::RunnerType, pub name: String, @@ -13,13 +12,13 @@ pub struct Command { impl Command { pub fn new( runner_type: runner_type::RunnerType, - command_name: String, + name: String, file_name: PathBuf, line_number: u32, ) -> Self { Self { runner_type, - name: command_name, + name, file_name, line_number, } @@ -28,6 +27,6 @@ impl Command { impl fmt::Display for Command { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "({}) {}", self.runner_type, self.name) + write!(f, "[{}] {}", self.runner_type, self.name) } } diff --git a/src/model/file_util.rs b/src/model/file_util.rs index e6d1f2be..a1d5f42a 100644 --- a/src/model/file_util.rs +++ b/src/model/file_util.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/model/histories.rs b/src/model/histories.rs index ac1bfde8..7b5d822e 100644 --- a/src/model/histories.rs +++ b/src/model/histories.rs @@ -1,148 +1,89 @@ -use simple_home_dir::home_dir; -use std::{env, path::PathBuf}; +use super::{command, runner_type}; +use std::path::PathBuf; +/// Histories is a all collection of History. This equals whole content of history.toml. +/// For now, we can define this as tuple like `pub struct Histories(Vec);` but we don't. +/// We respect that we can add some fields in the future easily. #[derive(Clone, PartialEq, Debug)] pub struct Histories { - histories: Vec, + pub histories: Vec, } impl Histories { - pub fn new(makefile_path: PathBuf, histories: Vec<(PathBuf, Vec)>) -> Self { - match histories.len() { - 0 => Self::default(makefile_path), - _ => Self::from(makefile_path, histories), - } - } - - // TODO(#321): Make this fn returns Vec - // pub fn get_histories(&self, paths: Vec) -> Vec { - // let mut histories: Vec = Vec::new(); - // - // for path in paths { - // let executed_targets = self - // .histories - // .iter() - // .find(|h| h.path == path) - // .map(|h| h.executed_targets.clone()) - // .unwrap_or(Vec::new()); - // histories = [histories, executed_targets].concat(); - // } - // - // histories - // } - - // pub fn append(&self, path: &PathBuf, executed_target: &str) -> Option { - // let mut new_histories = self.histories.clone(); - // - // new_histories - // .iter() - // .position(|h| h.path == *path) - // .map(|index| { - // let new_history = new_histories[index].append(executed_target.to_string()); - // new_histories[index] = new_history; - // - // Self { - // histories: new_histories, - // } - // }) - // } - - // pub fn to_tuple(&self) -> Vec<(PathBuf, Vec)> { - // let mut result = Vec::new(); - // - // for history in &self.histories { - // result.push((history.path.clone(), history.executed_targets.clone())); - // } - // result - // } - - // pub fn get_latest_target(&self, path: &PathBuf) -> Option<&String> { - // self.histories - // .iter() - // .find(|h| h.path == *path) - // .map(|h| h.executed_targets.first())? - // } - - fn default(path: PathBuf) -> Self { - let histories = vec![History::default(path)]; - Self { histories } - } - - fn from(makefile_path: PathBuf, histories: Vec<(PathBuf, Vec)>) -> Self { - let mut result = Histories { - histories: Vec::new(), + pub fn append(&self, current_dir: PathBuf, command: command::Command) -> Self { + // Update the command history for the current directory. + let new_history = { + match self.histories.iter().find(|h| h.path == current_dir) { + Some(history) => history.append(command.clone()), + None => History { + path: current_dir, + commands: vec![HistoryCommand::from(command)], + }, + } }; - for history in histories.clone() { - result.histories.push(History::from(history)); + // Update the whole histories. + let mut new_histories = self.histories.clone(); + match new_histories + .iter() + .position(|h| h.path == new_history.path) + { + Some(index) => { + new_histories[index] = new_history; + } + None => { + new_histories.insert(0, new_history); + } } - if !histories.iter().any(|h| h.0 == makefile_path) { - result.histories.push(History::default(makefile_path)); + Histories { + histories: new_histories, } - - result - } -} - -// TODO(#321): should return Result not Option(returns when it fails to get the home dir) -pub fn history_file_path() -> Option<(PathBuf, String)> { - const HISTORY_FILE_NAME: &str = "history.toml"; - - match env::var("FZF_MAKE_IS_TESTING") { - Ok(_) => { - // When testing - let cwd = std::env::current_dir().unwrap(); - Some(( - cwd.join(PathBuf::from("test_dir")), - HISTORY_FILE_NAME.to_string(), - )) - } - _ => home_dir().map(|home_dir| { - ( - home_dir.join(PathBuf::from(".config/fzf-make")), - HISTORY_FILE_NAME.to_string(), - ) - }), } } #[derive(Clone, PartialEq, Debug)] -struct History { - path: PathBuf, // TODO: rename to working_directory - executed_targets: Vec, // TODO: make this to Vec +pub struct History { + pub path: PathBuf, + /// The commands are sorted in descending order of execution time. + /// This means that the first element is the most recently executed command. + pub commands: Vec, } impl History { - fn default(path: PathBuf) -> Self { - Self { - path, - executed_targets: Vec::new(), + fn append(&self, executed_command: command::Command) -> Self { + let mut updated_commands = self.commands.clone(); + // removes the executed_command from the history + updated_commands.retain(|t| *t != HistoryCommand::from(executed_command.clone())); + updated_commands.insert(0, HistoryCommand::from(executed_command.clone())); + + const MAX_LENGTH: usize = 10; + if MAX_LENGTH < updated_commands.len() { + updated_commands.truncate(MAX_LENGTH); } - } - fn from(histories: (PathBuf, Vec)) -> Self { Self { - path: histories.0, - executed_targets: histories.1, + path: self.path.clone(), + commands: updated_commands, } } +} - // TODO(#321): remove - #[allow(dead_code)] - fn append(&self, executed_target: String) -> Self { - let mut executed_targets = self.executed_targets.clone(); - executed_targets.retain(|t| *t != executed_target); - executed_targets.insert(0, executed_target.clone()); - - const MAX_LENGTH: usize = 10; - if MAX_LENGTH < executed_targets.len() { - executed_targets.truncate(MAX_LENGTH); - } +/// In the history file, the command has only the name of the command and the runner type though +/// command::Command has `file_name`, `line_number` as well. +/// Because its file name where it's defined and line number is variable. +/// So we search them every time fzf-make is launched instead of storing them in the history file. +#[derive(PartialEq, Clone, Debug)] +pub struct HistoryCommand { + pub runner_type: runner_type::RunnerType, + pub name: String, +} +impl HistoryCommand { + pub fn from(command: command::Command) -> Self { Self { - path: self.path.clone(), - executed_targets, + runner_type: command.runner_type, + name: command.name, } } } @@ -150,79 +91,105 @@ impl History { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; + use std::path::PathBuf; #[test] - fn histories_new_test() { + fn histories_append_test() { struct Case { title: &'static str, - makefile_path: PathBuf, - histories: Vec<(PathBuf, Vec)>, - expect: Histories, + before: Histories, + command_to_append: command::Command, + after: Histories, } + + let path_to_append = PathBuf::from("/Users/user/code/fzf-make".to_string()); let cases = vec![ Case { - title: "histories.len() == 0", - makefile_path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - histories: vec![], - expect: Histories { - histories: vec![History { - path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - executed_targets: vec![], - }], - }, - }, - Case { - title: "histories.len() != 0(Including makefile_path)", - makefile_path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - histories: vec![ - ( - PathBuf::from("/Users/user/code/fzf-make".to_string()), - vec!["target1".to_string(), "target2".to_string()], - ), - ( - PathBuf::from("/Users/user/code/rustc".to_string()), - vec!["target-a".to_string(), "target-b".to_string()], - ), - ], - expect: Histories { + // Use raw string literal as workaround for + // https://github.com/rust-lang/rustfmt/issues/4800. + title: r#"The command executed is appended to the existing history if there is history for cwd."#, + before: Histories { histories: vec![ History { - path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - executed_targets: vec!["target1".to_string(), "target2".to_string()], + path: PathBuf::from("/Users/user/code/rustc".to_string()), + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], + }, + History { + path: path_to_append.clone(), + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], }, + ], + }, + command_to_append: command::Command { + runner_type: runner_type::RunnerType::Make, + name: "append".to_string(), + file_name: PathBuf::from("Makefile"), + line_number: 1, + }, + after: Histories { + histories: vec![ History { path: PathBuf::from("/Users/user/code/rustc".to_string()), - executed_targets: vec!["target-a".to_string(), "target-b".to_string()], + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], + }, + History { + path: path_to_append.clone(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "append".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + ], }, ], }, }, Case { - title: "histories.len() != 0(Not including makefile_path)", - makefile_path: PathBuf::from("/Users/user/code/cargo".to_string()), - histories: vec![ - ( - PathBuf::from("/Users/user/code/fzf-make".to_string()), - vec!["target1".to_string(), "target2".to_string()], - ), - ( - PathBuf::from("/Users/user/code/rustc".to_string()), - vec!["target-a".to_string(), "target-b".to_string()], - ), - ], - expect: Histories { + title: r#"A new history is appended if there is no history for cwd."#, + before: Histories { + histories: vec![History { + path: PathBuf::from("/Users/user/code/rustc".to_string()), + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], + }], + }, + command_to_append: command::Command { + runner_type: runner_type::RunnerType::Make, + name: "append".to_string(), + file_name: PathBuf::from("Makefile"), + line_number: 1, + }, + after: Histories { histories: vec![ History { - path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - executed_targets: vec!["target1".to_string(), "target2".to_string()], + path: path_to_append.clone(), + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "append".to_string(), + }], }, History { path: PathBuf::from("/Users/user/code/rustc".to_string()), - executed_targets: vec!["target-a".to_string(), "target-b".to_string()], - }, - History { - path: PathBuf::from("/Users/user/code/cargo".to_string()), - executed_targets: vec![], + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], }, ], }, @@ -231,184 +198,223 @@ mod test { for case in cases { assert_eq!( - case.expect, - Histories::new(case.makefile_path, case.histories), + case.after, + case.before + .append(path_to_append.clone(), case.command_to_append), "\nFailed: ๐Ÿšจ{:?}๐Ÿšจ\n", case.title, ) } } - // TODO(#321): comment in this test - // #[test] - // fn histories_append_test() { - // struct Case { - // title: &'static str, - // path: PathBuf, - // appending_target: &'static str, - // histories: Histories, - // expect: Option, - // } - // let cases = vec![ - // Case { - // title: "Success", - // path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - // appending_target: "history1", - // histories: Histories { - // histories: vec![ - // History { - // path: PathBuf::from("/Users/user/code/rustc".to_string()), - // executed_targets: vec!["history0".to_string(), "history1".to_string()], - // }, - // History { - // path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - // executed_targets: vec![ - // "history0".to_string(), - // "history1".to_string(), - // "history2".to_string(), - // ], - // }, - // ], - // }, - // expect: Some(Histories { - // histories: vec![ - // History { - // path: PathBuf::from("/Users/user/code/rustc".to_string()), - // executed_targets: vec!["history0".to_string(), "history1".to_string()], - // }, - // History { - // path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - // executed_targets: vec![ - // "history1".to_string(), - // "history0".to_string(), - // "history2".to_string(), - // ], - // }, - // ], - // }), - // }, - // Case { - // title: "Returns None when path is not found", - // path: PathBuf::from("/Users/user/code/non-existent-dir".to_string()), - // appending_target: "history1", - // histories: Histories { - // histories: vec![ - // History { - // path: PathBuf::from("/Users/user/code/rustc".to_string()), - // executed_targets: vec!["history0".to_string(), "history1".to_string()], - // }, - // History { - // path: PathBuf::from("/Users/user/code/fzf-make".to_string()), - // executed_targets: vec![ - // "history0".to_string(), - // "history1".to_string(), - // "history2".to_string(), - // ], - // }, - // ], - // }, - // expect: None, - // }, - // ]; - // - // for case in cases { - // assert_eq!( - // case.expect, - // case.histories.append(&case.path, case.appending_target), - // "\nFailed: ๐Ÿšจ{:?}๐Ÿšจ\n", - // case.title, - // ) - // } - // } #[test] fn history_append_test() { struct Case { title: &'static str, - appending_target: &'static str, - history: History, - expect: History, + before: History, + command_to_append: command::Command, + after: History, } let path = PathBuf::from("/Users/user/code/fzf-make".to_string()); let cases = vec![ Case { title: "Append to head", - appending_target: "history2", - history: History { + before: History { path: path.clone(), - executed_targets: vec!["history0".to_string(), "history1".to_string()], + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, + ], }, - expect: History { + command_to_append: command::Command::new( + runner_type::RunnerType::Make, + "history2".to_string(), + PathBuf::from("Makefile"), + 1, + ), + after: History { path: path.clone(), - executed_targets: vec![ - "history2".to_string(), - "history0".to_string(), - "history1".to_string(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, ], }, }, Case { title: "Append to head(Append to empty)", - appending_target: "history0", - history: History { + before: History { path: path.clone(), - executed_targets: vec![], + commands: vec![], }, - expect: History { + command_to_append: command::Command::new( + runner_type::RunnerType::Make, + "history0".to_string(), + PathBuf::from("Makefile"), + 4, + ), + after: History { path: path.clone(), - executed_targets: vec!["history0".to_string()], + commands: vec![HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }], }, }, Case { - title: "Append to head(Remove duplicated)", - appending_target: "history1", - history: History { + title: "Append to head(Remove duplicated command)", + before: History { path: path.clone(), - executed_targets: vec![ - "history0".to_string(), - "history1".to_string(), - "history2".to_string(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + }, ], }, - expect: History { + command_to_append: command::Command::new( + runner_type::RunnerType::Make, + "history2".to_string(), + PathBuf::from("Makefile"), + 1, + ), + after: History { path: path.clone(), - executed_targets: vec![ - "history1".to_string(), - "history0".to_string(), - "history2".to_string(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, ], }, }, Case { title: "Truncate when length exceeds 10", - appending_target: "history11", - history: History { + before: History { path: path.clone(), - executed_targets: vec![ - "history0".to_string(), - "history1".to_string(), - "history2".to_string(), - "history3".to_string(), - "history4".to_string(), - "history5".to_string(), - "history6".to_string(), - "history7".to_string(), - "history8".to_string(), - "history9".to_string(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history3".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history4".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history5".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history6".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history7".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history8".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history9".to_string(), + }, ], }, - expect: History { + command_to_append: command::Command::new( + runner_type::RunnerType::Make, + "history10".to_string(), + PathBuf::from("Makefile"), + 1, + ), + after: History { path: path.clone(), - executed_targets: vec![ - "history11".to_string(), - "history0".to_string(), - "history1".to_string(), - "history2".to_string(), - "history3".to_string(), - "history4".to_string(), - "history5".to_string(), - "history6".to_string(), - "history7".to_string(), - "history8".to_string(), + commands: vec![ + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history10".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history3".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history4".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history5".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history6".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history7".to_string(), + }, + HistoryCommand { + runner_type: runner_type::RunnerType::Make, + name: "history8".to_string(), + }, ], }, }, @@ -416,8 +422,8 @@ mod test { for case in cases { assert_eq!( - case.expect, - case.history.append(case.appending_target.to_string()), + case.after, + case.before.append(case.command_to_append), "\nFailed: ๐Ÿšจ{:?}๐Ÿšจ\n", case.title, ) diff --git a/src/model/make.rs b/src/model/make.rs index e9be47da..9cd7ef75 100644 --- a/src/model/make.rs +++ b/src/model/make.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use regex::Regex; use std::process; use std::{ - env, fs, + fs, path::{Path, PathBuf}, }; @@ -20,32 +20,22 @@ impl Make { format!("make {}", command.name) } - pub fn create_makefile() -> Result { - let Some(makefile_name) = Make::specify_makefile_name(".".to_string()) else { + pub fn new(current_dir: PathBuf) -> Result { + let Some(makefile_name) = Make::specify_makefile_name(current_dir, ".".to_string()) else { return Err(anyhow!("makefile not found.\n")); }; - Make::new(Path::new(&makefile_name).to_path_buf()) - } - - pub fn to_commands(&self) -> Vec { - let mut result: Vec = vec![]; - result.append(&mut self.targets.0.to_vec()); - for include_file in &self.include_files { - Vec::append(&mut result, &mut include_file.to_commands()); - } - - result + Make::new_internal(Path::new(&makefile_name).to_path_buf()) } // I gave up writing tests using temp_dir because it was too difficult (it was necessary to change the implementation to some extent). // It is not difficult to ensure that it works with manual tests, so I will not do it for now. - fn new(path: PathBuf) -> Result { + fn new_internal(path: PathBuf) -> Result { // If the file path does not exist, the make command cannot be executed in the first place, // so it is not handled here. let file_content = file_util::path_to_content(path.clone())?; let include_files = content_to_include_file_paths(file_content.clone()) .iter() - .map(|included_file_path| Make::new(included_file_path.clone())) + .map(|included_file_path| Make::new_internal(included_file_path.clone())) .filter_map(Result::ok) .collect(); @@ -56,7 +46,17 @@ impl Make { }) } - fn specify_makefile_name(target_path: String) -> Option { + pub fn to_commands(&self) -> Vec { + let mut result: Vec = vec![]; + result.append(&mut self.targets.0.to_vec()); + for include_file in &self.include_files { + Vec::append(&mut result, &mut include_file.to_commands()); + } + + result + } + + fn specify_makefile_name(current_dir: PathBuf, target_path: String) -> Option { // By default, when make looks for the makefile, it tries the following names, in order: GNUmakefile, makefile and Makefile. // https://www.gnu.org/software/make/manual/make.html#Makefile-Names // It needs to enumerate `Makefile` too not only `makefile` to make it work on case insensitive file system @@ -68,11 +68,6 @@ impl Make { let file_name = e.unwrap().file_name(); let file_name_string = file_name.to_str().unwrap(); if makefile_name_options.contains(&file_name_string) { - let current_dir = match env::current_dir() { - Err(_) => return None, - Ok(d) => d, - }; - temp_result.push(current_dir.join(file_name)); } } @@ -103,6 +98,7 @@ impl Make { #[cfg(test)] pub fn new_for_test() -> Make { use super::runner_type; + use std::env; Make { path: env::current_dir().unwrap().join(Path::new("Test.mk")), @@ -134,7 +130,7 @@ impl Make { /// The path should be relative path from current directory where make command is executed. /// So the path can be treated as it is. /// NOTE: path include `..` is not supported for now like `include ../c.mk`. -pub fn content_to_include_file_paths(file_content: String) -> Vec { +fn content_to_include_file_paths(file_content: String) -> Vec { let mut result: Vec = Vec::new(); for line in file_content.lines() { let Some(include_files) = line_to_including_file_paths(line.to_string()) else { @@ -173,11 +169,13 @@ fn line_to_including_file_paths(line: String) -> Option> { #[cfg(test)] mod test { - use crate::model::runner_type; - use super::*; - - use std::fs::{self, File}; + use crate::model::runner_type; + use pretty_assertions::assert_eq; + use std::{ + env, + fs::{self, File}, + }; use uuid::Uuid; #[test] @@ -230,7 +228,10 @@ mod test { assert_eq!( expect, - Make::specify_makefile_name(tmp_dir.to_string_lossy().to_string()), + Make::specify_makefile_name( + env::current_dir().unwrap(), + tmp_dir.to_string_lossy().to_string() + ), "\nFailed: ๐Ÿšจ{:?}๐Ÿšจ\n", case.title, ); diff --git a/src/model/runner.rs b/src/model/runner.rs index 6cbfc332..9a8028ae 100644 --- a/src/model/runner.rs +++ b/src/model/runner.rs @@ -6,8 +6,6 @@ use std::path::PathBuf; #[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] pub enum Runner { - // TODO(#321): Use associated constants if possible. - // ref: https://doc.rust-lang.org/reference/items/associated-items.html#associated-constants MakeCommand(make::Make), PnpmCommand(pnpm::Pnpm), } diff --git a/src/model/runner_type.rs b/src/model/runner_type.rs index dd588640..967883b4 100644 --- a/src/model/runner_type.rs +++ b/src/model/runner_type.rs @@ -1,13 +1,30 @@ +use super::runner; +use serde::{Deserialize, Serialize}; use std::fmt; -// TODO(#321): remove -#[allow(dead_code)] -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum RunnerType { Make, Pnpm, } +impl RunnerType { + pub fn to_runner(&self, runners: &Vec) -> Option { + match self { + RunnerType::Make => { + for r in runners { + if matches!(r, runner::Runner::MakeCommand(_)) { + return Some(r.clone()); + } + } + None + } + RunnerType::Pnpm => todo!("implement and write test"), + } + } +} + impl fmt::Display for RunnerType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { diff --git a/src/model/target.rs b/src/model/target.rs index 7b1ed1db..76ed9987 100644 --- a/src/model/target.rs +++ b/src/model/target.rs @@ -1,8 +1,6 @@ -use std::path::PathBuf; - -use regex::Regex; - use super::{command, runner_type}; +use regex::Regex; +use std::path::PathBuf; #[derive(Debug, Clone, PartialEq)] pub struct Targets(pub Vec); @@ -88,6 +86,7 @@ fn line_to_target(line: String) -> Option { #[cfg(test)] mod test { use super::*; + use pretty_assertions::assert_eq; #[test] fn content_to_targets_test() { diff --git a/src/usecase/help.rs b/src/usecase/help.rs index 29580216..6f13a281 100644 --- a/src/usecase/help.rs +++ b/src/usecase/help.rs @@ -30,7 +30,7 @@ USAGE: SUBCOMMANDS: repeat, --repeat, -r - Execute the last executed make target. + Execute the last executed command. history, --history, -h Launch fzf-make with the history pane focused. help, --help, -h diff --git a/src/usecase/repeat.rs b/src/usecase/repeat.rs index 85bb394d..de77ce11 100644 --- a/src/usecase/repeat.rs +++ b/src/usecase/repeat.rs @@ -1,10 +1,9 @@ -use crate::usecase::usecase_main::Usecase; -use anyhow::{anyhow, Result}; - use super::tui::{ app::{AppState, Model}, config, }; +use crate::usecase::usecase_main::Usecase; +use anyhow::{anyhow, Result}; pub struct Repeat; @@ -23,25 +22,13 @@ impl Usecase for Repeat { match Model::new(config::Config::default()) { Err(e) => Err(e), Ok(model) => match model.app_state { - AppState::SelectTarget(model) => { - match model.histories.map(|_h| { - // TODO(#321): Decide the specification of this. - // 1. Find the latest history that starts with cwd and execute it (need to save information about which one is the latest) - // 2. When there are multiple candidates, display the choices and let the user choose? - match &model.runners.first() { - Some(_runner) => { - None:: // TODO(#321): Fix this when history function is implemented - // h - // .get_latest_target(&runner.path()) - // .map(execute_make_command), - } - None => None, - } - }) { - Some(Some(_)) => Ok(()), - _ => Err(anyhow!("No target found")), - } - } + AppState::SelectCommand(state) => match state.get_latest_command() { + Some(c) => match state.get_runner(&c.runner_type) { + Some(runner) => runner.execute(c), + None => Err(anyhow!("runner not found.")), + }, + None => Err(anyhow!("fzf-make has not been executed in this path yet.")), + }, _ => Err(anyhow!("Invalid state")), }, } diff --git a/src/usecase/tui/app.rs b/src/usecase/tui/app.rs index ba63ec04..6a2ceb24 100644 --- a/src/usecase/tui/app.rs +++ b/src/usecase/tui/app.rs @@ -1,15 +1,14 @@ +use super::{config, ui::ui}; use crate::{ - file::{path_to_content, toml}, + file::toml, model::{ command, - histories::{history_file_path, Histories}, + histories::{self}, make::Make, - runner, + runner, runner_type, }, }; - -use super::{config, ui::ui}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent}, execute, @@ -23,6 +22,7 @@ use ratatui::{ }; use std::{ collections::HashMap, + env, io::{self, Stderr}, panic, path::PathBuf, @@ -36,8 +36,8 @@ use tui_textarea::TextArea; // See: https://www.youtube.com/watch?v=IcgmSRJHu_8 #[derive(PartialEq, Debug)] pub enum AppState<'a> { - SelectTarget(SelectTargetState<'a>), - ExecuteTarget(ExecuteTargetState), + SelectCommand(SelectCommandState<'a>), + ExecuteCommand(ExecuteCommandState), ShouldQuit, } @@ -48,9 +48,9 @@ pub struct Model<'a> { impl Model<'_> { pub fn new(config: config::Config) -> Result { - match SelectTargetState::new(config) { + match SelectCommandState::new(config) { Ok(s) => Ok(Model { - app_state: AppState::SelectTarget(s), + app_state: AppState::SelectCommand(s), }), Err(e) => Err(e), } @@ -58,21 +58,21 @@ impl Model<'_> { fn handle_key_input(&self, key: KeyEvent) -> Option { match &self.app_state { - AppState::SelectTarget(s) => match key.code { + AppState::SelectCommand(s) => match key.code { KeyCode::Tab => Some(Message::MoveToNextPane), KeyCode::Esc => Some(Message::Quit), _ => match s.current_pane { CurrentPane::Main => match key.code { - KeyCode::Down => Some(Message::NextTarget), - KeyCode::Up => Some(Message::PreviousTarget), - KeyCode::Enter => Some(Message::ExecuteTarget), + KeyCode::Down => Some(Message::NextCommand), + KeyCode::Up => Some(Message::PreviousCommand), + KeyCode::Enter => Some(Message::ExecuteCommand), _ => Some(Message::SearchTextAreaKeyInput(key)), }, CurrentPane::History => match key.code { KeyCode::Char('q') => Some(Message::Quit), KeyCode::Down => Some(Message::NextHistory), KeyCode::Up => Some(Message::PreviousHistory), - KeyCode::Enter | KeyCode::Char(' ') => Some(Message::ExecuteTarget), + KeyCode::Enter | KeyCode::Char(' ') => Some(Message::ExecuteCommand), _ => None, }, }, @@ -81,44 +81,75 @@ impl Model<'_> { } } - fn get_histories(makefile_path: PathBuf) -> Option { - history_file_path().map(|(history_file_dir, history_file_name)| { - let content = - match path_to_content::path_to_content(history_file_dir.join(history_file_name)) { - Err(_) => return Histories::new(makefile_path, vec![]), // NOTE: Show error message on message pane https://github.com/kyu08/fzf-make/issues/152 - Ok(c) => c, - }; - // TODO: Show error message on message pane if parsing history file failed. https://github.com/kyu08/fzf-make/issues/152 - let histories = toml::parse_history(content.to_string()).unwrap_or_default(); + // returns available commands in cwd from history file + fn get_histories( + current_working_directory: PathBuf, + runners: Vec, + ) -> Vec { + let histories = toml::Histories::into(toml::Histories::get_history()); - Histories::new(makefile_path, histories) - }) + let mut result: Vec = Vec::new(); + for history in histories.histories { + if history.path != current_working_directory { + continue; + } + result = Self::get_commands_from_history(history.commands, &runners); + break; + } + + result + } + + fn get_commands_from_history( + history_commands: Vec, + runners: &Vec, + ) -> Vec { + // TODO: Make this more readable and more performant. + let mut commands: Vec = Vec::new(); + for history_command in history_commands { + match history_command.runner_type { + runner_type::RunnerType::Make => { + for runner in runners { + if let runner::Runner::MakeCommand(make) = runner { + // PERF: This method is called every time. Memoize should be considered. + for c in make.to_commands() { + if c.name == history_command.name { + commands.push(c); + break; + } + } + } + } + } + runner_type::RunnerType::Pnpm => todo!(), + }; + } + commands } - fn transition_to_execute_target_state( + fn transition_to_execute_command_state( &mut self, runner: runner::Runner, command: command::Command, ) { - self.app_state = AppState::ExecuteTarget(ExecuteTargetState::new(runner, command)); + self.app_state = AppState::ExecuteCommand(ExecuteCommandState::new(runner, command)); } fn transition_to_should_quit_state(&mut self) { - // TODO: remove mut self.app_state = AppState::ShouldQuit; } - pub fn should_quit(&self) -> bool { + fn should_quit(&self) -> bool { self.app_state == AppState::ShouldQuit } - pub fn is_target_selected(&self) -> bool { - matches!(self.app_state, AppState::ExecuteTarget(_)) + fn is_command_selected(&self) -> bool { + matches!(self.app_state, AppState::ExecuteCommand(_)) } - pub fn command_to_execute(&self) -> Option<(runner::Runner, command::Command)> { + fn command_to_execute(&self) -> Option<(runner::Runner, command::Command)> { match &self.app_state { - AppState::ExecuteTarget(command) => { + AppState::ExecuteCommand(command) => { let command = command.clone(); Some((command.executor, command.command)) } @@ -142,7 +173,7 @@ pub fn main(config: config::Config) -> Result<()> { } let mut model = model.unwrap(); - let target = match run(&mut terminal, &mut model) { + let command = match run(&mut terminal, &mut model) { Ok(t) => t, Err(e) => { shutdown_terminal(&mut terminal)?; @@ -152,7 +183,7 @@ pub fn main(config: config::Config) -> Result<()> { shutdown_terminal(&mut terminal)?; - match target { + match command { Some((runner, command)) => { runner.show_command(&command); let _ = runner.execute(&command); // TODO: handle error @@ -184,7 +215,7 @@ fn run<'a, B: Backend>( match handle_event(model) { Ok(message) => { update(model, message); - if model.should_quit() || model.is_target_selected() { + if model.should_quit() || model.is_command_selected() { break; } } @@ -214,9 +245,9 @@ fn shutdown_terminal(terminal: &mut Terminal>) -> Resul enum Message { SearchTextAreaKeyInput(KeyEvent), - ExecuteTarget, - NextTarget, - PreviousTarget, + ExecuteCommand, + NextCommand, + PreviousCommand, MoveToNextPane, NextHistory, PreviousHistory, @@ -237,20 +268,19 @@ fn handle_event(model: &Model) -> io::Result> { // TODO: make this method Model's method // TODO: Make this function returns `Result` or have a field like Model.error to hold errors fn update(model: &mut Model, message: Option) { - if let AppState::SelectTarget(ref mut s) = model.app_state { + if let AppState::SelectCommand(ref mut s) = model.app_state { match message { Some(Message::SearchTextAreaKeyInput(key_event)) => s.handle_key_input(key_event), - Some(Message::ExecuteTarget) => { - if let Some(command) = s.get_selected_target() { - // TODO: make this a method of SelectTargetState - s.store_history(&command); - let executor: runner::Runner = s.runners[0].clone(); - - model.transition_to_execute_target_state(executor, command); + Some(Message::ExecuteCommand) => { + if let Some(command) = s.get_selected_command() { + s.store_history(command.clone()); + if let Some(r) = command.runner_type.to_runner(&s.runners) { + model.transition_to_execute_command_state(r, command); + } }; } - Some(Message::NextTarget) => s.next_target(), - Some(Message::PreviousTarget) => s.previous_target(), + Some(Message::NextCommand) => s.next_command(), + Some(Message::PreviousCommand) => s.previous_command(), Some(Message::MoveToNextPane) => s.move_to_next_pane(), Some(Message::NextHistory) => s.next_history(), Some(Message::PreviousHistory) => s.previous_history(), @@ -261,23 +291,27 @@ fn update(model: &mut Model, message: Option) { } #[derive(Debug)] -pub struct SelectTargetState<'a> { +pub struct SelectCommandState<'a> { + pub current_dir: PathBuf, pub current_pane: CurrentPane, pub runners: Vec, pub search_text_area: TextArea_<'a>, - pub targets_list_state: ListState, - pub histories: Option, - pub histories_list_state: ListState, + pub commands_list_state: ListState, + /// This field could have been of type `Vec`, but it was intentionally made of type `Vec`. + /// This is because it allows for future features such as displaying the contents of history in the preview window + /// or hiding commands that existed at the time of execution but no longer exist. + pub history: Vec, + pub history_list_state: ListState, } -impl PartialEq for SelectTargetState<'_> { +impl PartialEq for SelectCommandState<'_> { fn eq(&self, other: &Self) -> bool { - let without_runners = self.current_pane == other.current_pane + let other_than_runners = self.current_pane == other.current_pane && self.search_text_area == other.search_text_area - && self.targets_list_state == other.targets_list_state - && self.histories == other.histories - && self.histories_list_state == other.histories_list_state; - if !without_runners { + && self.commands_list_state == other.commands_list_state + && self.history == other.history + && self.history_list_state == other.history_list_state; + if !other_than_runners { return false; // Early return for performance } @@ -295,9 +329,13 @@ impl PartialEq for SelectTargetState<'_> { } } -impl SelectTargetState<'_> { +impl SelectCommandState<'_> { pub fn new(config: config::Config) -> Result { - let makefile = match Make::create_makefile() { + let current_dir = match env::current_dir() { + Ok(d) => d, + Err(e) => bail!("Failed to get current directory: {}", e), + }; + let makefile = match Make::new(current_dir.clone()) { Err(e) => return Err(e), Ok(f) => f, }; @@ -308,21 +346,22 @@ impl SelectTargetState<'_> { CurrentPane::Main }; let runner = { runner::Runner::MakeCommand(makefile) }; + let runners = vec![runner]; - let path = runner.path(); - Ok(SelectTargetState { + Ok(SelectCommandState { + current_dir: current_dir.clone(), current_pane, - runners: vec![runner], + runners: runners.clone(), search_text_area: TextArea_(TextArea::default()), - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), - histories: Model::get_histories(path), - histories_list_state: ListState::with_selected(ListState::default(), Some(0)), + commands_list_state: ListState::with_selected(ListState::default(), Some(0)), + history: Model::get_histories(current_dir, runners), + history_list_state: ListState::with_selected(ListState::default(), Some(0)), }) } - fn get_selected_target(&self) -> Option { + fn get_selected_command(&self) -> Option { match self.current_pane { - CurrentPane::Main => self.selected_target(), + CurrentPane::Main => self.selected_command(), CurrentPane::History => self.selected_history(), } } @@ -334,21 +373,9 @@ impl SelectTargetState<'_> { } } - // TODO(#321): comment in this method - // TODO: This method should return Result when it fails. - // pub fn append_history(&self, command: &str) -> Option { - // match &self.histories { - // Some(histories) => { - // histories.append(&self.runners[0].path(), command) - // // TODO(#321): For now, it is &self.runners[0] to pass the compilation, but it should be taken from runner::Command::path() - // } - // _ => None, - // } - // } - - fn selected_target(&self) -> Option { - match self.targets_list_state.selected() { - Some(i) => self.narrow_down_targets().get(i).cloned(), + fn selected_command(&self) -> Option { + match self.commands_list_state.selected() { + Some(i) => self.narrow_down_commands().get(i).cloned(), None => None, } } @@ -359,13 +386,13 @@ impl SelectTargetState<'_> { return None; } - match self.histories_list_state.selected() { + match self.history_list_state.selected() { Some(i) => history.get(i).cloned(), None => None, } } - pub fn narrow_down_targets(&self) -> Vec { + pub fn narrow_down_commands(&self) -> Vec { let commands = { let mut commands: Vec = Vec::new(); for runner in &self.runners { @@ -389,19 +416,19 @@ impl SelectTargetState<'_> { let matcher = SkimMatcherV2::default(); let mut list: Vec<(i64, String)> = commands .into_iter() - .filter_map(|target| { + .filter_map(|command| { let mut key_input = self.search_text_area.0.lines().join(""); key_input.retain(|c| !c.is_whitespace()); matcher - .fuzzy_indices(&target.to_string(), key_input.as_str()) - .map(|(score, _)| (score, target.to_string())) + .fuzzy_indices(&command.to_string(), key_input.as_str()) + .map(|(score, _)| (score, command.to_string())) }) .collect(); list.sort_by(|(score1, _), (score2, _)| score1.cmp(score2)); list.reverse(); - list.into_iter().map(|(_, target)| target).collect() + list.into_iter().map(|(_, command)| command).collect() }; let mut result: Vec = Vec::new(); @@ -416,30 +443,18 @@ impl SelectTargetState<'_> { } pub fn get_history(&self) -> Vec { - vec![] - // TODO(#321): implement when history function is implemented - // UIใซ่กจ็คบใ™ใ‚‹ใŸใ‚ใฎhistoryไธ€่ฆงใ‚’ๅ–ๅพ—ใ™ใ‚‹้–ขๆ•ฐใ€‚ - // runnersใ‚’ๆธกใ™ใจ้–ข้€ฃใ™ใ‚‹historyไธ€่ฆงใ‚’่ฟ”ใ™ใ‚ˆใ†ใซใ™ใ‚‹ใฎใŒใ‚ˆใ•ใใ†ใ€‚ - // let paths = self - // .runners - // .iter() - // .map(|r| r.path()) - // .collect::>(); - // - // self.histories - // .clone() - // .map_or(Vec::new(), |h| h.get_histories(paths)) - } - - fn next_target(&mut self) { - if self.narrow_down_targets().is_empty() { - self.targets_list_state.select(None); + self.history.clone() + } + + fn next_command(&mut self) { + if self.narrow_down_commands().is_empty() { + self.commands_list_state.select(None); return; } - let i = match self.targets_list_state.selected() { + let i = match self.commands_list_state.selected() { Some(i) => { - if self.narrow_down_targets().len() - 1 <= i { + if self.narrow_down_commands().len() - 1 <= i { 0 } else { i + 1 @@ -447,36 +462,36 @@ impl SelectTargetState<'_> { } None => 0, }; - self.targets_list_state.select(Some(i)); + self.commands_list_state.select(Some(i)); } - fn previous_target(&mut self) { - if self.narrow_down_targets().is_empty() { - self.targets_list_state.select(None); + fn previous_command(&mut self) { + if self.narrow_down_commands().is_empty() { + self.commands_list_state.select(None); return; } - let i = match self.targets_list_state.selected() { + let i = match self.commands_list_state.selected() { Some(i) => { if i == 0 { - self.narrow_down_targets().len() - 1 + self.narrow_down_commands().len() - 1 } else { i - 1 } } None => 0, }; - self.targets_list_state.select(Some(i)); + self.commands_list_state.select(Some(i)); } fn next_history(&mut self) { let history_list = self.get_history(); if history_list.is_empty() { - self.histories_list_state.select(None); + self.history_list_state.select(None); return; }; - let i = match self.histories_list_state.selected() { + let i = match self.history_list_state.selected() { Some(i) => { if history_list.len() - 1 <= i { 0 @@ -486,17 +501,17 @@ impl SelectTargetState<'_> { } None => 0, }; - self.histories_list_state.select(Some(i)); + self.history_list_state.select(Some(i)); } fn previous_history(&mut self) { let history_list_len = self.get_history().len(); match history_list_len { 0 => { - self.histories_list_state.select(None); + self.history_list_state.select(None); } _ => { - let i = match self.histories_list_state.selected() { + let i = match self.history_list_state.selected() { Some(i) => { if i == 0 { history_list_len - 1 @@ -506,7 +521,7 @@ impl SelectTargetState<'_> { } None => 0, }; - self.histories_list_state.select(Some(i)); + self.history_list_state.select(Some(i)); } }; } @@ -518,70 +533,96 @@ impl SelectTargetState<'_> { self.search_text_area.0.input(key_event); } - fn store_history(&mut self, _command: &command::Command) { - // TODO(#321): implement when history function is implemented - // NOTE: self.get_selected_target should be called before self.append_history. + fn store_history(&self, command: command::Command) { + // NOTE: self.get_selected_command should be called before self.append_history. // Because self.histories_list_state.selected keeps the selected index of the history list // before update. - // if let Some(h) = self.append_history(command) { - // self.histories = Some(h) - // }; - // if let (Some((dir, file_name)), Some(h)) = (history_file_path(), &self.histories) { - // // TODO: handle error - // let _ = toml::store_history(dir, file_name, h.to_tuple()); - // }; + if let Some((dir, file_name)) = toml::history_file_path() { + let all_histories = toml::Histories::get_history() + .into() + .append(self.current_dir.clone(), command); + + // TODO: handle error + let _ = toml::create_or_update_history_file(dir, file_name, all_histories); + }; } fn reset_selection(&mut self) { - if self.narrow_down_targets().is_empty() { - self.targets_list_state.select(None); + if self.narrow_down_commands().is_empty() { + self.commands_list_state.select(None); } - self.targets_list_state.select(Some(0)); + self.commands_list_state.select(Some(0)); } pub fn get_search_area_text(&self) -> String { self.search_text_area.0.lines().join("") } - #[cfg(test)] - fn init_histories(history_targets: Vec) -> Option { - use std::{env, path::Path}; + pub fn get_latest_command(&self) -> Option<&command::Command> { + self.history.first() + } - let makefile_path = env::current_dir().unwrap().join(Path::new("Test.mk")); - Some(Histories::new( - makefile_path.clone(), - vec![(makefile_path, history_targets)], - )) + pub fn get_runner(&self, runner_type: &runner_type::RunnerType) -> Option { + for runner in &self.runners { + match (runner_type, runner) { + (runner_type::RunnerType::Make, runner::Runner::MakeCommand(_)) => { + return Some(runner.clone()); + } + (runner_type::RunnerType::Pnpm, runner::Runner::PnpmCommand(_)) => { + return Some(runner.clone()); + } + _ => continue, + } + } + None } #[cfg(test)] fn new_for_test() -> Self { - SelectTargetState { + use crate::model::runner_type; + + SelectCommandState { + current_dir: env::current_dir().unwrap(), current_pane: CurrentPane::Main, runners: vec![runner::Runner::MakeCommand(Make::new_for_test())], search_text_area: TextArea_(TextArea::default()), - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), - histories: SelectTargetState::init_histories(vec![ - "history0".to_string(), - "history1".to_string(), - "history2".to_string(), - ]), - histories_list_state: ListState::with_selected(ListState::default(), Some(0)), + commands_list_state: ListState::with_selected(ListState::default(), Some(0)), + history: vec![ + command::Command { + runner_type: runner_type::RunnerType::Make, + name: "history0".to_string(), + file_name: PathBuf::from("Makefile"), + line_number: 1, + }, + command::Command { + runner_type: runner_type::RunnerType::Make, + name: "history1".to_string(), + file_name: PathBuf::from("Makefile"), + line_number: 4, + }, + command::Command { + runner_type: runner_type::RunnerType::Make, + name: "history2".to_string(), + file_name: PathBuf::from("Makefile"), + line_number: 7, + }, + ], + history_list_state: ListState::with_selected(ListState::default(), Some(0)), } } } #[derive(Clone, Debug, PartialEq)] -pub struct ExecuteTargetState { +pub struct ExecuteCommandState { /// It is possible to have one concrete type like Command struct here. /// But from the perspective of simpleness of code base, this field has trait object. executor: runner::Runner, command: command::Command, } -impl ExecuteTargetState { +impl ExecuteCommandState { fn new(executor: runner::Runner, command: command::Command) -> Self { - ExecuteTargetState { executor, command } + ExecuteCommandState { executor, command } } } @@ -613,9 +654,9 @@ impl<'a> PartialEq for TextArea_<'a> { #[cfg(test)] mod test { - use crate::model::runner_type; - use super::*; + use crate::model::runner_type; + use pretty_assertions::assert_eq; use std::env; #[test] @@ -630,40 +671,40 @@ mod test { Case { title: "MoveToNextPane(Main -> History)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { + app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::Main, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, message: Some(Message::MoveToNextPane), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { + app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, }, Case { title: "MoveToNextPane(History -> Main)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { + app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::History, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, message: Some(Message::MoveToNextPane), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { + app_state: AppState::SelectCommand(SelectCommandState { current_pane: CurrentPane::Main, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, }, Case { title: "Quit", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + ..SelectCommandState::new_for_test() }), }, message: Some(Message::Quit), @@ -674,97 +715,118 @@ mod test { Case { title: "SearchTextAreaKeyInput(a)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + ..SelectCommandState::new_for_test() }), }, message: Some(Message::SearchTextAreaKeyInput(KeyEvent::from( KeyCode::Char('a'), ))), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { + app_state: AppState::SelectCommand(SelectCommandState { search_text_area: { let mut text_area = TextArea::default(); text_area.input(KeyEvent::from(KeyCode::Char('a'))); TextArea_(text_area) }, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, }, Case { title: "Next(0 -> 1)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + ..SelectCommandState::new_for_test() }), }, - message: Some(Message::NextTarget), + message: Some(Message::NextCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(1)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(1), + ), + ..SelectCommandState::new_for_test() }), }, }, Case { title: "Next(2 -> 0)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(2)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(2), + ), + ..SelectCommandState::new_for_test() }), }, - message: Some(Message::NextTarget), + message: Some(Message::NextCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), + ..SelectCommandState::new_for_test() }), }, }, Case { title: "Previous(1 -> 0)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(1)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(1), + ), + ..SelectCommandState::new_for_test() }), }, - message: Some(Message::PreviousTarget), + message: Some(Message::PreviousCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), + ..SelectCommandState::new_for_test() }), }, }, Case { title: "Previous(0 -> 2)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), + ..SelectCommandState::new_for_test() }), }, - message: Some(Message::PreviousTarget), + message: Some(Message::PreviousCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(2)), - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(2), + ), + ..SelectCommandState::new_for_test() }), }, }, Case { - title: "ExecuteTarget(Main)", + title: "ExecuteCommand(Main)", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - ..SelectTargetState::new_for_test() + app_state: AppState::SelectCommand(SelectCommandState { + ..SelectCommandState::new_for_test() }), }, - message: Some(Message::ExecuteTarget), + message: Some(Message::ExecuteCommand), expect_model: Model { - app_state: AppState::ExecuteTarget(ExecuteTargetState::new( + app_state: AppState::ExecuteCommand(ExecuteCommandState::new( runner::Runner::MakeCommand(Make::new_for_test()), command::Command::new( runner_type::RunnerType::Make, @@ -775,70 +837,72 @@ mod test { )), }, }, - // TODO(#321): comment in this test - // Case { - // title: "ExecuteTarget(History)", - // model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(1), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // message: Some(Message::ExecuteTarget), - // expect_model: Model { - // app_state: AppState::ExecuteTarget(ExecuteTargetState::new( - // runner::Runner::MakeCommand(Make::new_for_test()), - // command::Command::new( - // runner_type::RunnerType::Make, - // "history1".to_string(), - // PathBuf::new(), - // 4, - // ) - // )), - // }, - // }, + Case { + title: "ExecuteCommand(History)", + model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected(ListState::default(), Some(1)), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::ExecuteCommand), + expect_model: Model { + app_state: AppState::ExecuteCommand(ExecuteCommandState::new( + runner::Runner::MakeCommand(Make::new_for_test()), + command::Command::new( + runner_type::RunnerType::Make, + "history1".to_string(), + PathBuf::from("Makefile"), + 4, + ), + )), + }, + }, Case { title: "Selecting position should be reset if some kind of char - was inputted when the target located not in top of the targets", + was inputted when the command located not in top of the commands", model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(1)), - ..SelectTargetState::new_for_test() + 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::Char('a'), ))), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), Some(0)), + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), search_text_area: { let mut text_area = TextArea::default(); text_area.input(KeyEvent::from(KeyCode::Char('a'))); TextArea_(text_area) }, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, }, Case { - title: "NextTarget when there is no targets to select, panic should not occur", + title: "NextCommand when there is no commands to select, panic should not occur", model: { let mut m = Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected( + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( ListState::default(), None, ), - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }; update( - // There should not be targets because init_model has ["target0", "target1", "target2"] as target. + // There should not be commands because init_model has ["target0", "target1", "target2"] as command. &mut m, Some(Message::SearchTextAreaKeyInput(KeyEvent::from( KeyCode::Char('w'), @@ -846,33 +910,33 @@ mod test { ); m }, - message: Some(Message::NextTarget), + message: Some(Message::NextCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), None), + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected(ListState::default(), None), search_text_area: { let mut text_area = TextArea::default(); text_area.input(KeyEvent::from(KeyCode::Char('w'))); TextArea_(text_area) }, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }, }, Case { - title: "PreviousTarget when there is no targets 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::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected( + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected( ListState::default(), None, ), - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() }), }; update( - // There should not be targets because init_model has ["target0", "target1", "target2"] as target. + // There should not be commands because init_model has ["target0", "target1", "target2"] as command. &mut m, Some(Message::SearchTextAreaKeyInput(KeyEvent::from( KeyCode::Char('w'), @@ -880,119 +944,103 @@ mod test { ); m }, - message: Some(Message::PreviousTarget), + message: Some(Message::PreviousCommand), expect_model: Model { - app_state: AppState::SelectTarget(SelectTargetState { - targets_list_state: ListState::with_selected(ListState::default(), None), + app_state: AppState::SelectCommand(SelectCommandState { + commands_list_state: ListState::with_selected(ListState::default(), None), search_text_area: { let mut text_area = TextArea::default(); text_area.input(KeyEvent::from(KeyCode::Char('w'))); TextArea_(text_area) }, - ..SelectTargetState::new_for_test() + ..SelectCommandState::new_for_test() + }), + }, + }, + Case { + title: "NextHistory", + model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected(ListState::default(), Some(0)), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::NextHistory), + expect_model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected(ListState::default(), Some(1)), + ..SelectCommandState::new_for_test() + }), + }, + }, + Case { + title: "PreviousHistory", + model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected(ListState::default(), Some(0)), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::NextHistory), + expect_model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected(ListState::default(), Some(1)), + ..SelectCommandState::new_for_test() + }), + }, + }, + Case { + 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), + ), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::NextHistory), + expect_model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + 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.", + model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected( + ListState::default(), + Some(0), + ), + ..SelectCommandState::new_for_test() + }), + }, + message: Some(Message::PreviousHistory), + expect_model: Model { + app_state: AppState::SelectCommand(SelectCommandState { + current_pane: CurrentPane::History, + history_list_state: ListState::with_selected( + ListState::default(), + Some(2), + ), + ..SelectCommandState::new_for_test() }), }, }, - // TODO(#321): comment in this test - // Case { - // title: "NextHistory", - // model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(0), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // message: Some(Message::NextHistory), - // expect_model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(1), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // }, - // TODO(#321): comment in this test - // Case { - // title: "PreviousHistory", - // model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(0), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // message: Some(Message::NextHistory), - // expect_model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(1), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // }, - // TODO(#321): comment in this test - // Case { - // title: "When the last history is selected and NextHistory is received, it returns to the beginning.", - // model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(2), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // message: Some(Message::NextHistory), - // expect_model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(0), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // }, - // TODO(#321): comment in this test - // Case { - // title: "When the first history is selected and PreviousHistory is received, it moves to the last history.", - // model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(0), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // message: Some(Message::PreviousHistory), - // expect_model: Model { - // app_state: AppState::SelectTarget(SelectTargetState { - // current_pane: CurrentPane::History, - // histories_list_state: ListState::with_selected( - // ListState::default(), - // Some(2), - // ), - // ..SelectTargetState::new_for_test() - // }), - // }, - // }, ]; // NOTE: When running tests, you need to set FZF_MAKE_IS_TESTING=true. Otherwise, the developer's history file will be overwritten. diff --git a/src/usecase/tui/ui.rs b/src/usecase/tui/ui.rs index 4863a331..1d0059f1 100644 --- a/src/usecase/tui/ui.rs +++ b/src/usecase/tui/ui.rs @@ -1,11 +1,5 @@ -use std::{ - path::PathBuf, - sync::{Arc, RwLock}, -}; - +use super::app::{AppState, CurrentPane, Model, SelectCommandState}; use crate::model::command; - -use super::app::{AppState, CurrentPane, Model, SelectTargetState}; use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; use ratatui::{ layout::{Constraint, Direction, Layout}, @@ -14,10 +8,14 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, Frame, }; +use std::{ + path::PathBuf, + sync::{Arc, RwLock}, +}; use tui_term::widget::PseudoTerminal; pub fn ui(f: &mut Frame, model: &mut Model) { - if let AppState::SelectTarget(model) = &mut model.app_state { + if let AppState::SelectCommand(model) = &mut model.app_state { let main_and_key_bindings = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(3), Constraint::Length(1)]) @@ -30,18 +28,18 @@ pub fn ui(f: &mut Frame, model: &mut Model) { .split(main_and_key_bindings[0]); render_input_block(model, f, main[1]); - let preview_and_targets = Layout::default() + let preview_and_commands = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) .split(main[0]); - render_preview_block(model, f, preview_and_targets[0]); + render_preview_block(model, f, preview_and_commands[0]); - let targets = Layout::default() + let commands = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) - .split(preview_and_targets[1]); - render_targets_block(model, f, targets[0]); - render_history_block(model, f, targets[1]); + .split(preview_and_commands[1]); + render_commands_block(model, f, commands[0]); + render_history_block(model, f, commands[1]); } } @@ -64,11 +62,11 @@ fn color_and_border_style_for_selectable( } // Because the setup process of the terminal and render_widget function need to be done in the same scope, the call of the render_widget function is included. -fn render_preview_block(model: &SelectTargetState, f: &mut Frame, chunk: ratatui::layout::Rect) { - let narrow_down_targets = model.narrow_down_targets(); +fn render_preview_block(model: &SelectCommandState, f: &mut Frame, chunk: ratatui::layout::Rect) { + let narrow_down_commands = model.narrow_down_commands(); let selecting_command = - narrow_down_targets.get(model.targets_list_state.selected().unwrap_or(0)); + narrow_down_commands.get(model.commands_list_state.selected().unwrap_or(0)); let (fg_color_, border_style) = color_and_border_style_for_selectable(model.current_pane.is_main()); @@ -81,7 +79,7 @@ fn render_preview_block(model: &SelectTargetState, f: &mut Frame, chunk: ratatui .title(title) .title_style(TITLE_STYLE); - if !model.get_search_area_text().is_empty() && narrow_down_targets.is_empty() { + if !model.get_search_area_text().is_empty() && narrow_down_commands.is_empty() { f.render_widget(block, chunk); return; } @@ -159,24 +157,24 @@ fn preview_command(file_path: PathBuf, line_number: u32) -> CommandBuilder { cmd } -fn render_targets_block( - model: &mut SelectTargetState, +fn render_commands_block( + model: &mut SelectCommandState, f: &mut Frame, chunk: ratatui::layout::Rect, ) { f.render_stateful_widget( - targets_block( - " ๐Ÿ“ข Targets ", - model.narrow_down_targets(), + commands_block( + " ๐Ÿ“ข Commands ", + model.narrow_down_commands(), model.current_pane.is_main(), ), chunk, // NOTE: It is against TEA's way to update the model value on the UI side, but it is unavoidable so it is allowed. - &mut model.targets_list_state, + &mut model.commands_list_state, ); } -fn render_input_block(model: &mut SelectTargetState, f: &mut Frame, chunk: ratatui::layout::Rect) { +fn render_input_block(model: &mut SelectCommandState, f: &mut Frame, chunk: ratatui::layout::Rect) { let (fg_color, border_style) = color_and_border_style_for_selectable(model.current_pane.is_main()); @@ -193,35 +191,35 @@ fn render_input_block(model: &mut SelectTargetState, f: &mut Frame, chunk: ratat model .search_text_area .0 - .set_placeholder_text("Type text to search target"); + .set_placeholder_text("Type text to search command"); f.render_widget(&model.search_text_area.0, chunk); } fn render_history_block( - model: &mut SelectTargetState, + model: &mut SelectCommandState, f: &mut Frame, chunk: ratatui::layout::Rect, ) { f.render_stateful_widget( - targets_block( + commands_block( " ๐Ÿ“š History ", model.get_history(), model.current_pane.is_history(), ), chunk, // NOTE: It is against TEA's way to update the model value on the UI side, but it is unavoidable so it is allowed. - &mut model.histories_list_state, + &mut model.history_list_state, ); } -fn render_hint_block(model: &mut SelectTargetState, f: &mut Frame, chunk: ratatui::layout::Rect) { +fn render_hint_block(model: &mut SelectCommandState, f: &mut Frame, chunk: ratatui::layout::Rect) { let hint_text = match model.current_pane { CurrentPane::Main => { - "Execute the selected target: | Select target: โ†‘/โ†“ | Narrow down target: (type any character) | Move to next tab: | Quit: " + "Execute the selected command: | Select command: โ†‘/โ†“ | Narrow down command: (type any character) | Move to next tab: | Quit: " } CurrentPane::History => { - "Execute the selected target: | Select target: โ†‘/โ†“ | Move to next tab: | Quit: q/" + "Execute the selected command: | Select command: โ†‘/โ†“ | Move to next tab: | Quit: q/" } }; let hint = Span::styled(hint_text, Style::default().fg(FG_COLOR_SELECTED)); @@ -232,16 +230,16 @@ fn render_hint_block(model: &mut SelectTargetState, f: &mut Frame, chunk: ratatu f.render_widget(key_notes_footer, chunk); } -fn targets_block( +fn commands_block( title: &str, - narrowed_down_targets: Vec, + narrowed_down_commands: Vec, is_current: bool, ) -> List<'_> { let (fg_color, border_style) = color_and_border_style_for_selectable(is_current); - let list: Vec = narrowed_down_targets + let list: Vec = narrowed_down_commands .into_iter() - .map(|target| ListItem::new(target.to_string()).style(Style::default())) + .map(|command| ListItem::new(command.to_string()).style(Style::default())) .collect(); List::new(list)