Reload folder tree and git status using recursive watcher

This commit is contained in:
Jeremy Soller 2025-11-11 13:24:50 -07:00
parent f0adef5fa0
commit 449ff6b88c
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
2 changed files with 175 additions and 34 deletions

View file

@ -26,10 +26,11 @@ use cosmic_files::{
mime_icon::{mime_for_path, mime_icon},
};
use cosmic_text::{Cursor, Edit, Family, Selection, SwashCache, SyntaxSystem, ViMode};
use notify::{RecursiveMode, Watcher};
use serde::{Deserialize, Serialize};
use std::{
any::TypeId,
collections::HashMap,
collections::{HashMap, HashSet},
env, fs, io,
path::{self, Path, PathBuf},
process,
@ -477,7 +478,10 @@ pub struct App {
project_search_id: widget::Id,
project_search_value: String,
project_search_result: Option<ProjectSearchResult>,
watcher_opt: Option<notify::RecommendedWatcher>,
watcher_opt: Option<(
notify::RecommendedWatcher,
HashSet<(PathBuf, RecursiveMode)>,
)>,
modifiers: Modifiers,
}
@ -572,6 +576,7 @@ impl App {
// Save the absolute path
self.projects.push((name.to_string(), path.to_path_buf()));
self.update_watcher();
// Add to recent projects, ensuring only one entry
self.config_state.recent_projects.retain(|x| x != path);
@ -613,16 +618,19 @@ impl App {
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) -> Option<segmented_button::Entity> {
match self.new_tab(path_opt)? {
NewTab::Exists(entity) => Some(entity),
NewTab::Tab(tab) => Some(
self.tab_model
NewTab::Tab(tab) => {
let entity = self
.tab_model
.insert()
.text(tab.title())
.icon(tab.icon(16))
.data::<Tab>(Tab::Editor(tab))
.closable()
.activate()
.id(),
),
.id();
self.update_watcher();
Some(entity)
}
}
}
@ -636,6 +644,7 @@ impl App {
NewTab::Exists(existing) => {
// Swap to existing tab and remove tab keyed by `entity`
self.tab_model.remove(entity);
self.update_watcher();
Some(existing)
}
NewTab::Tab(tab) => {
@ -644,6 +653,7 @@ impl App {
self.tab_model.icon_set(entity, tab.icon(16));
self.tab_model.data_set::<Tab>(entity, Tab::Editor(tab));
self.tab_model.activate(entity);
self.update_watcher();
Some(entity)
}
}
@ -688,7 +698,6 @@ impl App {
let mut tab = EditorTab::new(&self.config);
tab.open(canonical);
tab.watch(&mut self.watcher_opt);
Some(NewTab::Tab(tab))
}
None => Some(NewTab::Tab(EditorTab::new(&self.config))),
@ -892,6 +901,62 @@ impl App {
])
}
fn update_watcher(&mut self) {
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
let mut new_paths = HashSet::new();
for (_, project_path) in self.projects.iter() {
new_paths.insert((project_path.clone(), RecursiveMode::Recursive));
}
'tabs: for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if let Some(path) = &tab.path_opt {
for (_, project_path) in self.projects.iter() {
if path.starts_with(&project_path) {
// Do not watch tabs inside of already watched projects
continue 'tabs;
}
}
new_paths.insert((path.to_path_buf(), RecursiveMode::NonRecursive));
}
}
}
// Unwatch paths no longer used
for path_mode in old_paths.iter() {
if !new_paths.contains(path_mode) {
let (path, _) = path_mode;
match watcher.unwatch(path) {
Ok(()) => {
log::debug!("unwatching {:?}", path);
}
Err(err) => {
log::debug!("failed to unwatch {:?}: {}", path, err);
}
}
}
}
// Watch new paths
for path_mode in new_paths.iter() {
if !old_paths.contains(path_mode) {
let (path, mode) = path_mode;
match watcher.watch(path, *mode) {
Ok(()) => {
log::debug!("watching {:?} {:?}", path, mode);
}
Err(err) => {
log::debug!("failed to watch {:?} {:?}: {}", path, mode, err);
}
}
}
}
self.watcher_opt = Some((watcher, new_paths));
}
}
fn document_statistics(&self) -> Element<'_, Message> {
//TODO: calculate in the background
let mut character_count = 0;
@ -1697,6 +1762,7 @@ impl Application for App {
Message::CloseProject(project_i) => {
if project_i < self.projects.len() {
let (_project_name, project_path) = self.projects.remove(project_i);
self.update_watcher();
let mut position = 0;
let mut closing = false;
while let Some(id) = self.nav_model.entity_at(position) {
@ -2078,7 +2144,8 @@ impl Application for App {
}
}
Message::NotifyEvent(event) => {
let mut needs_reload = Vec::new();
// Reload tabs that changed
let mut tab_reload = Vec::new();
for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if let Some(path) = &tab.path_opt {
@ -2089,14 +2156,13 @@ impl Application for App {
path
);
} else {
needs_reload.push(entity);
tab_reload.push(entity);
}
}
}
}
}
for entity in needs_reload {
for entity in tab_reload {
match self.tab_model.data_mut::<Tab>(entity) {
Some(Tab::Editor(tab)) => {
tab.reload();
@ -2106,17 +2172,107 @@ impl Application for App {
}
}
}
// Reload folders that changed
let mut close_entities = Vec::new();
let mut open_paths = Vec::new();
for entity in self.nav_model.iter() {
let Some(ProjectNode::Folder {
path, open: true, ..
}) = self.nav_model.data::<ProjectNode>(entity)
else {
continue;
};
for event_path in event.paths.iter() {
if event_path == path || event_path.parent() == Some(path) {
close_entities.push(entity);
open_paths.push(path.to_path_buf());
break;
}
}
}
for entity in close_entities {
// Close folder
if let Some(ProjectNode::Folder { open, .. }) =
self.nav_model.data_mut::<ProjectNode>(entity)
{
*open = false;
} else {
continue;
}
// Remove children
let position = self.nav_model.position(entity).unwrap_or(0);
let indent = self.nav_model.indent(entity).unwrap_or(0);
while let Some(child) = self.nav_model.entity_at(position + 1) {
if let Some(ProjectNode::Folder {
path, open: true, ..
}) = self.nav_model.data::<ProjectNode>(child)
{
// Re-open children as needed
open_paths.push(path.to_path_buf());
}
if self.nav_model.indent(child).unwrap_or(0) > indent {
self.nav_model.remove(child);
} else {
break;
}
}
}
for open_path in open_paths {
let mut entity_opt = None;
for entity in self.nav_model.iter() {
let Some(ProjectNode::Folder {
path, open: false, ..
}) = self.nav_model.data::<ProjectNode>(entity)
else {
continue;
};
if open_path == *path {
entity_opt = Some(entity);
break;
}
}
let Some(entity) = entity_opt else { continue };
// Open folder
let icon = if let Some(node) = self.nav_model.data_mut::<ProjectNode>(entity) {
if let ProjectNode::Folder { open, .. } = node {
*open = true;
} else {
continue;
}
node.icon(16)
} else {
continue;
};
// Update icon
self.nav_model.icon_set(entity, icon);
let position = self.nav_model.position(entity).unwrap_or(0);
let indent = self.nav_model.indent(entity).unwrap_or(0);
self.open_folder(open_path, position + 1, indent + 1);
}
// Reload git status if necessary
if self.core.window.show_context && self.context_page == ContextPage::GitManagement
{
for (_, project_path) in self.projects.iter() {
for path in event.paths.iter() {
if let Ok(prefix) = path.strip_prefix(&project_path) {
// Manually ignore project .git folders
//TODO: use logic from ignore crate somehow?
if prefix.starts_with(".git") {
continue;
}
return self.update(Message::UpdateGitProjectStatus);
}
}
}
}
}
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() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
tab.watch(&mut self.watcher_opt);
}
}
self.watcher_opt = Some((watcher, HashSet::new()));
self.update_watcher();
}
None => {
log::warn!("message did not contain notify watcher");
@ -2594,6 +2750,7 @@ impl Application for App {
// Remove item
self.tab_model.remove(entity);
self.update_watcher();
// If that was the last tab, make a new empty one
if self.tab_model.iter().next().is_none() {

View file

@ -6,7 +6,6 @@ use cosmic::{
};
use cosmic_files::mime_icon::{FALLBACK_MIME_ICON, mime_for_path, mime_icon};
use cosmic_text::{Attrs, Buffer, Cursor, Edit, Selection, Shaping, SyntaxEditor, ViEditor, Wrap};
use notify::Watcher;
use regex::Regex;
use std::{
fs,
@ -279,21 +278,6 @@ impl EditorTab {
}
}
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()