Auto reload changed files

This commit is contained in:
2026-09-11 08:43:33 +02:00
parent 2bab19a1ae
commit d574680321
6 changed files with 353 additions and 3 deletions
+63
View File
@@ -17,6 +17,12 @@ use crate::view::{CanvasView, Clipboard, EditTarget, InsertRequest};
/// How long a status message stays on screen.
const STATUS_SECONDS: f64 = 5.0;
/// How often the files behind file nodes are looked at to see whether
/// something outside this editor changed them. Often enough that a file saved
/// in another program appears to update by itself, rarely enough that it is a
/// handful of `stat` calls a second and no more.
const FILE_CHECK_SECONDS: f64 = 1.0;
/// An action waiting for the user to decide what to do about unsaved changes.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Pending {
@@ -39,6 +45,7 @@ pub struct App {
show_about: bool,
show_shortcuts: bool,
title: String,
last_file_check: f64,
}
impl App {
@@ -57,6 +64,7 @@ impl App {
show_about: false,
show_shortcuts: false,
title: String::new(),
last_file_check: 0.0,
};
match open {
Some(path) => app.open_path(&path),
@@ -646,6 +654,52 @@ impl App {
self.set_status(message, ctx);
}
/// Looks for changes made to the files behind file nodes by something
/// other than this editor, every [`FILE_CHECK_SECONDS`].
///
/// A file with no unsaved edit here is read again and the node showing it
/// follows along, which is what a file node is for. A file edited here is
/// left alone and marked on its card instead, because the edit is the one
/// thing that cannot be read back from disk.
fn check_files(&mut self, ctx: &Context) {
if !self.view.files.is_watching() {
return;
}
ctx.request_repaint_after(std::time::Duration::from_secs_f64(FILE_CHECK_SECONDS));
let now = ctx.input(|i| i.time);
if now - self.last_file_check < FILE_CHECK_SECONDS {
return;
}
self.last_file_check = now;
let changes = self.view.files.check_disk();
if changes.is_empty() {
return;
}
for path in &changes.reloaded {
ctx.forget_image(&crate::nodes::file_uri(path));
}
self.view.reload_editor(&changes.reloaded);
let mut parts = Vec::new();
if !changes.reloaded.is_empty() {
parts.push(match changes.reloaded.len() {
1 => format!("Reloaded {}", name_of(&changes.reloaded[0])),
n => format!("Reloaded {n} files changed on disk"),
});
}
if !changes.conflicts.is_empty() {
parts.push(match changes.conflicts.len() {
1 => format!(
"{} changed on disk, kept the edit here",
name_of(&changes.conflicts[0])
),
n => format!("{n} files changed on disk, kept the edits here"),
});
}
self.set_status(parts.join(" \u{2014} "), ctx);
}
// -- chrome -----------------------------------------------------------
fn menu_bar(&mut self, ui: &mut Ui, ctx: &Context) {
@@ -1064,6 +1118,7 @@ impl eframe::App for App {
self.update_title(&ctx);
self.handle_shortcuts(&ctx);
self.handle_dropped_files(&ctx);
self.check_files(&ctx);
// Closing the window with unsaved changes asks first.
if ctx.input(|i| i.viewport().close_requested()) && !self.quitting {
@@ -1145,6 +1200,14 @@ const SHORTCUTS: &[(&str, &str)] = &[
("Ctrl+0 / Ctrl+1", "Zoom to fit / actual size"),
];
/// The file name of `path` for a status message, falling back to the whole
/// path for the odd one that has none.
fn name_of(path: &Path) -> String {
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string())
}
fn consume(ctx: &Context, modifiers: Modifiers, key: Key) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&KeyboardShortcut::new(modifiers, key)))
}