Reload folder tree and git status using recursive watcher
This commit is contained in:
parent
f0adef5fa0
commit
449ff6b88c
2 changed files with 175 additions and 34 deletions
193
src/main.rs
193
src/main.rs
|
|
@ -26,10 +26,11 @@ use cosmic_files::{
|
||||||
mime_icon::{mime_for_path, mime_icon},
|
mime_icon::{mime_for_path, mime_icon},
|
||||||
};
|
};
|
||||||
use cosmic_text::{Cursor, Edit, Family, Selection, SwashCache, SyntaxSystem, ViMode};
|
use cosmic_text::{Cursor, Edit, Family, Selection, SwashCache, SyntaxSystem, ViMode};
|
||||||
|
use notify::{RecursiveMode, Watcher};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
env, fs, io,
|
env, fs, io,
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
process,
|
process,
|
||||||
|
|
@ -477,7 +478,10 @@ pub struct App {
|
||||||
project_search_id: widget::Id,
|
project_search_id: widget::Id,
|
||||||
project_search_value: String,
|
project_search_value: String,
|
||||||
project_search_result: Option<ProjectSearchResult>,
|
project_search_result: Option<ProjectSearchResult>,
|
||||||
watcher_opt: Option<notify::RecommendedWatcher>,
|
watcher_opt: Option<(
|
||||||
|
notify::RecommendedWatcher,
|
||||||
|
HashSet<(PathBuf, RecursiveMode)>,
|
||||||
|
)>,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -572,6 +576,7 @@ impl App {
|
||||||
|
|
||||||
// Save the absolute path
|
// Save the absolute path
|
||||||
self.projects.push((name.to_string(), path.to_path_buf()));
|
self.projects.push((name.to_string(), path.to_path_buf()));
|
||||||
|
self.update_watcher();
|
||||||
|
|
||||||
// Add to recent projects, ensuring only one entry
|
// Add to recent projects, ensuring only one entry
|
||||||
self.config_state.recent_projects.retain(|x| x != path);
|
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> {
|
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) -> Option<segmented_button::Entity> {
|
||||||
match self.new_tab(path_opt)? {
|
match self.new_tab(path_opt)? {
|
||||||
NewTab::Exists(entity) => Some(entity),
|
NewTab::Exists(entity) => Some(entity),
|
||||||
NewTab::Tab(tab) => Some(
|
NewTab::Tab(tab) => {
|
||||||
self.tab_model
|
let entity = self
|
||||||
|
.tab_model
|
||||||
.insert()
|
.insert()
|
||||||
.text(tab.title())
|
.text(tab.title())
|
||||||
.icon(tab.icon(16))
|
.icon(tab.icon(16))
|
||||||
.data::<Tab>(Tab::Editor(tab))
|
.data::<Tab>(Tab::Editor(tab))
|
||||||
.closable()
|
.closable()
|
||||||
.activate()
|
.activate()
|
||||||
.id(),
|
.id();
|
||||||
),
|
self.update_watcher();
|
||||||
|
Some(entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,6 +644,7 @@ impl App {
|
||||||
NewTab::Exists(existing) => {
|
NewTab::Exists(existing) => {
|
||||||
// Swap to existing tab and remove tab keyed by `entity`
|
// Swap to existing tab and remove tab keyed by `entity`
|
||||||
self.tab_model.remove(entity);
|
self.tab_model.remove(entity);
|
||||||
|
self.update_watcher();
|
||||||
Some(existing)
|
Some(existing)
|
||||||
}
|
}
|
||||||
NewTab::Tab(tab) => {
|
NewTab::Tab(tab) => {
|
||||||
|
|
@ -644,6 +653,7 @@ impl App {
|
||||||
self.tab_model.icon_set(entity, tab.icon(16));
|
self.tab_model.icon_set(entity, tab.icon(16));
|
||||||
self.tab_model.data_set::<Tab>(entity, Tab::Editor(tab));
|
self.tab_model.data_set::<Tab>(entity, Tab::Editor(tab));
|
||||||
self.tab_model.activate(entity);
|
self.tab_model.activate(entity);
|
||||||
|
self.update_watcher();
|
||||||
Some(entity)
|
Some(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -688,7 +698,6 @@ impl App {
|
||||||
|
|
||||||
let mut tab = EditorTab::new(&self.config);
|
let mut tab = EditorTab::new(&self.config);
|
||||||
tab.open(canonical);
|
tab.open(canonical);
|
||||||
tab.watch(&mut self.watcher_opt);
|
|
||||||
Some(NewTab::Tab(tab))
|
Some(NewTab::Tab(tab))
|
||||||
}
|
}
|
||||||
None => Some(NewTab::Tab(EditorTab::new(&self.config))),
|
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> {
|
fn document_statistics(&self) -> Element<'_, Message> {
|
||||||
//TODO: calculate in the background
|
//TODO: calculate in the background
|
||||||
let mut character_count = 0;
|
let mut character_count = 0;
|
||||||
|
|
@ -1697,6 +1762,7 @@ impl Application for App {
|
||||||
Message::CloseProject(project_i) => {
|
Message::CloseProject(project_i) => {
|
||||||
if project_i < self.projects.len() {
|
if project_i < self.projects.len() {
|
||||||
let (_project_name, project_path) = self.projects.remove(project_i);
|
let (_project_name, project_path) = self.projects.remove(project_i);
|
||||||
|
self.update_watcher();
|
||||||
let mut position = 0;
|
let mut position = 0;
|
||||||
let mut closing = false;
|
let mut closing = false;
|
||||||
while let Some(id) = self.nav_model.entity_at(position) {
|
while let Some(id) = self.nav_model.entity_at(position) {
|
||||||
|
|
@ -2078,7 +2144,8 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::NotifyEvent(event) => {
|
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() {
|
for entity in self.tab_model.iter() {
|
||||||
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
|
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
|
||||||
if let Some(path) = &tab.path_opt {
|
if let Some(path) = &tab.path_opt {
|
||||||
|
|
@ -2089,14 +2156,13 @@ impl Application for App {
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
needs_reload.push(entity);
|
tab_reload.push(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for entity in tab_reload {
|
||||||
for entity in needs_reload {
|
|
||||||
match self.tab_model.data_mut::<Tab>(entity) {
|
match self.tab_model.data_mut::<Tab>(entity) {
|
||||||
Some(Tab::Editor(tab)) => {
|
Some(Tab::Editor(tab)) => {
|
||||||
tab.reload();
|
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()
|
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
|
||||||
{
|
{
|
||||||
Some(watcher) => {
|
Some(watcher) => {
|
||||||
self.watcher_opt = Some(watcher);
|
self.watcher_opt = Some((watcher, HashSet::new()));
|
||||||
|
self.update_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::warn!("message did not contain notify watcher");
|
log::warn!("message did not contain notify watcher");
|
||||||
|
|
@ -2594,6 +2750,7 @@ impl Application for App {
|
||||||
|
|
||||||
// Remove item
|
// Remove item
|
||||||
self.tab_model.remove(entity);
|
self.tab_model.remove(entity);
|
||||||
|
self.update_watcher();
|
||||||
|
|
||||||
// If that was the last tab, make a new empty one
|
// If that was the last tab, make a new empty one
|
||||||
if self.tab_model.iter().next().is_none() {
|
if self.tab_model.iter().next().is_none() {
|
||||||
|
|
|
||||||
16
src/tab.rs
16
src/tab.rs
|
|
@ -6,7 +6,6 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use cosmic_files::mime_icon::{FALLBACK_MIME_ICON, mime_for_path, mime_icon};
|
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 cosmic_text::{Attrs, Buffer, Cursor, Edit, Selection, Shaping, SyntaxEditor, ViEditor, Wrap};
|
||||||
use notify::Watcher;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
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 {
|
pub fn changed(&self) -> bool {
|
||||||
let editor = self.editor.lock().unwrap();
|
let editor = self.editor.lock().unwrap();
|
||||||
editor.changed()
|
editor.changed()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue