@@ -1159,7 +1159,7 @@ impl ProjectPanel {
}
}
- fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
+ fn rename_impl(&mut self, selection: Option<Range<usize>>, cx: &mut ViewContext<Self>) {
if let Some(SelectedEntry {
worktree_id,
entry_id,
@@ -1183,13 +1183,16 @@ impl ProjectPanel {
.map(|s| s.to_string_lossy())
.unwrap_or_default()
.to_string();
- let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
- let selection_end =
- file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
+ let selection = selection.unwrap_or_else(|| {
+ let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
+ let selection_end =
+ file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
+ 0..selection_end
+ });
self.filename_editor.update(cx, |editor, cx| {
editor.set_text(file_name, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.select_ranges([0..selection_end])
+ s.select_ranges([selection])
});
editor.focus(cx);
});
@@ -1201,6 +1204,10 @@ impl ProjectPanel {
}
}
+ fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
+ self.rename_impl(None, cx);
+ }
+
fn trash(&mut self, action: &Trash, cx: &mut ViewContext<Self>) {
self.remove(true, action.skip_prompt, cx);
}
@@ -1762,7 +1769,7 @@ impl ProjectPanel {
source: &SelectedEntry,
(worktree, target_entry): (Model<Worktree>, &Entry),
cx: &AppContext,
- ) -> Option<PathBuf> {
+ ) -> Option<(PathBuf, Option<Range<usize>>)> {
let mut new_path = target_entry.path.to_path_buf();
// If we're pasting into a file, or a directory into itself, go up one level.
if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
@@ -1778,6 +1785,8 @@ impl ProjectPanel {
new_path.push(&clipboard_entry_file_name);
let extension = new_path.extension().map(|e| e.to_os_string());
let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
+ let file_name_len = file_name_without_extension.to_string_lossy().len();
+ let mut disambiguation_range = None;
let mut ix = 0;
{
let worktree = worktree.read(cx);
@@ -1785,9 +1794,17 @@ impl ProjectPanel {
new_path.pop();
let mut new_file_name = file_name_without_extension.to_os_string();
- new_file_name.push(" copy");
+
+ let disambiguation = " copy";
+ let mut disambiguation_len = disambiguation.len();
+
+ new_file_name.push(disambiguation);
+
if ix > 0 {
- new_file_name.push(format!(" {}", ix));
+ let extra_disambiguation = format!(" {}", ix);
+ disambiguation_len += extra_disambiguation.len();
+
+ new_file_name.push(extra_disambiguation);
}
if let Some(extension) = extension.as_ref() {
new_file_name.push(".");
@@ -1795,10 +1812,11 @@ impl ProjectPanel {
}
new_path.push(new_file_name);
+ disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
ix += 1;
}
}
- Some(new_path)
+ Some((new_path, disambiguation_range))
}
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
@@ -1816,9 +1834,10 @@ impl ProjectPanel {
}
let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
IndexMap::default();
+ let mut disambiguation_range = None;
let clip_is_cut = clipboard_entries.is_cut();
for clipboard_entry in clipboard_entries.items() {
- let new_path =
+ let (new_path, new_disambiguation_range) =
self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
let clip_entry_id = clipboard_entry.entry_id;
let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
@@ -1855,8 +1874,11 @@ impl ProjectPanel {
};
let needs_delete = !is_same_worktree && clip_is_cut;
paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
+ disambiguation_range = new_disambiguation_range.or(disambiguation_range);
}
+ let item_count = paste_entry_tasks.len();
+
cx.spawn(|project_panel, mut cx| async move {
let mut last_succeed = None;
let mut need_delete_ids = Vec::new();
@@ -1877,17 +1899,6 @@ impl ProjectPanel {
}
}
}
- // update selection
- if let Some(entry_id) = last_succeed {
- project_panel
- .update(&mut cx, |project_panel, _cx| {
- project_panel.selection = Some(SelectedEntry {
- worktree_id,
- entry_id,
- });
- })
- .ok();
- }
// remove entry for cut in difference worktree
for entry_id in need_delete_ids {
project_panel
@@ -1899,6 +1910,22 @@ impl ProjectPanel {
})??
.await?;
}
+ // update selection
+ if let Some(entry_id) = last_succeed {
+ project_panel
+ .update(&mut cx, |project_panel, cx| {
+ project_panel.selection = Some(SelectedEntry {
+ worktree_id,
+ entry_id,
+ });
+
+ // if only one entry was pasted and it was disambiguated, open the rename editor
+ if item_count == 1 && disambiguation_range.is_some() {
+ project_panel.rename_impl(disambiguation_range, cx);
+ }
+ })
+ .ok();
+ }
anyhow::Ok(())
})
@@ -2606,23 +2633,55 @@ impl ProjectPanel {
let _ = maybe!({
let project = self.project.read(cx);
let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
+ let worktree_id = target_worktree.read(cx).id();
let target_entry = target_worktree
.read(cx)
.entry_for_id(target_entry_id)?
.clone();
+
+ let mut copy_tasks = Vec::new();
+ let mut disambiguation_range = None;
for selection in selections.items() {
- let new_path = self.create_paste_path(
+ let (new_path, new_disambiguation_range) = self.create_paste_path(
selection,
(target_worktree.clone(), &target_entry),
cx,
)?;
- self.project
- .update(cx, |project, cx| {
- project.copy_entry(selection.entry_id, None, new_path, cx)
- })
- .detach_and_log_err(cx)
+
+ let task = self.project.update(cx, |project, cx| {
+ project.copy_entry(selection.entry_id, None, new_path, cx)
+ });
+ copy_tasks.push(task);
+ disambiguation_range = new_disambiguation_range.or(disambiguation_range);
}
+ let item_count = copy_tasks.len();
+
+ cx.spawn(|project_panel, mut cx| async move {
+ let mut last_succeed = None;
+ for task in copy_tasks.into_iter() {
+ if let Some(Some(entry)) = task.await.log_err() {
+ last_succeed = Some(entry.id);
+ }
+ }
+ // update selection
+ if let Some(entry_id) = last_succeed {
+ project_panel
+ .update(&mut cx, |project_panel, cx| {
+ project_panel.selection = Some(SelectedEntry {
+ worktree_id,
+ entry_id,
+ });
+
+ // if only one entry was dragged and it was disambiguated, open the rename editor
+ if item_count == 1 && disambiguation_range.is_some() {
+ project_panel.rename_impl(disambiguation_range, cx);
+ }
+ })
+ .ok();
+ }
+ })
+ .detach();
Some(())
});
} else {
@@ -5283,11 +5342,22 @@ mod tests {
//
"v root1",
" one.txt",
- " one copy.txt <== selected",
+ " [EDITOR: 'one copy.txt'] <== selected",
" one.two.txt",
]
);
+ panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ let file_name_selections = editor.selections.all::<usize>(cx);
+ assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+ let file_name_selection = &file_name_selections[0];
+ assert_eq!(file_name_selection.start, "one".len(), "Should select the file name disambiguation after the original file name");
+ assert_eq!(file_name_selection.end, "one copy".len(), "Should select the file name disambiguation until the extension");
+ });
+ assert!(panel.confirm_edit(cx).is_none());
+ });
+
panel.update(cx, |panel, cx| {
panel.paste(&Default::default(), cx);
});
@@ -5300,10 +5370,12 @@ mod tests {
"v root1",
" one.txt",
" one copy.txt",
- " one copy 1.txt <== selected",
+ " [EDITOR: 'one copy 1.txt'] <== selected",
" one.two.txt",
]
);
+
+ panel.update(cx, |panel, cx| assert!(panel.confirm_edit(cx).is_none()));
}
#[gpui::test]
@@ -5495,11 +5567,14 @@ mod tests {
" four.txt",
" one.txt",
" three.txt",
- " three copy.txt <== selected",
+ " [EDITOR: 'three copy.txt'] <== selected",
" two.txt",
]
);
+ panel.update(cx, |panel, cx| panel.cancel(&menu::Cancel {}, cx));
+ cx.executor().run_until_parked();
+
select_path(&panel, "root1/a", cx);
panel.update(cx, |panel, cx| {
panel.copy(&Default::default(), cx);
@@ -5603,6 +5678,48 @@ mod tests {
select_path(&panel, "root", cx);
panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ //
+ "v root",
+ " > a",
+ " > [EDITOR: 'a copy'] <== selected",
+ " v b",
+ " v a",
+ " v inner_dir",
+ " four.txt",
+ " three.txt",
+ " one.txt",
+ " two.txt"
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("c", cx));
+ panel.confirm_edit(cx).unwrap()
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ //
+ "v root",
+ " > a",
+ " > [PROCESSING: 'c'] <== selected",
+ " v b",
+ " v a",
+ " v inner_dir",
+ " four.txt",
+ " three.txt",
+ " one.txt",
+ " two.txt"
+ ]
+ );
+
+ confirm.await.unwrap();
+
panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
cx.executor().run_until_parked();
assert_eq!(
@@ -5611,18 +5728,18 @@ mod tests {
//
"v root",
" > a",
- " v a copy",
- " > a <== selected",
- " > inner_dir",
- " one.txt",
- " two.txt",
" v b",
" v a",
" v inner_dir",
" four.txt",
" three.txt",
" one.txt",
- " two.txt"
+ " two.txt",
+ " v c",
+ " > a <== selected",
+ " > inner_dir",
+ " one.txt",
+ " two.txt",
]
);
}
@@ -5703,6 +5820,33 @@ mod tests {
],
"Should copy dir1 as well as c.txt into dir2"
);
+
+ // Disambiguating multiple files should not open the rename editor.
+ select_path(&panel, "test/dir2", cx);
+ panel.update(cx, |panel, cx| {
+ panel.paste(&Default::default(), cx);
+ });
+ cx.executor().run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..15, cx),
+ &[
+ "v test",
+ " v dir1 <== marked",
+ " a.txt",
+ " b.txt",
+ " v dir2",
+ " v dir1",
+ " a.txt",
+ " b.txt",
+ " > dir1 copy <== selected",
+ " c.txt",
+ " c copy.txt",
+ " c.txt <== marked",
+ " d.txt",
+ ],
+ "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
+ );
}
#[gpui::test]