Add tab reload on external change

This commit is contained in:
Jeremy Soller 2023-11-21 11:57:11 -07:00
parent 68675b1a99
commit f207db0e84
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
4 changed files with 242 additions and 6 deletions

63
Cargo.lock generated
View file

@ -253,6 +253,24 @@ dependencies = [
"libloading 0.7.4",
]
[[package]]
name = "ashpd"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7370b58af1d7e96df3ca0f454b57e69acf9aa42ed2d7337bd206923bae0d5754"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"once_cell",
"rand",
"serde",
"serde_repr",
"tokio",
"url",
"zbus",
]
[[package]]
name = "async-broadcast"
version = "0.5.1"
@ -329,7 +347,7 @@ dependencies = [
"polling 2.8.0",
"rustix 0.37.27",
"slab",
"socket2",
"socket2 0.4.10",
"waker-fn",
]
@ -645,6 +663,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cairo-sys-rs"
version = "0.18.2"
@ -963,11 +987,13 @@ dependencies = [
"lazy_static",
"libcosmic",
"log",
"notify",
"rfd",
"rust-embed",
"serde",
"syntect",
"systemicons",
"tokio",
"two-face",
]
@ -2416,6 +2442,7 @@ dependencies = [
"futures",
"iced_core",
"log",
"tokio",
"wasm-bindgen-futures",
"wasm-timer",
]
@ -2829,6 +2856,7 @@ version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#001fd744c5f80c9ce058eb0e22ae92f19d12c844"
dependencies = [
"apply",
"ashpd",
"cosmic-config",
"cosmic-theme",
"css-color",
@ -2850,9 +2878,11 @@ dependencies = [
"slotmap",
"taffy",
"thiserror",
"tokio",
"tracing",
"unicode-segmentation",
"url",
"zbus",
]
[[package]]
@ -4813,6 +4843,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "socket2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "softbuffer"
version = "0.3.3"
@ -5198,6 +5238,24 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio 0.8.9",
"num_cpus",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.5",
"tracing",
"windows-sys 0.48.0",
]
[[package]]
name = "toml"
version = "0.5.11"
@ -5472,6 +5530,7 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@ -6419,6 +6478,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"winapi",
@ -6499,6 +6559,7 @@ dependencies = [
"libc",
"serde",
"static_assertions",
"url",
"zvariant_derive",
]

View file

@ -9,9 +9,12 @@ license = "GPL-3.0-only"
env_logger = "0.10.0"
lazy_static = "1.4.0"
log = "0.4.20"
notify = "6.1.1"
#TODO: this is using gtk for file dialogues
rfd = { version = "0.12.0", optional = true }
serde = { version = "1", features = ["serde_derive"] }
tokio = { version = "1", features = ["time"] }
# Extra syntax highlighting
syntect = "5.1.0"
two-face = "0.3.0"
# Internationalization
@ -27,7 +30,7 @@ features = ["syntect", "vi"]
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["winit", "wgpu"]
features = ["tokio", "winit", "wgpu"]
#path = "../libcosmic"
#TODO: clean up and send changes upstream

View file

@ -5,7 +5,9 @@ use cosmic::{
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme, executor,
iced::{
clipboard, event, keyboard, subscription,
clipboard, event,
futures::{self, SinkExt},
keyboard, subscription,
widget::{row, text},
window, Alignment, Length, Point,
},
@ -20,6 +22,7 @@ use std::{
process,
sync::Mutex,
};
use tokio::time;
use config::{Action, AppTheme, Config, CONFIG_VERSION};
mod config;
@ -104,6 +107,23 @@ pub struct Flags {
config: Config,
}
#[derive(Debug)]
pub struct WatcherWrapper {
watcher_opt: Option<notify::RecommendedWatcher>,
}
impl Clone for WatcherWrapper {
fn clone(&self) -> Self {
Self { watcher_opt: None }
}
}
impl PartialEq for WatcherWrapper {
fn eq(&self, _other: &Self) -> bool {
false
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub enum Message {
@ -118,6 +138,8 @@ pub enum Message {
Key(keyboard::Modifiers, keyboard::KeyCode),
NewFile,
NewWindow,
NotifyEvent(notify::Event),
NotifyWatcher(WatcherWrapper),
OpenFileDialog,
OpenFile(PathBuf),
OpenProjectDialog,
@ -171,6 +193,7 @@ pub struct App {
font_sizes: Vec<u16>,
theme_names: Vec<String>,
context_page: ContextPage,
watcher_opt: Option<notify::RecommendedWatcher>,
}
impl App {
@ -304,6 +327,7 @@ impl App {
let mut tab = Tab::new(&self.config);
tab.open(canonical);
tab.watch(&mut self.watcher_opt);
tab
}
None => Tab::new(&self.config),
@ -337,9 +361,7 @@ impl App {
log::error!("failed to save config: {}", err);
}
},
None => {
//TODO: log that there is no handler?
}
None => {}
}
self.update_config()
}
@ -485,6 +507,7 @@ impl Application for App {
font_sizes,
theme_names,
context_page: ContextPage::Settings,
watcher_opt: None,
};
for arg in env::args().skip(1) {
@ -681,6 +704,57 @@ impl Application for App {
}
}
}
Message::NotifyEvent(event) => {
let mut needs_reload = Vec::new();
for entity in self.tab_model.iter() {
match self.tab_model.data::<Tab>(entity) {
Some(tab) => {
if let Some(path) = &tab.path_opt {
if event.paths.contains(&path) {
if tab.changed() {
log::warn!(
"file changed externally before being saved: {:?}",
path
);
} else {
needs_reload.push(entity);
}
}
}
}
None => {}
}
}
for entity in needs_reload {
match self.tab_model.data_mut::<Tab>(entity) {
Some(tab) => {
tab.reload();
}
None => {
log::warn!("failed to find tab {:?} that needs reload", entity);
}
}
}
}
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
{
Some(watcher) => {
self.watcher_opt = Some(watcher);
for entity in self.tab_model.iter() {
match self.tab_model.data::<Tab>(entity) {
Some(tab) => {
tab.watch(&mut self.watcher_opt);
}
None => {}
}
}
}
None => {
log::warn!("message did not contain notify watcher");
}
},
Message::OpenFileDialog => {
#[cfg(feature = "rfd")]
return Command::perform(
@ -1113,6 +1187,63 @@ impl Application for App {
}) => Some(Message::Key(modifiers, key_code)),
_ => None,
}),
subscription::channel(0, 100, |mut output| async move {
let watcher_res = {
let mut output = output.clone();
notify::recommended_watcher(
move |event_res: Result<notify::Event, notify::Error>| match event_res {
Ok(event) => {
match &event.kind {
notify::EventKind::Access(_)
| notify::EventKind::Modify(
notify::event::ModifyKind::Metadata(_),
) => {
// Data not mutated
return;
}
_ => {}
}
match futures::executor::block_on(async {
output.send(Message::NotifyEvent(event)).await
}) {
Ok(()) => {}
Err(err) => {
log::warn!("failed to send notify event: {:?}", err);
}
}
}
Err(err) => {
log::warn!("failed to watch files: {:?}", err);
}
},
)
};
match watcher_res {
Ok(watcher) => {
match output
.send(Message::NotifyWatcher(WatcherWrapper {
watcher_opt: Some(watcher),
}))
.await
{
Ok(()) => {}
Err(err) => {
log::warn!("failed to send notify watcher: {:?}", err);
}
}
}
Err(err) => {
log::warn!("failed to create file watcher: {:?}", err);
}
}
//TODO: how to properly kill this task?
loop {
time::sleep(time::Duration::new(1, 0)).await;
}
}),
cosmic_config::config_subscription(0, Self::APP_ID.into(), CONFIG_VERSION).map(
|(_, res)| match res {
Ok(config) => Message::Config(config),

View file

@ -5,6 +5,7 @@ use cosmic::{
widget::{icon, Icon},
};
use cosmic_text::{Attrs, Buffer, Edit, Shaping, SyntaxEditor, ViEditor, Wrap};
use notify::Watcher;
use std::{fs, path::PathBuf, sync::Mutex};
use crate::{fl, mime_icon, Config, FALLBACK_MIME_ICON, FONT_SYSTEM, SYNTAX_SYSTEM};
@ -82,6 +83,31 @@ impl Tab {
}
}
pub fn reload(&mut self) {
let mut editor = self.editor.lock().unwrap();
let mut font_system = FONT_SYSTEM.lock().unwrap();
let mut editor = editor.borrow_with(&mut font_system);
if let Some(path) = &self.path_opt {
// Save scroll
let scroll = editor.buffer().scroll();
//TODO: save/restore more?
match editor.load_text(path, self.attrs) {
Ok(()) => {
log::info!("reloaded {:?}", path);
}
Err(err) => {
log::error!("failed to reload {:?}: {}", path, err);
}
}
// Restore scroll
editor.buffer_mut().set_scroll(scroll);
} else {
log::warn!("tried to reload with no path");
}
}
pub fn save(&mut self) {
if let Some(path) = &self.path_opt {
let mut editor = self.editor.lock().unwrap();
@ -104,6 +130,21 @@ impl Tab {
}
}
pub fn watch(&self, watcher_opt: &mut Option<notify::RecommendedWatcher>) {
if let Some(path) = &self.path_opt {
if let Some(watcher) = watcher_opt {
match watcher.watch(&path, notify::RecursiveMode::NonRecursive) {
Ok(()) => {
log::info!("watching {:?} for changes", path);
}
Err(err) => {
log::warn!("failed to watch {:?} for changes: {:?}", path, err);
}
}
}
}
}
pub fn changed(&self) -> bool {
let editor = self.editor.lock().unwrap();
editor.changed()