From 87b5bd58bcd92f5aafb6dcc127df709ad172f650 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:53:14 +0000 Subject: [PATCH 01/52] feat: add API to register additional hooks with Pickrs --- helix-term/src/ui/picker.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3986ad479b39..81ced9a8ec86 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -47,6 +47,7 @@ use helix_core::{ use helix_view::{ editor::Action, graphics::{CursorKind, Margin, Modifier, Rect}, + input::KeyEvent, theme::Style, view::ViewPosition, Document, DocumentId, Editor, @@ -258,6 +259,7 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, + custom_key_handlers: PickerKeyHandler, pub truncate_start: bool, /// Caches paths to documents @@ -385,6 +387,7 @@ impl Picker { completion_height: 0, widths, preview_cache: HashMap::new(), + custom_key_handlers: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), @@ -392,6 +395,11 @@ impl Picker { } } + pub fn with_key_handler(mut self, handlers: PickerKeyHandler) -> Self { + self.custom_key_handlers = handlers; + self + } + pub fn injector(&self) -> Injector { Injector { dst: self.matcher.injector(), @@ -509,6 +517,15 @@ impl Picker { self.show_preview = !self.show_preview; } + fn custom_event_handler(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + if let Some(callback) = self.custom_key_handlers.get(event) { + callback(cx); + EventResult::Consumed(None) + } else { + EventResult::Ignored(None) + } + } + fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { self.handle_prompt_change(matches!(event, Event::Paste(_))); @@ -1113,8 +1130,13 @@ impl Component for Picker { self.toggle_preview(); } - _ => { - self.prompt_handle_event(event, ctx); + key_event => { + if !matches!( + self.custom_event_handler(&key_event, ctx), + EventResult::Consumed(_) + ) { + self.prompt_handle_event(event, ctx); + }; } } @@ -1157,3 +1179,4 @@ impl Drop for Picker { } type PickerCallback = Box; +type PickerKeyHandler = HashMap>; From f4e5c2611277fe3e1f64b3ca9dc55ca6ca3edc29 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:09:04 +0000 Subject: [PATCH 02/52] feat: figure out how to pass custom callback function set by keymap to picker --- helix-term/src/ui/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bbd71ca5c832..dc2b020f5a5e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -14,11 +14,12 @@ mod statusline; mod text; mod text_decorations; -use crate::compositor::Compositor; -use crate::filter_picker_entry; +use crate::compositor::{Compositor, Context}; use crate::job::{self, Callback}; +use crate::{ctrl, filter_picker_entry}; pub use completion::Completion; pub use editor::EditorView; +use helix_core::hashmap; use helix_stdx::rope; use helix_view::theme::Style; pub use markdown::Markdown; @@ -32,6 +33,7 @@ pub use text::Text; use helix_view::Editor; use tui::text::Span; +use std::collections::HashMap; use std::path::Path; use std::{error::Error, path::PathBuf}; @@ -296,6 +298,11 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result = Box::new(|cx: &mut Context| { + log::error!("1"); + }); + let picker = Picker::new( columns, 0, @@ -324,7 +331,8 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result delete_file)); Ok(picker) } From 9a28d4fa425e6d5b0b29514583e80fe524647dda Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:22:51 +0000 Subject: [PATCH 03/52] feat: create operations for create, delete, copy, rename in file explorer --- helix-term/src/ui/mod.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index dc2b020f5a5e..fe88f422bcf1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ mod text_decorations; use crate::compositor::{Compositor, Context}; use crate::job::{self, Callback}; -use crate::{ctrl, filter_picker_entry}; +use crate::{alt, ctrl, filter_picker_entry}; pub use completion::Completion; pub use editor::EditorView; use helix_core::hashmap; @@ -299,8 +299,17 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result = Box::new(|cx: &mut Context| { - log::error!("1"); + let delete: Box = Box::new(|cx: &mut Context| { + log::error!("delete file"); + }); + let create: Box = Box::new(|cx: &mut Context| { + log::error!("create file"); + }); + let rename: Box = Box::new(|cx: &mut Context| { + log::error!("rename file"); + }); + let copy: Box = Box::new(|cx: &mut Context| { + log::error!("copy file"); }); let picker = Picker::new( @@ -332,7 +341,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result delete_file)); + .with_key_handler(HashMap::from([ + (alt!('c'), create), + (alt!('d'), delete), + (alt!('y'), copy), + (alt!('r'), rename), + ])); Ok(picker) } From 5d29a175f34ee642e927d293ea9db0f757168200 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:41:23 +0000 Subject: [PATCH 04/52] refactor: utility macro to declare multiple handlers with ease --- helix-term/src/ui/mod.rs | 44 ++++++++++++++++++++++--------------- helix-term/src/ui/picker.rs | 4 ++-- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index fe88f422bcf1..d6bb666108c4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -37,6 +37,8 @@ use std::collections::HashMap; use std::path::Path; use std::{error::Error, path::PathBuf}; +use self::picker::PickerKeyHandler; + struct Utf8PathBuf { path: String, is_dir: bool, @@ -299,18 +301,16 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result = Box::new(|cx: &mut Context| { - log::error!("delete file"); - }); - let create: Box = Box::new(|cx: &mut Context| { - log::error!("create file"); - }); - let rename: Box = Box::new(|cx: &mut Context| { - log::error!("rename file"); - }); - let copy: Box = Box::new(|cx: &mut Context| { - log::error!("copy file"); - }); + macro_rules! declare_key_handlers { + ($($op:literal $key:expr => $handler:expr),*) => { + hashmap!( + $( + $key => Box::new($handler) + as Box + ),* + ) + }; + } let picker = Picker::new( columns, @@ -341,12 +341,20 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result |cx: &mut Context| { + log::error!("create file"); + }, + "Delete" alt!('d') => |cx: &mut Context| { + log::error!("delete file"); + }, + "Copy" alt!('y') => |cx: &mut Context| { + log::error!("copy file"); + }, + "Rename" alt!('r') => |cx: &mut Context| { + log::error!("rename file"); + } + }); Ok(picker) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 81ced9a8ec86..3513717b6f50 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -395,7 +395,7 @@ impl Picker { } } - pub fn with_key_handler(mut self, handlers: PickerKeyHandler) -> Self { + pub fn with_key_handlers(mut self, handlers: PickerKeyHandler) -> Self { self.custom_key_handlers = handlers; self } @@ -1179,4 +1179,4 @@ impl Drop for Picker { } type PickerCallback = Box; -type PickerKeyHandler = HashMap>; +pub type PickerKeyHandler = HashMap>; From d5fb7b29994920b6398b131b8e9ce676f02917c7 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:54:45 +0000 Subject: [PATCH 05/52] refactor: improve the declare_key_handlers macro --- helix-term/src/ui/mod.rs | 18 +++++++++--------- helix-term/src/ui/picker.rs | 12 +++++++----- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d6bb666108c4..ee8bbb32dbba 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -302,11 +302,11 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result $handler:expr),*) => { + ($($op:literal $key:expr => |$cx:ident, $path:ident| $handler:block),* $(,)?) => { hashmap!( $( - $key => Box::new($handler) - as Box + $key => Box::new(|$cx: &mut Context, $path: &(PathBuf, bool)| $handler) + as Box ),* ) }; @@ -342,18 +342,18 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result |cx: &mut Context| { + "Create" alt!('c') => |cx, path| { log::error!("create file"); }, - "Delete" alt!('d') => |cx: &mut Context| { + "Rename" alt!('r') => |cx, path| { + log::error!("rename file"); + }, + "Delete" alt!('d') => |cx, path| { log::error!("delete file"); }, - "Copy" alt!('y') => |cx: &mut Context| { + "Copy" alt!('y') => |cx, path| { log::error!("copy file"); }, - "Rename" alt!('r') => |cx: &mut Context| { - log::error!("rename file"); - } }); Ok(picker) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3513717b6f50..7bc998bfe6eb 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -259,7 +259,7 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, - custom_key_handlers: PickerKeyHandler, + custom_key_handlers: PickerKeyHandler, pub truncate_start: bool, /// Caches paths to documents @@ -395,7 +395,7 @@ impl Picker { } } - pub fn with_key_handlers(mut self, handlers: PickerKeyHandler) -> Self { + pub fn with_key_handlers(mut self, handlers: PickerKeyHandler) -> Self { self.custom_key_handlers = handlers; self } @@ -518,8 +518,10 @@ impl Picker { } fn custom_event_handler(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { - if let Some(callback) = self.custom_key_handlers.get(event) { - callback(cx); + if let (Some(callback), Some(selected)) = + (self.custom_key_handlers.get(event), self.selection()) + { + callback(cx, selected); EventResult::Consumed(None) } else { EventResult::Ignored(None) @@ -1179,4 +1181,4 @@ impl Drop for Picker { } type PickerCallback = Box; -pub type PickerKeyHandler = HashMap>; +pub type PickerKeyHandler = HashMap>; From 43f40d318f68c3e36e26069cc89df8203677634f Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:08:31 +0000 Subject: [PATCH 06/52] chore: add TODO comments --- helix-term/src/ui/mod.rs | 35 +++++++++++++++++++---------------- helix-term/src/ui/picker.rs | 13 +++++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ee8bbb32dbba..f1a9162c9a31 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ mod text_decorations; use crate::compositor::{Compositor, Context}; use crate::job::{self, Callback}; -use crate::{alt, ctrl, filter_picker_entry}; +use crate::{alt, ctrl, declare_key_handlers, filter_picker_entry}; pub use completion::Completion; pub use editor::EditorView; use helix_core::hashmap; @@ -301,17 +301,6 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result |$cx:ident, $path:ident| $handler:block),* $(,)?) => { - hashmap!( - $( - $key => Box::new(|$cx: &mut Context, $path: &(PathBuf, bool)| $handler) - as Box - ),* - ) - }; - } - let picker = Picker::new( columns, 0, @@ -342,16 +331,30 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result |cx, path| { + // TODO: add a way to get user input in the Picker component and then + // execute an action based on that. Maybe re-use the existing Prompt component somehow? + |cx, path: &(PathBuf, bool)|, + // create + alt!('c') => { + // TODO: ask user for name of file to be created + // Fill in with the picker's current directory log::error!("create file"); }, - "Rename" alt!('r') => |cx, path| { + // move + alt!('m') => { + // TODO: ask the user for new name of file + // on enter move the file to the new location log::error!("rename file"); }, - "Delete" alt!('d') => |cx, path| { + // delete + alt!('d') => { + // TODO: ask user for confirmation, while showing which file + // will be deleted log::error!("delete file"); }, - "Copy" alt!('y') => |cx, path| { + // copy + alt!('y') => { + // TODO: ask the user for new name of file log::error!("copy file"); }, }); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7bc998bfe6eb..4ddeafd3bcce 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1182,3 +1182,16 @@ impl Drop for Picker { type PickerCallback = Box; pub type PickerKeyHandler = HashMap>; + +/// Convenience macro to add custom keybindings per picker +#[macro_export] +macro_rules! declare_key_handlers { + (|$cx:ident, $item:ident : $t:ty|, $($key:expr => $handler:block),* $(,)?) => { + hashmap!( + $( + $key => Box::new(|$cx: &mut Context, $item: $t| $handler) + as Box + ),* + ) + }; + } From 7dc631de9ac2ef09f3683d1f7020c1cdf6b8c388 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:24:13 +0000 Subject: [PATCH 07/52] chore: allow macro to destructure --- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f1a9162c9a31..b79125f828e6 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -333,7 +333,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { // TODO: ask user for name of file to be created diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 4ddeafd3bcce..ca17feaeade5 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1186,7 +1186,7 @@ pub type PickerKeyHandler = HashMap $handler:block),* $(,)?) => { + (|$cx:ident, $item:tt : $t:ty|, $($key:expr => $handler:block),* $(,)?) => { hashmap!( $( $key => Box::new(|$cx: &mut Context, $item: $t| $handler) From 2eef82e4df158afa225344f9cc14b798fd8ee42b Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:08:17 +0000 Subject: [PATCH 08/52] feat: implement basic callback functions for prompt operarions --- helix-term/src/compositor.rs | 1 + helix-term/src/ui/mod.rs | 49 ++++++++++++++++++++++++++++-------- helix-term/src/ui/prompt.rs | 6 +++++ helix-view/src/editor.rs | 2 ++ 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 28c8651a2e40..af510ed59a4a 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -15,6 +15,7 @@ pub enum EventResult { Consumed(Option), } +use crate::commands; use crate::job::Jobs; use crate::ui::picker; use helix_view::Editor; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index b79125f828e6..b91cfe1cbe96 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -285,6 +285,40 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; +fn create_file_operation_prompt( + prompt: &'static str, + cx: &mut Context, + path: &Path, + callback: fn(&Path, &str), +) { + cx.editor.path_editing = Some(path.to_path_buf()); + let callback = Box::pin(async move { + let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { + // let path = path.clone(); + let mut prompt = Prompt::new( + prompt.into(), + None, + crate::ui::completers::none, + move |_cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + }; + + callback(path, input); + }, + ); + + if let Some(path_editing) = &editor.path_editing { + prompt.set_line_no_recalculate(path_editing.display().to_string()); + } + + compositor.push(Box::new(prompt)); + })); + Ok(call) + }); + cx.jobs.callback(callback); +} + pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; @@ -336,26 +370,19 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - // TODO: ask user for name of file to be created - // Fill in with the picker's current directory - log::error!("create file"); + create_file_operation_prompt("create:", cx, path, |path, input| ()) }, // move alt!('m') => { - // TODO: ask the user for new name of file - // on enter move the file to the new location - log::error!("rename file"); + create_file_operation_prompt("move:", cx, path, |path, input| ()) }, // delete alt!('d') => { - // TODO: ask user for confirmation, while showing which file - // will be deleted - log::error!("delete file"); + create_file_operation_prompt("delete? (y/n):", cx, path, |path, input| ()) }, // copy alt!('y') => { - // TODO: ask the user for new name of file - log::error!("copy file"); + create_file_operation_prompt("copy-to:", cx, path, |path, input| ()) }, }); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 03adeb05bbf5..5444a78fd4aa 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -122,6 +122,12 @@ impl Prompt { self.recalculate_completion(editor); } + pub fn set_line_no_recalculate(&mut self, line: String) { + let cursor = line.len(); + self.line = line; + self.cursor = cursor; + } + pub fn with_language( mut self, language: &'static str, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 739dcfb4982d..8e1ccd61886d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1035,6 +1035,7 @@ pub struct Editor { pub tree: Tree, pub next_document_id: DocumentId, pub documents: BTreeMap, + pub path_editing: Option, // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>. // https://stackoverflow.com/a/66875668 @@ -1223,6 +1224,7 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + path_editing: None, } } From f59c5966f6c44824c4cf7c26be7fe4f0d550fd0c Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:37:58 +0000 Subject: [PATCH 09/52] feat: implement creating new files and directory --- helix-term/src/ui/mod.rs | 50 +++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index b91cfe1cbe96..c014006383be 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -34,7 +34,8 @@ use helix_view::Editor; use tui::text::Span; use std::collections::HashMap; -use std::path::Path; +use std::fs; +use std::path::{Path, MAIN_SEPARATOR}; use std::{error::Error, path::PathBuf}; use self::picker::PickerKeyHandler; @@ -289,7 +290,7 @@ fn create_file_operation_prompt( prompt: &'static str, cx: &mut Context, path: &Path, - callback: fn(&Path, &str), + callback: fn(&Path, &str) -> Result, ) { cx.editor.path_editing = Some(path.to_path_buf()); let callback = Box::pin(async move { @@ -299,12 +300,20 @@ fn create_file_operation_prompt( prompt.into(), None, crate::ui::completers::none, - move |_cx, input: &str, event: PromptEvent| { + move |cx, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; }; - callback(path, input); + if let Some(path) = &cx.editor.path_editing { + match callback(path, input) { + Ok(msg) => cx.editor.set_status(msg), + Err(msg) => cx.editor.set_error(msg), + }; + } else { + cx.editor + .set_error("Unable to determine path of selected file") + } }, ); @@ -370,19 +379,44 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |path, input| ()) + create_file_operation_prompt("create:", cx, path, |_path, create| { + let path = helix_stdx::path::expand_tilde(PathBuf::from(create)); + + if path.exists() { + return Err(format!("Path {create} already exists.")) + }; + + if create.ends_with(std::path::MAIN_SEPARATOR) { + fs::create_dir_all(path).map_err(|err| format!("Unable to create directory {create}: {err}"))?; + + Ok(format!("Created directory: {create}")) + } else { + fs::File::create(path).map_err(|err| format!("Unable to create file {create}: {err}"))?; + + Ok(format!("Created file: {create}")) + } + }) }, // move alt!('m') => { - create_file_operation_prompt("move:", cx, path, |path, input| ()) + create_file_operation_prompt("move:", cx, path, |path, input| { + Ok("".into()) + + }) }, // delete alt!('d') => { - create_file_operation_prompt("delete? (y/n):", cx, path, |path, input| ()) + create_file_operation_prompt("delete? (y/n):", cx, path, |path, input| { + Ok("".into()) + + }) }, // copy alt!('y') => { - create_file_operation_prompt("copy-to:", cx, path, |path, input| ()) + create_file_operation_prompt("copy-to:", cx, path, |path, input| { + Ok("".into()) + + }) }, }); From 469115e5eef9c3b838de31ff5a67efe399a34ed2 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:46:04 +0000 Subject: [PATCH 10/52] feat: implement delete files --- helix-term/src/ui/mod.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index c014006383be..f31dd0daf5fd 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -379,21 +379,21 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |_path, create| { - let path = helix_stdx::path::expand_tilde(PathBuf::from(create)); + create_file_operation_prompt("create:", cx, path, |_, to_create_str| { + let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - if path.exists() { - return Err(format!("Path {create} already exists.")) + if to_create.exists() { + return Err(format!("Path {to_create_str} already exists")) }; - if create.ends_with(std::path::MAIN_SEPARATOR) { - fs::create_dir_all(path).map_err(|err| format!("Unable to create directory {create}: {err}"))?; + if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + fs::create_dir_all(to_create).map_err(|err| format!("Unable to create directory {to_create_str}: {err}"))?; - Ok(format!("Created directory: {create}")) + Ok(format!("Created directory: {to_create_str}")) } else { - fs::File::create(path).map_err(|err| format!("Unable to create file {create}: {err}"))?; + fs::File::create(to_create).map_err(|err| format!("Unable to create file {to_create_str}: {err}"))?; - Ok(format!("Created file: {create}")) + Ok(format!("Created file: {to_create_str}")) } }) }, @@ -406,9 +406,25 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("delete? (y/n):", cx, path, |path, input| { - Ok("".into()) + create_file_operation_prompt("delete? (y/n):", cx, path, |_, to_delete_str| { + let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(to_delete_str)); + if matches!(to_delete_str, "y" | "n") { + if to_delete.exists() { + return Err(format!("Path {to_delete_str} does not exist")) + }; + + if to_delete_str.ends_with(std::path::MAIN_SEPARATOR) { + fs::remove_dir_all(to_delete).map_err(|err| format!("Unable to delete directory {to_delete_str}: {err}"))?; + + Ok(format!("Deleted directory: {to_delete_str}")) + } else { + fs::remove_file(to_delete).map_err(|err| format!("Unable to delete file {to_delete_str}: {err}"))?; + Ok(format!("Deleted file: {to_delete_str}")) + } + } else { + Ok(format!("Did not delete: {to_delete_str}")) + } }) }, // copy From a96841dac1c0e17bdf68e5dc0e2adea1cca70ff2 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:54:37 +0000 Subject: [PATCH 11/52] feat: implement copying --- helix-term/src/ui/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f31dd0daf5fd..ccab602ef6b9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -429,9 +429,21 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("copy-to:", cx, path, |path, input| { - Ok("".into()) + create_file_operation_prompt("copy-to:", cx, path, |copy_from, copy_to_str| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + if copy_to_str.ends_with('/') { + Err(format!("Copying directories is not supported: {} is a directory", copy_from.display())) + } else if copy_to.exists() { + // TODO: confirmation prompt when overwriting + Err(format!("Path {copy_to_str} exists")) + } else { + std::fs::copy(copy_from, copy_to).map_err( + |err| format!("Unable to copy from file {} to {copy_to_str}: {err}", + copy_from.display() + ))?; + Ok(format!("Copied contents of file {} to {copy_to_str}", copy_from.display())) + } }) }, }); From 835cda11f18dea7f90855b8560034b7cd2aefdf0 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:27:58 +0000 Subject: [PATCH 12/52] refactor: variable renaming --- helix-term/src/ui/mod.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ccab602ef6b9..4488f55e553c 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -399,31 +399,32 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("move:", cx, path, |path, input| { + create_file_operation_prompt("move:", cx, path, |move_from, move_to| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to)); Ok("".into()) }) }, // delete alt!('d') => { - create_file_operation_prompt("delete? (y/n):", cx, path, |_, to_delete_str| { - let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(to_delete_str)); - if matches!(to_delete_str, "y" | "n") { + create_file_operation_prompt("delete? (y/n):", cx, path, |_, input| { + let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(input)); + if matches!(input, "y" | "n") { if to_delete.exists() { - return Err(format!("Path {to_delete_str} does not exist")) + return Err(format!("Path {} does not exist", to_delete.display())) }; - if to_delete_str.ends_with(std::path::MAIN_SEPARATOR) { - fs::remove_dir_all(to_delete).map_err(|err| format!("Unable to delete directory {to_delete_str}: {err}"))?; + if input.ends_with(std::path::MAIN_SEPARATOR) { + fs::remove_dir_all(&to_delete).map_err(|err| format!("Unable to delete directory {}: {err}", to_delete.display()))?; - Ok(format!("Deleted directory: {to_delete_str}")) + Ok(format!("Deleted directory: {}", to_delete.display())) } else { - fs::remove_file(to_delete).map_err(|err| format!("Unable to delete file {to_delete_str}: {err}"))?; + fs::remove_file(&to_delete).map_err(|err| format!("Unable to delete file {}: {err}", to_delete.display()))?; - Ok(format!("Deleted file: {to_delete_str}")) + Ok(format!("Deleted file: {}", to_delete.display())) } } else { - Ok(format!("Did not delete: {to_delete_str}")) + Ok(format!("Did not delete: {}", to_delete.display())) } }) }, From b3d0f16276e977017ac4056bd1013beaa8dc9a3b Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:48:38 +0000 Subject: [PATCH 13/52] feat: use display method on paths --- helix-term/src/ui/mod.rs | 48 +++++++++++++++++++++---------------- helix-term/src/ui/picker.rs | 13 +++++----- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 4488f55e553c..0322a65650f4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -378,22 +378,22 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { + alt!('n') => { create_file_operation_prompt("create:", cx, path, |_, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); if to_create.exists() { - return Err(format!("Path {to_create_str} already exists")) + return Err(format!("Path {} already exists", to_create.display())) }; if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - fs::create_dir_all(to_create).map_err(|err| format!("Unable to create directory {to_create_str}: {err}"))?; + fs::create_dir_all(&to_create).map_err(|err| format!("Unable to create directory {}: {err}", to_create.display()))?; - Ok(format!("Created directory: {to_create_str}")) + Ok(format!("Created directory: {}", to_create.display())) } else { - fs::File::create(to_create).map_err(|err| format!("Unable to create file {to_create_str}: {err}"))?; + fs::File::create(&to_create).map_err(|err| format!("Unable to create file {}: {err}", to_create.display()))?; - Ok(format!("Created file: {to_create_str}")) + Ok(format!("Created file: {}", to_create.display())) } }) }, @@ -407,14 +407,14 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("delete? (y/n):", cx, path, |_, input| { - let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(input)); - if matches!(input, "y" | "n") { - if to_delete.exists() { + create_file_operation_prompt("delete? (y/n):", cx, path, |_, to_delete_str| { + let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(to_delete_str)); + if to_delete_str == "y" { + if !to_delete.exists() { return Err(format!("Path {} does not exist", to_delete.display())) }; - if input.ends_with(std::path::MAIN_SEPARATOR) { + if to_delete_str.ends_with(std::path::MAIN_SEPARATOR) { fs::remove_dir_all(&to_delete).map_err(|err| format!("Unable to delete directory {}: {err}", to_delete.display()))?; Ok(format!("Deleted directory: {}", to_delete.display())) @@ -424,29 +424,37 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { + // copy contents + alt!('c') => { create_file_operation_prompt("copy-to:", cx, path, |copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if copy_to_str.ends_with('/') { + if copy_from.is_dir() || copy_to_str.ends_with('/') { + // TODO: support copying directories (recursively)?. This isn't built-in to the standard library Err(format!("Copying directories is not supported: {} is a directory", copy_from.display())) } else if copy_to.exists() { // TODO: confirmation prompt when overwriting - Err(format!("Path {copy_to_str} exists")) + Err(format!("Path {} exists", copy_to.display())) } else { - std::fs::copy(copy_from, copy_to).map_err( - |err| format!("Unable to copy from file {} to {copy_to_str}: {err}", - copy_from.display() + std::fs::copy(copy_from, ©_to).map_err( + |err| format!("Unable to copy from file {} to {}: {err}", + copy_from.display(), copy_to.display() ))?; - Ok(format!("Copied contents of file {} to {copy_to_str}", copy_from.display())) + Ok(format!("Copied contents of file {} to {}", copy_from.display(), copy_to.display())) } }) }, + // copy path + alt!('y') => { + // TODO + // cx. + // cx.editor.registers + // .unwrap_or(cx.editor.config().default_yank_register) + } }); Ok(picker) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ca17feaeade5..7eb6a9d82a12 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1065,6 +1065,10 @@ impl Component for Picker { self.move_by(1, Direction::Backward); @@ -1132,13 +1136,8 @@ impl Component for Picker { self.toggle_preview(); } - key_event => { - if !matches!( - self.custom_event_handler(&key_event, ctx), - EventResult::Consumed(_) - ) { - self.prompt_handle_event(event, ctx); - }; + _ => { + self.prompt_handle_event(event, ctx); } } From 7fd7b7274abd38f0bf88019f1acf70ff03cb1a4e Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:11:02 +0000 Subject: [PATCH 14/52] feat: implement copy path of selected item --- helix-term/src/ui/mod.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0322a65650f4..e0ab20eb07a3 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -399,10 +399,27 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("move:", cx, path, |move_from, move_to| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to)); - Ok("".into()) + create_file_operation_prompt("move:", cx, path, |move_from, move_to_str| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + if move_to.exists() { + // TODO: overwrite prompt + Err(format!("Path {} already exists", move_to.display())) + } else { + fs::rename(move_from, &move_to).map_err(|err| + format!( + "Unable to move {} {} -> {}: {err}", + if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { + "directory" + } else { + "file" + }, + move_from.display(), + move_to.display() + ) + )?; + Ok("".into()) + } }) }, // delete @@ -450,10 +467,15 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - // TODO - // cx. - // cx.editor.registers - // .unwrap_or(cx.editor.config().default_yank_register) + let register = cx.editor.selected_register.unwrap_or(cx.editor.config().default_yank_register); + let path = helix_stdx::path::get_relative_path(path); + let path = path.to_string_lossy().to_string(); + let message = format!("Yanked {} to register {register}", path); + + match cx.editor.registers.write(register, vec![path]) { + Ok(_) => cx.editor.set_status(message), + Err(err) => cx.editor.set_error(err.to_string()) + }; } }); From eecabdbeb565d82fb7ace279a107bcf37ce2b3cf Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:16:15 +0000 Subject: [PATCH 15/52] feat: pass context to all callbacks in file operations --- helix-term/src/ui/mod.rs | 17 +++++++++-------- helix-term/src/ui/prompt.rs | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e0ab20eb07a3..ebd4137e55f2 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -290,12 +290,11 @@ fn create_file_operation_prompt( prompt: &'static str, cx: &mut Context, path: &Path, - callback: fn(&Path, &str) -> Result, + callback: fn(&mut Context, &Path, &str) -> Result, ) { cx.editor.path_editing = Some(path.to_path_buf()); let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { - // let path = path.clone(); let mut prompt = Prompt::new( prompt.into(), None, @@ -305,8 +304,10 @@ fn create_file_operation_prompt( return; }; - if let Some(path) = &cx.editor.path_editing { - match callback(path, input) { + let path = cx.editor.path_editing.clone(); + + if let Some(path) = path { + match callback(cx, &path, input) { Ok(msg) => cx.editor.set_status(msg), Err(msg) => cx.editor.set_error(msg), }; @@ -379,7 +380,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |_, to_create_str| { + create_file_operation_prompt("create:", cx, path, |_, _, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); if to_create.exists() { @@ -399,7 +400,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("move:", cx, path, |move_from, move_to_str| { + create_file_operation_prompt("move:", cx, path, |_, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); if move_to.exists() { @@ -424,7 +425,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("delete? (y/n):", cx, path, |_, to_delete_str| { + create_file_operation_prompt("delete? (y/n):", cx, path, |_, _, to_delete_str| { let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(to_delete_str)); if to_delete_str == "y" { if !to_delete.exists() { @@ -447,7 +448,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("copy-to:", cx, path, |copy_from, copy_to_str| { + create_file_operation_prompt("copy-to:", cx, path, |_, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); if copy_from.is_dir() || copy_to_str.ends_with('/') { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 5444a78fd4aa..e887415d91ad 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -123,6 +123,7 @@ impl Prompt { } pub fn set_line_no_recalculate(&mut self, line: String) { + assert!(self.completion.is_empty()); let cursor = line.len(); self.line = line; self.cursor = cursor; From eafd8ace185b1685e86b0ebc4c0bb135e7867fcc Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:28:30 +0000 Subject: [PATCH 16/52] style: formatting --- helix-term/src/ui/mod.rs | 32 +++++++++++++++++++++++--------- helix-view/src/editor.rs | 4 ++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ebd4137e55f2..927b7c5913c4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -292,7 +292,7 @@ fn create_file_operation_prompt( path: &Path, callback: fn(&mut Context, &Path, &str) -> Result, ) { - cx.editor.path_editing = Some(path.to_path_buf()); + cx.editor.file_explorer_selected_path = Some(path.to_path_buf()); let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { let mut prompt = Prompt::new( @@ -304,7 +304,7 @@ fn create_file_operation_prompt( return; }; - let path = cx.editor.path_editing.clone(); + let path = cx.editor.file_explorer_selected_path.clone(); if let Some(path) = path { match callback(cx, &path, input) { @@ -318,7 +318,7 @@ fn create_file_operation_prompt( }, ); - if let Some(path_editing) = &editor.path_editing { + if let Some(path_editing) = &editor.file_explorer_selected_path { prompt.set_line_no_recalculate(path_editing.display().to_string()); } @@ -380,7 +380,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |_, _, to_create_str| { + create_file_operation_prompt("create:", cx, path, |_cx, _path, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); if to_create.exists() { @@ -388,11 +388,15 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result { - let register = cx.editor.selected_register.unwrap_or(cx.editor.config().default_yank_register); + let register = cx.editor.selected_register.unwrap_or( + cx.editor.config().default_yank_register + ); let path = helix_stdx::path::get_relative_path(path); let path = path.to_string_lossy().to_string(); let message = format!("Yanked {} to register {register}", path); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 8e1ccd61886d..abda8bb63e5f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1035,7 +1035,6 @@ pub struct Editor { pub tree: Tree, pub next_document_id: DocumentId, pub documents: BTreeMap, - pub path_editing: Option, // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>. // https://stackoverflow.com/a/66875668 @@ -1102,6 +1101,7 @@ pub struct Editor { pub mouse_down_range: Option, pub cursor_cache: CursorCache, + pub file_explorer_selected_path: Option, } pub type Motion = Box; @@ -1224,7 +1224,7 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), - path_editing: None, + file_explorer_selected_path: None, } } From a099ae1dbe053220c281d0f4bc5b09097852c324 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:33:01 +0000 Subject: [PATCH 17/52] style: formatting --- helix-term/src/ui/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 927b7c5913c4..abc76664012b 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -464,7 +464,9 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result cx.editor.set_status(message), + Ok(()) => cx.editor.set_status(message), Err(err) => cx.editor.set_error(err.to_string()) }; } From e177c48208cfb954eada1ffba96284f4f93d2dff Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 11:42:03 +0000 Subject: [PATCH 18/52] refactor: use Option to indicate if a status message should not be changed --- helix-term/src/ui/mod.rs | 80 +++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index abc76664012b..bd7a2e441c31 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -290,7 +290,7 @@ fn create_file_operation_prompt( prompt: &'static str, cx: &mut Context, path: &Path, - callback: fn(&mut Context, &Path, &str) -> Result, + callback: fn(&mut Context, &Path, &str) -> Option>, ) { cx.editor.file_explorer_selected_path = Some(path.to_path_buf()); let callback = Box::pin(async move { @@ -308,8 +308,9 @@ fn create_file_operation_prompt( if let Some(path) = path { match callback(cx, &path, input) { - Ok(msg) => cx.editor.set_status(msg), - Err(msg) => cx.editor.set_error(msg), + Some(Ok(msg)) => cx.editor.set_status(msg), + Some(Err(msg)) => cx.editor.set_error(msg), + None => (), }; } else { cx.editor @@ -384,21 +385,26 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result {}: {err}", if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { @@ -422,8 +428,10 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result { create_file_operation_prompt("copy-to:", cx, path, |_, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); if copy_from.is_dir() || copy_to_str.ends_with('/') { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library - Err(format!( + Some(Err(format!( "Copying directories is not supported: {} is a directory", copy_from.display() - )) + ))) } else if copy_to.exists() { - // TODO: confirmation prompt when overwriting - Err(format!("Path {} exists", copy_to.display())) + // TODO: confirmation prompt + Some(Err(format!("Path {} exists", copy_to.display()))) } else { - std::fs::copy(copy_from, ©_to).map_err( + if let Err(err) = std::fs::copy(copy_from, ©_to).map_err( |err| format!("Unable to copy from file {} to {}: {err}", copy_from.display(), copy_to.display() - ))?; + )) { + return Some(Err(err)); + }; - Ok(format!( + Some(Ok(format!( "Copied contents of file {} to {}", copy_from.display(), copy_to.display() - )) + ))) } }) }, - // copy path + // copy path into register alt!('y') => { let register = cx.editor.selected_register.unwrap_or( cx.editor.config().default_yank_register From 382803c803c5e052fb38507f1440aa4341605c5a Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:15:35 +0000 Subject: [PATCH 19/52] feat: add confirmation prompt when overwriting --- helix-term/src/ui/mod.rs | 142 +++++++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 35 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bd7a2e441c31..a0cf06a3a684 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -286,6 +286,39 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; +fn create_confirmation_prompt( + input: String, + cx: &mut Context, + to_create_str: String, + to_create: PathBuf, + callback: fn(&str, &Path) -> Option>, +) { + let callback = Box::pin(async move { + let call: Callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let prompt = Prompt::new( + input.into(), + None, + crate::ui::completers::none, + move |cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate || input != "y" { + return; + }; + + match callback(&to_create_str, &to_create) { + Some(Ok(msg)) => cx.editor.set_status(msg), + Some(Err(msg)) => cx.editor.set_error(msg), + None => (), + }; + }, + ); + + compositor.push(Box::new(prompt)); + })); + Ok(call) + }); + cx.jobs.callback(callback); +} + fn create_file_operation_prompt( prompt: &'static str, cx: &mut Context, @@ -381,42 +414,52 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |_cx, _path, to_create_str| { + create_file_operation_prompt("create:", cx, path, |cx, _path, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - if to_create.exists() { - // TODO: confirmation prompt - return Some(Err(format!("Path {} already exists", to_create.display()))) - }; + let create = |to_create_str: &str, to_create: &Path| { + if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + if let Err(err) = fs::create_dir_all(to_create).map_err( + |err| format!("Unable to create directory {}: {err}", to_create.display()) + ) { + return Some(Err(err)); + } - if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::create_dir_all(&to_create).map_err( - |err| format!("Unable to create directory {}: {err}", to_create.display()) - ) { - return Some(Err(err)); + Some(Ok(format!("Created directory: {}", to_create.display()))) + } else { + if let Err(err) = fs::File::create(to_create).map_err( + |err| format!("Unable to create file {}: {err}", to_create.display()) + ) { + return Some(Err(err)); + }; + + Some(Ok(format!("Created file: {}", to_create.display()))) } + }; - Some(Ok(format!("Created directory: {}", to_create.display()))) - } else { - if let Err(err) = fs::File::create(&to_create).map_err( - |err| format!("Unable to create file {}: {err}", to_create.display()) - ) { - return Some(Err(err)); - }; + if to_create.exists() { + create_confirmation_prompt( + format!( + "Path {} already exists. Overwrite? (y/n):", to_create.display() + ), + cx, + to_create_str.to_string(), + to_create.to_path_buf(), + create + ); + return None; + }; - Some(Ok(format!("Created file: {}", to_create.display()))) - } + create(to_create_str, &to_create) }) }, // move alt!('m') => { - create_file_operation_prompt("move:", cx, path, |_, move_from, move_to_str| { + create_file_operation_prompt("move:", cx, path, |cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - if move_to.exists() { - // TODO: confirmation prompt - Some(Err(format!("Path {} already exists", move_to.display()))) - } else { + let move_op = |move_to_str: &str, move_from: &Path| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| format!( "Unable to move {} {} -> {}: {err}", @@ -432,7 +475,22 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result { - create_file_operation_prompt("copy-to:", cx, path, |_, copy_from, copy_to_str| { + create_file_operation_prompt("copy-to:", cx, path, |cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if copy_from.is_dir() || copy_to_str.ends_with('/') { - // TODO: support copying directories (recursively)?. This isn't built-in to the standard library - Some(Err(format!( - "Copying directories is not supported: {} is a directory", copy_from.display() - ))) - } else if copy_to.exists() { - // TODO: confirmation prompt - Some(Err(format!("Path {} exists", copy_to.display()))) - } else { + + let copy_op = |copy_to_str: &str, copy_from: &Path| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); if let Err(err) = std::fs::copy(copy_from, ©_to).map_err( |err| format!("Unable to copy from file {} to {}: {err}", copy_from.display(), copy_to.display() @@ -493,6 +545,26 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Date: Tue, 18 Feb 2025 12:19:20 +0000 Subject: [PATCH 20/52] docs: add file explorer keymap info --- book/src/keymap.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/book/src/keymap.md b/book/src/keymap.md index 2797eaee2908..07971ec64980 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -463,6 +463,18 @@ See the documentation page on [pickers](./pickers.md) for more info. | `Ctrl-t` | Toggle preview | | `Escape`, `Ctrl-c` | Close picker | +### File Explorer + +There are additional keys accessible when using the File Explorer. + +| Key | Description | +| ----- | ------------- | +| `Alt-m` | Move (& rename) selected file or directory | +| `Alt-n` | Create a new file or directory | +| `Alt-d` | Delete the selected file or directory | +| `Alt-c` | Copy the selected file | +| `Alt-y` | Yank the path to the selected file or directory | + ## Prompt Keys to use within prompt, Remapping currently not supported. From cd6584f0ab1b0b437851f8bc685255934abe4620 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:20:37 +0000 Subject: [PATCH 21/52] chore: appease clippy --- helix-term/src/compositor.rs | 1 - helix-term/src/ui/mod.rs | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index af510ed59a4a..28c8651a2e40 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -15,7 +15,6 @@ pub enum EventResult { Consumed(Option), } -use crate::commands; use crate::job::Jobs; use crate::ui::picker; use helix_view::Editor; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index a0cf06a3a684..93da51ce64e6 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ mod text_decorations; use crate::compositor::{Compositor, Context}; use crate::job::{self, Callback}; -use crate::{alt, ctrl, declare_key_handlers, filter_picker_entry}; +use crate::{alt, declare_key_handlers, filter_picker_entry}; pub use completion::Completion; pub use editor::EditorView; use helix_core::hashmap; @@ -33,13 +33,10 @@ pub use text::Text; use helix_view::Editor; use tui::text::Span; -use std::collections::HashMap; use std::fs; -use std::path::{Path, MAIN_SEPARATOR}; +use std::path::Path; use std::{error::Error, path::PathBuf}; -use self::picker::PickerKeyHandler; - struct Utf8PathBuf { path: String, is_dir: bool, From 0e6e3e8aebc0762c2c7f1ea053ee00ecf63803ea Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:24:45 +0000 Subject: [PATCH 22/52] refactor: rename variable --- helix-term/src/ui/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 93da51ce64e6..f41b7184780e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -414,7 +414,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Date: Tue, 18 Feb 2025 12:36:54 +0000 Subject: [PATCH 23/52] feat: better initial prompts when using file picker commands --- helix-term/src/ui/mod.rs | 35 ++++++++++++++++++++++++++++------- helix-term/src/ui/prompt.rs | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f41b7184780e..c46cb8611e51 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -320,7 +320,8 @@ fn create_file_operation_prompt( prompt: &'static str, cx: &mut Context, path: &Path, - callback: fn(&mut Context, &Path, &str) -> Option>, + compute_initial_line: fn(&Path) -> String, + file_op: fn(&mut Context, &Path, &str) -> Option>, ) { cx.editor.file_explorer_selected_path = Some(path.to_path_buf()); let callback = Box::pin(async move { @@ -337,7 +338,7 @@ fn create_file_operation_prompt( let path = cx.editor.file_explorer_selected_path.clone(); if let Some(path) = path { - match callback(cx, &path, input) { + match file_op(cx, &path, input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -350,7 +351,7 @@ fn create_file_operation_prompt( ); if let Some(path_editing) = &editor.file_explorer_selected_path { - prompt.set_line_no_recalculate(path_editing.display().to_string()); + prompt.set_line_no_recalculate(compute_initial_line(path_editing)); } compositor.push(Box::new(prompt)); @@ -411,7 +412,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("create:", cx, path, |cx, _path, to_create_str| { + create_file_operation_prompt( + "create:", + cx, + path, + |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), + |cx, _path, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); let create_op = |to_create_str: &str, to_create: &Path| { @@ -452,7 +458,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("move:", cx, path, |cx, move_from, move_to_str| { + create_file_operation_prompt( + "move:", + cx, + path, + |path| path.display().to_string(), + |cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); let move_op = |move_to_str: &str, move_from: &Path| { @@ -492,7 +503,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("delete? (y/n):", cx, path, |_, _, to_delete_str| { + create_file_operation_prompt( + "delete? (y/n):", + cx, + path, + |_| "".to_string(), + |_, _, to_delete_str| { let to_delete = helix_stdx::path::expand_tilde(PathBuf::from(to_delete_str)); if to_delete_str == "y" { if !to_delete.exists() { @@ -527,7 +543,12 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { - create_file_operation_prompt("copy-to:", cx, path, |cx, copy_from, copy_to_str| { + create_file_operation_prompt( + "copy-to:", + cx, + path, + |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), + |cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); let copy_op = |copy_to_str: &str, copy_from: &Path| { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index e887415d91ad..5435d5f948f4 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -123,7 +123,7 @@ impl Prompt { } pub fn set_line_no_recalculate(&mut self, line: String) { - assert!(self.completion.is_empty()); + debug_assert!(self.completion.is_empty()); let cursor = line.len(); self.line = line; self.cursor = cursor; From b6bbd4f18ae8001d149671132f5e26f56b144d5c Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:38:11 +0000 Subject: [PATCH 24/52] fix: delete path, not confirmation e.g. `y` --- helix-term/src/ui/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index c46cb8611e51..095868ebbab1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -508,14 +508,13 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Date: Tue, 18 Feb 2025 12:38:27 +0000 Subject: [PATCH 25/52] chore: appease clippy --- helix-term/src/ui/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 095868ebbab1..d535968ec6e4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -515,7 +515,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Date: Tue, 18 Feb 2025 13:11:37 +0000 Subject: [PATCH 26/52] feat: refresh picker when directory operations are performed --- helix-term/src/ui/mod.rs | 68 +++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d535968ec6e4..753a899de350 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -283,12 +283,15 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; +type OnConfirm = fn(cx: &mut Context, picker_root: PathBuf, &str, &Path) -> Option>; + fn create_confirmation_prompt( input: String, cx: &mut Context, - to_create_str: String, - to_create: PathBuf, - callback: fn(&str, &Path) -> Option>, + operation_input_str: String, + operation_input: PathBuf, + picker_root: PathBuf, + on_confirm: OnConfirm, ) { let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { @@ -301,7 +304,7 @@ fn create_confirmation_prompt( return; }; - match callback(&to_create_str, &to_create) { + match on_confirm(cx, picker_root.clone(), &operation_input_str, &operation_input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -361,6 +364,19 @@ fn create_file_operation_prompt( cx.jobs.callback(callback); } +fn refresh(cx: &mut Context, root: PathBuf) { + let callback = Box::pin(async move { + let call: Callback = + Callback::EditorCompositor(Box::new(move |editor, compositor| { + if let Ok(picker) = file_explorer(root, editor) { + compositor.push(Box::new(overlay::overlaid(picker))); + } + })); + Ok(call) + }); + cx.jobs.callback(callback); +} + pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; @@ -385,16 +401,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result Result Result Result Result Result Result Result Result Result Result Date: Tue, 18 Feb 2025 13:52:09 +0000 Subject: [PATCH 27/52] chore: remove TODO comment --- helix-term/src/ui/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 753a899de350..3f15d32175af 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -414,8 +414,6 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { From 24bd14863b74fc3d621af797177f00a1a1c3636c Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:04:10 +0000 Subject: [PATCH 28/52] feat: restore cursor when performing file operations --- helix-term/src/commands.rs | 6 ++-- helix-term/src/ui/mod.rs | 62 ++++++++++++++++++++++--------------- helix-term/src/ui/picker.rs | 15 ++++++--- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 19a22601e3d1..7672dc868877 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3013,7 +3013,7 @@ fn file_explorer(cx: &mut Context) { return; } - if let Ok(picker) = ui::file_explorer(root, cx.editor) { + if let Ok(picker) = ui::file_explorer(None, root, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } @@ -3040,7 +3040,7 @@ fn file_explorer_in_current_buffer_directory(cx: &mut Context) { } }; - if let Ok(picker) = ui::file_explorer(path, cx.editor) { + if let Ok(picker) = ui::file_explorer(None, path, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } @@ -3053,7 +3053,7 @@ fn file_explorer_in_current_directory(cx: &mut Context) { return; } - if let Ok(picker) = ui::file_explorer(cwd, cx.editor) { + if let Ok(picker) = ui::file_explorer(None, cwd, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3f15d32175af..d2cc6046f036 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -283,9 +283,10 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; -type OnConfirm = fn(cx: &mut Context, picker_root: PathBuf, &str, &Path) -> Option>; +type OnConfirm = fn(cursor: u32, cx: &mut Context, picker_root: PathBuf, &str, &Path) -> Option>; fn create_confirmation_prompt( + cursor: u32, input: String, cx: &mut Context, operation_input_str: String, @@ -304,7 +305,7 @@ fn create_confirmation_prompt( return; }; - match on_confirm(cx, picker_root.clone(), &operation_input_str, &operation_input) { + match on_confirm(cursor, cx, picker_root.clone(), &operation_input_str, &operation_input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -319,12 +320,15 @@ fn create_confirmation_prompt( cx.jobs.callback(callback); } +type FileOperation = fn(u32, &mut Context, &Path, &str) -> Option>; + fn create_file_operation_prompt( + cursor: u32, prompt: &'static str, cx: &mut Context, path: &Path, compute_initial_line: fn(&Path) -> String, - file_op: fn(&mut Context, &Path, &str) -> Option>, + file_op: FileOperation, ) { cx.editor.file_explorer_selected_path = Some(path.to_path_buf()); let callback = Box::pin(async move { @@ -341,7 +345,7 @@ fn create_file_operation_prompt( let path = cx.editor.file_explorer_selected_path.clone(); if let Some(path) = path { - match file_op(cx, &path, input) { + match file_op(cursor, cx, &path, input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -364,11 +368,11 @@ fn create_file_operation_prompt( cx.jobs.callback(callback); } -fn refresh(cx: &mut Context, root: PathBuf) { +fn refresh(cursor: Option, cx: &mut Context, root: PathBuf) { let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { - if let Ok(picker) = file_explorer(root, editor) { + if let Ok(picker) = file_explorer(cursor, root, editor) { compositor.push(Box::new(overlay::overlaid(picker))); } })); @@ -377,7 +381,7 @@ fn refresh(cx: &mut Context, root: PathBuf) { cx.jobs.callback(callback); } -pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result { +pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Result { let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; @@ -401,7 +405,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result { create_file_operation_prompt( + cursor, "create:", cx, path, |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), - |cx, path, to_create_str| { + |cursor, cx, path, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - let create_op = |cx: &mut Context, root: PathBuf, to_create_str: &str, to_create: &Path| { + let create_op = |cursor: u32, cx: &mut Context, root: PathBuf, to_create_str: &str, to_create: &Path| { if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { if let Err(err) = fs::create_dir_all(to_create).map_err( |err| format!("Unable to create directory {}: {err}", to_create.display()) ) { return Some(Err(err)); } - refresh(cx, root); + refresh(Some(cursor), cx, root); Some(Ok(format!("Created directory: {}", to_create.display()))) } else { @@ -441,7 +447,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result { create_file_operation_prompt( + cursor, "move:", cx, path, |path| path.display().to_string(), - |cx, move_from, move_to_str| { + |cursor, cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - let move_op = |cx: &mut Context, root: PathBuf, move_to_str: &str, move_from: &Path| { + let move_op = |cursor: u32, cx: &mut Context, root: PathBuf, move_to_str: &str, move_from: &Path| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| format!( @@ -492,7 +500,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result { create_file_operation_prompt( + cursor, "delete? (y/n):", cx, path, |_| "".to_string(), - |cx, to_delete, confirmation| { + |cursor, cx, to_delete, confirmation| { if confirmation == "y" { if !to_delete.exists() { return Some(Err(format!("Path {} does not exist", to_delete.display()))) @@ -538,7 +548,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result { create_file_operation_prompt( + cursor, "copy-to:", cx, path, |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), - |cx, copy_from, copy_to_str| { + |cursor, cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - let copy_op = |cx: &mut Context, root: PathBuf, copy_to_str: &str, copy_from: &Path| { + let copy_op = |cursor: u32, cx: &mut Context, root: PathBuf, copy_to_str: &str, copy_from: &Path| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); if let Err(err) = std::fs::copy(copy_from, ©_to).map_err( |err| format!("Unable to copy from file {} to {}: {err}", @@ -576,7 +587,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result Result Result Picker { .saturating_sub(1); } + pub fn with_cursor(mut self, cursor: u32) -> Self { + self.cursor = cursor; + self + } + pub fn selection(&self) -> Option<&T> { self.matcher .snapshot() @@ -521,7 +526,7 @@ impl Picker { if let (Some(callback), Some(selected)) = (self.custom_key_handlers.get(event), self.selection()) { - callback(cx, selected); + callback(cx, selected, self.cursor); EventResult::Consumed(None) } else { EventResult::Ignored(None) @@ -1180,16 +1185,16 @@ impl Drop for Picker { } type PickerCallback = Box; -pub type PickerKeyHandler = HashMap>; +pub type PickerKeyHandler = HashMap>; /// Convenience macro to add custom keybindings per picker #[macro_export] macro_rules! declare_key_handlers { - (|$cx:ident, $item:tt : $t:ty|, $($key:expr => $handler:block),* $(,)?) => { + (|$cx:ident, $item:tt : $t:ty, $cursor:ident|, $($key:expr => $handler:block),* $(,)?) => { hashmap!( $( - $key => Box::new(|$cx: &mut Context, $item: $t| $handler) - as Box + $key => Box::new(|$cx: &mut Context, $item: $t, $cursor: u32| $handler) + as Box ),* ) }; From 4fabd7927dd1121b4e0189b7d3c730462e6e1ecf Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:08:16 +0000 Subject: [PATCH 29/52] refactor: remove unneeded macro --- helix-term/src/ui/mod.rs | 25 ++++++++++++------------- helix-term/src/ui/picker.rs | 13 ------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d2cc6046f036..7f056fe4e912 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ mod text_decorations; use crate::compositor::{Compositor, Context}; use crate::job::{self, Callback}; -use crate::{alt, declare_key_handlers, filter_picker_entry}; +use crate::{alt, filter_picker_entry}; pub use completion::Completion; pub use editor::EditorView; use helix_core::hashmap; @@ -418,10 +418,9 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res ) .with_cursor(cursor.unwrap_or_default()) .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) - .with_key_handlers(declare_key_handlers! { - |cx, (path, _is_dir): &(PathBuf, bool), cursor|, + .with_key_handlers(hashmap! { // create - alt!('n') => { + alt!('n') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { create_file_operation_prompt( cursor, "create:", @@ -472,9 +471,9 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res create_op(cursor, cx, root, to_create_str, &to_create) }) - }, + }) as Box, // move - alt!('m') => { + alt!('m') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ create_file_operation_prompt( cursor, "move:", @@ -523,9 +522,9 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res move_op(cursor, cx, root, move_to_str, move_from) }) - }, + }) as Box, // delete - alt!('d') => { + alt!('d') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ create_file_operation_prompt( cursor, "delete? (y/n):", @@ -567,9 +566,9 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res None } }) - }, + }) as Box, // copy file / directory - alt!('c') => { + alt!('c') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ create_file_operation_prompt( cursor, "copy-to:", @@ -618,9 +617,9 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res copy_op(cursor, cx, root, copy_to_str, copy_from) } }) - }, + }) as Box, // copy path into register - alt!('y') => { + alt!('y') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32|{ let register = cx.editor.selected_register.unwrap_or( cx.editor.config().default_yank_register ); @@ -632,7 +631,7 @@ pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Res Ok(()) => cx.editor.set_status(message), Err(err) => cx.editor.set_error(err.to_string()) }; - } + }) as Box }); Ok(picker) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ac484c080c73..87937cc7b20a 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1186,16 +1186,3 @@ impl Drop for Picker { type PickerCallback = Box; pub type PickerKeyHandler = HashMap>; - -/// Convenience macro to add custom keybindings per picker -#[macro_export] -macro_rules! declare_key_handlers { - (|$cx:ident, $item:tt : $t:ty, $cursor:ident|, $($key:expr => $handler:block),* $(,)?) => { - hashmap!( - $( - $key => Box::new(|$cx: &mut Context, $item: $t, $cursor: u32| $handler) - as Box - ),* - ) - }; - } From a97ebc2ed06ba17aab7479da517b794c30247988 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:17:31 +0000 Subject: [PATCH 30/52] style: format --- helix-term/src/ui/mod.rs | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 7f056fe4e912..585898a6ccb7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -283,7 +283,13 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; -type OnConfirm = fn(cursor: u32, cx: &mut Context, picker_root: PathBuf, &str, &Path) -> Option>; +type OnConfirm = fn( + cursor: u32, + cx: &mut Context, + picker_root: PathBuf, + &str, + &Path, +) -> Option>; fn create_confirmation_prompt( cursor: u32, @@ -305,7 +311,13 @@ fn create_confirmation_prompt( return; }; - match on_confirm(cursor, cx, picker_root.clone(), &operation_input_str, &operation_input) { + match on_confirm( + cursor, + cx, + picker_root.clone(), + &operation_input_str, + &operation_input, + ) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -370,18 +382,21 @@ fn create_file_operation_prompt( fn refresh(cursor: Option, cx: &mut Context, root: PathBuf) { let callback = Box::pin(async move { - let call: Callback = - Callback::EditorCompositor(Box::new(move |editor, compositor| { - if let Ok(picker) = file_explorer(cursor, root, editor) { - compositor.push(Box::new(overlay::overlaid(picker))); - } - })); + let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { + if let Ok(picker) = file_explorer(cursor, root, editor) { + compositor.push(Box::new(overlay::overlaid(picker))); + } + })); Ok(call) }); cx.jobs.callback(callback); } -pub fn file_explorer(cursor: Option, root: PathBuf, editor: &Editor) -> Result { +pub fn file_explorer( + cursor: Option, + root: PathBuf, + editor: &Editor, +) -> Result { let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; From e6e80e21855a5843da28a0d8d0c080d5c5665caf Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:27:11 +0000 Subject: [PATCH 31/52] fix: remove unneeded panics --- helix-term/src/ui/mod.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 585898a6ccb7..85db1a112bc4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -467,7 +467,9 @@ pub fn file_explorer( } }; - let root = path.parent().unwrap().to_path_buf(); + let root = path.parent().map( + |p| p.to_path_buf() + ).unwrap_or(helix_stdx::env::current_working_dir()); if to_create.exists() { create_confirmation_prompt( @@ -518,7 +520,9 @@ pub fn file_explorer( None }; - let root = move_from.parent().unwrap().to_path_buf(); + let root = move_from.parent().map( + |p| p.to_path_buf() + ).unwrap_or(helix_stdx::env::current_working_dir()); if move_to.exists() { create_confirmation_prompt( @@ -552,7 +556,9 @@ pub fn file_explorer( return Some(Err(format!("Path {} does not exist", to_delete.display()))) }; - let root = to_delete.parent().unwrap().to_path_buf(); + let root = to_delete.parent().map( + |p| p.to_path_buf() + ).unwrap_or(helix_stdx::env::current_working_dir()); if confirmation.ends_with(std::path::MAIN_SEPARATOR) { if let Err(err) = fs::remove_dir_all(to_delete).map_err( @@ -608,7 +614,9 @@ pub fn file_explorer( ))) }; - let root = copy_from.parent().unwrap().to_path_buf(); + let root = copy_to.parent().map( + |p| p.to_path_buf() + ).unwrap_or(helix_stdx::env::current_working_dir()); if copy_from.is_dir() || copy_to_str.ends_with('/') { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library From 6dbb09f1faee92914d5573814d99e6f54b1d05af Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:45:13 +0000 Subject: [PATCH 32/52] style: format mod.rs --- helix-term/src/ui/mod.rs | 307 ++++++++++++++++++++++----------------- 1 file changed, 173 insertions(+), 134 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 85db1a112bc4..32da635e1d95 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -380,7 +380,7 @@ fn create_file_operation_prompt( cx.jobs.callback(callback); } -fn refresh(cursor: Option, cx: &mut Context, root: PathBuf) { +fn refresh_file_explorer(cursor: Option, cx: &mut Context, root: PathBuf) { let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { if let Ok(picker) = file_explorer(cursor, root, editor) { @@ -412,97 +412,104 @@ pub fn file_explorer( }, )]; - let picker = Picker::new( - columns, - 0, - directory_content, - (root, directory_style), - move |cx, (path, is_dir): &(PathBuf, bool), action| { - if *is_dir { - let new_root = helix_stdx::path::normalize(path); - refresh(None, cx, new_root); - } else if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }, - ) - .with_cursor(cursor.unwrap_or_default()) - .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) - .with_key_handlers(hashmap! { - // create - alt!('n') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { - create_file_operation_prompt( - cursor, - "create:", - cx, - path, - |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), - |cursor, cx, path, to_create_str| { + let copy_path = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32| { + let register = cx + .editor + .selected_register + .unwrap_or(cx.editor.config().default_yank_register); + let path = helix_stdx::path::get_relative_path(path); + let path = path.to_string_lossy().to_string(); + let message = format!("Yanked path {} to register {register}", path); + + match cx.editor.registers.write(register, vec![path]) { + Ok(()) => cx.editor.set_status(message), + Err(err) => cx.editor.set_error(err.to_string()), + }; + }; + + let create_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { + create_file_operation_prompt( + cursor, + "create:", + cx, + path, + |path| { + path.parent() + .map(|p| p.display().to_string()) + .unwrap_or_default() + }, + |cursor, cx, path, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - let create_op = |cursor: u32, cx: &mut Context, root: PathBuf, to_create_str: &str, to_create: &Path| { + let create_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + to_create_str: &str, + to_create: &Path| { if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::create_dir_all(to_create).map_err( - |err| format!("Unable to create directory {}: {err}", to_create.display()) - ) { + if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { + format!("Unable to create directory {}: {err}", to_create.display()) + }) { return Some(Err(err)); } - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); Some(Ok(format!("Created directory: {}", to_create.display()))) } else { - if let Err(err) = fs::File::create(to_create).map_err( - |err| format!("Unable to create file {}: {err}", to_create.display()) - ) { + if let Err(err) = fs::File::create(to_create).map_err(|err| { + format!("Unable to create file {}: {err}", to_create.display()) + }) { return Some(Err(err)); }; - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); Some(Ok(format!("Created file: {}", to_create.display()))) } }; - let root = path.parent().map( - |p| p.to_path_buf() - ).unwrap_or(helix_stdx::env::current_working_dir()); + let root = path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(helix_stdx::env::current_working_dir()); if to_create.exists() { create_confirmation_prompt( cursor, format!( - "Path {} already exists. Overwrite? (y/n):", to_create.display() + "Path {} already exists. Overwrite? (y/n):", + to_create.display() ), cx, to_create_str.to_string(), to_create.to_path_buf(), root, - create_op + create_op, ); return None; }; create_op(cursor, cx, root, to_create_str, &to_create) - }) - }) as Box, - // move - alt!('m') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ - create_file_operation_prompt( - cursor, - "move:", - cx, - path, - |path| path.display().to_string(), - |cursor, cx, move_from, move_to_str| { + }, + ) + }; + + let move_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { + create_file_operation_prompt( + cursor, + "move:", + cx, + path, + |path| path.display().to_string(), + |cursor, cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - let move_op = |cursor: u32, cx: &mut Context, root: PathBuf, move_to_str: &str, move_from: &Path| { + let move_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + move_to_str: &str, + move_from: &Path| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| + if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { format!( "Unable to move {} {} -> {}: {err}", if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { @@ -513,148 +520,180 @@ pub fn file_explorer( move_from.display(), move_to.display() ) - ) { - return Some(Err(err)) + }) { + return Some(Err(err)); }; - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); None }; - let root = move_from.parent().map( - |p| p.to_path_buf() - ).unwrap_or(helix_stdx::env::current_working_dir()); + let root = move_from + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(helix_stdx::env::current_working_dir()); if move_to.exists() { create_confirmation_prompt( cursor, format!( - "Path {} already exists. Overwrite? (y/n):", move_to.display() + "Path {} already exists. Overwrite? (y/n):", + move_to.display() ), cx, move_to_str.to_string(), move_from.to_path_buf(), root, - move_op + move_op, ); return None; }; move_op(cursor, cx, root, move_to_str, move_from) - }) - }) as Box, - // delete - alt!('d') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ - create_file_operation_prompt( - cursor, - "delete? (y/n):", - cx, - path, - |_| "".to_string(), - |cursor, cx, to_delete, confirmation| { + }, + ) + }; + + let delete_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { + create_file_operation_prompt( + cursor, + "delete? (y/n):", + cx, + path, + |_| "".to_string(), + |cursor, cx, to_delete, confirmation| { if confirmation == "y" { if !to_delete.exists() { - return Some(Err(format!("Path {} does not exist", to_delete.display()))) + return Some(Err(format!("Path {} does not exist", to_delete.display()))); }; - let root = to_delete.parent().map( - |p| p.to_path_buf() - ).unwrap_or(helix_stdx::env::current_working_dir()); + let root = to_delete + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(helix_stdx::env::current_working_dir()); if confirmation.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::remove_dir_all(to_delete).map_err( - |err| format!( - "Unable to delete directory {}: {err}", to_delete.display() - ) - ) { + if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { + format!("Unable to delete directory {}: {err}", to_delete.display()) + }) { return Some(Err(err)); }; - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); Some(Ok(format!("Deleted directory: {}", to_delete.display()))) } else { - if let Err(err) = fs::remove_file(to_delete).map_err( - |err| format!( - "Unable to delete file {}: {err}", to_delete.display() - ) - ) { + if let Err(err) = fs::remove_file(to_delete).map_err(|err| { + format!("Unable to delete file {}: {err}", to_delete.display()) + }) { return Some(Err(err)); }; - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); Some(Ok(format!("Deleted file: {}", to_delete.display()))) } } else { None } - }) - }) as Box, - // copy file / directory - alt!('c') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32|{ - create_file_operation_prompt( - cursor, - "copy-to:", - cx, - path, - |path| path.parent().map(|p| p.display().to_string()).unwrap_or_default(), - |cursor, cx, copy_from, copy_to_str| { + }, + ) + }; + + let copy_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { + create_file_operation_prompt( + cursor, + "copy-to:", + cx, + path, + |path| { + path.parent() + .map(|p| p.display().to_string()) + .unwrap_or_default() + }, + |cursor, cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - let copy_op = |cursor: u32, cx: &mut Context, root: PathBuf, copy_to_str: &str, copy_from: &Path| { + let copy_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + copy_to_str: &str, + copy_from: &Path| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if let Err(err) = std::fs::copy(copy_from, ©_to).map_err( - |err| format!("Unable to copy from file {} to {}: {err}", - copy_from.display(), copy_to.display() - )) { + if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { + format!( + "Unable to copy from file {} to {}: {err}", + copy_from.display(), + copy_to.display() + ) + }) { return Some(Err(err)); }; - refresh(Some(cursor), cx, root); + refresh_file_explorer(Some(cursor), cx, root); Some(Ok(format!( - "Copied contents of file {} to {}", copy_from.display(), copy_to.display() + "Copied contents of file {} to {}", + copy_from.display(), + copy_to.display() ))) }; - let root = copy_to.parent().map( - |p| p.to_path_buf() - ).unwrap_or(helix_stdx::env::current_working_dir()); - + let root = copy_to + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(helix_stdx::env::current_working_dir()); + if copy_from.is_dir() || copy_to_str.ends_with('/') { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library Some(Err(format!( - "Copying directories is not supported: {} is a directory", copy_from.display() + "Copying directories is not supported: {} is a directory", + copy_from.display() ))) } else if copy_to.exists() { create_confirmation_prompt( cursor, format!( - "Path {} already exists. Overwrite? (y/n):", copy_to.display() + "Path {} already exists. Overwrite? (y/n):", + copy_to.display() ), cx, copy_to_str.to_string(), copy_from.to_path_buf(), root, - copy_op + copy_op, ); None } else { copy_op(cursor, cx, root, copy_to_str, copy_from) } - }) - }) as Box, - // copy path into register - alt!('y') => Box::new(|cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32|{ - let register = cx.editor.selected_register.unwrap_or( - cx.editor.config().default_yank_register - ); - let path = helix_stdx::path::get_relative_path(path); - let path = path.to_string_lossy().to_string(); - let message = format!("Yanked path {} to register {register}", path); + }, + ) + }; - match cx.editor.registers.write(register, vec![path]) { - Ok(()) => cx.editor.set_status(message), - Err(err) => cx.editor.set_error(err.to_string()) - }; - }) as Box + let picker = Picker::new( + columns, + 0, + directory_content, + (root, directory_style), + move |cx, (path, is_dir): &(PathBuf, bool), action| { + if *is_dir { + let new_root = helix_stdx::path::normalize(path); + refresh_file_explorer(None, cx, new_root); + } else if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }, + ) + .with_cursor(cursor.unwrap_or_default()) + .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) + .with_key_handlers(hashmap! { + alt!('n') => Box::new(create_file) as Box, + alt!('m') => Box::new(move_file) as Box, + alt!('d') => Box::new(delete_file) as Box, + alt!('c') => Box::new(copy_file) as Box, + alt!('y') => Box::new(copy_path) as Box }); Ok(picker) From a9612dad1da8bc30d8688c4791070a4a7f015143 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:52:49 +0000 Subject: [PATCH 33/52] refactor: extract into a type alias --- helix-term/src/ui/mod.rs | 14 +++++++++----- helix-term/src/ui/picker.rs | 7 ++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 32da635e1d95..93a734b22a82 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -37,6 +37,8 @@ use std::fs; use std::path::Path; use std::{error::Error, path::PathBuf}; +use self::picker::PickerKeyHandler; + struct Utf8PathBuf { path: String, is_dir: bool, @@ -667,6 +669,8 @@ pub fn file_explorer( ) }; + type KeyHandler = PickerKeyHandler<(PathBuf, bool)>; + let picker = Picker::new( columns, 0, @@ -689,11 +693,11 @@ pub fn file_explorer( .with_cursor(cursor.unwrap_or_default()) .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) .with_key_handlers(hashmap! { - alt!('n') => Box::new(create_file) as Box, - alt!('m') => Box::new(move_file) as Box, - alt!('d') => Box::new(delete_file) as Box, - alt!('c') => Box::new(copy_file) as Box, - alt!('y') => Box::new(copy_path) as Box + alt!('n') => Box::new(create_file) as KeyHandler, + alt!('m') => Box::new(move_file) as KeyHandler, + alt!('d') => Box::new(delete_file) as KeyHandler, + alt!('c') => Box::new(copy_file) as KeyHandler, + alt!('y') => Box::new(copy_path) as KeyHandler, }); Ok(picker) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 87937cc7b20a..639fb87320b3 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -259,7 +259,7 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, - custom_key_handlers: PickerKeyHandler, + custom_key_handlers: PickerKeyHandlers, pub truncate_start: bool, /// Caches paths to documents @@ -395,7 +395,7 @@ impl Picker { } } - pub fn with_key_handlers(mut self, handlers: PickerKeyHandler) -> Self { + pub fn with_key_handlers(mut self, handlers: PickerKeyHandlers) -> Self { self.custom_key_handlers = handlers; self } @@ -1185,4 +1185,5 @@ impl Drop for Picker { } type PickerCallback = Box; -pub type PickerKeyHandler = HashMap>; +pub type PickerKeyHandler = Box; +pub type PickerKeyHandlers = HashMap>; From ed570d9f451ae4b7c6a9e19746e2798f67ebb6da Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:02:38 +0000 Subject: [PATCH 34/52] fix: use MAIN_SEPARATOR instead of just unix separator --- helix-term/src/ui/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 93a734b22a82..af9268f5ab26 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -33,9 +33,9 @@ pub use text::Text; use helix_view::Editor; use tui::text::Span; -use std::fs; use std::path::Path; use std::{error::Error, path::PathBuf}; +use std::{fs, path}; use self::picker::PickerKeyHandler; @@ -414,7 +414,7 @@ pub fn file_explorer( }, )]; - let copy_path = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32| { + let yank_path = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32| { let register = cx .editor .selected_register @@ -642,7 +642,7 @@ pub fn file_explorer( .map(|p| p.to_path_buf()) .unwrap_or(helix_stdx::env::current_working_dir()); - if copy_from.is_dir() || copy_to_str.ends_with('/') { + if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library Some(Err(format!( "Copying directories is not supported: {} is a directory", @@ -697,7 +697,7 @@ pub fn file_explorer( alt!('m') => Box::new(move_file) as KeyHandler, alt!('d') => Box::new(delete_file) as KeyHandler, alt!('c') => Box::new(copy_file) as KeyHandler, - alt!('y') => Box::new(copy_path) as KeyHandler, + alt!('y') => Box::new(yank_path) as KeyHandler, }); Ok(picker) From eb35b604b51367793057874c4fb6583a849c792e Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:17:01 +0000 Subject: [PATCH 35/52] fix: remove previous pickers when refreshing the current one --- helix-term/src/ui/mod.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index af9268f5ab26..46c2e32c6774 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -33,9 +33,9 @@ pub use text::Text; use helix_view::Editor; use tui::text::Span; +use std::fs; use std::path::Path; use std::{error::Error, path::PathBuf}; -use std::{fs, path}; use self::picker::PickerKeyHandler; @@ -382,9 +382,17 @@ fn create_file_operation_prompt( cx.jobs.callback(callback); } -fn refresh_file_explorer(cursor: Option, cx: &mut Context, root: PathBuf) { +fn refresh_file_explorer( + remove_previous: bool, + cursor: Option, + cx: &mut Context, + root: PathBuf, +) { let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { + if remove_previous { + compositor.pop(); + } if let Ok(picker) = file_explorer(cursor, root, editor) { compositor.push(Box::new(overlay::overlaid(picker))); } @@ -454,7 +462,7 @@ pub fn file_explorer( }) { return Some(Err(err)); } - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); Some(Ok(format!("Created directory: {}", to_create.display()))) } else { @@ -463,7 +471,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); Some(Ok(format!("Created file: {}", to_create.display()))) } @@ -525,7 +533,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); None }; @@ -579,7 +587,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); Some(Ok(format!("Deleted directory: {}", to_delete.display()))) } else { @@ -588,7 +596,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); Some(Ok(format!("Deleted file: {}", to_delete.display()))) } @@ -628,7 +636,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(Some(cursor), cx, root); + refresh_file_explorer(true, Some(cursor), cx, root); Some(Ok(format!( "Copied contents of file {} to {}", @@ -679,7 +687,7 @@ pub fn file_explorer( move |cx, (path, is_dir): &(PathBuf, bool), action| { if *is_dir { let new_root = helix_stdx::path::normalize(path); - refresh_file_explorer(None, cx, new_root); + refresh_file_explorer(false, None, cx, new_root); } else if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) From 7fdf2ba92a387fb5ae2a43cb3ad8e695eb39c7a0 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:20:34 +0000 Subject: [PATCH 36/52] refactor: simplify function --- helix-term/src/ui/mod.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 46c2e32c6774..bd1bffdb2e77 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -382,18 +382,12 @@ fn create_file_operation_prompt( cx.jobs.callback(callback); } -fn refresh_file_explorer( - remove_previous: bool, - cursor: Option, - cx: &mut Context, - root: PathBuf, -) { +fn refresh_file_explorer(cursor: u32, cx: &mut Context, root: PathBuf) { let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { - if remove_previous { - compositor.pop(); - } - if let Ok(picker) = file_explorer(cursor, root, editor) { + // replace the old file explorer with the new one + compositor.pop(); + if let Ok(picker) = file_explorer(Some(cursor), root, editor) { compositor.push(Box::new(overlay::overlaid(picker))); } })); @@ -462,7 +456,7 @@ pub fn file_explorer( }) { return Some(Err(err)); } - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); Some(Ok(format!("Created directory: {}", to_create.display()))) } else { @@ -471,7 +465,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); Some(Ok(format!("Created file: {}", to_create.display()))) } @@ -533,7 +527,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); None }; @@ -587,7 +581,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); Some(Ok(format!("Deleted directory: {}", to_delete.display()))) } else { @@ -596,7 +590,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); Some(Ok(format!("Deleted file: {}", to_delete.display()))) } @@ -636,7 +630,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(true, Some(cursor), cx, root); + refresh_file_explorer(cursor, cx, root); Some(Ok(format!( "Copied contents of file {} to {}", @@ -687,7 +681,16 @@ pub fn file_explorer( move |cx, (path, is_dir): &(PathBuf, bool), action| { if *is_dir { let new_root = helix_stdx::path::normalize(path); - refresh_file_explorer(false, None, cx, new_root); + let callback = Box::pin(async move { + let call: Callback = + Callback::EditorCompositor(Box::new(move |editor, compositor| { + if let Ok(picker) = file_explorer(None, new_root, editor) { + compositor.push(Box::new(overlay::overlaid(picker))); + } + })); + Ok(call) + }); + cx.jobs.callback(callback); } else if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) From f193705ca78bfdf2221f56ad73cf1e2f4b0a79d2 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:38:56 +0000 Subject: [PATCH 37/52] feat: add main separator when showing current file's directory --- helix-term/src/ui/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bd1bffdb2e77..675c1c5b3280 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -439,7 +439,7 @@ pub fn file_explorer( path, |path| { path.parent() - .map(|p| p.display().to_string()) + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) .unwrap_or_default() }, |cursor, cx, path, to_create_str| { @@ -609,7 +609,7 @@ pub fn file_explorer( path, |path| { path.parent() - .map(|p| p.display().to_string()) + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) .unwrap_or_default() }, |cursor, cx, copy_from, copy_to_str| { From 67ca955baa5b716bdc05dac2d87ebd11e5244f8d Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:53:56 +0000 Subject: [PATCH 38/52] refactor: extract into a function --- helix-term/src/ui/mod.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 675c1c5b3280..bf9bbb0c6095 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -396,6 +396,16 @@ fn refresh_file_explorer(cursor: u32, cx: &mut Context, root: PathBuf) { cx.jobs.callback(callback); } +/// We don't have access to the file explorer's current directory directly, +/// but we can get it by taking any of the children of the explorer +/// and obtaining their parents. +fn root_from_child(child: &Path) -> PathBuf { + child + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or(helix_stdx::env::current_working_dir()) +} + pub fn file_explorer( cursor: Option, root: PathBuf, @@ -471,10 +481,7 @@ pub fn file_explorer( } }; - let root = path - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(helix_stdx::env::current_working_dir()); + let root = root_from_child(path); if to_create.exists() { create_confirmation_prompt( @@ -531,10 +538,7 @@ pub fn file_explorer( None }; - let root = move_from - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(helix_stdx::env::current_working_dir()); + let root = root_from_child(move_from); if move_to.exists() { create_confirmation_prompt( @@ -570,10 +574,7 @@ pub fn file_explorer( return Some(Err(format!("Path {} does not exist", to_delete.display()))); }; - let root = to_delete - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(helix_stdx::env::current_working_dir()); + let root = root_from_child(to_delete); if confirmation.ends_with(std::path::MAIN_SEPARATOR) { if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { @@ -639,10 +640,7 @@ pub fn file_explorer( ))) }; - let root = copy_to - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(helix_stdx::env::current_working_dir()); + let root = root_from_child(©_to); if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library From 984ad4bca9beffbfb6783a2316e4a405a8d4051f Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:49:00 +0000 Subject: [PATCH 39/52] refactor: pass EditorData to callbacks, do not "compute" the root --- helix-term/src/ui/mod.rs | 408 ++++++++++++++++++------------------ helix-term/src/ui/picker.rs | 10 +- 2 files changed, 209 insertions(+), 209 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bf9bbb0c6095..76ff90643452 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -35,6 +35,7 @@ use tui::text::Span; use std::fs; use std::path::Path; +use std::sync::Arc; use std::{error::Error, path::PathBuf}; use self::picker::PickerKeyHandler; @@ -283,7 +284,14 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi picker } -type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; +/// for each path: (path to item, is the path a directory?) +type ExplorerItem = (PathBuf, bool); +/// (file explorer root, directory style) +type ExplorerData = (PathBuf, Style); + +type FileExplorer = Picker; + +type KeyHandler = PickerKeyHandler; type OnConfirm = fn( cursor: u32, @@ -334,13 +342,14 @@ fn create_confirmation_prompt( cx.jobs.callback(callback); } -type FileOperation = fn(u32, &mut Context, &Path, &str) -> Option>; +type FileOperation = fn(PathBuf, u32, &mut Context, &Path, &str) -> Option>; fn create_file_operation_prompt( cursor: u32, prompt: &'static str, cx: &mut Context, path: &Path, + data: Arc, compute_initial_line: fn(&Path) -> String, file_op: FileOperation, ) { @@ -359,7 +368,7 @@ fn create_file_operation_prompt( let path = cx.editor.file_explorer_selected_path.clone(); if let Some(path) = path { - match file_op(cursor, cx, &path, input) { + match file_op(data.0.clone(), cursor, cx, &path, input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -396,16 +405,6 @@ fn refresh_file_explorer(cursor: u32, cx: &mut Context, root: PathBuf) { cx.jobs.callback(callback); } -/// We don't have access to the file explorer's current directory directly, -/// but we can get it by taking any of the children of the explorer -/// and obtaining their parents. -fn root_from_child(child: &Path) -> PathBuf { - child - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(helix_stdx::env::current_working_dir()) -} - pub fn file_explorer( cursor: Option, root: PathBuf, @@ -426,156 +425,159 @@ pub fn file_explorer( }, )]; - let yank_path = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), _cursor: u32| { - let register = cx - .editor - .selected_register - .unwrap_or(cx.editor.config().default_yank_register); - let path = helix_stdx::path::get_relative_path(path); - let path = path.to_string_lossy().to_string(); - let message = format!("Yanked path {} to register {register}", path); - - match cx.editor.registers.write(register, vec![path]) { - Ok(()) => cx.editor.set_status(message), - Err(err) => cx.editor.set_error(err.to_string()), + let yank_path = + |cx: &mut Context, (path, _is_dir): &ExplorerItem, _: Arc, _cursor: u32| { + let register = cx + .editor + .selected_register + .unwrap_or(cx.editor.config().default_yank_register); + let path = helix_stdx::path::get_relative_path(path); + let path = path.to_string_lossy().to_string(); + let message = format!("Yanked path {} to register {register}", path); + + match cx.editor.registers.write(register, vec![path]) { + Ok(()) => cx.editor.set_status(message), + Err(err) => cx.editor.set_error(err.to_string()), + }; }; - }; - let create_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { - create_file_operation_prompt( - cursor, - "create:", - cx, - path, - |path| { - path.parent() - .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) - .unwrap_or_default() - }, - |cursor, cx, path, to_create_str| { - let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - - let create_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - to_create_str: &str, - to_create: &Path| { - if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { - format!("Unable to create directory {}: {err}", to_create.display()) - }) { - return Some(Err(err)); + let create_file = + |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { + create_file_operation_prompt( + cursor, + "create:", + cx, + path, + data, + |path| { + path.parent() + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) + .unwrap_or_default() + }, + |root, cursor, cx, _, to_create_str| { + let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); + + let create_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + to_create_str: &str, + to_create: &Path| { + if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { + format!("Unable to create directory {}: {err}", to_create.display()) + }) { + return Some(Err(err)); + } + refresh_file_explorer(cursor, cx, root); + + Some(Ok(format!("Created directory: {}", to_create.display()))) + } else { + if let Err(err) = fs::File::create(to_create).map_err(|err| { + format!("Unable to create file {}: {err}", to_create.display()) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); + + Some(Ok(format!("Created file: {}", to_create.display()))) } - refresh_file_explorer(cursor, cx, root); + }; - Some(Ok(format!("Created directory: {}", to_create.display()))) - } else { - if let Err(err) = fs::File::create(to_create).map_err(|err| { - format!("Unable to create file {}: {err}", to_create.display()) + if to_create.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + to_create.display() + ), + cx, + to_create_str.to_string(), + to_create.to_path_buf(), + root, + create_op, + ); + return None; + }; + + create_op(cursor, cx, root, to_create_str, &to_create) + }, + ) + }; + + let move_file = + |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { + create_file_operation_prompt( + cursor, + "move:", + cx, + path, + data, + |path| path.display().to_string(), + |root, cursor, cx, move_from, move_to_str| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + + let move_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + move_to_str: &str, + move_from: &Path| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { + format!( + "Unable to move {} {} -> {}: {err}", + if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { + "directory" + } else { + "file" + }, + move_from.display(), + move_to.display() + ) }) { return Some(Err(err)); }; refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!("Created file: {}", to_create.display()))) - } - }; - - let root = root_from_child(path); - - if to_create.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - to_create.display() - ), - cx, - to_create_str.to_string(), - to_create.to_path_buf(), - root, - create_op, - ); - return None; - }; - - create_op(cursor, cx, root, to_create_str, &to_create) - }, - ) - }; - - let move_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { - create_file_operation_prompt( - cursor, - "move:", - cx, - path, - |path| path.display().to_string(), - |cursor, cx, move_from, move_to_str| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - - let move_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - move_to_str: &str, - move_from: &Path| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { - format!( - "Unable to move {} {} -> {}: {err}", - if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { - "directory" - } else { - "file" - }, - move_from.display(), - move_to.display() - ) - }) { - return Some(Err(err)); + None }; - refresh_file_explorer(cursor, cx, root); - None - }; - - let root = root_from_child(move_from); - if move_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - move_to.display() - ), - cx, - move_to_str.to_string(), - move_from.to_path_buf(), - root, - move_op, - ); - return None; - }; + if move_to.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + move_to.display() + ), + cx, + move_to_str.to_string(), + move_from.to_path_buf(), + root, + move_op, + ); + return None; + }; - move_op(cursor, cx, root, move_to_str, move_from) - }, - ) - }; + move_op(cursor, cx, root, move_to_str, move_from) + }, + ) + }; - let delete_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { + let delete_file = |cx: &mut Context, + (path, _is_dir): &ExplorerItem, + data: Arc, + cursor: u32| { create_file_operation_prompt( cursor, "delete? (y/n):", cx, path, + data, |_| "".to_string(), - |cursor, cx, to_delete, confirmation| { + |root, cursor, cx, to_delete, confirmation| { if confirmation == "y" { if !to_delete.exists() { return Some(Err(format!("Path {} does not exist", to_delete.display()))); }; - let root = root_from_child(to_delete); - if confirmation.ends_with(std::path::MAIN_SEPARATOR) { if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { format!("Unable to delete directory {}: {err}", to_delete.display()) @@ -602,81 +604,79 @@ pub fn file_explorer( ) }; - let copy_file = |cx: &mut Context, (path, _is_dir): &(PathBuf, bool), cursor: u32| { - create_file_operation_prompt( - cursor, - "copy-to:", - cx, - path, - |path| { - path.parent() - .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) - .unwrap_or_default() - }, - |cursor, cx, copy_from, copy_to_str| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - - let copy_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - copy_to_str: &str, - copy_from: &Path| { + let copy_file = + |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { + create_file_operation_prompt( + cursor, + "copy-to:", + cx, + path, + data, + |path| { + path.parent() + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) + .unwrap_or_default() + }, + |root, cursor, cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { - format!( - "Unable to copy from file {} to {}: {err}", - copy_from.display(), - copy_to.display() - ) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!( - "Copied contents of file {} to {}", - copy_from.display(), - copy_to.display() - ))) - }; - let root = root_from_child(©_to); + let copy_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + copy_to_str: &str, + copy_from: &Path| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { + format!( + "Unable to copy from file {} to {}: {err}", + copy_from.display(), + copy_to.display() + ) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); - if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { - // TODO: support copying directories (recursively)?. This isn't built-in to the standard library - Some(Err(format!( - "Copying directories is not supported: {} is a directory", - copy_from.display() - ))) - } else if copy_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", + Some(Ok(format!( + "Copied contents of file {} to {}", + copy_from.display(), copy_to.display() - ), - cx, - copy_to_str.to_string(), - copy_from.to_path_buf(), - root, - copy_op, - ); - None - } else { - copy_op(cursor, cx, root, copy_to_str, copy_from) - } - }, - ) - }; + ))) + }; - type KeyHandler = PickerKeyHandler<(PathBuf, bool)>; + if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { + // TODO: support copying directories (recursively)?. This isn't built-in to the standard library + Some(Err(format!( + "Copying directories is not supported: {} is a directory", + copy_from.display() + ))) + } else if copy_to.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + copy_to.display() + ), + cx, + copy_to_str.to_string(), + copy_from.to_path_buf(), + root, + copy_op, + ); + None + } else { + copy_op(cursor, cx, root, copy_to_str, copy_from) + } + }, + ) + }; let picker = Picker::new( columns, 0, directory_content, (root, directory_style), - move |cx, (path, is_dir): &(PathBuf, bool), action| { + move |cx, (path, is_dir): &ExplorerItem, action| { if *is_dir { let new_root = helix_stdx::path::normalize(path); let callback = Box::pin(async move { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 639fb87320b3..42e7bd672721 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -259,7 +259,7 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, - custom_key_handlers: PickerKeyHandlers, + custom_key_handlers: PickerKeyHandlers, pub truncate_start: bool, /// Caches paths to documents @@ -395,7 +395,7 @@ impl Picker { } } - pub fn with_key_handlers(mut self, handlers: PickerKeyHandlers) -> Self { + pub fn with_key_handlers(mut self, handlers: PickerKeyHandlers) -> Self { self.custom_key_handlers = handlers; self } @@ -526,7 +526,7 @@ impl Picker { if let (Some(callback), Some(selected)) = (self.custom_key_handlers.get(event), self.selection()) { - callback(cx, selected, self.cursor); + callback(cx, selected, Arc::clone(&self.editor_data), self.cursor); EventResult::Consumed(None) } else { EventResult::Ignored(None) @@ -1185,5 +1185,5 @@ impl Drop for Picker { } type PickerCallback = Box; -pub type PickerKeyHandler = Box; -pub type PickerKeyHandlers = HashMap>; +pub type PickerKeyHandler = Box, u32) + 'static>; +pub type PickerKeyHandlers = HashMap>; From a6e110937b6e55ed5ec4506c341e8d38a12b89e8 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:57:45 +0000 Subject: [PATCH 40/52] refactor: do not explicitlys specify the types everywhere --- helix-term/src/ui/mod.rs | 385 +++++++++++++++++++-------------------- 1 file changed, 189 insertions(+), 196 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 76ff90643452..ce74570b0882 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -425,146 +425,140 @@ pub fn file_explorer( }, )]; - let yank_path = - |cx: &mut Context, (path, _is_dir): &ExplorerItem, _: Arc, _cursor: u32| { - let register = cx - .editor - .selected_register - .unwrap_or(cx.editor.config().default_yank_register); - let path = helix_stdx::path::get_relative_path(path); - let path = path.to_string_lossy().to_string(); - let message = format!("Yanked path {} to register {register}", path); - - match cx.editor.registers.write(register, vec![path]) { - Ok(()) => cx.editor.set_status(message), - Err(err) => cx.editor.set_error(err.to_string()), - }; + let yank_path: KeyHandler = Box::new(|cx, (path, _), _, _| { + let register = cx + .editor + .selected_register + .unwrap_or(cx.editor.config().default_yank_register); + let path = helix_stdx::path::get_relative_path(path); + let path = path.to_string_lossy().to_string(); + let message = format!("Yanked path {} to register {register}", path); + + match cx.editor.registers.write(register, vec![path]) { + Ok(()) => cx.editor.set_status(message), + Err(err) => cx.editor.set_error(err.to_string()), }; + }); - let create_file = - |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { - create_file_operation_prompt( - cursor, - "create:", - cx, - path, - data, - |path| { - path.parent() - .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) - .unwrap_or_default() - }, - |root, cursor, cx, _, to_create_str| { - let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - - let create_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - to_create_str: &str, - to_create: &Path| { - if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { - format!("Unable to create directory {}: {err}", to_create.display()) - }) { - return Some(Err(err)); - } - refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!("Created directory: {}", to_create.display()))) - } else { - if let Err(err) = fs::File::create(to_create).map_err(|err| { - format!("Unable to create file {}: {err}", to_create.display()) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!("Created file: {}", to_create.display()))) + let create_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + create_file_operation_prompt( + cursor, + "create:", + cx, + path, + data, + |path| { + path.parent() + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) + .unwrap_or_default() + }, + |root, cursor, cx, _, to_create_str| { + let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); + + let create_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + to_create_str: &str, + to_create: &Path| { + if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { + format!("Unable to create directory {}: {err}", to_create.display()) + }) { + return Some(Err(err)); } - }; - - if to_create.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - to_create.display() - ), - cx, - to_create_str.to_string(), - to_create.to_path_buf(), - root, - create_op, - ); - return None; - }; - - create_op(cursor, cx, root, to_create_str, &to_create) - }, - ) - }; - - let move_file = - |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { - create_file_operation_prompt( - cursor, - "move:", - cx, - path, - data, - |path| path.display().to_string(), - |root, cursor, cx, move_from, move_to_str| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + refresh_file_explorer(cursor, cx, root); - let move_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - move_to_str: &str, - move_from: &Path| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { - format!( - "Unable to move {} {} -> {}: {err}", - if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { - "directory" - } else { - "file" - }, - move_from.display(), - move_to.display() - ) + Some(Ok(format!("Created directory: {}", to_create.display()))) + } else { + if let Err(err) = fs::File::create(to_create).map_err(|err| { + format!("Unable to create file {}: {err}", to_create.display()) }) { return Some(Err(err)); }; refresh_file_explorer(cursor, cx, root); - None - }; - if move_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - move_to.display() - ), - cx, - move_to_str.to_string(), - move_from.to_path_buf(), - root, - move_op, - ); - return None; + Some(Ok(format!("Created file: {}", to_create.display()))) + } + }; + + if to_create.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + to_create.display() + ), + cx, + to_create_str.to_string(), + to_create.to_path_buf(), + root, + create_op, + ); + return None; + }; + + create_op(cursor, cx, root, to_create_str, &to_create) + }, + ) + }); + + let move_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + create_file_operation_prompt( + cursor, + "move:", + cx, + path, + data, + |path| path.display().to_string(), + |root, cursor, cx, move_from, move_to_str| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + + let move_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + move_to_str: &str, + move_from: &Path| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { + format!( + "Unable to move {} {} -> {}: {err}", + if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { + "directory" + } else { + "file" + }, + move_from.display(), + move_to.display() + ) + }) { + return Some(Err(err)); }; + refresh_file_explorer(cursor, cx, root); + None + }; - move_op(cursor, cx, root, move_to_str, move_from) - }, - ) - }; + if move_to.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + move_to.display() + ), + cx, + move_to_str.to_string(), + move_from.to_path_buf(), + root, + move_op, + ); + return None; + }; + + move_op(cursor, cx, root, move_to_str, move_from) + }, + ) + }); - let delete_file = |cx: &mut Context, - (path, _is_dir): &ExplorerItem, - data: Arc, - cursor: u32| { + let delete_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, "delete? (y/n):", @@ -602,74 +596,73 @@ pub fn file_explorer( } }, ) - }; - - let copy_file = - |cx: &mut Context, (path, _is_dir): &ExplorerItem, data: Arc, cursor: u32| { - create_file_operation_prompt( - cursor, - "copy-to:", - cx, - path, - data, - |path| { - path.parent() - .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) - .unwrap_or_default() - }, - |root, cursor, cx, copy_from, copy_to_str| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - - let copy_op = |cursor: u32, - cx: &mut Context, - root: PathBuf, - copy_to_str: &str, - copy_from: &Path| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { - format!( - "Unable to copy from file {} to {}: {err}", - copy_from.display(), - copy_to.display() - ) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); + }); - Some(Ok(format!( - "Copied contents of file {} to {}", + let copy_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + create_file_operation_prompt( + cursor, + "copy-to:", + cx, + path, + data, + |path| { + path.parent() + .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) + .unwrap_or_default() + }, + |root, cursor, cx, copy_from, copy_to_str| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + + let copy_op = |cursor: u32, + cx: &mut Context, + root: PathBuf, + copy_to_str: &str, + copy_from: &Path| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { + format!( + "Unable to copy from file {} to {}: {err}", copy_from.display(), copy_to.display() - ))) + ) + }) { + return Some(Err(err)); }; + refresh_file_explorer(cursor, cx, root); - if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { - // TODO: support copying directories (recursively)?. This isn't built-in to the standard library - Some(Err(format!( - "Copying directories is not supported: {} is a directory", - copy_from.display() - ))) - } else if copy_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - copy_to.display() - ), - cx, - copy_to_str.to_string(), - copy_from.to_path_buf(), - root, - copy_op, - ); - None - } else { - copy_op(cursor, cx, root, copy_to_str, copy_from) - } - }, - ) - }; + Some(Ok(format!( + "Copied contents of file {} to {}", + copy_from.display(), + copy_to.display() + ))) + }; + + if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { + // TODO: support copying directories (recursively)?. This isn't built-in to the standard library + Some(Err(format!( + "Copying directories is not supported: {} is a directory", + copy_from.display() + ))) + } else if copy_to.exists() { + create_confirmation_prompt( + cursor, + format!( + "Path {} already exists. Overwrite? (y/n):", + copy_to.display() + ), + cx, + copy_to_str.to_string(), + copy_from.to_path_buf(), + root, + copy_op, + ); + None + } else { + copy_op(cursor, cx, root, copy_to_str, copy_from) + } + }, + ) + }); let picker = Picker::new( columns, @@ -702,11 +695,11 @@ pub fn file_explorer( .with_cursor(cursor.unwrap_or_default()) .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) .with_key_handlers(hashmap! { - alt!('n') => Box::new(create_file) as KeyHandler, - alt!('m') => Box::new(move_file) as KeyHandler, - alt!('d') => Box::new(delete_file) as KeyHandler, - alt!('c') => Box::new(copy_file) as KeyHandler, - alt!('y') => Box::new(yank_path) as KeyHandler, + alt!('n') => create_file, + alt!('m') => move_file, + alt!('d') => delete_file, + alt!('c') => copy_file, + alt!('y') => yank_path, }); Ok(picker) From fae93aa30825092f9b8513a9139c9f91cff3bbb8 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:02:10 +0000 Subject: [PATCH 41/52] refactor: move statement elsewhere --- helix-term/src/ui/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ce74570b0882..0761034cf68e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -413,18 +413,6 @@ pub fn file_explorer( let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; - let columns = [PickerColumn::new( - "path", - |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { - let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); - if *is_dir { - Span::styled(format!("{}/", name), *directory_style).into() - } else { - name.into() - } - }, - )]; - let yank_path: KeyHandler = Box::new(|cx, (path, _), _, _| { let register = cx .editor @@ -664,6 +652,18 @@ pub fn file_explorer( ) }); + let columns = [PickerColumn::new( + "path", + |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { + let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); + if *is_dir { + Span::styled(format!("{}/", name), *directory_style).into() + } else { + name.into() + } + }, + )]; + let picker = Picker::new( columns, 0, From 488e9552fd10ee56cc5bd0e19aa3d4d2579a0393 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:02:41 +0000 Subject: [PATCH 42/52] refactor: use type aliases instead of fully writing out the type --- helix-term/src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0761034cf68e..d4fe83775fc9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -654,7 +654,7 @@ pub fn file_explorer( let columns = [PickerColumn::new( "path", - |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { + |(path, is_dir): &ExplorerItem, (root, directory_style): &ExplorerData| { let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); if *is_dir { Span::styled(format!("{}/", name), *directory_style).into() From d55b8f324275c7015dd9a71df216f53697da933b Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:07:09 +0000 Subject: [PATCH 43/52] feat: add file explorer keymappings to docs --- book/src/keymap.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 07971ec64980..30b6b36bd3b3 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -285,6 +285,8 @@ This layer is a kludge of mappings, mostly pickers. | ----- | ----------- | ------- | | `f` | Open file picker at LSP workspace root | `file_picker` | | `F` | Open file picker at current working directory | `file_picker_in_current_directory` | +| `e` | Open file explorer at LSP workspace root | `file_explorer` | +| `E` | Open file explorer at the opened file's directory | `file_explorer_in_current_buffer_directory`| | `b` | Open buffer picker | `buffer_picker` | | `j` | Open jumplist picker | `jumplist_picker` | | `g` | Open changed file picker | `changed_file_picker` | @@ -465,11 +467,11 @@ See the documentation page on [pickers](./pickers.md) for more info. ### File Explorer -There are additional keys accessible when using the File Explorer. +There are additional keys accessible when using the File Explorer (`Space-e` and `Space-E`). | Key | Description | | ----- | ------------- | -| `Alt-m` | Move (& rename) selected file or directory | +| `Alt-m` | Move selected file or directory | | `Alt-n` | Create a new file or directory | | `Alt-d` | Delete the selected file or directory | | `Alt-c` | Copy the selected file | From 7a52c3f0bf2793da9fb7cc1397b2b8820d37ccd7 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:24:59 +0000 Subject: [PATCH 44/52] fix: delete directories --- helix-term/src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d4fe83775fc9..0ce9e9e5bc4b 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -560,7 +560,7 @@ pub fn file_explorer( return Some(Err(format!("Path {} does not exist", to_delete.display()))); }; - if confirmation.ends_with(std::path::MAIN_SEPARATOR) { + if to_delete.is_dir() { if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { format!("Unable to delete directory {}: {err}", to_delete.display()) }) { From 59a1d244aa42d87f3869dccc93ee7a3e7d39f300 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:12:21 +0000 Subject: [PATCH 45/52] refactor: rename vars --- helix-term/src/ui/mod.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0ce9e9e5bc4b..35bbda23b51f 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -428,7 +428,7 @@ pub fn file_explorer( }; }); - let create_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, "create:", @@ -443,7 +443,7 @@ pub fn file_explorer( |root, cursor, cx, _, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - let create_op = |cursor: u32, + let do_create = |cursor: u32, cx: &mut Context, root: PathBuf, to_create_str: &str, @@ -480,17 +480,17 @@ pub fn file_explorer( to_create_str.to_string(), to_create.to_path_buf(), root, - create_op, + do_create, ); return None; }; - create_op(cursor, cx, root, to_create_str, &to_create) + do_create(cursor, cx, root, to_create_str, &to_create) }, ) }); - let move_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + let move_: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, "move:", @@ -501,7 +501,7 @@ pub fn file_explorer( |root, cursor, cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - let move_op = |cursor: u32, + let do_move = |cursor: u32, cx: &mut Context, root: PathBuf, move_to_str: &str, @@ -536,17 +536,17 @@ pub fn file_explorer( move_to_str.to_string(), move_from.to_path_buf(), root, - move_op, + do_move, ); return None; }; - move_op(cursor, cx, root, move_to_str, move_from) + do_move(cursor, cx, root, move_to_str, move_from) }, ) }); - let delete_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + let delete: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, "delete? (y/n):", @@ -586,7 +586,7 @@ pub fn file_explorer( ) }); - let copy_file: KeyHandler = Box::new(|cx, (path, _), data, cursor| { + let copy: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, "copy-to:", @@ -601,7 +601,7 @@ pub fn file_explorer( |root, cursor, cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - let copy_op = |cursor: u32, + let do_copy = |cursor: u32, cx: &mut Context, root: PathBuf, copy_to_str: &str, @@ -642,11 +642,11 @@ pub fn file_explorer( copy_to_str.to_string(), copy_from.to_path_buf(), root, - copy_op, + do_copy, ); None } else { - copy_op(cursor, cx, root, copy_to_str, copy_from) + do_copy(cursor, cx, root, copy_to_str, copy_from) } }, ) @@ -695,10 +695,10 @@ pub fn file_explorer( .with_cursor(cursor.unwrap_or_default()) .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))) .with_key_handlers(hashmap! { - alt!('n') => create_file, - alt!('m') => move_file, - alt!('d') => delete_file, - alt!('c') => copy_file, + alt!('n') => create, + alt!('m') => move_, + alt!('d') => delete, + alt!('c') => copy, alt!('y') => yank_path, }); From 674afbfd89885efd1e6719e1363473362757f91f Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:31:35 +0000 Subject: [PATCH 46/52] refactor: inversion of control --- helix-term/src/ui/mod.rs | 80 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 35bbda23b51f..32ef00f035da 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -456,17 +456,17 @@ pub fn file_explorer( } refresh_file_explorer(cursor, cx, root); - Some(Ok(format!("Created directory: {}", to_create.display()))) - } else { - if let Err(err) = fs::File::create(to_create).map_err(|err| { - format!("Unable to create file {}: {err}", to_create.display()) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!("Created file: {}", to_create.display()))) + return Some(Ok(format!("Created directory: {}", to_create.display()))); } + + if let Err(err) = fs::File::create(to_create).map_err(|err| { + format!("Unable to create file {}: {err}", to_create.display()) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); + + Some(Ok(format!("Created file: {}", to_create.display()))) }; if to_create.exists() { @@ -555,33 +555,33 @@ pub fn file_explorer( data, |_| "".to_string(), |root, cursor, cx, to_delete, confirmation| { - if confirmation == "y" { - if !to_delete.exists() { - return Some(Err(format!("Path {} does not exist", to_delete.display()))); - }; + if confirmation != "y" { + return None; + } - if to_delete.is_dir() { - if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { - format!("Unable to delete directory {}: {err}", to_delete.display()) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); + if !to_delete.exists() { + return Some(Err(format!("Path {} does not exist", to_delete.display()))); + }; - Some(Ok(format!("Deleted directory: {}", to_delete.display()))) - } else { - if let Err(err) = fs::remove_file(to_delete).map_err(|err| { - format!("Unable to delete file {}: {err}", to_delete.display()) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); + if to_delete.is_dir() { + if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| { + format!("Unable to delete directory {}: {err}", to_delete.display()) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); - Some(Ok(format!("Deleted file: {}", to_delete.display()))) - } - } else { - None + return Some(Ok(format!("Deleted directory: {}", to_delete.display()))); } + + if let Err(err) = fs::remove_file(to_delete) + .map_err(|err| format!("Unable to delete file {}: {err}", to_delete.display())) + { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); + + Some(Ok(format!("Deleted file: {}", to_delete.display()))) }, ) }); @@ -627,11 +627,13 @@ pub fn file_explorer( if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library - Some(Err(format!( + return Some(Err(format!( "Copying directories is not supported: {} is a directory", copy_from.display() - ))) - } else if copy_to.exists() { + ))); + } + + if copy_to.exists() { create_confirmation_prompt( cursor, format!( @@ -644,10 +646,10 @@ pub fn file_explorer( root, do_copy, ); - None - } else { - do_copy(cursor, cx, root, copy_to_str, copy_from) + return None; } + + do_copy(cursor, cx, root, copy_to_str, copy_from) }, ) }); From 4133e14a5e23311947ecc00be373ed81cce745e9 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Fri, 28 Feb 2025 14:33:28 +0000 Subject: [PATCH 47/52] feat: notify LSPs when moving path in File Explorer --- helix-term/src/ui/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 32ef00f035da..81392991e5d6 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -507,7 +507,8 @@ pub fn file_explorer( move_to_str: &str, move_from: &Path| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - if let Err(err) = fs::rename(move_from, &move_to).map_err(|err| { + + if let Err(err) = cx.editor.move_path(move_from, &move_to).map_err(|err| { format!( "Unable to move {} {} -> {}: {err}", if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { From 42d74bb9faf94ede0af6bedc53abf695b6f00605 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:19:02 +0000 Subject: [PATCH 48/52] feat: tell user which path they are operating on --- helix-term/src/ui/mod.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 81392991e5d6..05367dfb4e72 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -346,7 +346,7 @@ type FileOperation = fn(PathBuf, u32, &mut Context, &Path, &str) -> Option String, cx: &mut Context, path: &Path, data: Arc, @@ -357,7 +357,12 @@ fn create_file_operation_prompt( let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { let mut prompt = Prompt::new( - prompt.into(), + editor + .file_explorer_selected_path + .as_ref() + .map(|p| prompt(p)) + .unwrap_or_default() + .into(), None, crate::ui::completers::none, move |cx, input: &str, event: PromptEvent| { @@ -431,7 +436,7 @@ pub fn file_explorer( let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - "create:", + |_| "create:".into(), cx, path, data, @@ -493,7 +498,7 @@ pub fn file_explorer( let move_: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - "move:", + |path| format!("Move {} to:", path.display()), cx, path, data, @@ -550,7 +555,7 @@ pub fn file_explorer( let delete: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - "delete? (y/n):", + |path| format!("Delete {}? (y/n):", path.display()), cx, path, data, @@ -590,7 +595,7 @@ pub fn file_explorer( let copy: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - "copy-to:", + |path| format!("Copy {} to:", path.display()), cx, path, data, From e7d7f93d6310e0346adae03697b9fe89d71c10c5 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:19:23 +0000 Subject: [PATCH 49/52] feat: clear status message when receiving no message or error from performing file operation --- helix-term/src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 05367dfb4e72..9e3d8d517beb 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -376,7 +376,7 @@ fn create_file_operation_prompt( match file_op(data.0.clone(), cursor, cx, &path, input) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), - None => (), + None => cx.editor.clear_status(), }; } else { cx.editor From 27ad3f281c1e4bf5d5ee4f28a0ee917b932acc30 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:20:58 +0000 Subject: [PATCH 50/52] feat: use arrow -> for Copy and Move instead of colon --- helix-term/src/ui/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 9e3d8d517beb..45f26d33faae 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -436,7 +436,7 @@ pub fn file_explorer( let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - |_| "create:".into(), + |_| "Create:".into(), cx, path, data, @@ -498,7 +498,7 @@ pub fn file_explorer( let move_: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - |path| format!("Move {} to:", path.display()), + |path| format!("Move {} -> ", path.display()), cx, path, data, @@ -595,7 +595,7 @@ pub fn file_explorer( let copy: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - |path| format!("Copy {} to:", path.display()), + |path| format!("Copy {} -> ", path.display()), cx, path, data, From 386c4220fddbe1c22a3b9c5a9c4ef407083858a8 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:49:14 +0000 Subject: [PATCH 51/52] feat: create parent directories of file if they do not exist --- helix-term/src/ui/mod.rs | 262 ++++++++++++++++++++------------------- 1 file changed, 134 insertions(+), 128 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 45f26d33faae..3ba4804c6efb 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -293,7 +293,7 @@ type FileExplorer = Picker; type KeyHandler = PickerKeyHandler; -type OnConfirm = fn( +type OverwritePath = fn( cursor: u32, cx: &mut Context, picker_root: PathBuf, @@ -301,19 +301,33 @@ type OnConfirm = fn( &Path, ) -> Option>; -fn create_confirmation_prompt( +fn confirm_before_overwriting( + overwriting: PathBuf, cursor: u32, - input: String, cx: &mut Context, operation_input_str: String, operation_input: PathBuf, picker_root: PathBuf, - on_confirm: OnConfirm, -) { + overwrite: OverwritePath, +) -> Option> { + // No need for confirmation, as the path does not exist. We can freely write to it + if !overwriting.exists() { + return overwrite( + cursor, + cx, + picker_root, + &operation_input_str, + &operation_input, + ); + } let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { let prompt = Prompt::new( - input.into(), + format!( + "Path {} already exists. Ovewrite? (y/n):", + overwriting.display() + ) + .into(), None, crate::ui::completers::none, move |cx, input: &str, event: PromptEvent| { @@ -321,7 +335,7 @@ fn create_confirmation_prompt( return; }; - match on_confirm( + match overwrite( cursor, cx, picker_root.clone(), @@ -340,6 +354,8 @@ fn create_confirmation_prompt( Ok(call) }); cx.jobs.callback(callback); + + None } type FileOperation = fn(PathBuf, u32, &mut Context, &Path, &str) -> Option>; @@ -436,7 +452,7 @@ pub fn file_explorer( let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - |_| "Create:".into(), + |_| "Create: ".into(), cx, path, data, @@ -448,49 +464,59 @@ pub fn file_explorer( |root, cursor, cx, _, to_create_str| { let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); - let do_create = |cursor: u32, - cx: &mut Context, - root: PathBuf, - to_create_str: &str, - to_create: &Path| { - if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { - if let Err(err) = fs::create_dir_all(to_create).map_err(|err| { - format!("Unable to create directory {}: {err}", to_create.display()) - }) { - return Some(Err(err)); + confirm_before_overwriting( + to_create.to_path_buf(), + cursor, + cx, + to_create_str.to_string(), + to_create.to_path_buf(), + root, + |cursor: u32, + cx: &mut Context, + root: PathBuf, + to_create_str: &str, + to_create: &Path| { + if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + if let Err(err_create_dir) = + fs::create_dir_all(to_create).map_err(|err| { + format!( + "Unable to create directory {}: {err}", + to_create.display() + ) + }) + { + return Some(Err(err_create_dir)); + } + refresh_file_explorer(cursor, cx, root); + + return Some(Ok(format!("Created directory: {}", to_create.display()))); } - refresh_file_explorer(cursor, cx, root); - return Some(Ok(format!("Created directory: {}", to_create.display()))); - } + // allows to create a path like /path/to/somewhere.txt even if "to" does not exist. Creates intermediate directories + let Some(to_create_parent) = to_create.parent() else { + return Some(Err(format!( + "Failed to get parent directory of {}", + to_create.display() + ))); + }; - if let Err(err) = fs::File::create(to_create).map_err(|err| { - format!("Unable to create file {}: {err}", to_create.display()) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); + if let Err(err_create_parent) = fs::create_dir_all(to_create_parent) { + return Some(Err(format!( + "Could not create intermediate directories: {err_create_parent}" + ))); + } - Some(Ok(format!("Created file: {}", to_create.display()))) - }; + if let Err(err_create_file) = fs::File::create(to_create).map_err(|err| { + format!("Unable to create file {}: {err}", to_create.display()) + }) { + return Some(Err(err_create_file)); + }; - if to_create.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - to_create.display() - ), - cx, - to_create_str.to_string(), - to_create.to_path_buf(), - root, - do_create, - ); - return None; - }; + refresh_file_explorer(cursor, cx, root); - do_create(cursor, cx, root, to_create_str, &to_create) + Some(Ok(format!("Created file: {}", to_create.display()))) + }, + ) }, ) }); @@ -506,48 +532,38 @@ pub fn file_explorer( |root, cursor, cx, move_from, move_to_str| { let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - let do_move = |cursor: u32, - cx: &mut Context, - root: PathBuf, - move_to_str: &str, - move_from: &Path| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); - - if let Err(err) = cx.editor.move_path(move_from, &move_to).map_err(|err| { - format!( - "Unable to move {} {} -> {}: {err}", - if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { - "directory" - } else { - "file" - }, - move_from.display(), - move_to.display() - ) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); - None - }; - - if move_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - move_to.display() - ), - cx, - move_to_str.to_string(), - move_from.to_path_buf(), - root, - do_move, - ); - return None; - }; - - do_move(cursor, cx, root, move_to_str, move_from) + confirm_before_overwriting( + move_to.to_path_buf(), + cursor, + cx, + move_to_str.to_string(), + move_from.to_path_buf(), + root, + |cursor: u32, + cx: &mut Context, + root: PathBuf, + move_to_str: &str, + move_from: &Path| { + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + + if let Err(err) = cx.editor.move_path(move_from, &move_to).map_err(|err| { + format!( + "Unable to move {} {} -> {}: {err}", + if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { + "directory" + } else { + "file" + }, + move_from.display(), + move_to.display() + ) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); + None + }, + ) }, ) }); @@ -555,7 +571,7 @@ pub fn file_explorer( let delete: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( cursor, - |path| format!("Delete {}? (y/n):", path.display()), + |path| format!("Delete {}? (y/n): ", path.display()), cx, path, data, @@ -607,30 +623,6 @@ pub fn file_explorer( |root, cursor, cx, copy_from, copy_to_str| { let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - let do_copy = |cursor: u32, - cx: &mut Context, - root: PathBuf, - copy_to_str: &str, - copy_from: &Path| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); - if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { - format!( - "Unable to copy from file {} to {}: {err}", - copy_from.display(), - copy_to.display() - ) - }) { - return Some(Err(err)); - }; - refresh_file_explorer(cursor, cx, root); - - Some(Ok(format!( - "Copied contents of file {} to {}", - copy_from.display(), - copy_to.display() - ))) - }; - if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library return Some(Err(format!( @@ -639,23 +631,37 @@ pub fn file_explorer( ))); } - if copy_to.exists() { - create_confirmation_prompt( - cursor, - format!( - "Path {} already exists. Overwrite? (y/n):", - copy_to.display() - ), - cx, - copy_to_str.to_string(), - copy_from.to_path_buf(), - root, - do_copy, - ); - return None; - } + confirm_before_overwriting( + copy_to.to_path_buf(), + cursor, + cx, + copy_to_str.to_string(), + copy_from.to_path_buf(), + root, + |cursor: u32, + cx: &mut Context, + root: PathBuf, + copy_to_str: &str, + copy_from: &Path| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { + format!( + "Unable to copy from file {} to {}: {err}", + copy_from.display(), + copy_to.display() + ) + }) { + return Some(Err(err)); + }; + refresh_file_explorer(cursor, cx, root); - do_copy(cursor, cx, root, copy_to_str, copy_from) + Some(Ok(format!( + "Copied contents of file {} to {}", + copy_from.display(), + copy_to.display() + ))) + }, + ) }, ) }); From 7b89fb4699900e97c33f84cb52e48245cfff7d48 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:22:40 +0000 Subject: [PATCH 52/52] refactor: remove extra passage of arguments (unnecessary) --- helix-term/src/ui/mod.rs | 144 +++++++++++++++------------------------ 1 file changed, 55 insertions(+), 89 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3ba4804c6efb..821018836621 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -35,7 +35,6 @@ use tui::text::Span; use std::fs; use std::path::Path; -use std::sync::Arc; use std::{error::Error, path::PathBuf}; use self::picker::PickerKeyHandler; @@ -293,32 +292,22 @@ type FileExplorer = Picker; type KeyHandler = PickerKeyHandler; -type OverwritePath = fn( - cursor: u32, - cx: &mut Context, - picker_root: PathBuf, - &str, - &Path, -) -> Option>; - -fn confirm_before_overwriting( +/// Create a prompt that asks for the user's confirmation before overwriting a path +fn confirm_before_overwriting( + // Path that we are overwriting overwriting: PathBuf, - cursor: u32, + // Overwrite this path with + overwrite_with: PathBuf, cx: &mut Context, - operation_input_str: String, - operation_input: PathBuf, picker_root: PathBuf, - overwrite: OverwritePath, -) -> Option> { + overwrite: F, +) -> Option> +where + F: Fn(&mut Context, PathBuf, &Path) -> Option> + Send + 'static, +{ // No need for confirmation, as the path does not exist. We can freely write to it if !overwriting.exists() { - return overwrite( - cursor, - cx, - picker_root, - &operation_input_str, - &operation_input, - ); + return overwrite(cx, picker_root, &overwrite_with); } let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { @@ -335,13 +324,7 @@ fn confirm_before_overwriting( return; }; - match overwrite( - cursor, - cx, - picker_root.clone(), - &operation_input_str, - &operation_input, - ) { + match overwrite(cx, picker_root.clone(), &overwrite_with) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => (), @@ -358,17 +341,19 @@ fn confirm_before_overwriting( None } -type FileOperation = fn(PathBuf, u32, &mut Context, &Path, &str) -> Option>; - -fn create_file_operation_prompt( - cursor: u32, - prompt: fn(&Path) -> String, +fn create_file_operation_prompt( cx: &mut Context, + // Currently selected path of the picker path: &Path, - data: Arc, - compute_initial_line: fn(&Path) -> String, - file_op: FileOperation, -) { + // Text value of the prompt + prompt: fn(&Path) -> String, + // What to fill user's input with + prefill: fn(&Path) -> String, + // Action to take when the operation runs + file_op: F, +) where + F: Fn(&mut Context, &PathBuf, String) -> Option> + Send + 'static, +{ cx.editor.file_explorer_selected_path = Some(path.to_path_buf()); let callback = Box::pin(async move { let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { @@ -389,7 +374,7 @@ fn create_file_operation_prompt( let path = cx.editor.file_explorer_selected_path.clone(); if let Some(path) = path { - match file_op(data.0.clone(), cursor, cx, &path, input) { + match file_op(cx, &path, input.to_owned()) { Some(Ok(msg)) => cx.editor.set_status(msg), Some(Err(msg)) => cx.editor.set_error(msg), None => cx.editor.clear_status(), @@ -402,7 +387,7 @@ fn create_file_operation_prompt( ); if let Some(path_editing) = &editor.file_explorer_selected_path { - prompt.set_line_no_recalculate(compute_initial_line(path_editing)); + prompt.set_line_no_recalculate(prefill(path_editing)); } compositor.push(Box::new(prompt)); @@ -451,32 +436,25 @@ pub fn file_explorer( let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( - cursor, - |_| "Create: ".into(), cx, path, - data, + |_| "Create: ".into(), |path| { path.parent() .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) .unwrap_or_default() }, - |root, cursor, cx, _, to_create_str| { - let to_create = helix_stdx::path::expand_tilde(PathBuf::from(to_create_str)); + move |cx, _, to_create_string| { + let root = data.0.clone(); + let to_create = helix_stdx::path::expand_tilde(PathBuf::from(&to_create_string)); confirm_before_overwriting( to_create.to_path_buf(), - cursor, - cx, - to_create_str.to_string(), to_create.to_path_buf(), + cx, root, - |cursor: u32, - cx: &mut Context, - root: PathBuf, - to_create_str: &str, - to_create: &Path| { - if to_create_str.ends_with(std::path::MAIN_SEPARATOR) { + move |cx: &mut Context, root: PathBuf, to_create: &Path| { + if to_create_string.ends_with(std::path::MAIN_SEPARATOR) { if let Err(err_create_dir) = fs::create_dir_all(to_create).map_err(|err| { format!( @@ -523,33 +501,27 @@ pub fn file_explorer( let move_: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( - cursor, - |path| format!("Move {} -> ", path.display()), cx, path, - data, + |path| format!("Move {} -> ", path.display()), |path| path.display().to_string(), - |root, cursor, cx, move_from, move_to_str| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + move |cx, move_from, move_to_string| { + let root = data.0.clone(); + let move_to = helix_stdx::path::expand_tilde(PathBuf::from(&move_to_string)); confirm_before_overwriting( move_to.to_path_buf(), - cursor, - cx, - move_to_str.to_string(), move_from.to_path_buf(), + cx, root, - |cursor: u32, - cx: &mut Context, - root: PathBuf, - move_to_str: &str, - move_from: &Path| { - let move_to = helix_stdx::path::expand_tilde(PathBuf::from(move_to_str)); + move |cx: &mut Context, root: PathBuf, move_from: &Path| { + let move_to = + helix_stdx::path::expand_tilde(PathBuf::from(&move_to_string)); if let Err(err) = cx.editor.move_path(move_from, &move_to).map_err(|err| { format!( "Unable to move {} {} -> {}: {err}", - if move_to_str.ends_with(std::path::MAIN_SEPARATOR) { + if move_to_string.ends_with(std::path::MAIN_SEPARATOR) { "directory" } else { "file" @@ -570,13 +542,12 @@ pub fn file_explorer( let delete: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( - cursor, - |path| format!("Delete {}? (y/n): ", path.display()), cx, path, - data, + |path| format!("Delete {}? (y/n): ", path.display()), |_| "".to_string(), - |root, cursor, cx, to_delete, confirmation| { + move |cx, to_delete, confirmation| { + let root = data.0.clone(); if confirmation != "y" { return None; } @@ -610,20 +581,19 @@ pub fn file_explorer( let copy: KeyHandler = Box::new(|cx, (path, _), data, cursor| { create_file_operation_prompt( - cursor, - |path| format!("Copy {} -> ", path.display()), cx, path, - data, + |path| format!("Copy {} -> ", path.display()), |path| { path.parent() .map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR)) .unwrap_or_default() }, - |root, cursor, cx, copy_from, copy_to_str| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + move |cx, copy_from, copy_to_string| { + let root = data.0.clone(); + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(©_to_string)); - if copy_from.is_dir() || copy_to_str.ends_with(std::path::MAIN_SEPARATOR) { + if copy_from.is_dir() || copy_to_string.ends_with(std::path::MAIN_SEPARATOR) { // TODO: support copying directories (recursively)?. This isn't built-in to the standard library return Some(Err(format!( "Copying directories is not supported: {} is a directory", @@ -631,19 +601,15 @@ pub fn file_explorer( ))); } + let copy_to_str = copy_to_string.to_string(); + confirm_before_overwriting( copy_to.to_path_buf(), - cursor, - cx, - copy_to_str.to_string(), copy_from.to_path_buf(), + cx, root, - |cursor: u32, - cx: &mut Context, - root: PathBuf, - copy_to_str: &str, - copy_from: &Path| { - let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(copy_to_str)); + move |cx: &mut Context, picker_root: PathBuf, copy_from: &Path| { + let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(©_to_str)); if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| { format!( "Unable to copy from file {} to {}: {err}", @@ -653,7 +619,7 @@ pub fn file_explorer( }) { return Some(Err(err)); }; - refresh_file_explorer(cursor, cx, root); + refresh_file_explorer(cursor, cx, picker_root); Some(Ok(format!( "Copied contents of file {} to {}",