11use super :: command_prelude:: * ;
22use crate :: { get_book_dir, open} ;
3- use ignore:: gitignore:: Gitignore ;
43use mdbook:: errors:: Result ;
5- use mdbook:: utils;
64use mdbook:: MDBook ;
7- use pathdiff:: diff_paths;
85use std:: path:: { Path , PathBuf } ;
9- use std :: sync :: mpsc :: channel ;
10- use std :: thread :: sleep ;
11- use std :: time :: Duration ;
6+
7+ mod native ;
8+ mod poller ;
129
1310// Create clap subcommand arguments
1411pub fn make_subcommand ( ) -> Command {
@@ -17,12 +14,28 @@ pub fn make_subcommand() -> Command {
1714 . arg_dest_dir ( )
1815 . arg_root_dir ( )
1916 . arg_open ( )
17+ . arg_watcher ( )
18+ }
19+
20+ pub enum WatcherKind {
21+ Poll ,
22+ Native ,
23+ }
24+
25+ impl WatcherKind {
26+ pub fn from_str ( s : & str ) -> WatcherKind {
27+ match s {
28+ "poll" => WatcherKind :: Poll ,
29+ "native" => WatcherKind :: Native ,
30+ _ => panic ! ( "unsupported watcher {s}" ) ,
31+ }
32+ }
2033}
2134
2235// Watch command implementation
2336pub fn execute ( args : & ArgMatches ) -> Result < ( ) > {
2437 let book_dir = get_book_dir ( args) ;
25- let mut book = MDBook :: load ( book_dir) ?;
38+ let mut book = MDBook :: load ( & book_dir) ?;
2639
2740 let update_config = |book : & mut MDBook | {
2841 if let Some ( dest_dir) = args. get_one :: < PathBuf > ( "dest-dir" ) {
@@ -41,42 +54,21 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
4154 open ( path) ;
4255 }
4356
44- trigger_on_change ( & book, |paths, book_dir| {
45- info ! ( "Files changed: {:?}\n Building book...\n " , paths) ;
46- let result = MDBook :: load ( book_dir) . and_then ( |mut b| {
47- update_config ( & mut b) ;
48- b. build ( )
49- } ) ;
50-
51- if let Err ( e) = result {
52- error ! ( "Unable to build the book" ) ;
53- utils:: log_backtrace ( & e) ;
54- }
55- } ) ;
57+ let watcher = WatcherKind :: from_str ( args. get_one :: < String > ( "watcher" ) . unwrap ( ) ) ;
58+ rebuild_on_change ( watcher, & book_dir, & update_config, & || { } ) ;
5659
5760 Ok ( ( ) )
5861}
5962
60- fn remove_ignored_files ( book_root : & Path , paths : & [ PathBuf ] ) -> Vec < PathBuf > {
61- if paths. is_empty ( ) {
62- return vec ! [ ] ;
63- }
64-
65- match find_gitignore ( book_root) {
66- Some ( gitignore_path) => {
67- let ( ignore, err) = Gitignore :: new ( & gitignore_path) ;
68- if let Some ( err) = err {
69- warn ! (
70- "error reading gitignore `{}`: {err}" ,
71- gitignore_path. display( )
72- ) ;
73- }
74- filter_ignored_files ( ignore, paths)
75- }
76- None => {
77- // There is no .gitignore file.
78- paths. iter ( ) . map ( |path| path. to_path_buf ( ) ) . collect ( )
79- }
63+ pub fn rebuild_on_change (
64+ kind : WatcherKind ,
65+ book_dir : & Path ,
66+ update_config : & dyn Fn ( & mut MDBook ) ,
67+ post_build : & dyn Fn ( ) ,
68+ ) {
69+ match kind {
70+ WatcherKind :: Poll => self :: poller:: rebuild_on_change ( book_dir, update_config, post_build) ,
71+ WatcherKind :: Native => self :: native:: rebuild_on_change ( book_dir, update_config, post_build) ,
8072 }
8173}
8274
@@ -86,144 +78,3 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
8678 . map ( |p| p. join ( ".gitignore" ) )
8779 . find ( |p| p. exists ( ) )
8880}
89-
90- // Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
91- // For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
92- fn filter_ignored_files ( ignore : Gitignore , paths : & [ PathBuf ] ) -> Vec < PathBuf > {
93- let ignore_root = ignore
94- . path ( )
95- . canonicalize ( )
96- . expect ( "ignore root canonicalize error" ) ;
97-
98- paths
99- . iter ( )
100- . filter ( |path| {
101- let relative_path =
102- diff_paths ( path, & ignore_root) . expect ( "One of the paths should be an absolute" ) ;
103- !ignore
104- . matched_path_or_any_parents ( & relative_path, relative_path. is_dir ( ) )
105- . is_ignore ( )
106- } )
107- . map ( |path| path. to_path_buf ( ) )
108- . collect ( )
109- }
110-
111- /// Calls the closure when a book source file is changed, blocking indefinitely.
112- pub fn trigger_on_change < F > ( book : & MDBook , closure : F )
113- where
114- F : Fn ( Vec < PathBuf > , & Path ) ,
115- {
116- use notify:: RecursiveMode :: * ;
117-
118- // Create a channel to receive the events.
119- let ( tx, rx) = channel ( ) ;
120-
121- let mut debouncer = match notify_debouncer_mini:: new_debouncer ( Duration :: from_secs ( 1 ) , tx) {
122- Ok ( d) => d,
123- Err ( e) => {
124- error ! ( "Error while trying to watch the files:\n \n \t {:?}" , e) ;
125- std:: process:: exit ( 1 )
126- }
127- } ;
128- let watcher = debouncer. watcher ( ) ;
129-
130- // Add the source directory to the watcher
131- if let Err ( e) = watcher. watch ( & book. source_dir ( ) , Recursive ) {
132- error ! ( "Error while watching {:?}:\n {:?}" , book. source_dir( ) , e) ;
133- std:: process:: exit ( 1 ) ;
134- } ;
135-
136- let _ = watcher. watch ( & book. theme_dir ( ) , Recursive ) ;
137-
138- // Add the book.toml file to the watcher if it exists
139- let _ = watcher. watch ( & book. root . join ( "book.toml" ) , NonRecursive ) ;
140-
141- for dir in & book. config . build . extra_watch_dirs {
142- let path = book. root . join ( dir) ;
143- let canonical_path = path. canonicalize ( ) . unwrap_or_else ( |e| {
144- error ! ( "Error while watching extra directory {path:?}:\n {e}" ) ;
145- std:: process:: exit ( 1 ) ;
146- } ) ;
147-
148- if let Err ( e) = watcher. watch ( & canonical_path, Recursive ) {
149- error ! (
150- "Error while watching extra directory {:?}:\n {:?}" ,
151- canonical_path, e
152- ) ;
153- std:: process:: exit ( 1 ) ;
154- }
155- }
156-
157- info ! ( "Listening for changes..." ) ;
158-
159- loop {
160- let first_event = rx. recv ( ) . unwrap ( ) ;
161- sleep ( Duration :: from_millis ( 50 ) ) ;
162- let other_events = rx. try_iter ( ) ;
163-
164- let all_events = std:: iter:: once ( first_event) . chain ( other_events) ;
165-
166- let paths: Vec < _ > = all_events
167- . filter_map ( |event| match event {
168- Ok ( events) => Some ( events) ,
169- Err ( error) => {
170- log:: warn!( "error while watching for changes: {error}" ) ;
171- None
172- }
173- } )
174- . flatten ( )
175- . map ( |event| event. path )
176- . collect ( ) ;
177-
178- // If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
179- // ignored by gitignore. So we handle this case by including such files into the watched paths list.
180- let any_external_paths = paths. iter ( ) . filter ( |p| !p. starts_with ( & book. root ) ) . cloned ( ) ;
181- let mut paths = remove_ignored_files ( & book. root , & paths[ ..] ) ;
182- paths. extend ( any_external_paths) ;
183-
184- if !paths. is_empty ( ) {
185- closure ( paths, & book. root ) ;
186- }
187- }
188- }
189-
190- #[ cfg( test) ]
191- mod tests {
192- use super :: * ;
193- use ignore:: gitignore:: GitignoreBuilder ;
194- use std:: env;
195-
196- #[ test]
197- fn test_filter_ignored_files ( ) {
198- let current_dir = env:: current_dir ( ) . unwrap ( ) ;
199-
200- let ignore = GitignoreBuilder :: new ( & current_dir)
201- . add_line ( None , "*.html" )
202- . unwrap ( )
203- . build ( )
204- . unwrap ( ) ;
205- let should_remain = current_dir. join ( "record.text" ) ;
206- let should_filter = current_dir. join ( "index.html" ) ;
207-
208- let remain = filter_ignored_files ( ignore, & [ should_remain. clone ( ) , should_filter] ) ;
209- assert_eq ! ( remain, vec![ should_remain] )
210- }
211-
212- #[ test]
213- fn filter_ignored_files_should_handle_parent_dir ( ) {
214- let current_dir = env:: current_dir ( ) . unwrap ( ) ;
215-
216- let ignore = GitignoreBuilder :: new ( & current_dir)
217- . add_line ( None , "*.html" )
218- . unwrap ( )
219- . build ( )
220- . unwrap ( ) ;
221-
222- let parent_dir = current_dir. join ( ".." ) ;
223- let should_remain = parent_dir. join ( "record.text" ) ;
224- let should_filter = parent_dir. join ( "index.html" ) ;
225-
226- let remain = filter_ignored_files ( ignore, & [ should_remain. clone ( ) , should_filter] ) ;
227- assert_eq ! ( remain, vec![ should_remain] )
228- }
229- }
0 commit comments