Repository: lapce/lapce (language: rust)

Issue:
No menu in the file explorer.
## Lapce Version
0.3.1

## System information

Windows 10 

## Describe the bug

There is no menu opened with a way to make new files, if you right click in the File Explorer

## Additional information

I did not even know there was a menu, until I searched if someone requested such a menu as a feature. And saw a different bug about the menu. 

This makes this editor unusable for me. 


Discussion / hints:
You found a way to navigate on the File Explorer and create file? This editor is literally unusable without that, no one answer and they don't update anything...
Nope. 

Even looked at the yesterdays nightly, in case something changed. 
Some menu items got added, but create file item and other items don't show. 
(On v.3.1 those didn't show either.) 

![2024-02-09 09_25_29-hello_world - Lapce](https://github.com/lapce/lapce/assets/4290287/99b44e91-4520-48eb-971b-8d1ed3845b6b)


Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f3e96e6c5..09f2458ace 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - [#2910](https://github.com/lapce/lapce/pull/2910): Files can be compared in the diff editor
 - [#2918](https://github.com/lapce/lapce/pull/2918): Allow searching the shortcuts overview by shortcut (e.g. "Ctrl+P") or when condition (e.g. "list_focus")
 - [#2955](https://github.com/lapce/lapce/pull/2955): Using glob patterns to hide files or directories in explorer
+- [#3026](https://github.com/lapce/lapce/pull/3026): Add missing file explorer context menu entries
 
 ### Bug Fixes
 - [#2779](https://github.com/lapce/lapce/pull/2779): Fix files detection on fresh git/VCS repository
diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs
index f8e83f3746..ca519e3540 100644
--- a/lapce-app/src/command.rs
+++ b/lapce-app/src/command.rs
@@ -545,16 +545,23 @@ pub enum InternalCommand {
     OpenFileChanges {
         path: PathBuf,
     },
-    StartRenamePath {
-        path: PathBuf,
-    },
-    TestRenamePath {
+    ReloadFileExplorer,
+    /// Test whether a file/directory can be created at that path
+    TestPathCreation {
         new_path: PathBuf,
     },
     FinishRenamePath {
         current_path: PathBuf,
         new_path: PathBuf,
     },
+    FinishNewNode {
+        is_dir: bool,
+        path: PathBuf,
+    },
+    FinishDuplicate {
+        source: PathBuf,
+        path: PathBuf,
+    },
     GoToLocation {
         location: EditorLocation,
     },
diff --git a/lapce-app/src/file_explorer/data.rs b/lapce-app/src/file_explorer/data.rs
index 79bbc3446d..f33cdab632 100644
--- a/lapce-app/src/file_explorer/data.rs
+++ b/lapce-app/src/file_explorer/data.rs
@@ -12,16 +12,17 @@ use floem::{
     keyboard::ModifiersState,
     menu::{Menu, MenuItem},
     reactive::{RwSignal, Scope},
-    views::editor::id::EditorId,
+    views::editor::{id::EditorId, text::SystemClipboard},
     EventPropagation,
 };
 use globset::Glob;
 use lapce_core::{
     command::{EditCommand, FocusCommand},
     mode::Mode,
+    register::Clipboard,
 };
 use lapce_rpc::{
-    file::{FileNodeItem, RenameState},
+    file::{Duplicating, FileNodeItem, Naming, NamingState, NewNode, Renaming},
     proxy::ProxyResponse,
 };
 
@@ -43,10 +44,9 @@ enum RenamedPath {
 
 #[derive(Clone)]
 pub struct FileExplorerData {
-    pub id: RwSignal<usize>,
     pub root: RwSignal<FileNodeItem>,
-    pub rename_state: RwSignal<RenameState>,
-    pub rename_editor_data: EditorData,
+    pub naming: RwSignal<Naming>,
+    pub naming_editor_data: EditorData,
     pub common: Rc<CommonData>,
     left_diff_path: RwSignal<Option<PathBuf>>,
 }
@@ -57,8 +57,7 @@ impl KeyPressFocus for FileExplorerData {
     }
 
     fn check_condition(&self, condition: Condition) -> bool {
-        self.rename_state
-            .with_untracked(RenameState::is_accepting_input)
+        self.naming.with_untracked(Naming::is_accepting_input)
             && condition == Condition::ModalFocus
     }
 
@@ -68,34 +67,29 @@ impl KeyPressFocus for FileExplorerData {
         count: Option<usize>,
         mods: ModifiersState,
     ) -> CommandExecuted {
-        if self
-            .rename_state
-            .with_untracked(RenameState::is_accepting_input)
-        {
+        if self.naming.with_untracked(Naming::is_accepting_input) {
             match command.kind {
                 CommandKind::Focus(FocusCommand::ModalClose) => {
-                    self.cancel_rename();
+                    self.cancel_naming();
                     CommandExecuted::Yes
                 }
                 CommandKind::Edit(EditCommand::InsertNewLine) => {
-                    self.finish_rename();
+                    self.finish_naming();
                     CommandExecuted::Yes
                 }
                 CommandKind::Edit(_) => {
                     let command_executed =
-                        self.rename_editor_data.run_command(command, count, mods);
+                        self.naming_editor_data.run_command(command, count, mods);
 
-                    if let RenamedPath::Renamed { new_path, .. } =
-                        self.renamed_path()
-                    {
+                    if let Some(new_path) = self.naming_path() {
                         self.common
                             .internal_command
-                            .send(InternalCommand::TestRenamePath { new_path });
+                            .send(InternalCommand::TestPathCreation { new_path });
                     }
 
                     command_executed
                 }
-                _ => self.rename_editor_data.run_command(command, count, mods),
+                _ => self.naming_editor_data.run_command(command, count, mods),
             }
         } else {
             CommandExecuted::No
@@ -103,16 +97,13 @@ impl KeyPressFocus for FileExplorerData {
     }
 
     fn receive_char(&self, c: &str) {
-        if self
-            .rename_state
-            .with_untracked(RenameState::is_accepting_input)
-        {
-            self.rename_editor_data.receive_char(c);
+        if self.naming.with_untracked(Naming::is_accepting_input) {
+            self.naming_editor_data.receive_char(c);
 
-            if let RenamedPath::Renamed { new_path, .. } = self.renamed_path() {
+            if let Some(new_path) = self.naming_path() {
                 self.common
                     .internal_command
-                    .send(InternalCommand::TestRenamePath { new_path });
+                    .send(InternalCommand::TestPathCreation { new_path });
             }
         }
     }
@@ -133,13 +124,12 @@ impl FileExplorerData {
             children: HashMap::new(),
             children_open_count: 0,
         });
-        let rename_state = cx.create_rw_signal(RenameState::NotRenaming);
-        let rename_editor_data = EditorData::new_local(cx, editors, common.clone());
+        let naming = cx.create_rw_signal(Naming::None);
+        let naming_editor_data = EditorData::new_local(cx, editors, common.clone());
         let data = Self {
-            id: cx.create_rw_signal(0),
             root,
-            rename_state,
-            rename_editor_data,
+            naming,
+            naming_editor_data,
             common,
             left_diff_path: cx.create_rw_signal(None),
         };
@@ -150,16 +140,17 @@ impl FileExplorerData {
         data
     }
 
+    /// Reload the file explorer data via reading the root directory.  
+    /// Note that this will not update immediately.
     pub fn reload(&self) {
         let path = self.root.with_untracked(|root| root.path.clone());
         self.read_dir(&path);
     }
 
+    /// Toggle whether the directory is expanded or not.  
+    /// Does nothing if the path does not exist or is not a directory.
     pub fn toggle_expand(&self, path: &Path) {
-        self.id.update(|id| {
-            *id += 1;
-        });
-        if let Some(read) = self
+        let Some(read) = self
             .root
             .try_update(|root| {
                 let read = if let Some(node) = root.get_file_node_mut(path) {
@@ -171,78 +162,93 @@ impl FileExplorerData {
                 } else {
                     None
                 };
+
                 if Some(true) == read {
                     root.update_node_count_recursive(path);
                 }
                 read
             })
             .unwrap()
-        {
-            if !read {
-                self.read_dir(path);
-            }
+        else {
+            return;
+        };
+
+        // Read the directory's files if they haven't been read yet
+        if !read {
+            self.read_dir(path);
         }
     }
 
     pub fn read_dir(&self, path: &Path) {
+        self.read_dir_cb(path, |_| {});
+    }
+
+    /// Read the directory's information and update the file explorer tree.  
+    /// `done : FnOnce(was_read: bool)` is called when the operation is completed, whether success,
+    /// failure, or ignored.
+    pub fn read_dir_cb(&self, path: &Path, done: impl FnOnce(bool) + 'static) {
         let root = self.root;
-        let id = self.id;
         let data = self.clone();
         let config = self.common.config;
         let send = {
             let path = path.to_path_buf();
             create_ext_action(self.common.scope, move |result| {
-                if let Ok(ProxyResponse::ReadDirResponse { mut items }) = result {
-                    id.update(|id| {
-                        *id += 1;
-                    });
-                    root.update(|root| {
-                        if let Some(node) = root.get_file_node_mut(&path) {
-                            match Glob::new(&config.get().editor.files_exclude) {
-                                Ok(glob) => {
-                                    let matcher = glob.compile_matcher();
-                                    items.retain(|i| !matcher.is_match(&i.path));
-                                }
-                                Err(e) => tracing::error!(
-                                    target:"files_exclude",
-                                    "Failed to compile glob: {}",
-                                    e
-                                ),
-                            }
+                let Ok(ProxyResponse::ReadDirResponse { mut items }) = result else {
+                    done(false);
+                    return;
+                };
 
-                            node.read = true;
-                            let removed_paths: Vec<PathBuf> = node
-                                .children
-                                .keys()
-                                .filter(|p| !items.iter().any(|i| &&i.path == p))
-                                .map(PathBuf::from)
-                                .collect();
-                            for path in removed_paths {
-                                node.children.remove(&path);
+                root.update(|root| {
+                    // Get the node for this path, which should already exist if we're calling
+                    // read_dir on it.
+                    if let Some(node) = root.get_file_node_mut(&path) {
+                        // TODO: do not recreate glob every time we read a directory
+                        // Retain only items that are not excluded from view by the configuration
+                        match Glob::new(&config.get().editor.files_exclude) {
+                            Ok(glob) => {
+                                let matcher = glob.compile_matcher();
+                                items.retain(|i| !matcher.is_match(&i.path));
                             }
+                            Err(e) => tracing::error!(
+                                target:"files_exclude",
+                                "Failed to compile glob: {}",
+                                e
+                            ),
+                        }
 
-                            for item in items {
-                                if let Some(existing) = node.children.get(&item.path)
-                                {
-                                    if existing.read {
-                                        data.read_dir(&existing.path);
-                                    }
-                                } else {
-                                    // Add new paths
-                                    node.children.insert(item.path.clone(), item);
+                        node.read = true;
+
+                        // Remove paths that no longer exist
+                        let removed_paths: Vec<PathBuf> = node
+                            .children
+                            .keys()
+                            .filter(|p| !items.iter().any(|i| &&i.path == p))
+                            .map(PathBuf::from)
+                            .collect();
+                        for path in removed_paths {
+                            node.children.remove(&path);
+                        }
+
+                        // Reread dirs that were already read and add new paths
+                        for item in items {
+                            if let Some(existing) = node.children.get(&item.path) {
+                                if existing.read {
+                                    data.read_dir(&existing.path);
                                 }
+                            } else {
+                                node.children.insert(item.path.clone(), item);
                             }
                         }
-                        root.update_node_count_recursive(&path);
-                    });
-                }
+                    }
+                    root.update_node_count_recursive(&path);
+                });
+
+                done(true);
             })
         };
-        self.common
-            .proxy
-            .read_dir(path.to_path_buf(), move |result| {
-                send(result);
-            });
+
+        // Ask the proxy for the directory information
+        self.common.proxy.read_dir(path.to_path_buf(), send);
     }
 
     /// Returns `true` if `path` exists in the file explorer tree and is a directory, `false`
@@ -253,15 +259,53 @@ impl FileExplorerData {
         })
     }
 
+    /// The current path that we're renaming to / creating or duplicating a node at.  
+    /// Note: returns `None` when renaming if the file name has not changed.
+    fn naming_path(&self) -> Option<PathBuf> {
+        self.naming.with_untracked(|naming| match naming {
+            Naming::None => None,
+            Naming::Renaming(_) => {
+                if let RenamedPath::Renamed { new_path, .. } = self.renamed_path() {
+                    Some(new_path)
+                } else {
+                    None
+                }
+            }
+            Naming::NewNode(n) => {
+                let relative_path = self.naming_editor_data.text();
+                let relative_path: Cow<OsStr> = match relative_path.slice_to_cow(..)
+                {
+                    Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()),
+                    Cow::Owned(path) => Cow::Owned(path.into()),
+                };
+
+                Some(n.base_path.join(relative_path))
+            }
+            Naming::Duplicating(d) => {
+                let relative_path = self.naming_editor_data.text();
+                let relative_path: Cow<OsStr> = match relative_path.slice_to_cow(..)
+                {
+                    Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()),
+                    Cow::Owned(path) => Cow::Owned(path.into()),
+                };
+
+                let new_path =
+                    d.path.parent().unwrap_or("".as_ref()).join(relative_path);
+
+                Some(new_path)
+            }
+        })
+    }
+
     /// If there is an in progress rename and the user has entered a path that differs from the
     /// current path, gets the current and new paths of the renamed node.
     fn renamed_path(&self) -> RenamedPath {
-        self.rename_state.with_untracked(|rename_state| {
-            if let Some(current_path) = rename_state.path() {
+        self.naming.with_untracked(|naming| {
+            if let Some(current_path) = naming.as_renaming().map(|x| &x.path) {
                 let current_file_name = current_path.file_name().unwrap_or_default();
                 // `new_relative_path` is the new path relative to the parent directory, unless the
                 // user has entered an absolute path.
-                let new_relative_path = self.rename_editor_data.text();
+                let new_relative_path = self.naming_editor_data.text();
 
                 let new_relative_path: Cow<OsStr> =
                     match new_relative_path.slice_to_cow(..) {
@@ -288,27 +332,62 @@ impl FileExplorerData {
         })
     }
 
-    /// If a rename is in progress and the user has entered a path that differs from the current
-    /// path, sends a request to perform the rename.
-    pub fn finish_rename(&self) {
-        match self.renamed_path() {
-            RenamedPath::NotRenaming => (),
-            RenamedPath::NameUnchanged => self.cancel_rename(),
-            RenamedPath::Renamed {
-                current_path,
-                new_path,
-            } => self.common.internal_command.send(
-                InternalCommand::FinishRenamePath {
-                    current_path,
-                    new_path,
-                },
-            ),
+    /// If a naming is in progress and the user has entered a valid path, send the request to
+    /// actually perform the change.
+    pub fn finish_naming(&self) {
+        match self.naming.get_untracked() {
+            Naming::None => {}
+            Naming::Renaming(_) => {
+                let renamed_path = self.renamed_path();
+                match renamed_path {
+                    // Should not occur
+                    RenamedPath::NotRenaming => {}
+                    RenamedPath::NameUnchanged => {
+                        self.cancel_naming();
+                    }
+                    RenamedPath::Renamed {
+                        current_path,
+                        new_path,
+                    } => {
+                        self.common.internal_command.send(
+                            InternalCommand::FinishRenamePath {
+                                current_path,
+                                new_path,
+                            },
+                        );
+                    }
+                }
+            }
+            Naming::NewNode(n) => {
+                let Some(path) = self.naming_path() else {
+                    return;
+                };
+
+                self.common
+                    .internal_command
+                    .send(InternalCommand::FinishNewNode {
+                        is_dir: n.is_dir,
+                        path,
+                    });
+            }
+            Naming::Duplicating(d) => {
+                let Some(path) = self.naming_path() else {
+                    return;
+                };
+
+                self.common.internal_command.send(
+                    InternalCommand::FinishDuplicate {
+                        source: d.path.to_owned(),
+                        path,
+                    },
+                );
+            }
         }
     }
 
-    /// Closes the rename text box without renaming the item.
-    pub fn cancel_rename(&self) {
-        self.rename_state.set(RenameState::NotRenaming);
+    /// Closes the naming text box without applying the effect.
+    pub fn cancel_naming(&self) {
+        self.naming.set(Naming::None);
     }
 
     pub fn click(&self, path: &Path) {
@@ -335,41 +414,186 @@ impl FileExplorerData {
     }
 
     pub fn secondary_click(&self, path: &Path) {
-        let menu = {
+        let common = self.common.clone();
+        let path_a = path.to_owned();
+        let left_diff_path = self.left_diff_path;
+        // TODO: should we just pass is_dir into secondary click?
+        let is_dir = self.is_dir(path);
+
+        let Some(workspace_path) = self.common.workspace.path.as_ref() else {
+            // There is no context menu if we are not in a workspace
+            return;
+        };
+
+        let is_workspace = path == workspace_path;
+
+        let base_path_a = if is_dir {
+            Some(path_a.clone())
+        } else {
+            path_a.parent().map(ToOwned::to_owned)
+        };
+        let base_path_a = base_path_a.as_ref().unwrap_or(workspace_path);
+
+        let mut menu = Menu::new("");
+
+        let base_path = base_path_a.clone();
+        let data = self.clone();
+        let naming = self.naming;
+        menu = menu.entry(MenuItem::new("New File").action(move || {
+            let base_path_b = &base_path;
+            let base_path = base_path.clone();
+            data.read_dir_cb(base_path_b, move |was_read| {
+                if !was_read {
+                    tracing::warn!(
+                        "Failed to read directory, avoiding creating node in: {:?}",
+                        base_path
+                    );
+                    return;
+                }
+
+                naming.set(Naming::NewNode(NewNode {
+                    state: NamingState::Naming,
+                    base_path: base_path.clone(),
+                    is_dir: false,
+                    editor_needs_reset: true,
+                }));
+            });
+        }));
+
+        let base_path = base_path_a.clone();
+        let data = self.clone();
+        let naming = self.naming;
+        menu = menu.entry(MenuItem::new("New Directory").action(move || {
+            let base_path_b = &base_path;
+            let base_path = base_path.clone();
+            data.read_dir_cb(base_path_b, move |was_read| {
+                if !was_read {
+                    tracing::warn!(
+                        "Failed to read directory, avoiding creating node in: {:?}",
+                        base_path
+                    );
+                    return;
+                }
+
+                naming.set(Naming::NewNode(NewNode {
+                    state: NamingState::Naming,
+                    base_path: base_path.clone(),
+                    is_dir: true,
+                    editor_needs_reset: true,
+                }));
+            })
+        }));
+
+        menu = menu.separator();
+
+        // TODO: there are situations where we can open the file explorer to remote files
+        if !common.workspace.kind.is_remote() {
+            let path = path_a.clone();
+            menu = menu.entry(MenuItem::new("Reveal in file explorer").action(
+                move || {
+                    let path = path.parent().unwrap_or(&path);
+                    if !path.exists() {
+                        return;
+                    }
+
+                    if let Err(err) = open::that(path) {
+                        tracing::error!(
+                            "Failed to reveal file in system file explorer: {}",
+                            err
+                        );
+                    }
+                },
+            ));
+        }
+
+        if !is_workspace {
+            let path = path_a.clone();
+            menu = menu.entry(MenuItem::new("Rename").action(move || {
+                naming.set(Naming::Renaming(Renaming {
+                    state: NamingState::Naming,
+                    path: path.clone(),
+                    editor_needs_reset: true,
+                }));
+            }));
+
+            let path = path_a.clone();
+            menu = menu.entry(MenuItem::new("Duplicate").action(move || {
+                naming.set(Naming::Duplicating(Duplicating {
+                    state: NamingState::Naming,
+                    path: path.clone(),
+                    editor_needs_reset: true,
+                }));
+            }));
+
+            // TODO: it is common for shift+right click to make 'Move file to trash' an actual
+            // Delete, which can be useful for large files.
+            let path = path_a.clone();
+            let proxy = common.proxy.clone();
+            let trash_text = if is_dir {
+                "Move Directory to Trash"
+            } else {
+                "Move File to Trash"
+            };
+            menu = menu.entry(MenuItem::new(trash_text).action(move || {
+                proxy.trash_path(path.clone(), |res| {
+                    if let Err(err) = res {
+                        tracing::warn!("Failed to trash path: {:?}", err);
+                    }
+                })
+            }));
+        }
+
+        menu = menu.separator();
+
+        let path = path_a.clone();
+        menu = menu.entry(MenuItem::new("Copy Path").action(move || {
+            let mut clipboard = SystemClipboard::new();
+            clipboard.put_string(path.to_string_lossy());
+        }));
+
+        let path = path_a.clone();
+        let workspace = common.workspace.clone();
+        menu = menu.entry(MenuItem::new("Copy Relative Path").action(move || {
+            let relative_path = if let Some(workspace_path) = &workspace.path {
+                path.strip_prefix(workspace_path).unwrap_or(&path)
+            } else {
+                path.as_ref()
+            };
+
+            let mut clipboard = SystemClipboard::new();
+            clipboard.put_string(relative_path.to_string_lossy());
+        }));
+
+        menu = menu.separator();
+
+        let path = path_a.clone();
+        menu = menu.entry(
+            MenuItem::new("Select for Compare")
+                .action(move || left_diff_path.set(Some(path.clone()))),
+        );
+
+        if let Some(left_path) = self.left_diff_path.get_untracked() {
             let common = self.common.clone();
-            let path = path.to_owned();
-            let left_path = path.clone();
-            let left_diff_path = self.left_diff_path;
+            let right_path = path_a.to_owned();
 
-            Menu::new("")
-                .entry(MenuItem::new("Rename...").action(move || {
+            menu = menu.entry(MenuItem::new("Compare with Selected").action(
+                move || {
                     common
                         .internal_command
-                        .send(InternalCommand::StartRenamePath {
-                            path: path.clone(),
-                        });
-                }))
-                .entry(
-                    MenuItem::new("Select for Compare")
-                        .action(move || left_diff_path.set(Some(left_path.clone()))),
-                )
-        };
+                        .send(InternalCommand::OpenDiffFiles {
+                            left_path: left_path.clone(),
+                            right_path: right_path.clone(),
+                        })
+                },
+            ))
+        }
 
-        let menu = if let Some(left_path) = self.left_diff_path.get_untracked() {
-            let common = self.common.clone();
-            let right_path = path.to_owned();
+        menu = menu.separator();
 
-            menu.entry(MenuItem::new("Compare with Selected").action(move || {
-                common
-                    .internal_command
-                    .send(InternalCommand::OpenDiffFiles {
-                        left_path: left_path.clone(),
-                        right_path: right_path.clone(),
-                    })
-            }))
-        } else {
-            menu
-        };
+        let internal_command = common.internal_command;
+        menu = menu.entry(MenuItem::new("Refresh").action(move || {
+            internal_command.send(InternalCommand::ReloadFileExplorer);
+        }));
 
         show_context_menu(menu, None);
     }
diff --git a/lapce-app/src/file_explorer/node.rs b/lapce-app/src/file_explorer/node.rs
index 01bba66e19..633b65962a 100644
--- a/lapce-app/src/file_explorer/node.rs
+++ b/lapce-app/src/file_explorer/node.rs
@@ -1,18 +1,16 @@
 use floem::views::VirtualVector;
-use lapce_rpc::file::{FileNodeItem, FileNodeViewData};
-
-use lapce_rpc::file::RenameState;
+use lapce_rpc::file::{FileNodeItem, FileNodeViewData, Naming};
 
 pub struct FileNodeVirtualList {
     file_node_item: FileNodeItem,
-    rename_state: RenameState,
+    naming: Naming,
 }
 
 impl FileNodeVirtualList {
-    pub fn new(file_node_item: FileNodeItem, rename_state: RenameState) -> Self {
+    pub fn new(file_node_item: FileNodeItem, naming: Naming) -> Self {
         Self {
             file_node_item,
-            rename_state,
+            naming,
         }
     }
 }
@@ -26,23 +24,14 @@ impl VirtualVector<FileNodeViewData> for FileNodeVirtualList {
         &mut self,
         range: std::ops::Range<usize>,
     ) -> impl Iterator<Item = FileNodeViewData> {
+        let naming = &self.naming;
+        let root = &self.file_node_item;
+
         let min = range.start;
         let max = range.end;
-        let mut i = 0;
         let mut view_items = Vec::new();
-        for item in self.file_node_item.sorted_children() {
-            i = item.append_view_slice(
-                &mut view_items,
-                &self.rename_state,
-                min,
-                max,
-                i + 1,
-                0,
-            );
-            if i > max {
-                return view_items.into_iter();
-            }
-        }
+
+        root.append_children_view_slice(&mut view_items, naming, min, max, 0, 0);
 
         view_items.into_iter()
     }
diff --git a/lapce-app/src/file_explorer/view.rs b/lapce-app/src/file_explorer/view.rs
index 88217fe2dc..2bddd0d479 100644
--- a/lapce-app/src/file_explorer/view.rs
+++ b/lapce-app/src/file_explorer/view.rs
@@ -8,13 +8,12 @@ use floem::{
     style::{AlignItems, CursorStyle, Position, Style},
     view::View,
     views::{
-        container, dyn_stack, label, scroll, stack, svg, virtual_stack, Decorators,
-        VirtualDirection, VirtualItemSize,
+        container, dyn_stack, label, scroll, stack, svg, virtual_stack, Container,
+        Decorators, VirtualDirection, VirtualItemSize,
     },
-    EventPropagation,
 };
 use lapce_core::selection::Selection;
-use lapce_rpc::file::{FileNodeViewData, IsRenaming, RenameState};
+use lapce_rpc::file::{FileNodeViewData, FileNodeViewKind, Naming};
 use lapce_xi_rope::Rope;
 
 use super::{data::FileExplorerData, node::FileNodeVirtualList};
@@ -25,7 +24,7 @@ use crate::{
     editor_tab::{EditorTabChild, EditorTabData},
     panel::{kind::PanelKind, position::PanelPosition, view::panel_header},
     plugin::PluginData,
-    text_input::text_input,
+    text_input::text_input_key_focus,
     window_tab::{Focus, WindowTabData},
 };
 
@@ -87,11 +86,12 @@ pub fn file_explorer_panel(
     ))
     .style(move |s| {
         s.width_pct(100.0)
-            .apply_if(!position.is_bottom(), |s| s.flex_col())
+            .apply_if(!position.is_bottom(), |s: Style| s.flex_col())
     })
 }
 
-fn initialize_rename_editor(data: &FileExplorerData, path: &Path) {
+/// Initialize the file explorer's naming (renaming, creating, etc.) editor with the given path.
+fn initialize_naming_editor_with_path(data: &FileExplorerData, path: &Path) {
     let file_name = path.file_name().unwrap_or_default().to_string_lossy();
     // Start with the part of the file or directory name before the extension
     // selected.
@@ -104,136 +104,144 @@ fn initialize_rename_editor(data: &FileExplorerData, path: &Path) {
         idx + file_name.len() - without_leading_dot.len()
     };
 
-    let doc = data.rename_editor_data.doc();
-    doc.reload(Rope::from(&file_name), true);
-    data.rename_editor_data
+    initialize_naming_editor(data, &file_name, Some(selection_end));
+}
+
+fn initialize_naming_editor(
+    data: &FileExplorerData,
+    text: &str,
+    selection_end: Option<usize>,
+) {
+    let text = Rope::from(text);
+    let selection_end = selection_end.unwrap_or(text.len());
+
+    let doc = data.naming_editor_data.doc();
+    doc.reload(text, true);
+    data.naming_editor_data
         .cursor()
         .update(|cursor| cursor.set_insert(Selection::region(0, selection_end)));
 
-    data.rename_state
-        .update(|rename_state| rename_state.set_editor_needs_reset(false));
+    data.naming
+        .update(|naming| naming.set_editor_needs_reset(false));
 }
 
-fn file_node_text_view(
-    data: FileExplorerData,
-    node: FileNodeViewData,
-    path: &Path,
-) -> impl View {
+fn file_node_text_view(data: FileExplorerData, node: FileNodeViewData) -> impl View {
     let ui_line_height = data.common.ui_line_height;
 
-    let view = if let IsRenaming::Renaming { err } = node.is_renaming {
-        let rename_editor_data = data.rename_editor_data.clone();
-        let text_input_file_explorer_data = data.clone();
-        let focus = data.common.focus;
-        let config = data.common.config;
-
-        if data
-            .rename_state
-            .with_untracked(RenameState::editor_needs_reset)
-        {
-            initialize_rename_editor(&data, path);
-        }
-
-        let text_input_view = text_input(rename_editor_data.clone(), move || {
-            focus.with_untracked(|focus| {
-                focus == &Focus::Panel(PanelKind::FileExplorer)
-            })
-        })
-        .on_event_stop(EventListener::FocusLost, move |_| {
-            data.finish_rename();
-            data.rename_state
-                .set(lapce_rpc::file::RenameState::NotRenaming);
-        })
-        .on_event(EventListener::KeyDown, move |event| {
-            if let Event::KeyDown(event) = event {
-                let keypress = rename_editor_data.common.keypress.get_untracked();
-                if keypress.key_down(event, &text_input_file_explorer_data) {
-                    EventPropagation::Stop
-                } else {
-                    EventPropagation::Continue
-                }
-            } else {
-                EventPropagation::Continue
-            }
-        })
-        .style(move |s| {
-            s.width_full()
-                .height(ui_line_height.get())
-                .padding(0.0)
-                .margin(0.0)
-                .border_radius(6.0)
-                .border(1.0)
-                .border_color(config.get().color(LapceColor::LAPCE_BORDER))
-        });
-
-        let text_input_id = text_input_view.id();
-        text_input_id.request_focus();
-
-        if let Some(err) = err {
-            container(
-                stack((
-                    text_input_view,
-                    label(move || err.clone()).style(move |s| {
-                        let config = config.get();
-
-                        let editor_background_color =
-                            config.color(LapceColor::PANEL_CURRENT_BACKGROUND);
-                        let error_background_color =
-                            config.color(LapceColor::ERROR_LENS_ERROR_BACKGROUND);
-
-                        let background_color = blend_colors(
-                            editor_background_color,
-                            error_background_color,
-                        );
-
-                        s.position(Position::Absolute)
-                            .inset_top(ui_line_height.get())
-                            .width_full()
-                            .color(
-                                config
-                                    .color(LapceColor::ERROR_LENS_ERROR_FOREGROUND),
-                            )
-                            .background(background_color)
-                            .z_index(100)
-                    }),
-                ))
-                .style(|s| s.flex_grow(1.0)),
-            )
-        } else {
-            container(text_input_view)
-        }
-    } else {
-        container(
+    let view = match node.kind {
+        FileNodeViewKind::Path(path) => container(
             label(move || {
-                node.path
-                    .file_name()
+                path.file_name()
                     .map(|f| f.to_string_lossy().to_string())
                     .unwrap_or_default()
             })
             .style(move |s| s.flex_grow(1.0).height(ui_line_height.get())),
-        )
+        ),
+        FileNodeViewKind::Renaming { path, err } => {
+            if data.naming.with_untracked(Naming::editor_needs_reset) {
+                initialize_naming_editor_with_path(&data, &path);
+            }
+
+            file_node_input_view(data, err)
+        }
+        FileNodeViewKind::Naming { err } => {
+            if data.naming.with_untracked(Naming::editor_needs_reset) {
+                initialize_naming_editor(&data, "", None);
+            }
+
+            file_node_input_view(data, err)
+        }
+        FileNodeViewKind::Duplicating { source, err } => {
+            if data.naming.with_untracked(Naming::editor_needs_reset) {
+                initialize_naming_editor_with_path(&data, &source);
+            }
+
+            file_node_input_view(data, err)
+        }
     };
 
     view.style(|s| s.flex_grow(1.0).padding(0.0).margin(0.0))
 }
 
+/// Input used for naming a file/directory
+fn file_node_input_view(data: FileExplorerData, err: Option<String>) -> Container {
+    let ui_line_height = data.common.ui_line_height;
+
+    let naming_editor_data = data.naming_editor_data.clone();
+    let text_input_file_explorer_data = data.clone();
+    let focus = data.common.focus;
+    let config = data.common.config;
+
+    let is_focused = move || {
+        focus.with_untracked(|focus| focus == &Focus::Panel(PanelKind::FileExplorer))
+    };
+    let text_input_view = text_input_key_focus(
+        naming_editor_data.clone(),
+        Some(text_input_file_explorer_data),
+        is_focused,
+    )
+    .on_event_stop(EventListener::FocusLost, move |_| {
+        data.finish_naming();
+        data.naming.set(Naming::None);
+    })
+    .style(move |s| {
+        s.width_full()
+            .height(ui_line_height.get())
+            .padding(0.0)
+            .margin(0.0)
+            .border_radius(6.0)
+            .border(1.0)
+            .border_color(config.get().color(LapceColor::LAPCE_BORDER))
+    });
+
+    let text_input_id = text_input_view.id();
+    text_input_id.request_focus();
+
+    if let Some(err) = err {
+        container(
+            stack((
+                text_input_view,
+                label(move || err.clone()).style(move |s| {
+                    let config = config.get();
+
+                    let editor_background_color =
+                        config.color(LapceColor::PANEL_CURRENT_BACKGROUND);
+                    let error_background_color =
+                        config.color(LapceColor::ERROR_LENS_ERROR_BACKGROUND);
+
+                    let background_color = blend_colors(
+                        editor_background_color,
+                        error_background_color,
+                    );
+
+                    s.position(Position::Absolute)
+                        .inset_top(ui_line_height.get())
+                        .width_full()
+                        .color(config.color(LapceColor::ERROR_LENS_ERROR_FOREGROUND))
+                        .background(background_color)
+                        .z_index(100)
+                }),
+            ))
+            .style(|s| s.flex_grow(1.0)),
+        )
+    } else {
+        container(text_input_view)
+    }
+}
+
 fn new_file_node_view(data: FileExplorerData) -> impl View {
     let root = data.root;
     let ui_line_height = data.common.ui_line_height;
     let config = data.common.config;
+    let naming = data.naming;
+
+    let secondary_click_data = data.clone();
+
     virtual_stack(
         VirtualDirection::Vertical,
         VirtualItemSize::Fixed(Box::new(move || ui_line_height.get())),
-        move || FileNodeVirtualList::new(root.get(), data.rename_state.get()),
-        move |node| {
-            (
-                node.path.clone(),
-                node.is_dir,
-                node.open,
-                node.is_renaming.clone(),
-                node.level,
-            )
-        },
+        move || FileNodeVirtualList::new(root.get(), data.naming.get()),
+        move |node| (node.kind.clone(), node.is_dir, node.open, node.level),
         move |node| {
             let level = node.level;
             let data = data.clone();
@@ -241,14 +249,9 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
             let double_click_data = data.clone();
             let secondary_click_data = data.clone();
             let aux_click_data = data.clone();
-            let path = node.path.clone();
-            let click_path = node.path.clone();
-            let double_click_path = node.path.clone();
-            let secondary_click_path = node.path.clone();
-            let aux_click_path = path.clone();
+            let kind = node.kind.clone();
             let open = node.open;
             let is_dir = node.is_dir;
-            let is_renaming = node.is_renaming.clone();
 
             let view = stack((
                 svg(move || {
@@ -274,8 +277,9 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
                         .color(color)
                 }),
                 {
-                    let path = path.clone();
-                    let path_for_style = path.clone();
+                    let kind = kind.clone();
+                    let kind_for_style = kind.clone();
+                    // TODO: use the current naming input as the path for the file svg
                     svg(move || {
                         let config = config.get();
                         if is_dir {
@@ -284,8 +288,10 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
                                 false => LapceIcons::DIRECTORY_CLOSED,
                             };
                             config.ui_svg(svg_str)
+                        } else if let Some(path) = kind.path() {
+                            config.file_svg(path).0
                         } else {
-                            config.file_svg(&path).0
+                            config.ui_svg(LapceIcons::FILE)
                         }
                     })
                     .style(move |s| {
@@ -300,13 +306,15 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
                             })
                             .apply_if(!is_dir, |s| {
                                 s.apply_opt(
-                                    config.file_svg(&path_for_style).1,
+                                    kind_for_style
+                                        .path()
+                                        .and_then(|p| config.file_svg(p).1),
                                     Style::color,
                                 )
                             })
                     })
                 },
-                file_node_text_view(data, node, &path),
+                file_node_text_view(data, node),
             ))
             .style(move |s| {
                 s.padding_right(5.0)
@@ -320,7 +328,12 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
                     })
             });
 
-            if let IsRenaming::NotRenaming = is_renaming {
+            // Only handle click events if we are not naming the file node
+            if let FileNodeViewKind::Path(path) = kind {
+                let click_path = path.clone();
+                let double_click_path = path.clone();
+                let secondary_click_path = path.clone();
+                let aux_click_path = path;
                 view.on_click_stop(move |_| {
                     click_data.click(&click_path);
                 })
@@ -345,7 +358,19 @@ fn new_file_node_view(data: FileExplorerData) -> impl View {
             }
         },
     )
-    .style(|s| s.flex_col().align_items(AlignItems::Stretch).width_full())
+    .style(|s| {
+        s.flex_col()
+            .align_items(AlignItems::Stretch)
+            .width_full()
+            .height_full()
+    })
+    .on_secondary_click_stop(move |_| {
+        if let Naming::None = naming.get_untracked() {
+            if let Some(path) = &secondary_click_data.common.workspace.path {
+                secondary_click_data.secondary_click(path);
+            }
+        }
+    })
 }
 
 fn open_editors_view(window_tab_data: Rc<WindowTabData>) -> impl View {
diff --git a/lapce-app/src/keypress.rs b/lapce-app/src/keypress.rs
index f85c0992c0..8beda5041e 100644
--- a/lapce-app/src/keypress.rs
+++ b/lapce-app/src/keypress.rs
@@ -58,6 +58,34 @@ pub trait KeyPressFocus {
 
     fn receive_char(&self, c: &str);
 }
+impl KeyPressFocus for () {
+    fn get_mode(&self) -> Mode {
+        Mode::Normal
+    }
+
+    fn check_condition(&self, _condition: Condition) -> bool {
+        false
+    }
+
+    fn run_command(
+        &self,
+        _command: &LapceCommand,
+        _count: Option<usize>,
+        _mods: ModifiersState,
+    ) -> CommandExecuted {
+        CommandExecuted::No
+    }
+
+    fn expect_char(&self) -> bool {
+        false
+    }
+
+    fn focus_only(&self) -> bool {
+        false
+    }
+
+    fn receive_char(&self, _c: &str) {}
+}
 
 #[derive(Clone, Copy, Debug)]
 pub enum EventRef<'a> {
@@ -134,7 +162,7 @@ impl KeyPressData {
         self.commands_without_keymap = Rc::new(commands_without_keymap);
     }
 
-    fn handle_count<T: KeyPressFocus>(
+    fn handle_count<T: KeyPressFocus + ?Sized>(
         &self,
         focus: &T,
         keypress: &KeyPress,
@@ -164,7 +192,7 @@ impl KeyPressData {
         false
     }
 
-    fn run_command<T: KeyPressFocus>(
+    fn run_command<T: KeyPressFocus + ?Sized>(
         &self,
         command: &str,
         count: Option<usize>,
@@ -198,7 +226,7 @@ impl KeyPressData {
         Some(keypress)
     }
 
-    pub fn key_down<'a, T: KeyPressFocus>(
+    pub fn key_down<'a, T: KeyPressFocus + ?Sized>(
         &self,
         event: impl Into<EventRef<'a>>,
         focus: &T,
@@ -314,7 +342,7 @@ impl KeyPressData {
         mods
     }
 
-    fn match_keymap<T: KeyPressFocus>(
+    fn match_keymap<T: KeyPressFocus + ?Sized>(
         &self,
         keypresses: &[KeyPress],
         check: &T,
@@ -365,8 +393,11 @@ impl KeyPressData {
         }
     }
 
-    fn check_condition<T: KeyPressFocus>(condition: &str, check: &T) -> bool {
-        fn check_one_condition<T: KeyPressFocus>(
+    fn check_condition<T: KeyPressFocus + ?Sized>(
+        condition: &str,
+        check: &T,
+    ) -> bool {
+        fn check_one_condition<T: KeyPressFocus + ?Sized>(
             condition: &str,
             check: &T,
         ) -> bool {
diff --git a/lapce-app/src/text_input.rs b/lapce-app/src/text_input.rs
index 259863aa3e..0105510210 100644
--- a/lapce-app/src/text_input.rs
+++ b/lapce-app/src/text_input.rs
@@ -31,6 +31,7 @@ use lapce_core::{
 use crate::{
     config::{color::LapceColor, LapceConfig},
     editor::{DocSignal, EditorData},
+    keypress::KeyPressFocus,
 };
 
 prop_extracter! {
@@ -44,9 +45,25 @@ prop_extracter! {
     }
 }
 
+/// Create a basic single line text input while specifying the `key_focus`
+/// `e_data` is the editor data that this input is associated with.  
+/// `key_focus` is what receives the keydown events, leave as `None` to default to editor.  
+/// `is_focused` is a function that returns if the input is focused, used for certain events.
 pub fn text_input(
     e_data: EditorData,
     is_focused: impl Fn() -> bool + 'static,
+) -> TextInput {
+    text_input_key_focus::<()>(e_data, None, is_focused)
+}
+
+/// Create a basic single line text input while specifying the `key_focus`
+/// `e_data` is the editor data that this input is associated with.  
+/// `key_focus` is what receives the keydown events, leave as `None` to default to editor.  
+/// `is_focused` is a function that returns if the input is focused, used for certain events.
+pub fn text_input_key_focus<T: KeyPressFocus + 'static>(
+    e_data: EditorData,
+    key_focus: Option<T>,
+    is_focused: impl Fn() -> bool + 'static,
 ) -> TextInput {
     let id = Id::next();
 
@@ -168,7 +185,11 @@ pub fn text_input(
     .on_event(EventListener::KeyDown, move |event| {
         if let Event::KeyDown(key_event) = event {
             let keypress = keypress.get_untracked();
-            if keypress.key_down(key_event, &e_data) {
+            let key_focus = key_focus
+                .as_ref()
+                .map(|k| k as &dyn KeyPressFocus)
+                .unwrap_or(&e_data);
+            if keypress.key_down(key_event, key_focus) {
                 EventPropagation::Stop
             } else {
                 EventPropagation::Continue
diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs
index f2d47e4b72..8bfb5db806 100644
--- a/lapce-app/src/window_tab.rs
+++ b/lapce-app/src/window_tab.rs
@@ -27,7 +27,7 @@ use lapce_core::{
 use lapce_rpc::{
     core::CoreNotification,
     dap_types::RunDebugConfig,
-    file::{PathObject, RenameState},
+    file::{Naming, PathObject},
     proxy::{ProxyResponse, ProxyRpcHandler, ProxyStatus},
     source_control::FileDiff,
     terminal::TermId,
@@ -1313,25 +1313,20 @@ impl WindowTabData {
             InternalCommand::OpenFileChanges { path } => {
                 self.main_split.open_file_changes(path);
             }
-            InternalCommand::StartRenamePath { path } => {
-                self.file_explorer.rename_state.set(RenameState::Renaming {
-                    path,
-                    editor_needs_reset: true,
-                });
+            InternalCommand::ReloadFileExplorer => {
+                self.file_explorer.reload();
             }
-            InternalCommand::TestRenamePath { new_path } => {
-                let rename_state = self.file_explorer.rename_state;
+            InternalCommand::TestPathCreation { new_path } => {
+                let naming = self.file_explorer.naming;
 
                 let send = create_ext_action(
                     self.scope,
                     move |response: Result<ProxyResponse, RpcError>| match response {
                         Ok(_) => {
-                            rename_state.update(RenameState::set_ok);
+                            naming.update(Naming::set_ok);
                         }
                         Err(err) => {
-                            rename_state.update(|rename_state| {
-                                rename_state.set_err(err.message)
-                            });
+                            naming.update(|naming| naming.set_err(err.message));
                         }
                     },
                 );
@@ -1342,6 +1337,7 @@ impl WindowTabData {
                 current_path,
                 new_path,
             } => {
+                println!("FinishRenamePath");
                 let send_current_path = current_path.clone();
                 let send_new_path = new_path.clone();
                 let file_explorer = self.file_explorer.clone();
@@ -1351,6 +1347,7 @@ impl WindowTabData {
                     self.scope,
                     move |response: Result<ProxyResponse, RpcError>| match response {
                         Ok(response) => {
+                            println!("Got rename path response");
                             // Get the canonicalized new path from the proxy.
                             let new_path =
                                 if let ProxyResponse::CreatePathResponse { path } =
@@ -1400,23 +1397,80 @@ impl WindowTabData {
                             }
 
                             file_explorer.reload();
-                            file_explorer.rename_state.set(RenameState::NotRenaming);
+                            file_explorer.naming.set(Naming::None);
                         }
                         Err(err) => {
-                            file_explorer.rename_state.update(|rename_state| {
-                                rename_state.set_err(err.message)
-                            });
+                            file_explorer
+                                .naming
+                                .update(|naming| naming.set_err(err.message));
                         }
                     },
                 );
 
-                self.file_explorer
-                    .rename_state
-                    .update(RenameState::set_pending);
+                self.file_explorer.naming.update(Naming::set_pending);
                 self.common
                     .proxy
                     .rename_path(current_path.clone(), new_path, send);
             }
+            InternalCommand::FinishNewNode { is_dir, path } => {
+                let file_explorer = self.file_explorer.clone();
+                let internal_command = self.common.internal_command;
+
+                let send = create_ext_action(
+                    self.scope,
+                    move |response: Result<ProxyResponse, RpcError>| {
+                        match response {
+                            Ok(response) => {
+                                file_explorer.reload();
+                                file_explorer.naming.set(Naming::None);
+
+                                // Open a new file in the editor
+                                if let ProxyResponse::CreatePathResponse { path } =
+                                    response
+                                {
+                                    if !is_dir {
+                                        internal_command.send(
+                                            InternalCommand::OpenFile { path },
+                                        );
+                                    }
+                                }
+                            }
+                            Err(err) => {
+                                file_explorer
+                                    .naming
+                                    .update(|naming| naming.set_err(err.message));
+                            }
+                        }
+                    },
+                );
+
+                self.file_explorer.naming.update(Naming::set_pending);
+                if is_dir {
+                    self.common.proxy.create_directory(path, send);
+                } else {
+                    self.common.proxy.create_file(path, send);
+                }
+            }
+            InternalCommand::FinishDuplicate { source, path } => {
+                let file_explorer = self.file_explorer.clone();
+
+                let send = create_ext_action(
+                    self.scope,
+                    move |response: Result<_, RpcError>| {
+                        if let Err(err) = response {
+                            file_explorer
+                                .naming
+                                .update(|naming| naming.set_err(err.message));
+                        } else {
+                            file_explorer.reload();
+                            file_explorer.naming.set(Naming::None);
+                        }
+                    },
+                );
+
+                self.file_explorer.naming.update(Naming::set_pending);
+                self.common.proxy.duplicate_path(source, path, send);
+            }
             InternalCommand::GoToLocation { location } => {
                 self.main_split.go_to_location(location, None);
             }
diff --git a/lapce-rpc/src/file.rs b/lapce-rpc/src/file.rs
index e20a3fc858..438187578c 100644
--- a/lapce-rpc/src/file.rs
+++ b/lapce-rpc/src/file.rs
@@ -1,7 +1,6 @@
 use std::{
     cmp::{Ord, Ordering, PartialOrd},
     collections::HashMap,
-    mem,
     path::{Path, PathBuf},
 };
 
@@ -48,183 +47,213 @@ impl PathObject {
     }
 }
 
-/// Stores the state of any in progress rename of a path.
-///
-/// The `editor_needs_reset` field is `true` if the rename editor should have its contents reset
-/// when the view function next runs.
-#[derive(Clone)]
-pub enum RenameState {
-    NotRenaming,
-    Renaming {
-        path: PathBuf,
-        editor_needs_reset: bool,
-    },
-    RenameRequestPending {
-        path: PathBuf,
-        editor_needs_reset: bool,
-    },
-    RenameErr {
-        path: PathBuf,
-        editor_needs_reset: bool,
-        err: String,
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum FileNodeViewKind {
+    /// An actual file/directory
+    Path(PathBuf),
+    /// We are renaming the file at this path
+    Renaming { path: PathBuf, err: Option<String> },
+    /// We are naming a new file/directory
+    Naming { err: Option<String> },
+    Duplicating {
+        /// The path that is being duplicated
+        source: PathBuf,
+        err: Option<String>,
     },
 }
+impl FileNodeViewKind {
+    pub fn path(&self) -> Option<&Path> {
+        match self {
+            Self::Path(path) => Some(path),
+            Self::Renaming { path, .. } => Some(path),
+            Self::Naming { .. } => None,
+            Self::Duplicating { source, .. } => Some(source),
+        }
+    }
+}
 
-impl RenameState {
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum NamingState {
+    /// Actively naming
+    Naming,
+    /// Application of the naming is pending
+    Pending,
+    /// There's an active error with the typed name
+    Err { err: String },
+}
+impl NamingState {
     pub fn is_accepting_input(&self) -> bool {
         match self {
-            Self::NotRenaming | Self::RenameRequestPending { .. } => false,
-            Self::Renaming { .. } | Self::RenameErr { .. } => true,
+            Self::Naming | Self::Err { .. } => true,
+            Self::Pending => false,
         }
     }
 
     pub fn is_err(&self) -> bool {
         match self {
-            Self::NotRenaming
-            | Self::Renaming { .. }
-            | Self::RenameRequestPending { .. } => false,
-            Self::RenameErr { .. } => true,
+            Self::Naming | Self::Pending => false,
+            Self::Err { .. } => true,
         }
     }
 
-    pub fn set_ok(&mut self) {
-        if let &mut Self::RenameErr {
-            ref mut path,
-            editor_needs_reset,
-            ..
-        } = self
-        {
-            let path = mem::take(path);
-
-            *self = Self::Renaming {
-                path,
-                editor_needs_reset,
-            };
+    pub fn err(&self) -> Option<&str> {
+        match self {
+            Self::Err { err } => Some(err.as_str()),
+            _ => None,
         }
     }
 
+    pub fn set_ok(&mut self) {
+        *self = Self::Naming;
+    }
+
     pub fn set_pending(&mut self) {
-        if let &mut Self::Renaming {
-            ref mut path,
-            editor_needs_reset,
-        }
-        | &mut Self::RenameErr {
-            ref mut path,
-            editor_needs_reset,
-            ..
-        } = self
-        {
-            let path = mem::take(path);
-
-            *self = Self::RenameRequestPending {
-                path,
-                editor_needs_reset,
-            };
-        }
+        *self = Self::Pending;
     }
 
     pub fn set_err(&mut self, err: String) {
-        if let &mut Self::Renaming {
-            ref mut path,
-            editor_needs_reset,
+        *self = Self::Err { err };
+    }
+}
+
+/// Stores the state of any in progress rename of a path.
+///
+/// The `editor_needs_reset` field is `true` if the rename editor should have its contents reset
+/// when the view function next runs.
+#[derive(Debug, Clone)]
+pub struct Renaming {
+    pub state: NamingState,
+    /// Original file's path
+    pub path: PathBuf,
+    pub editor_needs_reset: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct NewNode {
+    pub state: NamingState,
+    /// If true, then we are creating a directory
+    pub is_dir: bool,
+    /// The folder that the file/directory is being created within
+    pub base_path: PathBuf,
+    pub editor_needs_reset: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct Duplicating {
+    pub state: NamingState,
+    /// Path to the item being duplicated
+    pub path: PathBuf,
+    pub editor_needs_reset: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum Naming {
+    None,
+    Renaming(Renaming),
+    NewNode(NewNode),
+    Duplicating(Duplicating),
+}
+impl Naming {
+    pub fn state(&self) -> Option<&NamingState> {
+        match self {
+            Self::None => None,
+            Self::Renaming(rename) => Some(&rename.state),
+            Self::NewNode(state) => Some(&state.state),
+            Self::Duplicating(state) => Some(&state.state),
         }
-        | &mut Self::RenameRequestPending {
-            ref mut path,
-            editor_needs_reset,
+    }
+
+    pub fn state_mut(&mut self) -> Option<&mut NamingState> {
+        match self {
+            Self::None => None,
+            Self::Renaming(rename) => Some(&mut rename.state),
+            Self::NewNode(state) => Some(&mut state.state),
+            Self::Duplicating(state) => Some(&mut state.state),
         }
-        | &mut Self::RenameErr {
-            ref mut path,
-            editor_needs_reset,
-            ..
-        } = self
-        {
-            let path = mem::take(path);
-
-            *self = Self::RenameErr {
-                path,
-                editor_needs_reset,
-                err,
-            };
+    }
+
+    pub fn is_accepting_input(&self) -> bool {
+        self.state().map_or(false, NamingState::is_accepting_input)
+    }
+
+    pub fn editor_needs_reset(&self) -> bool {
+        match self {
+            Naming::None => false,
+            Naming::Renaming(rename) => rename.editor_needs_reset,
+            Naming::NewNode(state) => state.editor_needs_reset,
+            Naming::Duplicating(state) => state.editor_needs_reset,
         }
     }
 
     pub fn set_editor_needs_reset(&mut self, needs_reset: bool) {
-        if let Self::Renaming {
-            editor_needs_reset, ..
+        match self {
+            Naming::None => {}
+            Naming::Renaming(rename) => rename.editor_needs_reset = needs_reset,
+            Naming::NewNode(state) => state.editor_needs_reset = needs_reset,
+            Naming::Duplicating(state) => state.editor_needs_reset = needs_reset,
         }
-        | Self::RenameRequestPending {
-            editor_needs_reset, ..
+    }
+
+    pub fn set_ok(&mut self) {
+        if let Some(state) = self.state_mut() {
+            state.set_ok();
         }
-        | Self::RenameErr {
-            editor_needs_reset, ..
-        } = self
-        {
-            *editor_needs_reset = needs_reset;
+    }
+
+    pub fn set_pending(&mut self) {
+        if let Some(state) = self.state_mut() {
+            state.set_pending();
         }
     }
 
-    pub fn path(&self) -> Option<&Path> {
-        match self {
-            Self::NotRenaming => None,
-            Self::Renaming { path, .. }
-            | Self::RenameRequestPending { path, .. }
-            | Self::RenameErr { path, .. } => Some(path),
+    pub fn set_err(&mut self, err: String) {
+        if let Some(state) = self.state_mut() {
+            state.set_err(err);
         }
     }
 
-    pub fn editor_needs_reset(&self) -> bool {
+    pub fn as_renaming(&self) -> Option<&Renaming> {
         match self {
-            Self::NotRenaming => false,
-            &Self::Renaming {
-                editor_needs_reset, ..
-            }
-            | &Self::RenameRequestPending {
-                editor_needs_reset, ..
-            }
-            | &Self::RenameErr {
-                editor_needs_reset, ..
-            } => editor_needs_reset,
+            Naming::Renaming(rename) => Some(rename),
+            _ => None,
         }
     }
-}
 
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
-pub enum IsRenaming {
-    NotRenaming,
-    Renaming { err: Option<String> },
-}
-
-impl IsRenaming {
-    fn is_node_renaming(rename_state: &RenameState, node_path: &Path) -> Self {
-        match rename_state {
-            RenameState::NotRenaming => Self::NotRenaming,
-            RenameState::Renaming { path, .. }
-            | RenameState::RenameRequestPending { path, .. } => {
-                if path == node_path {
-                    Self::Renaming { err: None }
-                } else {
-                    Self::NotRenaming
-                }
-            }
-            RenameState::RenameErr { path, err, .. } => {
-                if path == node_path {
-                    Self::Renaming {
-                        err: Some(err.clone()),
-                    }
-                } else {
-                    Self::NotRenaming
-                }
-            }
+    /// The extra node that should be added after the node at `path`
+    pub fn extra_node(
+        &self,
+        is_dir: bool,
+        level: usize,
+        path: &Path,
+    ) -> Option<FileNodeViewData> {
+        match self {
+            Naming::NewNode(n) if n.base_path == path => Some(FileNodeViewData {
+                kind: FileNodeViewKind::Naming {
+                    err: n.state.err().map(ToString::to_string),
+                },
+                is_dir: n.is_dir,
+                open: false,
+                level: level + 1,
+            }),
+            Naming::Duplicating(d) if d.path == path => Some(FileNodeViewData {
+                kind: FileNodeViewKind::Duplicating {
+                    source: d.path.to_path_buf(),
+                    err: d.state.err().map(ToString::to_string),
+                },
+                is_dir,
+                open: false,
+                level: level + 1,
+            }),
+            _ => None,
         }
     }
 }
 
 #[derive(Debug)]
 pub struct FileNodeViewData {
-    pub path: PathBuf,
+    pub kind: FileNodeViewKind,
     pub is_dir: bool,
     pub open: bool,
-    pub is_renaming: IsRenaming,
     pub level: usize,
 }
 
@@ -232,9 +261,14 @@ pub struct FileNodeViewData {
 pub struct FileNodeItem {
     pub path: PathBuf,
     pub is_dir: bool,
+    /// Whether the directory's children have been read.  
+    /// Does nothing if not a directory.
     pub read: bool,
+    /// Whether the directory is open in the explorer view.
     pub open: bool,
     pub children: HashMap<PathBuf, FileNodeItem>,
+    /// The number of child (directories) that are open themselves  
+    /// Used for sizing of the explorer list
     pub children_open_count: usize,
 }
 
@@ -264,12 +298,16 @@ impl Ord for FileNodeItem {
 }
 
 impl FileNodeItem {
+    /// Collect the children, sorted by name.  
+    /// Note: this will be empty if the directory has not been read.
     pub fn sorted_children(&self) -> Vec<&FileNodeItem> {
         let mut children = self.children.values().collect::<Vec<&FileNodeItem>>();
         children.sort();
         children
     }
 
+    /// Collect the children, sorted by name.  
+    /// Note: this will be empty if the directory has not been read.
     pub fn sorted_children_mut(&mut self) -> Vec<&mut FileNodeItem> {
         let mut children = self
             .children
@@ -316,16 +354,20 @@ impl FileNodeItem {
         Some(ancestors.into_iter().rev())
     }
 
+    /// Recursively get the node at `path`.
     pub fn get_file_node(&self, path: &Path) -> Option<&FileNodeItem> {
         self.ancestors_rev(path)?
             .try_fold(self, |node, path| node.children.get(path))
     }
 
+    /// Recursively get the (mutable) node at `path`.
     pub fn get_file_node_mut(&mut self, path: &Path) -> Option<&mut FileNodeItem> {
         self.ancestors_rev(path)?
             .try_fold(self, |node, path| node.children.get_mut(path))
     }
 
+    /// Remove a specific child from the node.  
+    /// The path is recursive and will remove the child from parent indicated by the path.
     pub fn remove_child(&mut self, path: &Path) -> Option<FileNodeItem> {
         let parent = path.parent()?;
         let node = self.get_file_node_mut(parent)?;
@@ -337,6 +379,7 @@ impl FileNodeItem {
         Some(node)
     }
 
+    /// Add a new (unread & unopened) child to the node.
     pub fn add_child(&mut self, path: &Path, is_dir: bool) -> Option<()> {
         let parent = path.parent()?;
         let node = self.get_file_node_mut(parent)?;
@@ -358,6 +401,8 @@ impl FileNodeItem {
         Some(())
     }
 
+    /// Set the children of the node.  
+    /// Note: this opens the node.
     pub fn set_item_children(
         &mut self,
         path: &Path,
@@ -398,7 +443,7 @@ impl FileNodeItem {
     pub fn append_view_slice(
         &self,
         view_items: &mut Vec<FileNodeViewData>,
-        rename_state: &RenameState,
+        naming: &Naming,
         min: usize,
         max: usize,
         current: usize,
@@ -411,32 +456,107 @@ impl FileNodeItem {
             return current + self.children_open_count;
         }
 
-        let mut i = current;
         if current >= min {
+            let kind = if let Naming::Renaming(r) = &naming {
+                if r.path == self.path {
+                    FileNodeViewKind::Renaming {
+                        path: self.path.clone(),
+                        err: r.state.err().map(ToString::to_string),
+                    }
+                } else {
+                    FileNodeViewKind::Path(self.path.clone())
+                }
+            } else {
+                FileNodeViewKind::Path(self.path.clone())
+            };
             view_items.push(FileNodeViewData {
-                path: self.path.clone(),
+                kind,
                 is_dir: self.is_dir,
                 open: self.open,
-                is_renaming: IsRenaming::is_node_renaming(rename_state, &self.path),
                 level,
             });
         }
 
-        if self.open {
-            for item in self.sorted_children() {
-                i = item.append_view_slice(
-                    view_items,
-                    rename_state,
-                    min,
-                    max,
-                    i + 1,
-                    level + 1,
-                );
-                if i > max {
-                    return i;
+        self.append_children_view_slice(view_items, naming, min, max, current, level)
+    }
+
+    /// Append the children of this item with the given level
+    pub fn append_children_view_slice(
+        &self,
+        view_items: &mut Vec<FileNodeViewData>,
+        naming: &Naming,
+        min: usize,
+        max: usize,
+        mut i: usize,
+        level: usize,
+    ) -> usize {
+        let mut naming_extra = naming.extra_node(self.is_dir, level, &self.path);
+
+        if !self.open {
+            // If the folder isn't open, then we just put it right at the top
+            if i >= min {
+                if let Some(naming_extra) = naming_extra {
+                    view_items.push(naming_extra);
+                    i += 1;
+                }
+            }
+            return i;
+        }
+
+        let naming_is_dir = naming_extra.as_ref().map(|n| n.is_dir).unwrap_or(false);
+        // Immediately put the naming entry first if it's a directory
+        if naming_is_dir {
+            if let Some(node) = naming_extra.take() {
+                // Actually add the node if it's within the range
+                if i >= min {
+                    view_items.push(node);
+                    i += 1;
+                }
+            }
+        }
+
+        let mut after_dirs = false;
+
+        for item in self.sorted_children() {
+            // If we're naming a file at the root, then wait until we've added the directories
+            // before adding the input node
+            if naming_extra.is_some()
+                && !naming_is_dir
+                && !item.is_dir
+                && !after_dirs
+            {
+                after_dirs = true;
+
+                // If we're creating a new file node, then we show it after the directories
+                // TODO(minor): should this be i >= min or i + 1 >= min?
+                if i >= min {
+                    if let Some(node) = naming_extra.take() {
+                        view_items.push(node);
+                        i += 1;
+                    }
                 }
             }
+            i = item.append_view_slice(
+                view_items,
+                naming,
+                min,
+                max,
+                i + 1,
+                level + 1,
+            );
+            if i > max {
+                return i;
+            }
         }
+
+        // If it has not been added yet, add it now.
+        if i >= min {
+            if let Some(node) = naming_extra {
+                view_items.push(node);
+                i += 1;
+            }
+        }
+
         i
     }
 }


Test patch: