11use anyhow:: { Context , Error , Result , bail} ;
22use crossterm:: { QueueableCommand , cursor, terminal} ;
3- use serde:: Deserialize ;
43use std:: {
54 collections:: HashSet ,
6- env,
75 fs:: { File , OpenOptions } ,
86 io:: { Read , Seek , StdoutLock , Write } ,
97 path:: { MAIN_SEPARATOR_STR , Path } ,
@@ -12,12 +10,13 @@ use std::{
1210 atomic:: { AtomicUsize , Ordering :: Relaxed } ,
1311 mpsc,
1412 } ,
15- thread:: { self , JoinHandle } ,
13+ thread,
1614} ;
1715
1816use crate :: {
1917 clear_terminal,
2018 cmd:: CmdRunner ,
19+ editor:: { Editor , EditorJoinHandle } ,
2120 embedded:: EMBEDDED_FILES ,
2221 exercise:: { Exercise , RunnableExercise } ,
2322 info_file:: ExerciseInfo ,
@@ -50,44 +49,6 @@ pub enum CheckProgress {
5049 Pending ,
5150}
5251
53- #[ derive( Deserialize ) ]
54- struct Pane {
55- id : u32 ,
56- }
57-
58- #[ must_use]
59- pub struct EditCmdJoinHandle ( Option < JoinHandle < Result < ( String , u32 ) > > > ) ;
60-
61- fn parse_pane_id ( b : & [ u8 ] ) -> Option < ( String , u32 ) > {
62- // Remove newline
63- let b = b. get ( "terminal_" . len ( ) ..b. len ( ) . saturating_sub ( 1 ) ) ?;
64- let id_str = str:: from_utf8 ( b) . ok ( ) ?;
65-
66- let ( first, rest) = b. split_first ( ) ?;
67- let mut id = u32:: from ( first - b'0' ) ;
68-
69- for c in rest {
70- id = 10 * id + u32:: from ( c - b'0' ) ;
71- }
72-
73- Some ( ( id_str. to_owned ( ) , id) )
74- }
75-
76- fn close_pane ( pane_id : & str ) -> Result < ( ) > {
77- Command :: new ( "zellij" )
78- . arg ( "action" )
79- . arg ( "close-pane" )
80- . arg ( "-p" )
81- . arg ( pane_id)
82- . stdin ( Stdio :: null ( ) )
83- . stdout ( Stdio :: null ( ) )
84- . stderr ( Stdio :: null ( ) )
85- . status ( )
86- . context ( "Failed to run `zellij action close-pane -p ID`" ) ?;
87-
88- Ok ( ( ) )
89- }
90-
9152pub struct AppState {
9253 current_exercise_ind : usize ,
9354 exercises : Vec < Exercise > ,
@@ -100,15 +61,14 @@ pub struct AppState {
10061 official_exercises : bool ,
10162 cmd_runner : CmdRunner ,
10263 emit_file_links : bool ,
103- zellij : bool ,
104- open_pane : Option < ( String , u32 , usize ) > ,
64+ editor : Option < Editor > ,
10565}
10666
10767impl AppState {
10868 pub fn new (
10969 exercise_infos : Vec < ExerciseInfo > ,
11070 final_message : & ' static str ,
111- zellij : bool ,
71+ editor : Option < Editor > ,
11272 ) -> Result < ( Self , StateFileStatus ) > {
11373 let cmd_runner = CmdRunner :: build ( ) ?;
11474 let mut state_file = OpenOptions :: new ( )
@@ -150,7 +110,9 @@ impl AppState {
150110 Exercise {
151111 name : exercise_info. name ,
152112 dir : exercise_info. dir ,
153- path : exercise_info. path ( ) ,
113+ // Leaking for `Editor::open`.
114+ // Leaking is fine since the app state exists until the end of the program.
115+ path : exercise_info. path ( ) . leak ( ) ,
154116 canonical_path,
155117 test : exercise_info. test ,
156118 strict_clippy : exercise_info. strict_clippy ,
@@ -216,9 +178,8 @@ impl AppState {
216178 official_exercises : !Path :: new ( "info.toml" ) . exists ( ) ,
217179 cmd_runner,
218180 // VS Code has its own file link handling
219- emit_file_links : env:: var_os ( "TERM_PROGRAM" ) . is_none_or ( |v| v != "vscode" ) ,
220- zellij,
221- open_pane : None ,
181+ emit_file_links : !matches ! ( editor, Some ( Editor :: VSCode ) ) ,
182+ editor,
222183 } ;
223184
224185 Ok ( ( slf, state_file_status) )
@@ -376,9 +337,9 @@ impl AppState {
376337 pub fn reset_current_exercise ( & mut self ) -> Result < & str > {
377338 self . set_pending ( self . current_exercise_ind ) ?;
378339 let exercise = self . current_exercise ( ) ;
379- self . reset ( self . current_exercise_ind , & exercise. path ) ?;
340+ self . reset ( self . current_exercise_ind , exercise. path ) ?;
380341
381- Ok ( & exercise. path )
342+ Ok ( exercise. path )
382343 }
383344
384345 // Reset the exercise by index and return its name.
@@ -389,7 +350,7 @@ impl AppState {
389350
390351 self . set_pending ( exercise_ind) ?;
391352 let exercise = & self . exercises [ exercise_ind] ;
392- self . reset ( exercise_ind, & exercise. path ) ?;
353+ self . reset ( exercise_ind, exercise. path ) ?;
393354
394355 Ok ( exercise. name )
395356 }
@@ -598,81 +559,23 @@ impl AppState {
598559 Ok ( ( ) )
599560 }
600561
601- pub fn close_pane ( & mut self ) -> Result < ( ) > {
602- if let Some ( ( pane_id_str , _ , _ ) ) = self . open_pane . take ( ) {
603- close_pane ( & pane_id_str ) ? ;
562+ pub fn open_editor ( & mut self ) -> Result < EditorJoinHandle > {
563+ if let Some ( editor ) = self . editor . take ( ) {
564+ return editor . open ( self . current_exercise_ind , self . current_exercise ( ) . path ) ;
604565 }
605566
606- Ok ( ( ) )
567+ Ok ( EditorJoinHandle :: default ( ) )
607568 }
608569
609- pub fn edit_cmd ( & mut self ) -> Result < EditCmdJoinHandle > {
610- if !self . zellij {
611- return Ok ( EditCmdJoinHandle ( None ) ) ;
612- }
613-
614- let open_pane = self . open_pane . take ( ) ;
615- let current_exercise_ind = self . current_exercise_ind ;
616- let mut edit_cmd = Command :: new ( "zellij" ) ;
617- edit_cmd
618- . arg ( "action" )
619- . arg ( "edit" )
620- . arg ( & self . current_exercise ( ) . path )
621- . stdin ( Stdio :: null ( ) )
622- . stderr ( Stdio :: null ( ) ) ;
623-
624- let handle = thread:: Builder :: new ( )
625- . spawn ( move || {
626- if let Some ( ( pane_id_str, pane_id, exercise_ind) ) = open_pane {
627- if exercise_ind == current_exercise_ind {
628- // Check if the pane is still open
629- let mut output = Command :: new ( "zellij" )
630- . arg ( "action" )
631- . arg ( "list-panes" )
632- . arg ( "-j" )
633- . stdin ( Stdio :: null ( ) )
634- . stderr ( Stdio :: null ( ) )
635- . output ( )
636- . context ( "Failed to run `zellij action list-panes -j`" ) ?;
637-
638- if !output. status . success ( ) {
639- bail ! ( "`zellij action list-panes -j` didn't exit successfully" ) ;
640- }
641-
642- // Remove newline
643- output. stdout . pop ( ) ;
644-
645- let panes = serde_json:: de:: from_slice :: < Vec < Pane > > ( & output. stdout )
646- . context (
647- "Failed to parse the output of `zellij action list-panes -j`" ,
648- ) ?;
570+ pub fn join_editor_handle ( & mut self , handle : EditorJoinHandle ) -> Result < ( ) > {
571+ self . editor = handle. join ( ) ?;
649572
650- if panes. iter ( ) . any ( |pane| pane. id == pane_id) {
651- return Ok ( ( pane_id_str, pane_id) ) ;
652- }
653- } else {
654- close_pane ( & pane_id_str) ?;
655- }
656- }
657-
658- let output = edit_cmd. output ( ) ?;
659-
660- if !output. status . success ( ) {
661- bail ! ( "Failed to open a new Zellij editor pane" ) ;
662- }
663-
664- parse_pane_id ( & output. stdout )
665- . context ( "Failed to parse the ID of the new Zellij pane" )
666- } )
667- . context ( "Failed to spawn a thread to open and close Zellij panes" ) ?;
668-
669- Ok ( EditCmdJoinHandle ( Some ( handle) ) )
573+ Ok ( ( ) )
670574 }
671575
672- pub fn join_edit_cmd ( & mut self , handle : EditCmdJoinHandle ) -> Result < ( ) > {
673- if let Some ( handle) = handle. 0 {
674- let ( pane_id_str, pane_id) = handle. join ( ) . unwrap ( ) ?;
675- self . open_pane = Some ( ( pane_id_str, pane_id, self . current_exercise_ind ) ) ;
576+ pub fn close_editor ( & mut self ) -> Result < ( ) > {
577+ if let Some ( editor) = & mut self . editor {
578+ editor. close ( ) ?;
676579 }
677580
678581 Ok ( ( ) )
@@ -711,7 +614,7 @@ mod tests {
711614 Exercise {
712615 name : "0" ,
713616 dir : None ,
714- path : String :: from ( "exercises/0.rs" ) ,
617+ path : "exercises/0.rs" ,
715618 canonical_path : None ,
716619 test : false ,
717620 strict_clippy : false ,
@@ -732,8 +635,7 @@ mod tests {
732635 official_exercises : true ,
733636 cmd_runner : CmdRunner :: build ( ) . unwrap ( ) ,
734637 emit_file_links : true ,
735- zellij : false ,
736- open_pane : None ,
638+ editor : None ,
737639 } ;
738640
739641 let mut assert = |done : [ bool ; 3 ] , expected : [ Option < usize > ; 3 ] | {
0 commit comments