Compare commits

...

9 commits

Author SHA1 Message Date
Kacper Poneta 58a08cd5e8
Merge 59e8f70e55 into 4e4b65711a 2024-09-18 09:19:24 +03:00
mo8it 4e4b65711a Only handle file changes for the current exercise, no jumping back 2024-09-18 01:44:13 +02:00
mo8it 89c40ba256 Optimize the file watcher 2024-09-18 01:43:48 +02:00
mo8it e56ae6d651 Update deps 2024-09-17 23:33:48 +02:00
mo8it 59e8f70e55 Format code 2024-07-12 18:31:23 +02:00
mo8it 4c8365fe88 Update dev/Cargo.toml 2024-07-12 18:25:01 +02:00
Kacper Poneta 52af0674c1 changed the task to make it more appropriate 2024-07-12 18:14:40 +02:00
Kacper Poneta 938b90e5f2 very small solution update 2024-07-11 22:55:48 +02:00
Kacper Poneta 55cc8584bd added exercise 2024-07-11 22:53:38 +02:00
10 changed files with 218 additions and 119 deletions

36
Cargo.lock generated
View file

@ -65,9 +65,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -139,21 +139,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.28.1" version = "0.28.1"
@ -379,7 +364,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"crossbeam-channel",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
@ -391,16 +375,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "notify-debouncer-mini"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [
"log",
"notify",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.19.0"
@ -488,7 +462,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"crossterm", "crossterm",
"notify-debouncer-mini", "notify",
"os_pipe", "os_pipe",
"rustix", "rustix",
"rustlings-macros", "rustlings-macros",
@ -646,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.20" version = "0.22.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",

View file

@ -20,7 +20,7 @@ rust-version = "1.80"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } toml_edit = { version = "0.22.21", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -47,10 +47,10 @@ include = [
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false } ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.88" anyhow = "1.0.89"
clap = { version = "4.5.17", features = ["derive"] } clap = { version = "4.5.17", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] }
os_pipe = "1.2.1" os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.128" serde_json = "1.0.128"

View file

@ -116,6 +116,8 @@ bin = [
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" }, { name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" }, { name = "generics2", path = "../exercises/14_generics/generics2.rs" },
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" }, { name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
{ name = "generics3", path = "../exercises/14_generics/generics3.rs" },
{ name = "generics3_sol", path = "../solutions/14_generics/generics3.rs" },
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" }, { name = "traits1", path = "../exercises/15_traits/traits1.rs" },
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" }, { name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" }, { name = "traits2", path = "../exercises/15_traits/traits2.rs" },

View file

@ -0,0 +1,54 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// This function should take an array of `Option` elements and returns array of not None elements
// TODO fix this function signature
fn into_dispose_nulls(list: Vec<Option<&str>>) -> Vec<&str> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
#[allow(dead_code)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -749,6 +749,17 @@ hint = """
Related section in The Book: Related section in The Book:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions""" https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
[[exercises]]
name = "generics3"
dir = "14_generics"
hint = """
Vectors in Rust use generics to create dynamically-sized arrays of any type.
The `into_dispose_nulls` function takes a vector as an argument, but only accepts vectors that store the &str type.
To allow the function to accept vectors that store any type, you can leverage your knowledge about generics.
If you're unsure how to proceed, please refer to the Rust Book at:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-function-definitions.
"""
# TRAITS # TRAITS
[[exercises]] [[exercises]]

View file

@ -0,0 +1,53 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// Here we added generic type `T` to function signature
// Now this function can be used with vector of any
fn into_dispose_nulls<T>(list: Vec<Option<T>>) -> Vec<T> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -1,12 +1,12 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use notify_debouncer_mini::{ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
new_debouncer,
notify::{self, RecursiveMode},
};
use std::{ use std::{
io::{self, Write}, io::{self, Write},
path::Path, path::Path,
sync::mpsc::channel, sync::{
atomic::{AtomicBool, Ordering::Relaxed},
mpsc::channel,
},
time::Duration, time::Duration,
}; };
@ -21,6 +21,27 @@ mod notify_event;
mod state; mod state;
mod terminal_event; mod terminal_event;
static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false);
// Private unit type to force using the constructor function.
#[must_use = "When the guard is dropped, the input is unpaused"]
pub struct InputPauseGuard(());
impl InputPauseGuard {
#[inline]
pub fn scoped_pause() -> Self {
EXERCISE_RUNNING.store(true, Relaxed);
Self(())
}
}
impl Drop for InputPauseGuard {
#[inline]
fn drop(&mut self) {
EXERCISE_RUNNING.store(false, Relaxed);
}
}
enum WatchEvent { enum WatchEvent {
Input(InputEvent), Input(InputEvent),
FileChange { exercise_ind: usize }, FileChange { exercise_ind: usize },
@ -47,21 +68,21 @@ fn run_watch(
let mut manual_run = false; let mut manual_run = false;
// Prevent dropping the guard until the end of the function. // Prevent dropping the guard until the end of the function.
// Otherwise, the file watcher exits. // Otherwise, the file watcher exits.
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
let mut debouncer = new_debouncer( let mut watcher = RecommendedWatcher::new(
Duration::from_millis(200),
NotifyEventHandler { NotifyEventHandler {
sender: watch_event_sender.clone(), sender: watch_event_sender.clone(),
exercise_names, exercise_names,
}, },
Config::default().with_poll_interval(Duration::from_secs(1)),
) )
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
debouncer
.watcher() watcher
.watch(Path::new("exercises"), RecursiveMode::Recursive) .watch(Path::new("exercises"), RecursiveMode::Recursive)
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
Some(debouncer) Some(watcher)
} else { } else {
manual_run = true; manual_run = true;
None None

View file

@ -1,7 +1,10 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use notify::{
use std::sync::mpsc::Sender; event::{MetadataKind, ModifyKind},
Event, EventKind,
};
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
use super::WatchEvent; use super::{WatchEvent, EXERCISE_RUNNING};
pub struct NotifyEventHandler { pub struct NotifyEventHandler {
pub sender: Sender<WatchEvent>, pub sender: Sender<WatchEvent>,
@ -9,44 +12,56 @@ pub struct NotifyEventHandler {
pub exercise_names: &'static [&'static [u8]], pub exercise_names: &'static [&'static [u8]],
} }
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { impl notify::EventHandler for NotifyEventHandler {
fn handle_event(&mut self, input_event: DebounceEventResult) { fn handle_event(&mut self, input_event: notify::Result<Event>) {
let output_event = match input_event { if EXERCISE_RUNNING.load(Relaxed) {
Ok(input_event) => { return;
let Some(exercise_ind) = input_event }
.iter()
.filter_map(|input_event| {
if input_event.kind != DebouncedEventKind::Any {
return None;
}
let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); let input_event = match input_event {
Ok(v) => v,
if file_name.len() < 4 { Err(e) => {
return None; // An error occurs when the receiver is dropped.
} // After dropping the receiver, the debouncer guard should also be dropped.
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); let _ = self.sender.send(WatchEvent::NotifyErr(e));
return;
if ext != b".rs" {
return None;
}
self.exercise_names
.iter()
.position(|exercise_name| *exercise_name == file_name_without_ext)
})
.min()
else {
return;
};
WatchEvent::FileChange { exercise_ind }
} }
Err(e) => WatchEvent::NotifyErr(e),
}; };
// An error occurs when the receiver is dropped. match input_event.kind {
// After dropping the receiver, the debouncer guard should also be dropped. EventKind::Any => (),
let _ = self.sender.send(output_event); EventKind::Modify(modify_kind) => match modify_kind {
ModifyKind::Any | ModifyKind::Data(_) => (),
ModifyKind::Metadata(metadata_kind) => match metadata_kind {
MetadataKind::Any | MetadataKind::WriteTime => (),
MetadataKind::AccessTime
| MetadataKind::Permissions
| MetadataKind::Ownership
| MetadataKind::Extended
| MetadataKind::Other => return,
},
ModifyKind::Name(_) | ModifyKind::Other => return,
},
EventKind::Access(_)
| EventKind::Create(_)
| EventKind::Remove(_)
| EventKind::Other => return,
}
let _ = input_event
.paths
.into_iter()
.filter_map(|path| {
let file_name = path.file_name()?.to_str()?.as_bytes();
let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else {
return None;
};
self.exercise_names
.iter()
.position(|exercise_name| *exercise_name == file_name_without_ext)
})
.try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind }));
} }
} }

View file

@ -18,10 +18,7 @@ use crate::{
term::progress_bar, term::progress_bar,
}; };
use super::{ use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
terminal_event::{terminal_event_handler, InputPauseGuard},
WatchEvent,
};
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
enum DoneStatus { enum DoneStatus {
@ -103,14 +100,10 @@ impl<'a> WatchState<'a> {
exercise_ind: usize, exercise_ind: usize,
stdout: &mut StdoutLock, stdout: &mut StdoutLock,
) -> Result<()> { ) -> Result<()> {
// Don't skip exercises on file changes to avoid confusion from missing exercises. if self.app_state.current_exercise_ind() != exercise_ind {
// Skipping exercises must be explicit in the interactive list.
// But going back to an earlier exercise on file change is fine.
if self.app_state.current_exercise_ind() < exercise_ind {
return Ok(()); return Ok(());
} }
self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise(stdout) self.run_current_exercise(stdout)
} }

View file

@ -1,31 +1,7 @@
use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::sync::{ use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
atomic::{AtomicBool, Ordering::Relaxed},
mpsc::Sender,
};
use super::WatchEvent; use super::{WatchEvent, EXERCISE_RUNNING};
static INPUT_PAUSED: AtomicBool = AtomicBool::new(false);
// Private unit type to force using the constructor function.
#[must_use = "When the guard is dropped, the input is unpaused"]
pub struct InputPauseGuard(());
impl InputPauseGuard {
#[inline]
pub fn scoped_pause() -> Self {
INPUT_PAUSED.store(true, Relaxed);
Self(())
}
}
impl Drop for InputPauseGuard {
#[inline]
fn drop(&mut self) {
INPUT_PAUSED.store(false, Relaxed);
}
}
pub enum InputEvent { pub enum InputEvent {
Run, Run,
@ -44,7 +20,7 @@ pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) {
KeyEventKind::Press => (), KeyEventKind::Press => (),
} }
if INPUT_PAUSED.load(Relaxed) { if EXERCISE_RUNNING.load(Relaxed) {
continue; continue;
} }