diff --git a/README.md b/README.md index bb17bf9..a038ccd 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,4 @@ My starting point is [this article](https://www.joshmcguigan.com/blog/build-your ## To fix * Autocomplete : when nothing found, return input * Autocomplete : Weird permission issues sometimes (ignore unallowed files and folders) +* Bug with find_common_chars (if I type Cargo.t and press tab, we can't choose between Cargo.toml and Cargo.lock) diff --git a/src/lib.rs b/src/lib.rs index c1ac871..53ca509 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,8 +248,9 @@ pub mod shell { if *&mycommand.len() == 0 { continue; } + //Search - let (res, list) = autocomplete(&mycommand); + let (res, list) = Search::build(&mycommand).unwrap().autocomplete(); write!(stdout, "\r\n{}\r\n", list); mycommand.clear(); mycommand.push_str(&res); diff --git a/src/shell/autocomplete.rs b/src/shell/autocomplete.rs index b1da450..e9f7c1d 100644 --- a/src/shell/autocomplete.rs +++ b/src/shell/autocomplete.rs @@ -4,76 +4,118 @@ use std::path::PathBuf; use std::{env, fs}; -pub fn autocomplete(input: &String) -> (String, String) { - if !input.contains(' ') && !input.contains("./") { - return autocomplete_cmd(input); - } else { - match autocomplete_file(input) { - Ok(t) => return t, - Err(e) => { - println!("\n\r{}", e); - return (String::from(input), String::new()); - }, +///Contains all necesseary info and methods to perform an autocomplete search. +///Built from user input. +#[derive(Debug)] +pub struct Search { + //Left part of user input. May be empty. in "cat foo", command is cat and searchee is foo. + command: String, + //Where to search. Current_folder by default, but the user may have given a path + //.eg "cat /foo/bar" + search_path: PathBuf, + //What to search for + searchee: String, + //Set to true if we are searching in current folder. + local_search: bool, +} + +impl Search { + ///Returns a Search instance, taking the command line input. + pub fn build(input: &String) -> std::io::Result { + + //Getting cmd and searched string + let mut words = input.split(' ').collect::>(); + let buff_searchee = String::from(*words.last().unwrap_or(&"")); + words.pop(); + let input_cmd = words.join(" "); + + //Are we looking in a specific path ? + //eg. am I doing cat mystuff.txt or cat folder/subfolder/mystuff.txt ? + let buff_search = buff_searchee + .replace(" ", ""); + let mut buff_search = buff_search + .split('/') + .collect::>(); + let searchee = String::from(*buff_search.last().unwrap_or(&"")); + buff_search.pop(); + let mut search_path = PathBuf::new(); + let mut local = false; + + //We may need to modify the searchpath before storing it as a PathBuf + let mut path_temp = String::new(); + //NOT local path; + if buff_search.len() > 0 { + *&mut path_temp = buff_search.join("/"); + //Making sure we have the final / in the search_path + if !path_temp.ends_with('/') { + *&mut path_temp.push_str("/"); + } + + //local path; + } else { + *&mut path_temp = env::current_dir()? + .into_os_string() + .into_string() + .unwrap(); + if !&path_temp.ends_with('/') { + *&mut path_temp.push_str("/"); + } + local = true; + } + search_path.push(&path_temp); + + let s = Search { + command: input_cmd, + search_path: search_path, + searchee: searchee, + local_search: local, }; + return Ok(s); + } + + ///Performs the search and returns (most likely result, result lines to display) + ///A found file is preferred to a found command + pub fn autocomplete(&self) -> (String, String) { + let (found_cmd, found_cmd_lines) = autocomplete_cmd(&self.searchee); + let (found_file, found_file_lines) = match autocomplete_file(&self){ + Ok(t) => t, + Err(_) => (String::new(), String::new()), + }; + + let mut res_lines = String::new(); + if found_file.len() > 0 { + res_lines = found_file_lines.clone(); + } else { + res_lines = found_cmd_lines.clone(); + } + + let mut res = String::new(); + if found_file.len() > 0 { + res = String::from(&found_file); + } else { + res = String::from(&found_cmd); + } + if !&self.local_search { + let return_val = format!("{} {}{}", &self.command, &self.search_path.display(), res); + return(return_val, res_lines); + } else { + let return_val = format!("{} {}", &self.command, res); + return(return_val, res_lines); + } } } + ///Look for a file using the string after the last space in the input, ///in the current folder. -fn autocomplete_file(input: &String) -> std::io::Result<(String, String)> { +fn autocomplete_file(search: &Search) -> std::io::Result<(String, String)> { - //Keep the last part of the cmd... - let mut input_searchee = match input - .split(' ') - .collect::>() - .last() - .copied() { - Some(c) => c, - None => { - return Ok((String::from(""), String::from(""))); - }, - }; - - //And also the beginning ! - let mut input_buff = input - .split(' ') - .collect::>(); - _ = input_buff.pop(); - let input_origin = format!("{} ", input_buff.join(" ")); - - //Store the results - let mut results = Vec::new(); - - //Where do we search ? - let mut folder_to_search = PathBuf::new(); - //Display folder in search result ? - let mut searching_not_current = false; - //Searching somewhere else... - //Here we need to correctly divide the string into the folder - //and the searched file itself. The next 10 lines or so of code - //suck, but sometimes Rust pisses in your boots. - if input_searchee.contains('/') { - //Correctly taking first and last part... - let splitted_searchstring = &input_searchee.split('/'); - let trimmed_searchee = &splitted_searchstring.clone().last().unwrap(); - let splitted_searchstring = &mut input_searchee.clone().split('/').collect::>(); - splitted_searchstring.pop(); - let mut tosearch = splitted_searchstring.join("/"); - tosearch.push_str("/"); - folder_to_search.push(tosearch); - *&mut input_searchee = &trimmed_searchee; - *&mut searching_not_current = true; - //Searching the current folder - } else { - folder_to_search = env::current_dir()?; - } - - //Search happens 'round here let mut all_res = String::new(); let mut nbr_found: u16 = 0; let mut last_found = String::new(); + let mut results = Vec::new(); - for file in fs::read_dir(&folder_to_search)? { + for file in fs::read_dir(search.search_path.clone())? { let filepath = file.unwrap().path(); let filename = filepath .iter() @@ -81,20 +123,20 @@ fn autocomplete_file(input: &String) -> std::io::Result<(String, String)> { .unwrap() .to_str() .unwrap(); - let re = format!("^{}.*$", input_searchee); + let re = format!("^{}.*$", search.searchee); let regex = Regex::new(&re).unwrap(); if regex.is_match(filename) { - if !searching_not_current { + if search.local_search { *&mut results.push(format!("{}", filename)); } else { - *&mut results.push(format!("{}{}", &folder_to_search.display(), filename)); + *&mut results.push(format!("{}{}", &search.search_path.display(), filename)); } *&mut nbr_found += 1; let matchline = &format!("\r\n* {}", filename); &mut all_res.push_str(&matchline); //if what we found is a folder, we add an / after - let fullpath = format!("{}/{}", &folder_to_search.display(), filename); + let fullpath = format!("{}/{}", &search.search_path.display(), filename); let md = fs::metadata(fullpath).unwrap(); //Will be added to the end of the result let mut ending = String::new(); @@ -102,15 +144,12 @@ fn autocomplete_file(input: &String) -> std::io::Result<(String, String)> { *&mut ending.push_str("/"); } - if searching_not_current { - *&mut last_found = format!("{}{}{}{}", - &input_origin, - &folder_to_search.display(), + if !search.local_search { + *&mut last_found = format!("{}{}", filename, ending); } else { - *&mut last_found = format!("{}{}{}", - &input_origin, + *&mut last_found = format!("{}{}", filename, ending); } @@ -123,8 +162,7 @@ fn autocomplete_file(input: &String) -> std::io::Result<(String, String)> { //Otherwise, just display a list and use what all the results have in common } else { let longest = find_common_chars(results); - let ret_string = format!("{}{}", input_origin, &longest); - return Ok((ret_string, all_res)); + return Ok((longest, all_res)); } } @@ -142,7 +180,7 @@ fn autocomplete_cmd(input: &String) -> (String, String) { return (res_string, res_list); } else if found_bins.len() == 0 { let list = String::from("Nothing adequate."); - let res = String::from(input); + let res = String::new(); return (res, list); } else { let mut all_res = String::new(); diff --git a/src/shell/autocomplete_old.rs b/src/shell/autocomplete_old.rs new file mode 100644 index 0000000..b1da450 --- /dev/null +++ b/src/shell/autocomplete_old.rs @@ -0,0 +1,205 @@ +use which::which_re; +use regex::Regex; +use std::path::PathBuf; +use std::{env, fs}; + + +pub fn autocomplete(input: &String) -> (String, String) { + if !input.contains(' ') && !input.contains("./") { + return autocomplete_cmd(input); + } else { + match autocomplete_file(input) { + Ok(t) => return t, + Err(e) => { + println!("\n\r{}", e); + return (String::from(input), String::new()); + }, + }; + } +} + +///Look for a file using the string after the last space in the input, +///in the current folder. +fn autocomplete_file(input: &String) -> std::io::Result<(String, String)> { + + //Keep the last part of the cmd... + let mut input_searchee = match input + .split(' ') + .collect::>() + .last() + .copied() { + Some(c) => c, + None => { + return Ok((String::from(""), String::from(""))); + }, + }; + + //And also the beginning ! + let mut input_buff = input + .split(' ') + .collect::>(); + _ = input_buff.pop(); + let input_origin = format!("{} ", input_buff.join(" ")); + + //Store the results + let mut results = Vec::new(); + + //Where do we search ? + let mut folder_to_search = PathBuf::new(); + //Display folder in search result ? + let mut searching_not_current = false; + //Searching somewhere else... + //Here we need to correctly divide the string into the folder + //and the searched file itself. The next 10 lines or so of code + //suck, but sometimes Rust pisses in your boots. + if input_searchee.contains('/') { + //Correctly taking first and last part... + let splitted_searchstring = &input_searchee.split('/'); + let trimmed_searchee = &splitted_searchstring.clone().last().unwrap(); + let splitted_searchstring = &mut input_searchee.clone().split('/').collect::>(); + splitted_searchstring.pop(); + let mut tosearch = splitted_searchstring.join("/"); + tosearch.push_str("/"); + folder_to_search.push(tosearch); + *&mut input_searchee = &trimmed_searchee; + *&mut searching_not_current = true; + //Searching the current folder + } else { + folder_to_search = env::current_dir()?; + } + + //Search happens 'round here + let mut all_res = String::new(); + let mut nbr_found: u16 = 0; + let mut last_found = String::new(); + + for file in fs::read_dir(&folder_to_search)? { + let filepath = file.unwrap().path(); + let filename = filepath + .iter() + .last() + .unwrap() + .to_str() + .unwrap(); + let re = format!("^{}.*$", input_searchee); + let regex = Regex::new(&re).unwrap(); + if regex.is_match(filename) { + if !searching_not_current { + *&mut results.push(format!("{}", filename)); + } else { + *&mut results.push(format!("{}{}", &folder_to_search.display(), filename)); + } + *&mut nbr_found += 1; + let matchline = &format!("\r\n* {}", filename); + &mut all_res.push_str(&matchline); + + //if what we found is a folder, we add an / after + let fullpath = format!("{}/{}", &folder_to_search.display(), filename); + let md = fs::metadata(fullpath).unwrap(); + //Will be added to the end of the result + let mut ending = String::new(); + if md.is_dir() { + *&mut ending.push_str("/"); + } + + if searching_not_current { + *&mut last_found = format!("{}{}{}{}", + &input_origin, + &folder_to_search.display(), + filename, + ending); + } else { + *&mut last_found = format!("{}{}{}", + &input_origin, + filename, + ending); + } + } + } + //Found one or zero, use what has been found directly + if nbr_found < 2 { + + return Ok((last_found, String::new())); + //Otherwise, just display a list and use what all the results have in common + } else { + let longest = find_common_chars(results); + let ret_string = format!("{}{}", input_origin, &longest); + return Ok((ret_string, all_res)); + } +} + + +fn autocomplete_cmd(input: &String) -> (String, String) { + let found_bins = find_bin(input); + if found_bins.len() == 1 { + let aaa = found_bins.clone(); + let bbb = &aaa[0] + .iter() + .last() + .unwrap(); + let res_string = String::from(bbb.to_str().unwrap()); + let res_list = res_string.clone(); + return (res_string, res_list); + } else if found_bins.len() == 0 { + let list = String::from("Nothing adequate."); + let res = String::from(input); + return (res, list); + } else { + let mut all_res = String::new(); + for path in found_bins { + let buff = String::from(path + .iter() + .last() + .unwrap() + .to_str() + .unwrap()); + let res_line = format!("* {}\r\n", buff); + all_res.push_str(&res_line); + } + let first_res = String::from(input); + return(first_res, all_res) + + } +} + +///Takes a string and returns a Vector of PathBuf containing all matchings +///commands +fn find_bin(command: &String) -> Vec { + let re = format!("^{}.*", command); + let re = Regex::new(&re).unwrap(); + let binaries: Vec = which_re(re).unwrap().collect(); + return binaries; +} + +fn find_common_chars(mut strvec: Vec) -> String { + let first_word = strvec[0].clone(); + strvec.remove(0); + let mut answer = String::new(); + let mut counter: usize = 0; + let mut still_ok = true; + + for letter in first_word.chars() { + for word in &strvec { + if *&still_ok == true { + match word.chars().nth(counter) { + Some(l) => { + if l != letter { + *&mut still_ok = false; + } + }, + None => { + *&mut still_ok = false; + }, + }; + } + } + if still_ok == true { + *&mut answer.push(letter); + *&mut counter += 1; + } + } + //answer.pop(); + return answer; +} + +