Compare commits

...

27 commits

Author SHA1 Message Date
Kacper Poneta a23bd69750
Merge 59e8f70e55 into 247bd19f93 2024-09-03 22:58:05 -04:00
mo8it 247bd19f93 Canonicalize exercise paths only once 2024-09-04 02:19:45 +02:00
mo8it e5ed115288 Match filter once 2024-09-04 01:20:48 +02:00
mo8it 03baa471d9 Simplify handling p in list 2024-09-04 01:07:08 +02:00
mo8it da8b3d143a Final touches to searching 2024-09-04 01:05:30 +02:00
Mo 20616ff954
Merge pull request #2098 from frroossst/main
Made the list of exercises searchable, ref #2093
2024-09-04 00:40:22 +02:00
Adhyan f463cf8662 passes clippy lints and removed extra code from the merge 2024-09-03 15:10:44 -06:00
Adhyan e9879eac91 merge of origin/main 2024-09-03 15:04:45 -06:00
Adhyan 47148e78a3 replaced enumerate() with position(); converted select_if_matches_search_query to apply_search_query 2024-09-03 15:03:25 -06:00
Adhyan fea917c8f2 removed unnecessary update_rows() call and minor refactoring 2024-09-03 14:52:09 -06:00
Adhyan 948e16e3c7 moved continue to end of if-block 2024-09-03 14:40:24 -06:00
Adhyan 1e7fc46406 Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-02 11:02:21 -06:00
Adhyan 71494264ca fixed clippy lints 2024-09-02 11:02:17 -06:00
Adhyan H. Patel 3125561474
Merge branch 'rust-lang:main' into main 2024-09-02 12:00:22 -05:00
Adhyan abf1228a0a search now filters the list first 2024-09-02 10:59:23 -06:00
Adhyan 547a9d947b escape/enter no longer exits the list, exits only the search 2024-09-02 10:45:45 -06:00
Adhyan 44ab7f995d Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-01 19:05:28 -06:00
Adhyan 92a1214dcd passes clippy lints 2024-09-01 19:05:23 -06:00
Adhyan 388f8da97f removed debug statements 2024-09-01 19:03:33 -06:00
Adhyan H. Patel e96623588c
Merge branch 'rust-lang:main' into main 2024-09-01 19:57:35 -05:00
Adhyan e1e316b931 Merge branch 'main' of https://github.com/frroossst/rustlings 2024-09-01 18:56:52 -06:00
Adhyan c4fd29541b added a way to search through list, ref #2093 2024-09-01 18:52:26 -06: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
12 changed files with 259 additions and 36 deletions

View file

@ -116,6 +116,8 @@ bin = [
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2", path = "../exercises/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_sol", path = "../solutions/15_traits/traits1.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:
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
[[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

@ -3,7 +3,7 @@ use std::{
env,
fs::{File, OpenOptions},
io::{self, Read, Seek, StdoutLock, Write},
path::Path,
path::{Path, MAIN_SEPARATOR_STR},
process::{Command, Stdio},
thread,
};
@ -15,6 +15,7 @@ use crate::{
embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo,
term,
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
@ -71,6 +72,7 @@ impl AppState {
format!("Failed to open or create the state file {STATE_FILE_NAME}")
})?;
let dir_canonical_path = term::canonicalize("exercises");
let mut exercises = exercise_infos
.into_iter()
.map(|exercise_info| {
@ -82,10 +84,32 @@ impl AppState {
let dir = exercise_info.dir.map(|dir| &*dir.leak());
let hint = exercise_info.hint.leak().trim_ascii();
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path;
if let Some(dir) = dir {
canonical_path = String::with_capacity(
2 + dir_canonical_path.len() + dir.len() + name.len(),
);
canonical_path.push_str(dir_canonical_path);
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(dir);
} else {
canonical_path =
String::with_capacity(1 + dir_canonical_path.len() + name.len());
canonical_path.push_str(dir_canonical_path);
}
canonical_path.push_str(MAIN_SEPARATOR_STR);
canonical_path.push_str(name);
canonical_path.push_str(".rs");
canonical_path
});
Exercise {
dir,
name,
path,
canonical_path,
test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy,
hint,
@ -486,6 +510,7 @@ mod tests {
dir: None,
name: "0",
path: "exercises/0.rs",
canonical_path: None,
test: false,
strict_clippy: false,
hint: "",

View file

@ -7,7 +7,7 @@ use std::io::{self, StdoutLock, Write};
use crate::{
cmd::CmdRunner,
term::{terminal_file_link, write_ansi},
term::{self, terminal_file_link, write_ansi, CountedWrite},
};
/// The initial capacity of the output buffer.
@ -18,7 +18,11 @@ pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::R
stdout.write_all(b"Solution")?;
stdout.queue(ResetColor)?;
stdout.write_all(b" for comparison: ")?;
terminal_file_link(stdout, solution_path, Color::Cyan)?;
if let Some(canonical_path) = term::canonicalize(solution_path) {
terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?;
} else {
stdout.write_all(solution_path.as_bytes())?;
}
stdout.write_all(b"\n")
}
@ -60,12 +64,23 @@ pub struct Exercise {
pub name: &'static str,
/// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str,
pub canonical_path: Option<String>,
pub test: bool,
pub strict_clippy: bool,
pub hint: &'static str,
pub done: bool,
}
impl Exercise {
pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> {
if let Some(canonical_path) = self.canonical_path.as_deref() {
return terminal_file_link(writer, self.path, canonical_path, Color::Blue);
}
writer.write_str(self.path)
}
}
pub trait RunnableExercise {
fn name(&self) -> &str;
fn dir(&self) -> Option<&str>;

View file

@ -21,6 +21,7 @@ mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::new(app_state, stdout)?;
let mut is_searching = false;
loop {
match event::read().context("Failed to read terminal event")? {
@ -32,6 +33,27 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
list_state.message.clear();
if is_searching {
match key.code {
KeyCode::Esc | KeyCode::Enter => {
is_searching = false;
list_state.search_query.clear();
}
KeyCode::Char(c) => {
list_state.search_query.push(c);
list_state.apply_search_query();
}
KeyCode::Backspace => {
list_state.search_query.pop();
list_state.apply_search_query();
}
_ => continue,
}
list_state.draw(stdout)?;
continue;
}
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(),
@ -50,15 +72,15 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
}
}
KeyCode::Char('p') => {
let message = if list_state.filter() == Filter::Pending {
if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None);
"Disabled filter PENDING"
list_state.message.push_str("Disabled filter PENDING");
} else {
list_state.set_filter(Filter::Pending);
"Enabled filter PENDING │ Press p again to disable the filter"
};
list_state.message.push_str(message);
list_state.message.push_str(
"Enabled filter PENDING │ Press p again to disable the filter",
);
}
}
KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => {
@ -66,6 +88,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
return Ok(());
}
}
KeyCode::Char('s' | '/') => {
is_searching = true;
list_state.apply_search_query();
}
// Redraw to remove the message.
KeyCode::Esc => (),
_ => continue,

View file

@ -46,7 +46,7 @@ impl ScrollState {
self.selected
}
fn set_selected(&mut self, selected: usize) {
pub fn set_selected(&mut self, selected: usize) {
self.selected = Some(selected);
self.update_offset();
}

View file

@ -13,7 +13,7 @@ use std::{
use crate::{
app_state::AppState,
exercise::Exercise,
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter},
term::{progress_bar, CountedWrite, MaxLenWriter},
};
use super::scroll_state::ScrollState;
@ -37,6 +37,7 @@ pub enum Filter {
pub struct ListState<'a> {
/// Footer message to be displayed if not empty.
pub message: String,
pub search_query: String,
app_state: &'a mut AppState,
scroll_state: ScrollState,
name_col_padding: Vec<u8>,
@ -68,6 +69,7 @@ impl<'a> ListState<'a> {
let mut slf = Self {
message: String::with_capacity(128),
search_query: String::new(),
app_state,
scroll_state,
name_col_padding,
@ -156,7 +158,7 @@ impl<'a> ListState<'a> {
if self.app_state.vs_code() {
writer.write_str(exercise.path)?;
} else {
terminal_file_link(&mut writer, exercise.path, Color::Blue)?;
exercise.terminal_file_link(&mut writer)?;
}
next_ln(stdout)?;
@ -345,6 +347,33 @@ impl<'a> ListState<'a> {
Ok(())
}
pub fn apply_search_query(&mut self) {
self.message.push_str("search:");
self.message.push_str(&self.search_query);
self.message.push('|');
if self.search_query.is_empty() {
return;
}
let is_search_result = |exercise: &Exercise| exercise.name.contains(&self.search_query);
let mut iter = self.app_state.exercises().iter();
let ind = match self.filter {
Filter::None => iter.position(is_search_result),
Filter::Done => iter
.filter(|exercise| exercise.done)
.position(is_search_result),
Filter::Pending => iter
.filter(|exercise| !exercise.done)
.position(is_search_result),
};
match ind {
Some(exercise_ind) => self.scroll_state.set_selected(exercise_ind),
None => self.message.push_str(" (not found)"),
}
}
// Return `true` if there was something to select.
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
let Some(selected) = self.scroll_state.selected() else {

View file

@ -11,7 +11,6 @@ use std::{
use crate::{
app_state::{AppState, ExercisesProgress},
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::terminal_file_link,
};
pub fn run(app_state: &mut AppState) -> Result<()> {
@ -26,7 +25,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
app_state.set_pending(app_state.current_exercise_ind())?;
stdout.write_all(b"Ran ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b" with errors\n")?;
exit(1);
}
@ -46,7 +47,9 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => {
stdout.write_all(b"Next exercise: ")?;
terminal_file_link(&mut stdout, app_state.current_exercise().path, Color::Blue)?;
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b"\n")?;
}
ExercisesProgress::AllDone => (),

View file

@ -1,14 +1,13 @@
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
use crossterm::{
cursor::MoveTo,
style::{Attribute, Color, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType},
Command, QueueableCommand,
};
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>,
@ -151,25 +150,29 @@ pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\n")
}
/// Canonicalize, convert to string and remove verbatim part on Windows.
pub fn canonicalize(path: &str) -> Option<String> {
fs::canonicalize(path)
.ok()?
.into_os_string()
.into_string()
.ok()
.map(|mut path| {
// Windows itself can't handle its verbatim paths.
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
path.drain(..4);
}
path
})
}
pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>,
path: &str,
canonical_path: &str,
color: Color,
) -> io::Result<()> {
let canonical_path = fs::canonicalize(path).ok();
let Some(canonical_path) = canonical_path.as_deref().and_then(|p| p.to_str()) else {
return writer.write_str(path);
};
// Windows itself can't handle its verbatim paths.
#[cfg(windows)]
let canonical_path = if canonical_path.len() > 5 && &canonical_path[0..4] == r"\\?\" {
&canonical_path[4..]
} else {
canonical_path
};
writer
.stdout()
.queue(SetForegroundColor(color))?

View file

@ -11,7 +11,7 @@ use crate::{
app_state::{AppState, ExercisesProgress},
clear_terminal,
exercise::{solution_link_line, RunnableExercise, OUTPUT_CAPACITY},
term::{progress_bar, terminal_file_link},
term::progress_bar,
};
#[derive(PartialEq, Eq)]
@ -184,7 +184,9 @@ impl<'a> WatchState<'a> {
)?;
stdout.write_all(b"\nCurrent exercise: ")?;
terminal_file_link(stdout, self.app_state.current_exercise().path, Color::Blue)?;
self.app_state
.current_exercise()
.terminal_file_link(stdout)?;
stdout.write_all(b"\n\n")?;
self.show_prompt(stdout)?;