Compare commits

...

21 commits

Author SHA1 Message Date
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
8 changed files with 139 additions and 36 deletions

View file

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

View file

@ -7,7 +7,7 @@ use std::io::{self, StdoutLock, Write};
use crate::{ use crate::{
cmd::CmdRunner, cmd::CmdRunner,
term::{terminal_file_link, write_ansi}, term::{self, terminal_file_link, write_ansi, CountedWrite},
}; };
/// The initial capacity of the output buffer. /// 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.write_all(b"Solution")?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
stdout.write_all(b" for comparison: ")?; 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") stdout.write_all(b"\n")
} }
@ -60,12 +64,23 @@ pub struct Exercise {
pub name: &'static str, pub name: &'static str,
/// Path of the exercise file starting with the `exercises/` directory. /// Path of the exercise file starting with the `exercises/` directory.
pub path: &'static str, pub path: &'static str,
pub canonical_path: Option<String>,
pub test: bool, pub test: bool,
pub strict_clippy: bool, pub strict_clippy: bool,
pub hint: &'static str, pub hint: &'static str,
pub done: bool, 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 { pub trait RunnableExercise {
fn name(&self) -> &str; fn name(&self) -> &str;
fn dir(&self) -> Option<&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<()> { fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::new(app_state, stdout)?; let mut list_state = ListState::new(app_state, stdout)?;
let mut is_searching = false;
loop { loop {
match event::read().context("Failed to read terminal event")? { 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(); 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 { match key.code {
KeyCode::Char('q') => return Ok(()), KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => list_state.select_next(), 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') => { KeyCode::Char('p') => {
let message = if list_state.filter() == Filter::Pending { if list_state.filter() == Filter::Pending {
list_state.set_filter(Filter::None); list_state.set_filter(Filter::None);
"Disabled filter PENDING" list_state.message.push_str("Disabled filter PENDING");
} else { } else {
list_state.set_filter(Filter::Pending); list_state.set_filter(Filter::Pending);
"Enabled filter PENDING │ Press p again to disable the filter" list_state.message.push_str(
}; "Enabled filter PENDING │ Press p again to disable the filter",
);
list_state.message.push_str(message); }
} }
KeyCode::Char('r') => list_state.reset_selected()?, KeyCode::Char('r') => list_state.reset_selected()?,
KeyCode::Char('c') => { KeyCode::Char('c') => {
@ -66,6 +88,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()>
return Ok(()); return Ok(());
} }
} }
KeyCode::Char('s' | '/') => {
is_searching = true;
list_state.apply_search_query();
}
// Redraw to remove the message. // Redraw to remove the message.
KeyCode::Esc => (), KeyCode::Esc => (),
_ => continue, _ => continue,

View file

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

View file

@ -13,7 +13,7 @@ use std::{
use crate::{ use crate::{
app_state::AppState, app_state::AppState,
exercise::Exercise, exercise::Exercise,
term::{progress_bar, terminal_file_link, CountedWrite, MaxLenWriter}, term::{progress_bar, CountedWrite, MaxLenWriter},
}; };
use super::scroll_state::ScrollState; use super::scroll_state::ScrollState;
@ -37,6 +37,7 @@ pub enum Filter {
pub struct ListState<'a> { pub struct ListState<'a> {
/// Footer message to be displayed if not empty. /// Footer message to be displayed if not empty.
pub message: String, pub message: String,
pub search_query: String,
app_state: &'a mut AppState, app_state: &'a mut AppState,
scroll_state: ScrollState, scroll_state: ScrollState,
name_col_padding: Vec<u8>, name_col_padding: Vec<u8>,
@ -68,6 +69,7 @@ impl<'a> ListState<'a> {
let mut slf = Self { let mut slf = Self {
message: String::with_capacity(128), message: String::with_capacity(128),
search_query: String::new(),
app_state, app_state,
scroll_state, scroll_state,
name_col_padding, name_col_padding,
@ -156,7 +158,7 @@ impl<'a> ListState<'a> {
if self.app_state.vs_code() { if self.app_state.vs_code() {
writer.write_str(exercise.path)?; writer.write_str(exercise.path)?;
} else { } else {
terminal_file_link(&mut writer, exercise.path, Color::Blue)?; exercise.terminal_file_link(&mut writer)?;
} }
next_ln(stdout)?; next_ln(stdout)?;
@ -345,6 +347,33 @@ impl<'a> ListState<'a> {
Ok(()) 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. // Return `true` if there was something to select.
pub fn selected_to_current_exercise(&mut self) -> Result<bool> { pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
let Some(selected) = self.scroll_state.selected() else { let Some(selected) = self.scroll_state.selected() else {

View file

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

View file

@ -1,14 +1,13 @@
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
use crossterm::{ use crossterm::{
cursor::MoveTo, cursor::MoveTo,
style::{Attribute, Color, SetAttribute, SetForegroundColor}, style::{Attribute, Color, SetAttribute, SetForegroundColor},
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
Command, QueueableCommand, Command, QueueableCommand,
}; };
use std::{
fmt, fs,
io::{self, BufRead, StdoutLock, Write},
};
pub struct MaxLenWriter<'a, 'b> { pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'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") 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>( pub fn terminal_file_link<'a>(
writer: &mut impl CountedWrite<'a>, writer: &mut impl CountedWrite<'a>,
path: &str, path: &str,
canonical_path: &str,
color: Color, color: Color,
) -> io::Result<()> { ) -> 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 writer
.stdout() .stdout()
.queue(SetForegroundColor(color))? .queue(SetForegroundColor(color))?

View file

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