1use std::cell::RefCell;
5use std::path::{Path, PathBuf};
6use std::process::ExitCode;
7use std::rc::Rc;
8use std::time::Duration;
9
10use nifty_interp::{ErrorHandler, LiveApp, RuntimeError};
11use nifty_platform::PlatformBackend;
12use nifty_runtime::Waker;
13use nifty_syntax::project;
14use notify_debouncer_mini::notify::{RecommendedWatcher, RecursiveMode};
15use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer};
16
17use crate::error::CliError;
18use crate::frontend::Sources;
19use crate::{CommandResult, report};
20
21const SAVE_SETTLE_TIME: Duration = Duration::from_millis(100);
23
24type Showing = Rc<RefCell<Option<Rc<Sources>>>>;
26
27pub(crate) fn run(target: &Path) -> CommandResult {
28 let first = Sources::read(target)?;
29 let (folder, mode) = watched(target);
30 let showing = Showing::default();
31 let session = Rc::new(DevSession {
32 target: target.to_path_buf(),
33 live: LiveApp::new(
34 nifty_platform::platform_backend(),
35 report_and_continue(&showing),
36 ),
37 showing,
38 last_read: RefCell::new(None),
39 });
40 session.show(first);
41
42 let waker = session.live.waker(Box::new({
43 let session = Rc::downgrade(&session);
44 move || {
45 if let Some(session) = session.upgrade() {
46 session.reload();
47 }
48 }
49 }));
50 let _watcher = watch(&folder, mode, waker)?;
51 println!(
52 "watching {} for changes; quit the app or press Ctrl-C to stop",
53 folder.display()
54 );
55 session.live.run();
56 Ok(ExitCode::SUCCESS)
57}
58
59struct DevSession {
60 target: PathBuf,
61 live: LiveApp<PlatformBackend>,
62 showing: Showing,
63 last_read: RefCell<Option<Rc<Sources>>>,
65}
66
67impl DevSession {
68 fn reload(&self) {
69 match Sources::read(&self.target) {
70 Ok(sources) => self.show(sources),
71 Err(error) => eprintln!("error: {error}"),
72 }
73 }
74
75 fn show(&self, sources: Sources) {
78 if self.last_read.borrow().as_deref() == Some(&sources) {
79 return;
80 }
81 let sources = Rc::new(sources);
82 self.last_read.replace(Some(Rc::clone(&sources)));
83
84 let was_showing = self.showing.borrow().is_some();
85 if self.load(&sources) {
86 let verb = if was_showing { "reloaded" } else { "started" };
87 println!("{}: {verb}", sources.name());
88 self.showing.replace(Some(sources));
89 } else if was_showing {
90 eprintln!("still showing the last version that worked; save a fix to reload");
91 } else {
92 eprintln!("save a fix to start the app");
93 }
94 }
95
96 fn load(&self, sources: &Sources) -> bool {
98 match sources.compile() {
99 Ok(program) => match self.live.load(program) {
100 Ok(()) => true,
101 Err(error) => {
102 report::runtime_error(sources, &error);
103 false
104 }
105 },
106 Err(diagnostics) => {
107 if let Err(error) = report::print(sources, &diagnostics) {
108 eprintln!("error: {error}");
109 }
110 false
111 }
112 }
113 }
114}
115
116fn report_and_continue(showing: &Showing) -> ErrorHandler {
119 let showing = Rc::clone(showing);
120 Rc::new(move |error: RuntimeError| match &*showing.borrow() {
121 Some(sources) => report::runtime_error(sources, &error),
122 None => eprintln!("error: {}", error.message),
123 })
124}
125
126fn watched(target: &Path) -> (PathBuf, RecursiveMode) {
130 if target.is_dir() {
131 return (target.to_path_buf(), RecursiveMode::Recursive);
132 }
133 let folder = match target.parent() {
134 Some(folder) if !folder.as_os_str().is_empty() => folder.to_path_buf(),
135 _ => PathBuf::from("."),
136 };
137 (folder, RecursiveMode::NonRecursive)
138}
139
140fn watch(
142 folder: &Path,
143 mode: RecursiveMode,
144 waker: impl Waker,
145) -> Result<Debouncer<RecommendedWatcher>, CliError> {
146 let watch_error = |error| {
147 CliError::new(format!(
148 "can't watch {} for changes: {error}",
149 folder.display()
150 ))
151 };
152 let mut debouncer =
153 new_debouncer(
154 SAVE_SETTLE_TIME,
155 move |result: DebounceEventResult| match result {
156 Ok(events) => {
157 if events.iter().any(|event| project::is_source(&event.path)) {
158 waker.wake();
159 }
160 }
161 Err(error) => eprintln!("error: watching for changes failed: {error}"),
162 },
163 )
164 .map_err(watch_error)?;
165 debouncer
166 .watcher()
167 .watch(folder, mode)
168 .map_err(watch_error)?;
169 Ok(debouncer)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::test_support::scratch_dir;
176
177 #[test]
178 fn a_single_file_is_watched_in_its_folder() {
179 assert_eq!(
180 watched(Path::new("demo.nf")),
181 (PathBuf::from("."), RecursiveMode::NonRecursive)
182 );
183 assert_eq!(
184 watched(Path::new("apps/demo.nf")),
185 (PathBuf::from("apps"), RecursiveMode::NonRecursive)
186 );
187 }
188
189 #[test]
190 fn a_project_is_watched_with_all_its_subfolders() {
191 let dir = scratch_dir("dev-watch-project");
192 assert_eq!(watched(&dir), (dir.clone(), RecursiveMode::Recursive));
193 }
194
195 #[test]
196 fn only_nf_files_count_as_source() {
197 assert!(project::is_source(Path::new("/app/main.nf")));
198 assert!(!project::is_source(Path::new("/app/main.nf.swp")));
199 assert!(!project::is_source(Path::new("/app/notes.txt")));
200 }
201}