use super::*;
use crate::view::file_tree::TreeNode;
use std::path::PathBuf;
fn get_parent_dir_path(node: &TreeNode) -> PathBuf {
if node.is_dir() {
node.entry.path.clone()
} else {
node.entry
.path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| node.entry.path.clone())
}
}
fn timestamp_suffix() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
fn get_parent_node_id(
tree: &crate::view::file_tree::FileTree,
selected_id: crate::view::file_tree::NodeId,
node_is_dir: bool,
) -> crate::view::file_tree::NodeId {
if node_is_dir {
selected_id
} else {
tree.get_node(selected_id)
.and_then(|n| n.parent)
.unwrap_or(selected_id)
}
}
impl Editor {
pub fn file_explorer_visible(&self) -> bool {
self.file_explorer_visible
}
pub fn file_explorer(&self) -> Option<&FileTreeView> {
self.file_explorer.as_ref()
}
pub fn toggle_file_explorer(&mut self) {
self.file_explorer_visible = !self.file_explorer_visible;
if self.file_explorer_visible {
if self.file_explorer.is_none() {
self.init_file_explorer();
}
self.key_context = KeyContext::FileExplorer;
self.set_status_message("File explorer opened".to_string());
self.sync_file_explorer_to_active_file();
} else {
self.key_context = KeyContext::Normal;
self.set_status_message("File explorer closed".to_string());
}
}
pub fn show_file_explorer(&mut self) {
if !self.file_explorer_visible {
self.toggle_file_explorer();
}
}
pub fn sync_file_explorer_to_active_file(&mut self) {
if !self.file_explorer_visible {
return;
}
if self.file_explorer_sync_in_progress {
return;
}
if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
if let Some(file_path) = metadata.file_path() {
let target_path = file_path.clone();
let working_dir = self.working_dir.clone();
if target_path.starts_with(&working_dir) {
if let Some(mut view) = self.file_explorer.take() {
tracing::trace!(
"sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
target_path
);
if let (Some(runtime), Some(bridge)) =
(&self.tokio_runtime, &self.async_bridge)
{
let sender = bridge.sender();
self.file_explorer_sync_in_progress = true;
runtime.spawn(async move {
let _success = view.expand_and_select_file(&target_path).await;
let _ = sender.send(AsyncMessage::FileExplorerExpandedToPath(view));
});
} else {
self.file_explorer = Some(view);
}
}
}
}
}
}
pub fn focus_file_explorer(&mut self) {
if self.file_explorer_visible {
self.on_editor_focus_lost();
self.cancel_search_prompt_if_active();
self.key_context = KeyContext::FileExplorer;
self.set_status_message("File explorer focused".to_string());
self.sync_file_explorer_to_active_file();
} else {
self.toggle_file_explorer();
}
}
pub fn focus_editor(&mut self) {
self.key_context = KeyContext::Normal;
self.set_status_message("Editor focused".to_string());
}
pub(crate) fn init_file_explorer(&mut self) {
let root_path = self.working_dir.clone();
if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
let fs_manager = Arc::clone(&self.fs_manager);
let sender = bridge.sender();
runtime.spawn(async move {
match FileTree::new(root_path, fs_manager).await {
Ok(mut tree) => {
let root_id = tree.root_id();
if let Err(e) = tree.expand_node(root_id).await {
tracing::warn!("Failed to expand root directory: {}", e);
}
let view = FileTreeView::new(tree);
let _ = sender.send(AsyncMessage::FileExplorerInitialized(view));
}
Err(e) => {
tracing::error!("Failed to initialize file explorer: {}", e);
}
}
});
self.set_status_message("Initializing file explorer...".to_string());
}
}
pub fn file_explorer_navigate_up(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.select_prev();
explorer.update_scroll_for_selection();
}
}
pub fn file_explorer_navigate_down(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.select_next();
explorer.update_scroll_for_selection();
}
}
pub fn file_explorer_page_up(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.select_page_up();
explorer.update_scroll_for_selection();
}
}
pub fn file_explorer_page_down(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.select_page_down();
explorer.update_scroll_for_selection();
}
}
pub fn file_explorer_collapse(&mut self) {
let Some(explorer) = &self.file_explorer else {
return;
};
let Some(selected_id) = explorer.get_selected() else {
return;
};
let Some(node) = explorer.tree().get_node(selected_id) else {
return;
};
if node.is_dir() && node.is_expanded() {
self.file_explorer_toggle_expand();
return;
}
if let Some(explorer) = &mut self.file_explorer {
explorer.select_parent();
explorer.update_scroll_for_selection();
}
}
pub fn file_explorer_toggle_expand(&mut self) {
let selected_id = if let Some(explorer) = &self.file_explorer {
explorer.get_selected()
} else {
return;
};
let Some(selected_id) = selected_id else {
return;
};
let (is_dir, is_expanded, name) = if let Some(explorer) = &self.file_explorer {
let node = explorer.tree().get_node(selected_id);
if let Some(node) = node {
(node.is_dir(), node.is_expanded(), node.entry.name.clone())
} else {
return;
}
} else {
return;
};
if !is_dir {
return;
}
let status_msg = if is_expanded {
"Collapsing...".to_string()
} else {
format!("Loading {}...", name)
};
self.set_status_message(status_msg);
if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
let tree = explorer.tree_mut();
let result = runtime.block_on(tree.toggle_node(selected_id));
let final_name = explorer
.tree()
.get_node(selected_id)
.map(|n| n.entry.name.clone());
let final_expanded = explorer
.tree()
.get_node(selected_id)
.map(|n| n.is_expanded())
.unwrap_or(false);
match result {
Ok(()) => {
if final_expanded {
let dir_path = explorer
.tree()
.get_node(selected_id)
.map(|n| n.entry.path.clone());
if let Some(dir_path) = dir_path {
if let Err(e) = explorer.load_gitignore_for_dir(&dir_path) {
tracing::warn!(
"Failed to load .gitignore from {:?}: {}",
dir_path,
e
);
}
}
}
if let Some(name) = final_name {
let msg = if final_expanded {
format!("Expanded: {}", name)
} else {
format!("Collapsed: {}", name)
};
self.set_status_message(msg);
}
}
Err(e) => {
self.set_status_message(format!("Error: {}", e));
}
}
}
}
pub fn file_explorer_open_file(&mut self) -> io::Result<()> {
let entry_type = self
.file_explorer
.as_ref()
.and_then(|explorer| explorer.get_selected_entry())
.map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
if let Some((is_dir, path, name)) = entry_type {
if is_dir {
self.file_explorer_toggle_expand();
} else {
self.open_file(&path)?;
self.set_status_message(format!("Opened: {}", name));
self.focus_editor();
}
}
Ok(())
}
pub fn file_explorer_refresh(&mut self) {
let (selected_id, node_name) = if let Some(explorer) = &self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
let node_name = explorer
.tree()
.get_node(selected_id)
.map(|n| n.entry.name.clone());
(Some(selected_id), node_name)
} else {
(None, None)
}
} else {
return;
};
let Some(selected_id) = selected_id else {
return;
};
if let Some(name) = &node_name {
self.set_status_message(format!("Refreshing {}...", name));
}
if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
let tree = explorer.tree_mut();
let result = runtime.block_on(tree.refresh_node(selected_id));
match result {
Ok(()) => {
if let Some(name) = node_name {
self.set_status_message(format!("Refreshed: {}", name));
} else {
self.set_status_message("Refreshed".to_string());
}
}
Err(e) => {
self.set_status_message(format!("Error refreshing: {}", e));
}
}
}
}
pub fn file_explorer_new_file(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
let node = explorer.tree().get_node(selected_id);
if let Some(node) = node {
let parent_path = get_parent_dir_path(node);
let filename = format!("untitled_{}.txt", timestamp_suffix());
let file_path = parent_path.join(&filename);
if let Some(runtime) = &self.tokio_runtime {
let path_clone = file_path.clone();
let selected_id = selected_id;
let result =
runtime.block_on(async { tokio::fs::File::create(&path_clone).await });
match result {
Ok(_) => {
let parent_id =
get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
let tree = explorer.tree_mut();
let _ = runtime.block_on(tree.refresh_node(parent_id));
self.set_status_message(format!("Created {}", filename));
}
Err(e) => {
self.set_status_message(format!("Error creating file: {}", e));
}
}
}
}
}
}
}
pub fn file_explorer_new_directory(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
let node = explorer.tree().get_node(selected_id);
if let Some(node) = node {
let parent_path = get_parent_dir_path(node);
let dirname = format!("New Folder {}", timestamp_suffix());
let dir_path = parent_path.join(&dirname);
if let Some(runtime) = &self.tokio_runtime {
let path_clone = dir_path.clone();
let dirname_clone = dirname.clone();
let selected_id = selected_id;
let result =
runtime.block_on(async { tokio::fs::create_dir(&path_clone).await });
match result {
Ok(_) => {
let parent_id =
get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
let tree = explorer.tree_mut();
let _ = runtime.block_on(tree.refresh_node(parent_id));
self.set_status_message(format!("Created {}", dirname_clone));
let prompt = crate::view::prompt::Prompt::with_initial_text(
"Rename to: ".to_string(),
crate::view::prompt::PromptType::FileExplorerRename {
original_path: path_clone,
original_name: dirname_clone,
},
dirname,
);
self.prompt = Some(prompt);
}
Err(e) => {
self.set_status_message(format!("Error creating directory: {}", e));
}
}
}
}
}
}
}
pub fn file_explorer_delete(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
if selected_id == explorer.tree().root_id() {
self.set_status_message("Cannot delete project root".to_string());
return;
}
let node = explorer.tree().get_node(selected_id);
if let Some(node) = node {
let path = node.entry.path.clone();
let name = node.entry.name.clone();
if let Some(runtime) = &self.tokio_runtime {
let result = if node.is_dir() {
runtime.block_on(async { tokio::fs::remove_dir_all(&path).await })
} else {
runtime.block_on(async { tokio::fs::remove_file(&path).await })
};
match result {
Ok(_) => {
let parent_id =
get_parent_node_id(explorer.tree(), selected_id, false);
let tree = explorer.tree_mut();
let _ = runtime.block_on(tree.refresh_node(parent_id));
self.set_status_message(format!("Deleted {}", name));
}
Err(e) => {
self.set_status_message(format!("Error deleting: {}", e));
}
}
}
}
}
}
}
pub fn file_explorer_rename(&mut self) {
if let Some(explorer) = &self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
if selected_id == explorer.tree().root_id() {
self.set_status_message("Cannot rename project root".to_string());
return;
}
let node = explorer.tree().get_node(selected_id);
if let Some(node) = node {
let old_path = node.entry.path.clone();
let old_name = node.entry.name.clone();
let prompt = crate::view::prompt::Prompt::with_initial_text(
"Rename to: ".to_string(),
crate::view::prompt::PromptType::FileExplorerRename {
original_path: old_path,
original_name: old_name.clone(),
},
old_name,
);
self.prompt = Some(prompt);
}
}
}
}
pub fn perform_file_explorer_rename(
&mut self,
original_path: std::path::PathBuf,
original_name: String,
new_name: String,
) {
if new_name.is_empty() || new_name == original_name {
self.set_status_message("Rename cancelled".to_string());
return;
}
let new_path = original_path
.parent()
.map(|p| p.join(&new_name))
.unwrap_or_else(|| original_path.clone());
if let Some(runtime) = &self.tokio_runtime {
let result =
runtime.block_on(async { tokio::fs::rename(&original_path, &new_path).await });
match result {
Ok(_) => {
if let Some(explorer) = &mut self.file_explorer {
if let Some(selected_id) = explorer.get_selected() {
let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
let tree = explorer.tree_mut();
let _ = runtime.block_on(tree.refresh_node(parent_id));
}
explorer.navigate_to_path(&new_path);
}
self.set_status_message(format!("Renamed {} to {}", original_name, new_name));
}
Err(e) => {
self.set_status_message(format!("Error renaming: {}", e));
}
}
}
}
pub fn file_explorer_toggle_hidden(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.toggle_show_hidden();
let msg = if explorer.ignore_patterns().show_hidden() {
"Showing hidden files"
} else {
"Hiding hidden files"
};
self.set_status_message(msg.to_string());
}
}
pub fn file_explorer_toggle_gitignored(&mut self) {
if let Some(explorer) = &mut self.file_explorer {
explorer.toggle_show_gitignored();
let show = explorer.ignore_patterns().show_gitignored();
let msg = if show {
"Showing gitignored files"
} else {
"Hiding gitignored files"
};
self.set_status_message(msg.to_string());
}
}
}