mirror of
https://github.com/rust-lang/rustlings.git
synced 2025-01-09 20:03:24 +03:00
Compare commits
No commits in common. "cf3f6fd6a16e81905bb44676d623502aeb8e5d01" and "d9df809838191962a82e98ff01aaaa73950ba670" have entirely different histories.
cf3f6fd6a1
...
d9df809838
|
@ -37,7 +37,7 @@ Please be patient 😇
|
|||
- Name the file `exercises/yourTopic/yourTopicN.rs`.
|
||||
- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`.
|
||||
- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it.
|
||||
- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file.
|
||||
- Add the [metadata for your exercise](#exercise-metadata) in the `info.toml` file.
|
||||
- Make sure your exercise runs with `rustlings run yourTopicN`.
|
||||
- [Open a pull request](#pull-requests).
|
||||
|
||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
|||
|
||||
[[package]]
|
||||
name = "rustlings"
|
||||
version = "6.0.0-beta.9"
|
||||
version = "6.0.0-beta.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
@ -675,7 +675,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustlings-macros"
|
||||
version = "6.0.0-beta.9"
|
||||
version = "6.0.0-beta.6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
|
|
|
@ -8,7 +8,7 @@ exclude = [
|
|||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "6.0.0-beta.9"
|
||||
version = "6.0.0-beta.6"
|
||||
authors = [
|
||||
"Liv <mokou@fastmail.com>",
|
||||
"Mo Bitar <mo8it@proton.me>",
|
||||
|
@ -39,6 +39,7 @@ include = [
|
|||
"/src/",
|
||||
"/exercises/",
|
||||
"/solutions/",
|
||||
"/info.toml",
|
||||
# A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded.
|
||||
"/dev-Cargo.toml",
|
||||
"/README.md",
|
||||
|
@ -53,7 +54,7 @@ hashbrown = "0.14.5"
|
|||
notify-debouncer-mini = { version = "0.4.1", default-features = false }
|
||||
os_pipe = "1.1.5"
|
||||
ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] }
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" }
|
||||
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" }
|
||||
serde_json = "1.0.117"
|
||||
serde.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
|
|
@ -35,7 +35,7 @@ The following command will download and compile Rustlings:
|
|||
<!-- TODO: Remove @6.0.0-beta.x -->
|
||||
|
||||
```bash
|
||||
cargo install rustlings@6.0.0-beta.9
|
||||
cargo install rustlings@6.0.0-beta.6
|
||||
```
|
||||
|
||||
<details>
|
||||
|
@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.9
|
|||
<!-- TODO: Remove @6.0.0-beta.x -->
|
||||
|
||||
- Make sure you have the latest Rust version by running `rustup update`
|
||||
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked`
|
||||
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.6 --locked`
|
||||
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
|
||||
|
||||
</details>
|
||||
|
@ -98,7 +98,7 @@ It will rerun the current exercise automatically every time you change the exerc
|
|||
<details>
|
||||
<summary><strong>If detecting file changes in the <code>exercises/</code> directory fails…</strong> (<em>click to expand</em>)</summary>
|
||||
|
||||
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode.
|
||||
> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode.
|
||||
>
|
||||
> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
|
||||
|
||||
|
@ -106,7 +106,7 @@ It will rerun the current exercise automatically every time you change the exerc
|
|||
|
||||
### Exercise List
|
||||
|
||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list.
|
||||
In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list.
|
||||
|
||||
The list allows you to…
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// We sometimes encourage you to keep trying things on a given exercise, even
|
||||
// after you already figured it out. If you got everything working and feel
|
||||
// ready for the next exercise, enter `n` in the terminal.
|
||||
// ready for the next exercise, enter `n` (or `next`) in the terminal.
|
||||
//
|
||||
// The exercise file will be reloaded when you change one of the lines below!
|
||||
// Try adding a new `println!`.
|
||||
|
|
File diff suppressed because it is too large
Load diff
1
rustlings-macros/info.toml
Symbolic link
1
rustlings-macros/info.toml
Symbolic link
|
@ -0,0 +1 @@
|
|||
../info.toml
|
|
@ -15,8 +15,7 @@ struct InfoFile {
|
|||
|
||||
#[proc_macro]
|
||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||
let info_file = include_str!("../info.toml");
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(info_file)
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml"))
|
||||
.expect("Failed to parse `info.toml`")
|
||||
.exercises;
|
||||
|
||||
|
@ -47,7 +46,6 @@ pub fn include_files(_: TokenStream) -> TokenStream {
|
|||
|
||||
quote! {
|
||||
EmbeddedFiles {
|
||||
info_file: #info_file,
|
||||
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
|
||||
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
|
||||
}
|
||||
|
|
|
@ -21,12 +21,8 @@ const BAD_INDEX_ERR: &str = "The current exercise index is higher than the numbe
|
|||
|
||||
#[must_use]
|
||||
pub enum ExercisesProgress {
|
||||
// All exercises are done.
|
||||
AllDone,
|
||||
// The current exercise failed and is still pending.
|
||||
CurrentPending,
|
||||
// A new exercise is now pending.
|
||||
NewPending,
|
||||
Pending,
|
||||
}
|
||||
|
||||
pub enum StateFileStatus {
|
||||
|
@ -124,27 +120,7 @@ impl AppState {
|
|||
|
||||
let exercises = exercise_infos
|
||||
.into_iter()
|
||||
.map(|exercise_info| {
|
||||
// Leaking to be able to borrow in the watch mode `Table`.
|
||||
// Leaking is not a problem because the `AppState` instance lives until
|
||||
// the end of the program.
|
||||
let path = exercise_info.path().leak();
|
||||
let name = exercise_info.name.leak();
|
||||
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||
|
||||
let hint = exercise_info.hint.trim().to_owned();
|
||||
|
||||
Exercise {
|
||||
dir,
|
||||
name,
|
||||
path,
|
||||
test: exercise_info.test,
|
||||
strict_clippy: exercise_info.strict_clippy,
|
||||
hint,
|
||||
// Updated in `Self::update_from_file`.
|
||||
done: false,
|
||||
}
|
||||
})
|
||||
.map(Exercise::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut slf = Self {
|
||||
|
@ -218,10 +194,6 @@ impl AppState {
|
|||
}
|
||||
|
||||
pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
if exercise_ind == self.current_exercise_ind {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if exercise_ind >= self.exercises.len() {
|
||||
bail!(BAD_INDEX_ERR);
|
||||
}
|
||||
|
@ -330,8 +302,8 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Official exercises: Dump the solution file form the binary and return its path.
|
||||
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||
// Official exercises: Dump the solution file form the binary and return its path.
|
||||
// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||
if DEBUG_PROFILE {
|
||||
return Ok(None);
|
||||
|
@ -358,9 +330,9 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||
/// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||
// Mark the current exercise as done and move on to the next pending exercise if one exists.
|
||||
// If all exercises are marked as done, run all of them to make sure that they are actually
|
||||
// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
|
||||
pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result<ExercisesProgress> {
|
||||
let exercise = &mut self.exercises[self.current_exercise_ind];
|
||||
if !exercise.done {
|
||||
|
@ -371,7 +343,7 @@ impl AppState {
|
|||
if let Some(ind) = self.next_pending_exercise_ind() {
|
||||
self.set_current_exercise_ind(ind)?;
|
||||
|
||||
return Ok(ExercisesProgress::NewPending);
|
||||
return Ok(ExercisesProgress::Pending);
|
||||
}
|
||||
|
||||
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
|
||||
|
@ -394,7 +366,7 @@ impl AppState {
|
|||
|
||||
self.write()?;
|
||||
|
||||
return Ok(ExercisesProgress::NewPending);
|
||||
return Ok(ExercisesProgress::Pending);
|
||||
}
|
||||
|
||||
writeln!(writer, "{}", "ok".green())?;
|
||||
|
|
|
@ -2,10 +2,10 @@ use anyhow::{Context, Result};
|
|||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
|
||||
/// Return the start and end index of the content of the list `bin = […]`.
|
||||
/// bin = [xxxxxxxxxxxxxxxxx]
|
||||
/// |start_ind |
|
||||
/// |end_ind
|
||||
// Return the start and end index of the content of the list `bin = […]`.
|
||||
// bin = [xxxxxxxxxxxxxxxxx]
|
||||
// |start_ind |
|
||||
// |end_ind
|
||||
pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
||||
let start_ind = cargo_toml
|
||||
.find("bin = [")
|
||||
|
@ -20,8 +20,8 @@ pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> {
|
|||
Ok((start_ind, end_ind))
|
||||
}
|
||||
|
||||
/// Generate and append the content of the `bin` list in `Cargo.toml`.
|
||||
/// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
|
||||
// Generate and append the content of the `bin` list in `Cargo.toml`.
|
||||
// The `exercise_path_prefix` is the prefix of the `path` field of every list entry.
|
||||
pub fn append_bins(
|
||||
buf: &mut Vec<u8>,
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
|
@ -43,7 +43,7 @@ pub fn append_bins(
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the `bin` list and leave everything else unchanged.
|
||||
// Update the `bin` list and leave everything else unchanged.
|
||||
pub fn updated_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
|
|
14
src/cmd.rs
14
src/cmd.rs
|
@ -1,8 +1,8 @@
|
|||
use anyhow::{Context, Result};
|
||||
use std::{io::Read, path::Path, process::Command};
|
||||
|
||||
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||
// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> {
|
||||
let (mut reader, writer) = os_pipe::pipe()
|
||||
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?;
|
||||
|
@ -37,18 +37,18 @@ pub struct CargoCmd<'a> {
|
|||
pub args: &'a [&'a str],
|
||||
pub exercise_name: &'a str,
|
||||
pub description: &'a str,
|
||||
/// RUSTFLAGS="-A warnings"
|
||||
// RUSTFLAGS="-A warnings"
|
||||
pub hide_warnings: bool,
|
||||
/// Added as `--target-dir` if `Self::dev` is true.
|
||||
// Added as `--target-dir` if `Self::dev` is true.
|
||||
pub target_dir: &'a Path,
|
||||
/// The output buffer to append the merged stdout and stderr.
|
||||
// The output buffer to append the merged stdout and stderr.
|
||||
pub output: &'a mut Vec<u8>,
|
||||
/// true while developing Rustlings.
|
||||
// true while developing Rustlings.
|
||||
pub dev: bool,
|
||||
}
|
||||
|
||||
impl<'a> CargoCmd<'a> {
|
||||
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
||||
// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
||||
pub fn run(&mut self) -> Result<bool> {
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg(self.subcommand);
|
||||
|
|
|
@ -6,7 +6,6 @@ use std::{
|
|||
|
||||
use crate::info_file::ExerciseInfo;
|
||||
|
||||
/// Contains all embedded files.
|
||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -32,17 +31,12 @@ impl WriteStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
// Files related to one exercise.
|
||||
struct ExerciseFiles {
|
||||
// The content of the exercise file.
|
||||
exercise: &'static [u8],
|
||||
// The content of the solution file.
|
||||
solution: &'static [u8],
|
||||
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
|
||||
dir_ind: usize,
|
||||
}
|
||||
|
||||
// A directory in the `exercises/` directory.
|
||||
struct ExerciseDir {
|
||||
name: &'static str,
|
||||
readme: &'static [u8],
|
||||
|
@ -69,20 +63,18 @@ impl ExerciseDir {
|
|||
let mut readme_path = dir_path;
|
||||
readme_path.push_str("/README.md");
|
||||
|
||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)
|
||||
WriteStrategy::Overwrite.write(&readme_path, self.readme)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// All embedded files.
|
||||
pub struct EmbeddedFiles {
|
||||
/// The content of the `info.toml` file.
|
||||
pub info_file: &'static str,
|
||||
exercise_files: &'static [ExerciseFiles],
|
||||
exercise_dirs: &'static [ExerciseDir],
|
||||
}
|
||||
|
||||
impl EmbeddedFiles {
|
||||
/// Dump all the embedded files of the `exercises/` directory.
|
||||
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
|
||||
create_dir("exercises").context("Failed to create the directory `exercises`")?;
|
||||
|
||||
|
@ -103,21 +95,21 @@ impl EmbeddedFiles {
|
|||
}
|
||||
|
||||
pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> {
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind];
|
||||
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
dir.init_on_disk()?;
|
||||
WriteStrategy::Overwrite.write(path, exercise_files.exercise)
|
||||
}
|
||||
|
||||
/// Write the solution file to disk and return its path.
|
||||
// Write the solution file to disk and return its path.
|
||||
pub fn write_solution_to_disk(
|
||||
&self,
|
||||
exercise_ind: usize,
|
||||
exercise_name: &str,
|
||||
) -> Result<String> {
|
||||
let exercise_files = &self.exercise_files[exercise_ind];
|
||||
let dir = &self.exercise_dirs[exercise_files.dir_ind];
|
||||
let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind];
|
||||
let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind];
|
||||
|
||||
// 14 = 10 + 1 + 3
|
||||
// solutions/ + / + .rs
|
||||
|
@ -156,7 +148,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn dirs() {
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(EMBEDDED_FILES.info_file)
|
||||
let exercises = toml_edit::de::from_str::<InfoFile>(include_str!("../info.toml"))
|
||||
.expect("Failed to parse `info.toml`")
|
||||
.exercises;
|
||||
|
||||
|
|
|
@ -10,32 +10,30 @@ use std::{
|
|||
use crate::{
|
||||
cmd::{run_cmd, CargoCmd},
|
||||
in_official_repo,
|
||||
info_file::ExerciseInfo,
|
||||
terminal_link::TerminalFileLink,
|
||||
DEBUG_PROFILE,
|
||||
};
|
||||
|
||||
/// The initial capacity of the output buffer.
|
||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||
|
||||
/// See `info_file::ExerciseInfo`
|
||||
pub struct Exercise {
|
||||
pub dir: Option<&'static str>,
|
||||
// Exercise's unique name
|
||||
pub name: &'static str,
|
||||
/// Path of the exercise file starting with the `exercises/` directory.
|
||||
// Exercise's path
|
||||
pub path: &'static str,
|
||||
pub test: bool,
|
||||
pub strict_clippy: bool,
|
||||
// The hint text associated with the exercise
|
||||
pub hint: String,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
// Run the exercise's binary and append its output to the `output` buffer.
|
||||
// Compilation should be done before calling this method.
|
||||
fn run_bin(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||
writeln!(output, "{}", "Output".underlined())?;
|
||||
|
||||
// 7 = "/debug/".len()
|
||||
let mut bin_path =
|
||||
PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len());
|
||||
bin_path.push(target_dir);
|
||||
|
@ -45,23 +43,18 @@ impl Exercise {
|
|||
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?;
|
||||
|
||||
if !success {
|
||||
// This output is important to show the user that something went wrong.
|
||||
// Otherwise, calling something like `exit(1)` in an exercise without further output
|
||||
// leaves the user confused about why the exercise isn't done yet.
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
"The exercise didn't run successfully (nonzero exit code)"
|
||||
.bold()
|
||||
.red(),
|
||||
.red()
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
/// Compile, check and run the exercise.
|
||||
/// The output is written to the `output` buffer after clearing it.
|
||||
pub fn run(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> {
|
||||
output.clear();
|
||||
|
||||
|
@ -83,10 +76,9 @@ impl Exercise {
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
// Discard the output of `cargo build` because it will be shown again by Clippy.
|
||||
// Discard the output of `cargo build` because it will be shown again by the Cargo command.
|
||||
output.clear();
|
||||
|
||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
||||
let clippy_args: &[&str] = if self.strict_clippy {
|
||||
&["--profile", "test", "--", "-D", "warnings"]
|
||||
} else {
|
||||
|
@ -134,6 +126,34 @@ impl Exercise {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ExerciseInfo> for Exercise {
|
||||
fn from(mut exercise_info: ExerciseInfo) -> Self {
|
||||
// Leaking to be able to borrow in the watch mode `Table`.
|
||||
// Leaking is not a problem because the `AppState` instance lives until
|
||||
// the end of the program.
|
||||
let path = exercise_info.path().leak();
|
||||
|
||||
exercise_info.name.shrink_to_fit();
|
||||
let name = exercise_info.name.leak();
|
||||
let dir = exercise_info.dir.map(|mut dir| {
|
||||
dir.shrink_to_fit();
|
||||
&*dir.leak()
|
||||
});
|
||||
|
||||
let hint = exercise_info.hint.trim().to_owned();
|
||||
|
||||
Exercise {
|
||||
dir,
|
||||
name,
|
||||
path,
|
||||
test: exercise_info.test,
|
||||
strict_clippy: exercise_info.strict_clippy,
|
||||
hint,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Exercise {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.path.fmt(f)
|
||||
|
|
|
@ -2,71 +2,44 @@ use anyhow::{bail, Context, Error, Result};
|
|||
use serde::Deserialize;
|
||||
use std::{fs, io::ErrorKind};
|
||||
|
||||
use crate::embedded::EMBEDDED_FILES;
|
||||
|
||||
/// Deserialized from the `info.toml` file.
|
||||
// Deserialized from the `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct ExerciseInfo {
|
||||
/// Exercise's unique name.
|
||||
// Name of the exercise
|
||||
pub name: String,
|
||||
/// Exercise's directory name inside the `exercises/` directory.
|
||||
// The exercise's directory inside the `exercises` directory
|
||||
pub dir: Option<String>,
|
||||
#[serde(default = "default_true")]
|
||||
/// Run `cargo test` on the exercise.
|
||||
pub test: bool,
|
||||
/// Deny all Clippy warnings.
|
||||
#[serde(default)]
|
||||
pub strict_clippy: bool,
|
||||
/// The exercise's hint to be shown to the user on request.
|
||||
// The hint text associated with the exercise
|
||||
pub hint: String,
|
||||
}
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl ExerciseInfo {
|
||||
/// Path to the exercise file starting with the `exercises/` directory.
|
||||
pub fn path(&self) -> String {
|
||||
let mut path = if let Some(dir) = &self.dir {
|
||||
// 14 = 10 + 1 + 3
|
||||
// exercises/ + / + .rs
|
||||
let mut path = String::with_capacity(14 + dir.len() + self.name.len());
|
||||
path.push_str("exercises/");
|
||||
path.push_str(dir);
|
||||
path.push('/');
|
||||
path
|
||||
if let Some(dir) = &self.dir {
|
||||
format!("exercises/{dir}/{}.rs", self.name)
|
||||
} else {
|
||||
// 13 = 10 + 3
|
||||
// exercises/ + .rs
|
||||
let mut path = String::with_capacity(13 + self.name.len());
|
||||
path.push_str("exercises/");
|
||||
path
|
||||
};
|
||||
|
||||
path.push_str(&self.name);
|
||||
path.push_str(".rs");
|
||||
|
||||
path
|
||||
format!("exercises/{}.rs", self.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The deserialized `info.toml` file.
|
||||
#[derive(Deserialize)]
|
||||
pub struct InfoFile {
|
||||
/// For possible breaking changes in the future for third-party exercises.
|
||||
pub format_version: u8,
|
||||
/// Shown to users when starting with the exercises.
|
||||
pub welcome_message: Option<String>,
|
||||
/// Shown to users after finishing all exercises.
|
||||
pub final_message: Option<String>,
|
||||
/// List of all exercises.
|
||||
pub exercises: Vec<ExerciseInfo>,
|
||||
}
|
||||
|
||||
impl InfoFile {
|
||||
/// Official exercises: Parse the embedded `info.toml` file.
|
||||
/// Third-party exercises: Parse the `info.toml` file in the current directory.
|
||||
pub fn parse() -> Result<Self> {
|
||||
// Read a local `info.toml` if it exists.
|
||||
let slf = match fs::read_to_string("info.toml") {
|
||||
|
@ -74,7 +47,7 @@ impl InfoFile {
|
|||
.context("Failed to parse the `info.toml` file")?,
|
||||
Err(e) => {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
|
||||
return toml_edit::de::from_str(include_str!("../info.toml"))
|
||||
.context("Failed to parse the embedded `info.toml` file");
|
||||
}
|
||||
|
||||
|
|
24
src/init.rs
24
src/init.rs
|
@ -11,11 +11,8 @@ use std::{
|
|||
use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
// Prevent initialization in a directory that contains the file `Cargo.toml`.
|
||||
// This can mean that Rustlings was already initialized in this directory.
|
||||
// Otherwise, this can cause problems with Cargo workspaces.
|
||||
if Path::new("Cargo.toml").exists() {
|
||||
bail!(CARGO_TOML_EXISTS_ERR);
|
||||
if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() {
|
||||
bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR);
|
||||
}
|
||||
|
||||
let rustlings_path = Path::new("rustlings");
|
||||
|
@ -27,7 +24,7 @@ pub fn init() -> Result<()> {
|
|||
}
|
||||
|
||||
set_current_dir("rustlings")
|
||||
.context("Failed to change the current directory to `rustlings/`")?;
|
||||
.context("Failed to change the current directory to `rustlings`")?;
|
||||
|
||||
let info_file = InfoFile::parse()?;
|
||||
EMBEDDED_FILES
|
||||
|
@ -40,10 +37,9 @@ pub fn init() -> Result<()> {
|
|||
.as_bytes()
|
||||
.iter()
|
||||
.position(|c| *c == b'\n')
|
||||
.context("The embedded `Cargo.toml` is empty or contains only one line")?;
|
||||
let current_cargo_toml = current_cargo_toml
|
||||
.get(newline_ind + 1..)
|
||||
.context("The embedded `Cargo.toml` contains only one line")?;
|
||||
.context("The embedded `Cargo.toml` is empty or contains only one line.")?;
|
||||
let current_cargo_toml =
|
||||
¤t_cargo_toml[(newline_ind + 1).min(current_cargo_toml.len() - 1)..];
|
||||
let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
|
||||
.context("Failed to generate `Cargo.toml`")?;
|
||||
fs::write("Cargo.toml", updated_cargo_toml)
|
||||
|
@ -81,10 +77,12 @@ target
|
|||
|
||||
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
|
||||
|
||||
const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
|
||||
const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str =
|
||||
"A directory with the name `exercises` and a file with the name `Cargo.toml` already exist
|
||||
in the current directory. It looks like Rustlings was already initialized here.
|
||||
Run `rustlings` for instructions on getting started with the exercises.
|
||||
|
||||
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
|
||||
Otherwise, please run `rustlings init` again in another directory.";
|
||||
If you didn't already initialize Rustlings, please initialize it in another directory.";
|
||||
|
||||
const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
|
||||
"A directory with the name `rustlings` already exists in the current directory.
|
||||
|
|
|
@ -148,12 +148,14 @@ impl<'a> UiState<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_first(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn select_last(&mut self) {
|
||||
if self.n_rows > 0 {
|
||||
self.table_state.select(Some(self.n_rows - 1));
|
||||
|
|
|
@ -56,7 +56,7 @@ fn press_enter_prompt() -> io::Result<()> {
|
|||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Subcommands>,
|
||||
/// Manually run the current exercise using `r` in the watch mode.
|
||||
/// Manually run the current exercise using `r` or `run` in the watch mode.
|
||||
/// Only use this if Rustlings fails to detect exercise file changes.
|
||||
#[arg(long)]
|
||||
manual_run: bool,
|
||||
|
|
|
@ -12,7 +12,6 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
|||
const PROGRESS_EXCEEDS_MAX_ERR: &str =
|
||||
"The progress of the progress bar is higher than the maximum";
|
||||
|
||||
/// Terminal progress bar to be used when not using Ratataui.
|
||||
pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String> {
|
||||
use crossterm::style::Stylize;
|
||||
|
||||
|
@ -55,8 +54,6 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result<String
|
|||
Ok(line)
|
||||
}
|
||||
|
||||
/// Progress bar to be used with Ratataui.
|
||||
// Not using Ratatui's Gauge widget to keep the progress bar consistent.
|
||||
pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result<Line<'static>> {
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
|
|||
|
||||
match app_state.done_current_exercise(&mut stdout)? {
|
||||
ExercisesProgress::AllDone => (),
|
||||
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!(
|
||||
ExercisesProgress::Pending => println!(
|
||||
"Next exercise: {}",
|
||||
app_state.current_exercise().terminal_link(),
|
||||
),
|
||||
|
|
|
@ -7,18 +7,15 @@ pub struct TerminalFileLink<'a>(pub &'a str);
|
|||
|
||||
impl<'a> Display for TerminalFileLink<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let path = fs::canonicalize(self.0);
|
||||
|
||||
if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) {
|
||||
// Windows itself can't handle its verbatim paths.
|
||||
#[cfg(windows)]
|
||||
let path = if path.len() > 5 && &path[0..4] == r"\\?\" {
|
||||
&path[4..]
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0)
|
||||
if let Ok(Some(canonical_path)) = fs::canonicalize(self.0)
|
||||
.as_deref()
|
||||
.map(|path| path.to_str())
|
||||
{
|
||||
write!(
|
||||
f,
|
||||
"\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
|
||||
canonical_path, self.0,
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
|
|
16
src/watch.rs
16
src/watch.rs
|
@ -14,7 +14,7 @@ use std::{
|
|||
use crate::app_state::{AppState, ExercisesProgress};
|
||||
|
||||
use self::{
|
||||
notify_event::NotifyEventHandler,
|
||||
notify_event::DebounceEventHandler,
|
||||
state::WatchState,
|
||||
terminal_event::{terminal_event_handler, InputEvent},
|
||||
};
|
||||
|
@ -40,7 +40,6 @@ pub enum WatchExit {
|
|||
List,
|
||||
}
|
||||
|
||||
/// `notify_exercise_names` as None activates the manual run mode.
|
||||
pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
|
@ -53,7 +52,7 @@ pub fn watch(
|
|||
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names {
|
||||
let mut debouncer = new_debouncer(
|
||||
Duration::from_millis(200),
|
||||
NotifyEventHandler {
|
||||
DebounceEventHandler {
|
||||
tx: tx.clone(),
|
||||
exercise_names,
|
||||
},
|
||||
|
@ -80,8 +79,7 @@ pub fn watch(
|
|||
match event {
|
||||
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? {
|
||||
ExercisesProgress::AllDone => break,
|
||||
ExercisesProgress::CurrentPending => watch_state.render()?,
|
||||
ExercisesProgress::NewPending => watch_state.run_current_exercise()?,
|
||||
ExercisesProgress::Pending => watch_state.run_current_exercise()?,
|
||||
},
|
||||
WatchEvent::Input(InputEvent::Hint) => {
|
||||
watch_state.show_hint()?;
|
||||
|
@ -94,9 +92,11 @@ pub fn watch(
|
|||
break;
|
||||
}
|
||||
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?,
|
||||
WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?,
|
||||
WatchEvent::Input(InputEvent::Unrecognized(cmd)) => {
|
||||
watch_state.handle_invalid_cmd(&cmd)?;
|
||||
}
|
||||
WatchEvent::FileChange { exercise_ind } => {
|
||||
watch_state.handle_file_change(exercise_ind)?;
|
||||
watch_state.run_exercise_with_ind(exercise_ind)?;
|
||||
}
|
||||
WatchEvent::TerminalResize => {
|
||||
watch_state.render()?;
|
||||
|
@ -124,5 +124,5 @@ The automatic detection of exercise file changes failed :(
|
|||
Please try running `rustlings` again.
|
||||
|
||||
If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher.
|
||||
You need to manually trigger running the current exercise using `r` then.
|
||||
You need to manually trigger running the current exercise using `r` (or `run`) then.
|
||||
";
|
||||
|
|
|
@ -3,24 +3,23 @@ use std::sync::mpsc::Sender;
|
|||
|
||||
use super::WatchEvent;
|
||||
|
||||
pub struct NotifyEventHandler {
|
||||
pub struct DebounceEventHandler {
|
||||
pub tx: Sender<WatchEvent>,
|
||||
/// Used to report which exercise was modified.
|
||||
pub exercise_names: &'static [&'static [u8]],
|
||||
}
|
||||
|
||||
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
||||
fn handle_event(&mut self, input_event: DebounceEventResult) {
|
||||
let output_event = match input_event {
|
||||
Ok(input_event) => {
|
||||
let Some(exercise_ind) = input_event
|
||||
impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler {
|
||||
fn handle_event(&mut self, event: DebounceEventResult) {
|
||||
let event = match event {
|
||||
Ok(event) => {
|
||||
let Some(exercise_ind) = event
|
||||
.iter()
|
||||
.filter_map(|input_event| {
|
||||
if input_event.kind != DebouncedEventKind::Any {
|
||||
.filter_map(|event| {
|
||||
if event.kind != DebouncedEventKind::Any {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_name = input_event.path.file_name()?.to_str()?.as_bytes();
|
||||
let file_name = event.path.file_name()?.to_str()?.as_bytes();
|
||||
|
||||
if file_name.len() < 4 {
|
||||
return None;
|
||||
|
@ -47,6 +46,6 @@ impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler {
|
|||
|
||||
// An error occurs when the receiver is dropped.
|
||||
// After dropping the receiver, the debouncer guard should also be dropped.
|
||||
let _ = self.tx.send(output_event);
|
||||
let _ = self.tx.send(event);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
style::{style, Stylize},
|
||||
terminal,
|
||||
terminal::size,
|
||||
};
|
||||
use std::io::{self, StdoutLock, Write};
|
||||
|
||||
|
@ -13,7 +13,6 @@ use crate::{
|
|||
terminal_link::TerminalFileLink,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum DoneStatus {
|
||||
DoneWithSolution(String),
|
||||
DoneWithoutSolution,
|
||||
|
@ -72,22 +71,17 @@ impl<'a> WatchState<'a> {
|
|||
self.render()
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
// Don't skip exercises on file changes to avoid confusion from missing exercises.
|
||||
// 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(());
|
||||
}
|
||||
|
||||
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> {
|
||||
self.app_state.set_current_exercise_ind(exercise_ind)?;
|
||||
self.run_current_exercise()
|
||||
}
|
||||
|
||||
/// Move on to the next exercise if the current one is done.
|
||||
pub fn next_exercise(&mut self) -> Result<ExercisesProgress> {
|
||||
if self.done_status == DoneStatus::Pending {
|
||||
return Ok(ExercisesProgress::CurrentPending);
|
||||
if matches!(self.done_status, DoneStatus::Pending) {
|
||||
self.writer
|
||||
.write_all(b"The current exercise isn't done yet\n")?;
|
||||
self.show_prompt()?;
|
||||
return Ok(ExercisesProgress::Pending);
|
||||
}
|
||||
|
||||
self.app_state.done_current_exercise(&mut self.writer)
|
||||
|
@ -97,24 +91,24 @@ impl<'a> WatchState<'a> {
|
|||
self.writer.write_all(b"\n")?;
|
||||
|
||||
if self.manual_run {
|
||||
write!(self.writer, "{}:run / ", 'r'.bold())?;
|
||||
write!(self.writer, "{}un/", 'r'.bold())?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
write!(self.writer, "{}:next / ", 'n'.bold())?;
|
||||
if !matches!(self.done_status, DoneStatus::Pending) {
|
||||
write!(self.writer, "{}ext/", 'n'.bold())?;
|
||||
}
|
||||
|
||||
if !self.show_hint {
|
||||
write!(self.writer, "{}:hint / ", 'h'.bold())?;
|
||||
write!(self.writer, "{}int/", 'h'.bold())?;
|
||||
}
|
||||
|
||||
write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?;
|
||||
write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?;
|
||||
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<()> {
|
||||
// Prevent having the first line shifted if clearing wasn't successful.
|
||||
// Prevent having the first line shifted.
|
||||
self.writer.write_all(b"\n")?;
|
||||
|
||||
clear_terminal(&mut self.writer)?;
|
||||
|
@ -131,12 +125,12 @@ impl<'a> WatchState<'a> {
|
|||
)?;
|
||||
}
|
||||
|
||||
if self.done_status != DoneStatus::Pending {
|
||||
if !matches!(self.done_status, DoneStatus::Pending) {
|
||||
writeln!(
|
||||
self.writer,
|
||||
"{}\n",
|
||||
"Exercise done ✓
|
||||
When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
||||
When you are done experimenting, enter `n` (or `next`) to move on to the next exercise 🦀"
|
||||
.bold()
|
||||
.green(),
|
||||
)?;
|
||||
|
@ -146,11 +140,11 @@ When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
|||
writeln!(
|
||||
self.writer,
|
||||
"A solution file can be found at {}\n",
|
||||
style(TerminalFileLink(solution_path)).underlined().green(),
|
||||
style(TerminalFileLink(solution_path)).underlined().green()
|
||||
)?;
|
||||
}
|
||||
|
||||
let line_width = terminal::size()?.0;
|
||||
let line_width = size()?.0;
|
||||
let progress_bar = progress_bar(
|
||||
self.app_state.n_done(),
|
||||
self.app_state.exercises().len() as u16,
|
||||
|
@ -171,4 +165,15 @@ When you are done experimenting, enter `n` to move on to the next exercise 🦀"
|
|||
self.show_hint = true;
|
||||
self.render()
|
||||
}
|
||||
|
||||
pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> {
|
||||
self.writer.write_all(b"Invalid command: ")?;
|
||||
self.writer.write_all(cmd.as_bytes())?;
|
||||
if cmd.len() > 1 {
|
||||
self.writer
|
||||
.write_all(b" (confusing input can occur after resizing the terminal)")?;
|
||||
}
|
||||
self.writer.write_all(b"\n")?;
|
||||
self.show_prompt()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,11 @@ pub enum InputEvent {
|
|||
Hint,
|
||||
List,
|
||||
Quit,
|
||||
Unrecognized,
|
||||
Unrecognized(String),
|
||||
}
|
||||
|
||||
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
||||
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
|
||||
let mut last_input_valid = false;
|
||||
let mut input = String::with_capacity(8);
|
||||
|
||||
let last_input_event = loop {
|
||||
let terminal_event = match event::read() {
|
||||
|
@ -29,49 +28,37 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
|
|||
|
||||
match terminal_event {
|
||||
Event::Key(key) => {
|
||||
match key.kind {
|
||||
KeyEventKind::Release | KeyEventKind::Repeat => continue,
|
||||
KeyEventKind::Press => (),
|
||||
}
|
||||
|
||||
if key.modifiers != KeyModifiers::NONE {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_event = match key.code {
|
||||
match key.kind {
|
||||
KeyEventKind::Release => continue,
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => (),
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if last_input_valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
InputEvent::Unrecognized
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let input_event = match c {
|
||||
'n' => InputEvent::Next,
|
||||
'h' => InputEvent::Hint,
|
||||
'l' => break InputEvent::List,
|
||||
'q' => break InputEvent::Quit,
|
||||
'r' if manual_run => InputEvent::Run,
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
last_input_valid = true;
|
||||
input_event
|
||||
}
|
||||
_ => {
|
||||
last_input_valid = false;
|
||||
continue;
|
||||
}
|
||||
let input_event = match input.trim() {
|
||||
"n" | "next" => InputEvent::Next,
|
||||
"h" | "hint" => InputEvent::Hint,
|
||||
"l" | "list" => break InputEvent::List,
|
||||
"q" | "quit" => break InputEvent::Quit,
|
||||
"r" | "run" if manual_run => InputEvent::Run,
|
||||
_ => InputEvent::Unrecognized(input.clone()),
|
||||
};
|
||||
|
||||
if tx.send(WatchEvent::Input(input_event)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
input.clear();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
input.push(c);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Event::Resize(_, _) => {
|
||||
if tx.send(WatchEvent::TerminalResize).is_err() {
|
||||
|
|
Loading…
Reference in a new issue