1
1
use super :: command_prelude:: * ;
2
2
use crate :: { get_book_dir, open} ;
3
- use ignore:: gitignore:: Gitignore ;
4
3
use mdbook:: errors:: Result ;
5
- use mdbook:: utils;
6
4
use mdbook:: MDBook ;
7
- use pathdiff:: diff_paths;
8
5
use 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 ;
12
9
13
10
// Create clap subcommand arguments
14
11
pub fn make_subcommand ( ) -> Command {
@@ -17,12 +14,28 @@ pub fn make_subcommand() -> Command {
17
14
. arg_dest_dir ( )
18
15
. arg_root_dir ( )
19
16
. 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
+ }
20
33
}
21
34
22
35
// Watch command implementation
23
36
pub fn execute ( args : & ArgMatches ) -> Result < ( ) > {
24
37
let book_dir = get_book_dir ( args) ;
25
- let mut book = MDBook :: load ( book_dir) ?;
38
+ let mut book = MDBook :: load ( & book_dir) ?;
26
39
27
40
let update_config = |book : & mut MDBook | {
28
41
if let Some ( dest_dir) = args. get_one :: < PathBuf > ( "dest-dir" ) {
@@ -41,42 +54,21 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
41
54
open ( path) ;
42
55
}
43
56
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, & || { } ) ;
56
59
57
60
Ok ( ( ) )
58
61
}
59
62
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) ,
80
72
}
81
73
}
82
74
@@ -86,144 +78,3 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
86
78
. map ( |p| p. join ( ".gitignore" ) )
87
79
. find ( |p| p. exists ( ) )
88
80
}
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