mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-27 00:00:03 +03:00
Compare commits
21 commits
f696d98270
...
247bd19f93
Author | SHA1 | Date | |
---|---|---|---|
247bd19f93 | |||
e5ed115288 | |||
03baa471d9 | |||
da8b3d143a | |||
20616ff954 | |||
f463cf8662 | |||
e9879eac91 | |||
47148e78a3 | |||
fea917c8f2 | |||
948e16e3c7 | |||
1e7fc46406 | |||
71494264ca | |||
3125561474 | |||
abf1228a0a | |||
547a9d947b | |||
44ab7f995d | |||
92a1214dcd | |||
388f8da97f | |||
e96623588c | |||
e1e316b931 | |||
c4fd29541b |
|
@ -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: "",
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
38
src/list.rs
38
src/list.rs
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 => (),
|
||||||
|
|
41
src/term.rs
41
src/term.rs
|
@ -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))?
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
Loading…
Reference in a new issue