mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-08-14 22:36:57 +00:00
Allow processing for hints and messages in the info file
This commit is contained in:
parent
9065860390
commit
276c59e8bf
@ -16,16 +16,8 @@ struct InfoFile<'a> {
|
|||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||||
// Remove `\r` on Windows
|
let info_file = include_str!("../info.toml");
|
||||||
let info_file = String::from_utf8(
|
let exercises = toml::de::from_str::<InfoFile>(info_file)
|
||||||
include_bytes!("../info.toml")
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|c| *c != b'\r')
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.expect("Failed to parse `info.toml` as UTF8");
|
|
||||||
let exercises = toml::de::from_str::<InfoFile>(&info_file)
|
|
||||||
.expect("Failed to parse `info.toml`")
|
.expect("Failed to parse `info.toml`")
|
||||||
.exercises;
|
.exercises;
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ pub struct AppState {
|
|||||||
exercises: Vec<Exercise>,
|
exercises: Vec<Exercise>,
|
||||||
// Cache the number of done exercises to avoid iterating over all exercises every time.
|
// Cache the number of done exercises to avoid iterating over all exercises every time.
|
||||||
n_done: u32,
|
n_done: u32,
|
||||||
final_message: &'static str,
|
final_message: String,
|
||||||
state_file: File,
|
state_file: File,
|
||||||
// Preallocated buffer for reading and writing the state file.
|
// Preallocated buffer for reading and writing the state file.
|
||||||
file_buf: Vec<u8>,
|
file_buf: Vec<u8>,
|
||||||
@ -66,8 +66,8 @@ pub struct AppState {
|
|||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
exercise_infos: Vec<ExerciseInfo>,
|
exercise_infos: Vec<ExerciseInfo<'static>>,
|
||||||
final_message: &'static str,
|
final_message: String,
|
||||||
editor: Option<Editor>,
|
editor: Option<Editor>,
|
||||||
vs_code_term: bool,
|
vs_code_term: bool,
|
||||||
) -> Result<(Self, StateFileStatus)> {
|
) -> Result<(Self, StateFileStatus)> {
|
||||||
@ -111,13 +111,12 @@ impl AppState {
|
|||||||
Exercise {
|
Exercise {
|
||||||
name: exercise_info.name,
|
name: exercise_info.name,
|
||||||
dir: exercise_info.dir,
|
dir: exercise_info.dir,
|
||||||
// Leaking for `Editor::open`.
|
// LEAKING: For `Editor::open`. The app state is used until the end of the program.
|
||||||
// Leaking is fine since the app state exists until the end of the program.
|
|
||||||
path: exercise_info.path().leak(),
|
path: exercise_info.path().leak(),
|
||||||
canonical_path,
|
canonical_path,
|
||||||
test: exercise_info.test,
|
test: exercise_info.test,
|
||||||
strict_clippy: exercise_info.strict_clippy,
|
strict_clippy: exercise_info.strict_clippy,
|
||||||
hint: exercise_info.hint.trim_ascii(),
|
hint: exercise_info.hint,
|
||||||
// Updated below.
|
// Updated below.
|
||||||
done: false,
|
done: false,
|
||||||
}
|
}
|
||||||
@ -549,9 +548,9 @@ impl AppState {
|
|||||||
clear_terminal(stdout)?;
|
clear_terminal(stdout)?;
|
||||||
stdout.write_all(FINISH_LINE.as_bytes())?;
|
stdout.write_all(FINISH_LINE.as_bytes())?;
|
||||||
|
|
||||||
let final_message = self.final_message.trim_ascii();
|
let final_message = self.final_message.as_bytes().trim_ascii();
|
||||||
if !final_message.is_empty() {
|
if !final_message.is_empty() {
|
||||||
stdout.write_all(final_message.as_bytes())?;
|
stdout.write_all(final_message)?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +616,7 @@ mod tests {
|
|||||||
canonical_path: None,
|
canonical_path: None,
|
||||||
test: false,
|
test: false,
|
||||||
strict_clippy: false,
|
strict_clippy: false,
|
||||||
hint: "",
|
hint: String::new(),
|
||||||
done: false,
|
done: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -628,7 +627,7 @@ mod tests {
|
|||||||
current_exercise_ind: 0,
|
current_exercise_ind: 0,
|
||||||
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
|
||||||
n_done: 0,
|
n_done: 0,
|
||||||
final_message: "",
|
final_message: String::new(),
|
||||||
state_file: tempfile::tempfile().unwrap(),
|
state_file: tempfile::tempfile().unwrap(),
|
||||||
file_buf: Vec::new(),
|
file_buf: Vec::new(),
|
||||||
official_exercises: true,
|
official_exercises: true,
|
||||||
|
|||||||
@ -110,7 +110,7 @@ mod tests {
|
|||||||
dir: None,
|
dir: None,
|
||||||
test: true,
|
test: true,
|
||||||
strict_clippy: true,
|
strict_clippy: true,
|
||||||
hint: "",
|
hint: String::new(),
|
||||||
skip_check_unsolved: false,
|
skip_check_unsolved: false,
|
||||||
},
|
},
|
||||||
ExerciseInfo {
|
ExerciseInfo {
|
||||||
@ -118,7 +118,7 @@ mod tests {
|
|||||||
dir: Some("d"),
|
dir: Some("d"),
|
||||||
test: false,
|
test: false,
|
||||||
strict_clippy: false,
|
strict_clippy: false,
|
||||||
hint: "",
|
hint: String::new(),
|
||||||
skip_check_unsolved: false,
|
skip_check_unsolved: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -383,7 +383,7 @@ pub fn check(require_solutions: bool) -> Result<()> {
|
|||||||
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaking is fine since they are used until the end of the program.
|
// LEAKING: Used until the end of the program.
|
||||||
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
|
let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
|
||||||
let info_file = Box::leak(Box::new(info_file));
|
let info_file = Box::leak(Box::new(info_file));
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ pub struct Exercise {
|
|||||||
pub canonical_path: Option<String>,
|
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: String,
|
||||||
pub done: bool,
|
pub done: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise};
|
|||||||
|
|
||||||
/// Deserialized from the `info.toml` file.
|
/// Deserialized from the `info.toml` file.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ExerciseInfo {
|
pub struct ExerciseInfo<'a> {
|
||||||
/// Exercise's unique name.
|
/// Exercise's unique name.
|
||||||
pub name: &'static str,
|
pub name: &'a str,
|
||||||
/// Exercise's directory name inside the `exercises/` directory.
|
/// Exercise's directory name inside the `exercises/` directory.
|
||||||
pub dir: Option<&'static str>,
|
pub dir: Option<&'a str>,
|
||||||
/// Run `cargo test` on the exercise.
|
/// Run `cargo test` on the exercise.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub test: bool,
|
pub test: bool,
|
||||||
@ -18,7 +18,7 @@ pub struct ExerciseInfo {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub strict_clippy: bool,
|
pub strict_clippy: bool,
|
||||||
/// The exercise's hint to be shown to the user on request.
|
/// The exercise's hint to be shown to the user on request.
|
||||||
pub hint: &'static str,
|
pub hint: String,
|
||||||
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
|
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skip_check_unsolved: bool,
|
pub skip_check_unsolved: bool,
|
||||||
@ -27,7 +27,7 @@ const fn default_true() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExerciseInfo {
|
impl ExerciseInfo<'_> {
|
||||||
/// Path to the exercise file starting with the `exercises/` directory.
|
/// Path to the exercise file starting with the `exercises/` directory.
|
||||||
pub fn path(&self) -> String {
|
pub fn path(&self) -> String {
|
||||||
let mut path = if let Some(dir) = self.dir {
|
let mut path = if let Some(dir) = self.dir {
|
||||||
@ -53,7 +53,7 @@ impl ExerciseInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunnableExercise for ExerciseInfo {
|
impl RunnableExercise for ExerciseInfo<'_> {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
self.name
|
self.name
|
||||||
}
|
}
|
||||||
@ -77,11 +77,14 @@ pub struct InfoFile {
|
|||||||
/// For possible breaking changes in the future for community exercises.
|
/// For possible breaking changes in the future for community exercises.
|
||||||
pub format_version: u8,
|
pub format_version: u8,
|
||||||
/// Shown to users when starting with the exercises.
|
/// Shown to users when starting with the exercises.
|
||||||
pub welcome_message: Option<&'static str>,
|
#[serde(default)]
|
||||||
|
pub welcome_message: String,
|
||||||
/// Shown to users after finishing all exercises.
|
/// Shown to users after finishing all exercises.
|
||||||
pub final_message: Option<&'static str>,
|
#[serde(default)]
|
||||||
|
pub final_message: String,
|
||||||
/// List of all exercises.
|
/// List of all exercises.
|
||||||
pub exercises: Vec<ExerciseInfo>,
|
#[serde(borrow)]
|
||||||
|
pub exercises: Vec<ExerciseInfo<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfoFile {
|
impl InfoFile {
|
||||||
@ -89,15 +92,10 @@ impl InfoFile {
|
|||||||
/// Community exercises: Parse the `info.toml` file in the current directory.
|
/// Community exercises: Parse the `info.toml` file in the current directory.
|
||||||
pub fn parse() -> Result<Self> {
|
pub fn parse() -> Result<Self> {
|
||||||
// Read a local `info.toml` if it exists.
|
// Read a local `info.toml` if it exists.
|
||||||
let slf = match fs::read("info.toml") {
|
let slf = match fs::read_to_string("info.toml") {
|
||||||
Ok(file_content) => {
|
Ok(file_content) => {
|
||||||
// Remove `\r` on Windows.
|
// LEAKING: The info file is used until the end of the program.
|
||||||
// Leaking is fine since the info file is used until the end of the program.
|
toml::de::from_str::<Self>(file_content.leak())
|
||||||
let file_content =
|
|
||||||
String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect())
|
|
||||||
.context("Failed to parse `info.toml` as UTF8")?
|
|
||||||
.leak();
|
|
||||||
toml::de::from_str::<Self>(file_content)
|
|
||||||
.context("Failed to parse the `info.toml` file")?
|
.context("Failed to parse the `info.toml` file")?
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@ -70,24 +70,22 @@ fn main() -> Result<ExitCode> {
|
|||||||
|
|
||||||
let (mut app_state, state_file_status) = AppState::new(
|
let (mut app_state, state_file_status) = AppState::new(
|
||||||
info_file.exercises,
|
info_file.exercises,
|
||||||
info_file.final_message.unwrap_or_default(),
|
info_file.final_message,
|
||||||
editor,
|
editor,
|
||||||
vs_code_term,
|
vs_code_term,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Show the welcome message if the state file doesn't exist yet.
|
// Show the welcome message if the state file doesn't exist yet.
|
||||||
if let Some(welcome_message) = info_file.welcome_message {
|
let welcome_message = info_file.welcome_message.as_bytes().trim_ascii();
|
||||||
|
if !welcome_message.is_empty() {
|
||||||
match state_file_status {
|
match state_file_status {
|
||||||
StateFileStatus::NotRead => {
|
StateFileStatus::NotRead => {
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
clear_terminal(&mut stdout)?;
|
clear_terminal(&mut stdout)?;
|
||||||
|
|
||||||
let welcome_message = welcome_message.trim_ascii();
|
let welcome_message = welcome_message.trim_ascii();
|
||||||
write!(
|
stdout.write_all(welcome_message)?;
|
||||||
stdout,
|
stdout.write_all(b"\n\nPress ENTER to continue ")?;
|
||||||
"{welcome_message}\n\n\
|
|
||||||
Press ENTER to continue "
|
|
||||||
)?;
|
|
||||||
press_enter_prompt(&mut stdout)?;
|
press_enter_prompt(&mut stdout)?;
|
||||||
clear_terminal(&mut stdout)?;
|
clear_terminal(&mut stdout)?;
|
||||||
// Flush to be able to show errors occurring before printing a newline to stdout.
|
// Flush to be able to show errors occurring before printing a newline to stdout.
|
||||||
@ -106,8 +104,7 @@ fn main() -> Result<ExitCode> {
|
|||||||
let notify_exercise_names = if args.manual_run {
|
let notify_exercise_names = if args.manual_run {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// For the notify event handler thread.
|
// LEAKING: For the notify event handler thread. The slice is used until the end of the program.
|
||||||
// Leaking is fine since the slice is used until the end of the program.
|
|
||||||
Some(
|
Some(
|
||||||
&*app_state
|
&*app_state
|
||||||
.exercises()
|
.exercises()
|
||||||
@ -176,7 +173,7 @@ fn main() -> Result<ExitCode> {
|
|||||||
current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?;
|
current_exercise.terminal_file_link(&mut stdout, app_state.emit_file_links())?;
|
||||||
|
|
||||||
stdout.write_all(b"\n\nHint:\n")?;
|
stdout.write_all(b"\n\nHint:\n")?;
|
||||||
stdout.write_all(current_exercise.hint.as_bytes())?;
|
stdout.write_all(current_exercise.hint.as_bytes().trim_ascii())?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
}
|
}
|
||||||
// Handled in an earlier match.
|
// Handled in an earlier match.
|
||||||
|
|||||||
@ -224,7 +224,13 @@ impl<'a> WatchState<'a> {
|
|||||||
stdout.queue(ResetColor)?;
|
stdout.queue(ResetColor)?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
|
|
||||||
stdout.write_all(self.app_state.current_exercise().hint.as_bytes())?;
|
stdout.write_all(
|
||||||
|
self.app_state
|
||||||
|
.current_exercise()
|
||||||
|
.hint
|
||||||
|
.as_bytes()
|
||||||
|
.trim_ascii(),
|
||||||
|
)?;
|
||||||
stdout.write_all(b"\n\n")?;
|
stdout.write_all(b"\n\n")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user