summaryrefslogtreecommitdiff
path: root/src/exercise.rs
diff options
context:
space:
mode:
authorRoberto Vidal <vidal.roberto.j@gmail.com>2019-11-11 13:38:24 +0100
committerRoberto Vidal <vidal.roberto.j@gmail.com>2019-11-11 16:23:35 +0100
commit2cdd61294f0d9a53775ee24ad76435bec8a21e60 (patch)
tree36442292a4e3b0ae07072b0dfaf59a3bc799a397 /src/exercise.rs
parenta47a62172a5d4c479d242498b9b140b3111fb7c6 (diff)
feat: improve `watch` execution mode
The `watch` command now requires user action to move to the next exercise. BREAKING CHANGE: this changes the behavior of `watch`.
Diffstat (limited to 'src/exercise.rs')
-rw-r--r--src/exercise.rs113
1 files changed, 110 insertions, 3 deletions
diff --git a/src/exercise.rs b/src/exercise.rs
index 6f526e7..f27b545 100644
--- a/src/exercise.rs
+++ b/src/exercise.rs
@@ -1,16 +1,20 @@
+use regex::Regex;
use serde::Deserialize;
use std::fmt::{self, Display, Formatter};
-use std::fs::remove_file;
+use std::fs::{remove_file, File};
+use std::io::Read;
use std::path::PathBuf;
use std::process::{self, Command, Output};
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
+const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
+const CONTEXT: usize = 2;
fn temp_file() -> String {
format!("./temp_{}", process::id())
}
-#[derive(Deserialize)]
+#[derive(Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Compile,
@@ -28,6 +32,19 @@ pub struct Exercise {
pub mode: Mode,
}
+#[derive(PartialEq, Debug)]
+pub enum State {
+ Done,
+ Pending(Vec<ContextLine>),
+}
+
+#[derive(PartialEq, Debug)]
+pub struct ContextLine {
+ pub line: String,
+ pub number: usize,
+ pub important: bool,
+}
+
impl Exercise {
pub fn compile(&self) -> Output {
match self.mode {
@@ -52,6 +69,48 @@ impl Exercise {
pub fn clean(&self) {
let _ignored = remove_file(&temp_file());
}
+
+ pub fn state(&self) -> State {
+ let mut source_file =
+ File::open(&self.path).expect("We were unable to open the exercise file!");
+
+ let source = {
+ let mut s = String::new();
+ source_file
+ .read_to_string(&mut s)
+ .expect("We were unable to read the exercise file!");
+ s
+ };
+
+ let re = Regex::new(I_AM_DONE_REGEX).unwrap();
+
+ if !re.is_match(&source) {
+ return State::Done;
+ }
+
+ let matched_line_index = source
+ .lines()
+ .enumerate()
+ .filter_map(|(i, line)| if re.is_match(line) { Some(i) } else { None })
+ .next()
+ .expect("This should not happen at all");
+
+ let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize;
+ let max_line = matched_line_index + CONTEXT;
+
+ let context = source
+ .lines()
+ .enumerate()
+ .filter(|&(i, _)| i >= min_line && i <= max_line)
+ .map(|(i, line)| ContextLine {
+ line: line.to_string(),
+ number: i + 1,
+ important: i == matched_line_index,
+ })
+ .collect();
+
+ State::Pending(context)
+ }
}
impl Display for Exercise {
@@ -63,7 +122,6 @@ impl Display for Exercise {
#[cfg(test)]
mod test {
use super::*;
- use std::fs::File;
use std::path::Path;
#[test]
@@ -76,4 +134,53 @@ mod test {
exercise.clean();
assert!(!Path::new(&temp_file()).exists());
}
+
+ #[test]
+ fn test_pending_state() {
+ let exercise = Exercise {
+ path: PathBuf::from("tests/fixture/state/pending_exercise.rs"),
+ mode: Mode::Compile,
+ };
+
+ let state = exercise.state();
+ let expected = vec![
+ ContextLine {
+ line: "// fake_exercise".to_string(),
+ number: 1,
+ important: false,
+ },
+ ContextLine {
+ line: "".to_string(),
+ number: 2,
+ important: false,
+ },
+ ContextLine {
+ line: "// I AM NOT DONE".to_string(),
+ number: 3,
+ important: true,
+ },
+ ContextLine {
+ line: "".to_string(),
+ number: 4,
+ important: false,
+ },
+ ContextLine {
+ line: "fn main() {".to_string(),
+ number: 5,
+ important: false,
+ },
+ ];
+
+ assert_eq!(state, State::Pending(expected));
+ }
+
+ #[test]
+ fn test_finished_exercise() {
+ let exercise = Exercise {
+ path: PathBuf::from("tests/fixture/state/finished_exercise.rs"),
+ mode: Mode::Compile,
+ };
+
+ assert_eq!(exercise.state(), State::Done);
+ }
}