From fbad8439aec2d74f74470e2b3c8a0345f49f189a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 6 Nov 2025 19:48:49 -0700 Subject: [PATCH 1/7] Remove extra line in project search --- src/search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search.rs b/src/search.rs index 7013cf8..7eaed1a 100644 --- a/src/search.rs +++ b/src/search.rs @@ -72,7 +72,7 @@ impl ProjectSearchResult { Ok(Some(first)) => { lines.push(LineSearchResult { number, - text: text.to_string(), + text: text.trim_end().to_string(), first, }); }, From 0a3b248c673b5a806d105f1e8f6326b99d844595 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 6 Nov 2025 19:49:36 -0700 Subject: [PATCH 2/7] Close git diff tabs with same path when new one is opened --- src/main.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main.rs b/src/main.rs index 5e98415..c0a62ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2149,6 +2149,21 @@ impl Application for App { } } Message::OpenGitDiff(project_path, diff) => { + // Close any diff tabs with same path + { + let mut close = Vec::new(); + for entity in self.tab_model.iter() { + if let Some(Tab::GitDiff(other_tab)) = self.tab_model.data::(entity) { + if other_tab.diff.path == diff.path { + close.push(entity); + } + } + } + for entity in close { + self.tab_model.remove(entity); + } + } + let relative_path = match diff.path.strip_prefix(project_path.clone()) { Ok(ok) => ok, Err(err) => { From b48e7b0865eb379cfeea2dbea7483c3dbf5c232c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 6 Nov 2025 19:51:00 -0700 Subject: [PATCH 3/7] Make context drawer inline --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index c0a62ee..2de5809 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1307,7 +1307,9 @@ impl Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, flags: Self::Flags) -> (Self, Task) { + fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { + core.window.context_is_overlay = false; + // Update font name from config { let mut font_system = font_system().write().unwrap(); From 2e6487542ccd643d58053387114fd8d541292d93 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 11 Nov 2025 11:49:22 -0700 Subject: [PATCH 4/7] Use ignore crate to build tree view --- src/main.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2de5809..2a7f34c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -497,16 +497,13 @@ impl App { } fn open_folder>(&mut self, path: P, mut position: u16, indent: u16) { - let read_dir = match fs::read_dir(&path) { - Ok(ok) => ok, - Err(err) => { - log::error!("failed to read directory {:?}: {}", path.as_ref(), err); - return; - } - }; - let mut nodes = Vec::new(); - for entry_res in read_dir { + for entry_res in ignore::WalkBuilder::new(&path) + .filter_entry(|entry| entry.file_name() != ".git") + .hidden(false) + .max_depth(Some(1)) + .build() + { let entry = match entry_res { Ok(ok) => ok, Err(err) => { @@ -518,7 +515,9 @@ impl App { continue; } }; - + if entry.depth() == 0 { + continue; + } let entry_path = entry.path(); let node = match ProjectNode::new(&entry_path) { Ok(ok) => ok, From ec78c39a589fd12bbf71ed39d7f17c12e08ace9f Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 11 Nov 2025 11:50:47 -0700 Subject: [PATCH 5/7] Show open project placeholder only if no projects open --- src/main.rs | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2a7f34c..209fb08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -604,9 +604,9 @@ impl App { .text(node.name().to_string()) .data(node) .id(); + self.update_nav_bar_placeholder(); let position = self.nav_model.position(id).unwrap_or(0); - self.open_folder(path, position + 1, 1); } @@ -845,6 +845,27 @@ impl App { self.nav_model.activate(active_id); } + fn update_nav_bar_placeholder(&mut self) { + // Remove all placeholder items + let mut remove = Vec::new(); + for entity in self.nav_model.iter() { + if self.nav_model.data::(entity).is_none() { + remove.push(entity); + } + } + for entity in remove { + self.nav_model.remove(entity); + } + + // Add button to open a project if none provided + if self.nav_model.iter().next().is_none() { + self.nav_model + .insert() + .icon(icon_cache_get("folder-open-symbolic", 16)) + .text(fl!("open-project")); + } + } + // Call this any time the tab changes pub fn update_tab(&mut self) -> Task { self.update_nav_bar_active(); @@ -1422,13 +1443,7 @@ impl Application for App { } } - // Add button to open a project if none provided - if app.nav_model.iter().next().is_none() { - app.nav_model - .insert() - .icon(icon_cache_get("folder-open-symbolic", 16)) - .text(fl!("open-project")); - } + app.update_nav_bar_placeholder(); // Open an empty file if no arguments provided if app.tab_model.iter().next().is_none() { @@ -1709,6 +1724,7 @@ impl Application for App { position += 1; } } + self.update_nav_bar_placeholder(); } } Message::CloseWindow(window_id) => { From f0adef5fa0e0d12135fceae96971fad9bb22ab43 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 11 Nov 2025 13:21:46 -0700 Subject: [PATCH 6/7] Change text_box redraw log level to trace --- src/text_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text_box.rs b/src/text_box.rs index 6526ed5..971362f 100644 --- a/src/text_box.rs +++ b/src/text_box.rs @@ -911,7 +911,7 @@ where } let duration = instant.elapsed(); - log::debug!("redraw {}, {}: {:?}", view_w, view_h, duration); + log::trace!("redraw {}, {}: {:?}", view_w, view_h, duration); } fn on_event( From 449ff6b88c24819958272a0cbb7d8377bac08e5e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 11 Nov 2025 13:24:50 -0700 Subject: [PATCH 7/7] Reload folder tree and git status using recursive watcher --- src/main.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++++----- src/tab.rs | 16 ----- 2 files changed, 175 insertions(+), 34 deletions(-) diff --git a/src/main.rs b/src/main.rs index 209fb08..44b0043 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, - watcher_opt: Option, + 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) -> Option { 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::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::(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::(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::(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::(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::(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::(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::(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::(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::(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::(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() { diff --git a/src/tab.rs b/src/tab.rs index 4e95f70..4f0de70 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -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) { - 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()