Skip to content

Commit

Permalink
Add command line arg to include hidden files
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasschafer committed Nov 20, 2024
1 parent c0194a1 commit 8292cc5
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 25 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ If the instance you're attempting to replace has changed since the search was pe

## Features

Scooter respects both `.gitignore` and `.ignore` files.
Scooter respects both `.gitignore` and `.ignore` files. By default hidden files (such as those starting with a `.`) are ignored, but can be included with the `--hidden` (`-.`) flag.

You can add capture groups to the search regex and use them in the replacement string: for instance, if you use `(\d) - (\w+)` for the search text and `($2) "$1"` as the replacement, then `9 - foo` would be replaced with `(foo) "9"`.

Expand Down Expand Up @@ -69,7 +69,7 @@ Ensure you have cargo installed (see [here](https://doc.rust-lang.org/cargo/gett
```sh
git clone [email protected]:thomasschafer/scooter.git
cd scooter
cargo install --path .
cargo install --path . --locked
```
## Contributing
Expand Down
21 changes: 17 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ pub struct App {
pub search_fields: SearchFields,
pub results: Results,
pub directory: PathBuf,
pub include_hidden: bool,

pub running: bool,
pub event_sender: mpsc::UnboundedSender<AppEvent>,
Expand All @@ -292,7 +293,11 @@ pub struct App {
const BINARY_EXTENSIONS: &[&str] = &["png", "gif", "jpg", "jpeg", "ico", "svg", "pdf"];

impl App {
pub fn new(directory: Option<PathBuf>, event_sender: mpsc::UnboundedSender<AppEvent>) -> App {
pub fn new(
directory: Option<PathBuf>,
include_hidden: bool,
event_sender: mpsc::UnboundedSender<AppEvent>,
) -> App {
let directory = match directory {
Some(d) => d,
None => std::env::current_dir().unwrap(),
Expand All @@ -303,14 +308,19 @@ impl App {
search_fields: SearchFields::with_values("", "", false, ""),
results: Results::Loading,
directory, // TODO: add this as a field that can be edited, e.g. allow glob patterns
include_hidden,

running: true,
event_sender,
}
}

pub fn reset(&mut self) {
*self = Self::new(Some(self.directory.clone()), self.event_sender.clone());
*self = Self::new(
Some(self.directory.clone()),
self.include_hidden,
self.event_sender.clone(),
);
}

pub fn handle_event(&mut self, event: AppEvent) -> bool {
Expand Down Expand Up @@ -473,8 +483,11 @@ impl App {
}
};

let paths: Vec<_> = WalkBuilder::new(&self.directory)
.build()
let walker = WalkBuilder::new(&self.directory)
.hidden(!self.include_hidden)
.filter_entry(|entry| entry.file_name() != ".git")
.build();
let paths: Vec<_> = walker
.flatten()
.filter(|entry| entry.file_type().map_or(false, |ft| ft.is_file()))
.map(|entry| entry.path().to_path_buf())
Expand Down
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ struct Args {
/// Directory in which to search
#[arg(index = 1)]
directory: Option<String>,

/// Include hidden files and directories, such as those whose name starts with a dot (.)
#[arg(short = '.', long, default_value = "false")]
hidden: bool,
}

#[tokio::main]
Expand All @@ -39,7 +43,7 @@ async fn main() -> anyhow::Result<()> {

let events = EventHandler::new();
let app_event_sender = events.app_event_sender.clone();
let mut app = App::new(directory, app_event_sender);
let mut app = App::new(directory, args.hidden, app_event_sender);

let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend)?;
Expand Down
157 changes: 139 additions & 18 deletions tests/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async fn test_replace_state() {
#[tokio::test]
async fn test_app_reset() {
let events = EventHandler::new();
let mut app = App::new(None, events.app_event_sender);
let mut app = App::new(None, false, events.app_event_sender);
app.current_screen = CurrentScreen::Results;
app.results = Results::ReplaceComplete(ReplaceState {
num_successes: 5,
Expand All @@ -97,7 +97,7 @@ async fn test_app_reset() {
#[tokio::test]
async fn test_back_from_results() {
let events = EventHandler::new();
let mut app = App::new(None, events.app_event_sender);
let mut app = App::new(None, false, events.app_event_sender);
app.current_screen = CurrentScreen::Confirmation;
app.search_fields = SearchFields::with_values("foo", "bar", true, "pattern");

Expand Down Expand Up @@ -157,7 +157,7 @@ fn setup_env_simple_files() -> App {
};

let events = EventHandler::new();
App::new(Some(temp_dir.into_path()), events.app_event_sender)
App::new(Some(temp_dir.into_path()), false, events.app_event_sender)
}

#[tokio::test]
Expand Down Expand Up @@ -200,7 +200,7 @@ async fn test_update_search_results_regex() {
app.update_search_results().unwrap();

if let scooter::Results::SearchComplete(search_state) = &app.results {
assert_eq!(search_state.results.len(), 4,);
assert_eq!(search_state.results.len(), 4);

let mut file_match_counts = std::collections::HashMap::new();

Expand Down Expand Up @@ -267,7 +267,7 @@ fn setup_env_files_in_dirs() -> App {
"dir1/file1.txt" => {
"This is a test file",
"It contains some test content",
"For testing purposes",
"For testing purpose",
},
"dir2/file2.txt" => {
"Another test file",
Expand All @@ -282,7 +282,7 @@ fn setup_env_files_in_dirs() -> App {
};

let events = EventHandler::new();
App::new(Some(temp_dir.into_path()), events.app_event_sender)
App::new(Some(temp_dir.into_path()), false, events.app_event_sender)
}

#[tokio::test]
Expand All @@ -295,13 +295,12 @@ async fn test_update_search_results_filtered_dir() {
assert!(result.is_ok());

if let scooter::Results::SearchComplete(search_state) = &app.results {
assert_eq!(search_state.results.len(), 2);

for (file_path, num_matches) in [
let expected_matches = [
(Path::new("dir1").join("file1.txt"), 0),
(Path::new("dir2").join("file2.txt"), 1),
(Path::new("dir2").join("file3.txt"), 1),
] {
];
for (file_path, num_matches) in expected_matches.clone() {
assert_eq!(
search_state
.results
Expand All @@ -315,6 +314,13 @@ async fn test_update_search_results_filtered_dir() {
num_matches
);
}
assert_eq!(
search_state.results.len(),
expected_matches
.map(|(_, count)| count)
.into_iter()
.sum::<usize>()
);

for result in &search_state.results {
assert_eq!(result.replacement, result.line.replace("testing", "f"));
Expand All @@ -341,7 +347,7 @@ fn setup_env_files_with_gif() -> App {
};

let events = EventHandler::new();
App::new(Some(temp_dir.into_path()), events.app_event_sender)
App::new(Some(temp_dir.into_path()), false, events.app_event_sender)
}

#[tokio::test]
Expand All @@ -354,13 +360,12 @@ async fn test_ignores_gif_file() {
assert!(result.is_ok());

if let scooter::Results::SearchComplete(search_state) = &app.results {
assert_eq!(search_state.results.len(), 2);

for (file_path, num_matches) in [
let expected_matches = [
(Path::new("dir1").join("file1.txt"), 1),
(Path::new("dir2").join("file2.gif"), 0),
(Path::new("file3.txt").to_path_buf(), 1),
] {
];
for (file_path, num_matches) in expected_matches.clone() {
assert_eq!(
search_state
.results
Expand All @@ -374,6 +379,13 @@ async fn test_ignores_gif_file() {
num_matches
);
}
assert_eq!(
search_state.results.len(),
expected_matches
.map(|(_, count)| count)
.into_iter()
.sum::<usize>()
);

for result in &search_state.results {
assert_eq!(result.replacement, "Th a text file");
Expand All @@ -382,6 +394,115 @@ async fn test_ignores_gif_file() {
panic!("Expected SearchComplete results");
}
}
// TODO: add tests for:
// - replacing in files
// - more tests for passing in directory via CLI arg

fn setup_env_files_with_hidden(include_hidden: bool) -> App {
let temp_dir = TempDir::new().unwrap();

create_test_files! {
temp_dir,
"dir1/file1.txt" => {
"This is a text file",
},
".dir2/file2.rs" => {
"This is a file in a hidden directory",
},
".file3.txt" => {
"This is a hidden text file",
}
};

let events = EventHandler::new();
App::new(
Some(temp_dir.into_path()),
include_hidden,
events.app_event_sender,
)
}

#[tokio::test]
async fn test_ignores_hidden_files_by_default() {
let mut app = setup_env_files_with_hidden(false);

app.search_fields = SearchFields::with_values(r"This", "bar", false, "");

let result = app.update_search_results();
assert!(result.is_ok());

if let scooter::Results::SearchComplete(search_state) = &app.results {
let expected_matches = [
(Path::new("dir1").join("file1.txt"), 1),
(Path::new(".dir2").join("file2.rs"), 1),
(Path::new(".file3.txt").to_path_buf(), 1),
];
for (file_path, num_matches) in expected_matches.clone() {
assert_eq!(
search_state
.results
.iter()
.filter(|result| {
let result_path = result.path.to_str().unwrap();
let file_path = file_path.to_str().unwrap();
result_path.contains(file_path)
})
.count(),
num_matches
);
}
assert_eq!(
search_state.results.len(),
expected_matches
.map(|(_, count)| count)
.into_iter()
.sum::<usize>()
);
} else {
panic!("Expected SearchComplete results");
}
}

#[tokio::test]
async fn test_includes_hidden_files_with_flag() {
let mut app = setup_env_files_with_hidden(true);

app.search_fields = SearchFields::with_values(r"This", "bar", false, "");

let result = app.update_search_results();
assert!(result.is_ok());

if let scooter::Results::SearchComplete(search_state) = &app.results {
let expected_matches = [
(Path::new("dir1").join("file1.txt"), 1),
(Path::new(".dir2").join("file2.rs"), 0),
(Path::new(".file3.txt").to_path_buf(), 0),
];
for (file_path, num_matches) in expected_matches.clone() {
assert_eq!(
search_state
.results
.iter()
.filter(|result| {
let result_path = result.path.to_str().unwrap();
let file_path = file_path.to_str().unwrap();
result_path.contains(file_path)
})
.count(),
num_matches
);
}
assert_eq!(
search_state.results.len(),
expected_matches
.map(|(_, count)| count)
.into_iter()
.sum::<usize>()
);
} else {
panic!("Expected SearchComplete results");
}
}

// TODO:
// - Add tests for:
// - replacing in files
// - more tests for passing in directory via CLI arg
// - Tidy up tests - lots of duplication

0 comments on commit 8292cc5

Please sign in to comment.