11use anyhow:: { bail, Context , Result } ;
22use std:: {
33 env,
4- fs:: { self , File } ,
5- io:: { self , Read , StdoutLock , Write } ,
4+ fs:: { File , OpenOptions } ,
5+ io:: { self , Read , Seek , StdoutLock , Write } ,
66 path:: Path ,
77 process:: { Command , Stdio } ,
88 thread,
@@ -18,7 +18,6 @@ use crate::{
1818} ;
1919
2020const STATE_FILE_NAME : & str = ".rustlings-state.txt" ;
21- const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
2221
2322#[ must_use]
2423pub enum ExercisesProgress {
@@ -47,6 +46,7 @@ pub struct AppState {
4746 // Caches the number of done exercises to avoid iterating over all exercises every time.
4847 n_done : u16 ,
4948 final_message : String ,
49+ state_file : File ,
5050 // Preallocated buffer for reading and writing the state file.
5151 file_buf : Vec < u8 > ,
5252 official_exercises : bool ,
@@ -56,59 +56,22 @@ pub struct AppState {
5656}
5757
5858impl AppState {
59- // Update the app state from the state file.
60- fn update_from_file ( & mut self ) -> StateFileStatus {
61- self . file_buf . clear ( ) ;
62- self . n_done = 0 ;
63-
64- if File :: open ( STATE_FILE_NAME )
65- . and_then ( |mut file| file. read_to_end ( & mut self . file_buf ) )
66- . is_err ( )
67- {
68- return StateFileStatus :: NotRead ;
69- }
70-
71- // See `Self::write` for more information about the file format.
72- let mut lines = self . file_buf . split ( |c| * c == b'\n' ) . skip ( 2 ) ;
73-
74- let Some ( current_exercise_name) = lines. next ( ) else {
75- return StateFileStatus :: NotRead ;
76- } ;
77-
78- if current_exercise_name. is_empty ( ) || lines. next ( ) . is_none ( ) {
79- return StateFileStatus :: NotRead ;
80- }
81-
82- let mut done_exercises = hash_set_with_capacity ( self . exercises . len ( ) ) ;
83-
84- for done_exerise_name in lines {
85- if done_exerise_name. is_empty ( ) {
86- break ;
87- }
88- done_exercises. insert ( done_exerise_name) ;
89- }
90-
91- for ( ind, exercise) in self . exercises . iter_mut ( ) . enumerate ( ) {
92- if done_exercises. contains ( exercise. name . as_bytes ( ) ) {
93- exercise. done = true ;
94- self . n_done += 1 ;
95- }
96-
97- if exercise. name . as_bytes ( ) == current_exercise_name {
98- self . current_exercise_ind = ind;
99- }
100- }
101-
102- StateFileStatus :: Read
103- }
104-
10559 pub fn new (
10660 exercise_infos : Vec < ExerciseInfo > ,
10761 final_message : String ,
10862 ) -> Result < ( Self , StateFileStatus ) > {
10963 let cmd_runner = CmdRunner :: build ( ) ?;
110-
111- let exercises = exercise_infos
64+ let mut state_file = OpenOptions :: new ( )
65+ . create ( true )
66+ . read ( true )
67+ . write ( true )
68+ . truncate ( false )
69+ . open ( STATE_FILE_NAME )
70+ . with_context ( || {
71+ format ! ( "Failed to open or create the state file {STATE_FILE_NAME}" )
72+ } ) ?;
73+
74+ let mut exercises = exercise_infos
11275 . into_iter ( )
11376 . map ( |exercise_info| {
11477 // Leaking to be able to borrow in the watch mode `Table`.
@@ -126,25 +89,69 @@ impl AppState {
12689 test : exercise_info. test ,
12790 strict_clippy : exercise_info. strict_clippy ,
12891 hint,
129- // Updated in `Self::update_from_file` .
92+ // Updated below .
13093 done : false ,
13194 }
13295 } )
13396 . collect :: < Vec < _ > > ( ) ;
13497
135- let mut slf = Self {
136- current_exercise_ind : 0 ,
98+ let mut current_exercise_ind = 0 ;
99+ let mut n_done = 0 ;
100+ let mut file_buf = Vec :: with_capacity ( 2048 ) ;
101+ let state_file_status = ' block: {
102+ if state_file. read_to_end ( & mut file_buf) . is_err ( ) {
103+ break ' block StateFileStatus :: NotRead ;
104+ }
105+
106+ // See `Self::write` for more information about the file format.
107+ let mut lines = file_buf. split ( |c| * c == b'\n' ) . skip ( 2 ) ;
108+
109+ let Some ( current_exercise_name) = lines. next ( ) else {
110+ break ' block StateFileStatus :: NotRead ;
111+ } ;
112+
113+ if current_exercise_name. is_empty ( ) || lines. next ( ) . is_none ( ) {
114+ break ' block StateFileStatus :: NotRead ;
115+ }
116+
117+ let mut done_exercises = hash_set_with_capacity ( exercises. len ( ) ) ;
118+
119+ for done_exerise_name in lines {
120+ if done_exerise_name. is_empty ( ) {
121+ break ;
122+ }
123+ done_exercises. insert ( done_exerise_name) ;
124+ }
125+
126+ for ( ind, exercise) in exercises. iter_mut ( ) . enumerate ( ) {
127+ if done_exercises. contains ( exercise. name . as_bytes ( ) ) {
128+ exercise. done = true ;
129+ n_done += 1 ;
130+ }
131+
132+ if exercise. name . as_bytes ( ) == current_exercise_name {
133+ current_exercise_ind = ind;
134+ }
135+ }
136+
137+ StateFileStatus :: Read
138+ } ;
139+
140+ file_buf. clear ( ) ;
141+ file_buf. extend_from_slice ( STATE_FILE_HEADER ) ;
142+
143+ let slf = Self {
144+ current_exercise_ind,
137145 exercises,
138- n_done : 0 ,
146+ n_done,
139147 final_message,
140- file_buf : Vec :: with_capacity ( 2048 ) ,
148+ state_file,
149+ file_buf,
141150 official_exercises : !Path :: new ( "info.toml" ) . exists ( ) ,
142151 cmd_runner,
143152 vs_code : env:: var_os ( "TERM_PROGRAM" ) . is_some_and ( |v| v == "vscode" ) ,
144153 } ;
145154
146- let state_file_status = slf. update_from_file ( ) ;
147-
148155 Ok ( ( slf, state_file_status) )
149156 }
150157
@@ -187,10 +194,8 @@ impl AppState {
187194 // - The fourth line is an empty line.
188195 // - All remaining lines are the names of done exercises.
189196 fn write ( & mut self ) -> Result < ( ) > {
190- self . file_buf . clear ( ) ;
197+ self . file_buf . truncate ( STATE_FILE_HEADER . len ( ) ) ;
191198
192- self . file_buf
193- . extend_from_slice ( b"DON'T EDIT THIS FILE!\n \n " ) ;
194199 self . file_buf
195200 . extend_from_slice ( self . current_exercise ( ) . name . as_bytes ( ) ) ;
196201 self . file_buf . push ( b'\n' ) ;
@@ -202,7 +207,14 @@ impl AppState {
202207 }
203208 }
204209
205- fs:: write ( STATE_FILE_NAME , & self . file_buf )
210+ self . state_file
211+ . rewind ( )
212+ . with_context ( || format ! ( "Failed to rewind the state file {STATE_FILE_NAME}" ) ) ?;
213+ self . state_file
214+ . set_len ( 0 )
215+ . with_context ( || format ! ( "Failed to truncate the state file {STATE_FILE_NAME}" ) ) ?;
216+ self . state_file
217+ . write_all ( & self . file_buf )
206218 . with_context ( || format ! ( "Failed to write the state file {STATE_FILE_NAME}" ) ) ?;
207219
208220 Ok ( ( ) )
@@ -440,11 +452,12 @@ impl AppState {
440452 }
441453}
442454
455+ const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
456+ const STATE_FILE_HEADER : & [ u8 ] = b"DON'T EDIT THIS FILE!\n \n " ;
443457const RERUNNING_ALL_EXERCISES_MSG : & [ u8 ] = b"
444458All exercises seem to be done.
445459Recompiling and running all exercises to make sure that all of them are actually done.
446460" ;
447-
448461const FENISH_LINE : & str = "+----------------------------------------------------+
449462| You made it to the Fe-nish line! |
450463+-------------------------- ------------------------+
@@ -490,6 +503,7 @@ mod tests {
490503 exercises : vec ! [ dummy_exercise( ) , dummy_exercise( ) , dummy_exercise( ) ] ,
491504 n_done : 0 ,
492505 final_message : String :: new ( ) ,
506+ state_file : tempfile:: tempfile ( ) . unwrap ( ) ,
493507 file_buf : Vec :: new ( ) ,
494508 official_exercises : true ,
495509 cmd_runner : CmdRunner :: build ( ) . unwrap ( ) ,
0 commit comments