Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/keymaps/default-linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,8 @@
"alt-ctrl-shift-c": "workspace::CopyRelativePath",
"undo": "project_panel::Undo",
"ctrl-z": "project_panel::Undo",
"redo": "project_panel::Redo",
"ctrl-shift-z": "project_panel::Redo",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
Expand Down
1 change: 1 addition & 0 deletions assets/keymaps/default-macos.json
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@
"cmd-alt-c": "workspace::CopyPath",
"alt-cmd-shift-c": "workspace::CopyRelativePath",
"cmd-z": "project_panel::Undo",
"cmd-shift-z": "project_panel::Redo",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
Expand Down
1 change: 1 addition & 0 deletions assets/keymaps/default-windows.json
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@
"shift-alt-c": "project_panel::CopyPath",
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
"ctrl-z": "project_panel::Undo",
"ctrl-shift-z": "project_panel::Redo",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
Expand Down
46 changes: 27 additions & 19 deletions crates/project_panel/src/project_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ actions!(
CompareMarkedFiles,
/// Undoes the last file operation.
Undo,
/// Redoes the last undone file operation.
Redo,
]
);

Expand Down Expand Up @@ -900,7 +902,7 @@ impl ProjectPanel {
unfolded_dir_ids: Default::default(),
},
update_visible_entries_task: Default::default(),
undo_manager: UndoManager::new(workspace.weak_handle()),
undo_manager: UndoManager::new(workspace.weak_handle(), cx.weak_entity()),
};
this.update_visible_entries(None, false, false, window, cx);

Expand Down Expand Up @@ -1243,6 +1245,11 @@ impl ProjectPanel {
"Undo",
Box::new(Undo),
)
.action_disabled_when(
!self.undo_manager.can_redo(),
"Redo",
Box::new(Redo),
)
})
.when(is_remote, |menu| {
menu.separator()
Expand Down Expand Up @@ -1942,8 +1949,8 @@ impl ProjectPanel {
if new_entry.is_ok() {
let operation = if let Some(old_entry) = edited_entry {
ProjectPanelOperation::Rename {
old_path: (worktree_id, old_entry.path).into(),
new_path: new_project_path,
from: (worktree_id, old_entry.path).into(),
to: new_project_path,
}
} else {
ProjectPanelOperation::Create {
Expand Down Expand Up @@ -2208,6 +2215,11 @@ impl ProjectPanel {
cx.notify();
}

pub fn redo(&mut self, _: &Redo, _window: &mut Window, cx: &mut Context<Self>) {
self.undo_manager.redo(cx);
cx.notify();
}

fn rename_impl(
&mut self,
selection: Option<Range<usize>>,
Expand Down Expand Up @@ -3137,8 +3149,8 @@ impl ProjectPanel {
enum PasteTask {
Rename {
task: Task<Result<CreatedEntry>>,
old_path: ProjectPath,
new_path: ProjectPath,
from: ProjectPath,
to: ProjectPath,
},
Copy {
task: Task<Result<Option<Entry>>>,
Expand All @@ -3155,14 +3167,14 @@ impl ProjectPanel {
let clip_entry_id = clipboard_entry.entry_id;
let destination: ProjectPath = (worktree_id, new_path).into();
let task = if clipboard_entries.is_cut() {
let old_path = self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
let original_path = self.project.read(cx).path_for_entry(clip_entry_id, cx)?;
let task = self.project.update(cx, |project, cx| {
project.rename_entry(clip_entry_id, destination.clone(), cx)
});
PasteTask::Rename {
task,
old_path,
new_path: destination,
from: original_path,
to: destination,
}
} else {
let task = self.project.update(cx, |project, cx| {
Expand All @@ -3183,17 +3195,12 @@ impl ProjectPanel {

for task in paste_tasks {
match task {
PasteTask::Rename {
task,
old_path,
new_path,
} => {
PasteTask::Rename { task, from, to } => {
if let Some(CreatedEntry::Included(entry)) = task
.await
.notify_workspace_async_err(workspace.clone(), &mut cx)
{
operations
.push(ProjectPanelOperation::Rename { old_path, new_path });
operations.push(ProjectPanelOperation::Rename { from, to });
last_succeed = Some(entry);
}
}
Expand Down Expand Up @@ -4625,8 +4632,8 @@ impl ProjectPanel {
(old_paths.get(&entry_id), destination_worktree_id)
{
operations.push(ProjectPanelOperation::Rename {
old_path: old_path.clone(),
new_path: (worktree_id, new_entry.path).into(),
from: old_path.clone(),
to: (worktree_id, new_entry.path).into(),
});
}
}
Expand All @@ -4652,8 +4659,8 @@ impl ProjectPanel {
(old_paths.get(&entry_id), destination_worktree_id)
{
operations.push(ProjectPanelOperation::Rename {
old_path: old_path.clone(),
new_path: (worktree_id, new_entry.path.clone()).into(),
from: old_path.clone(),
to: (worktree_id, new_entry.path.clone()).into(),
});
}
move_results.push((entry_id, new_entry));
Expand Down Expand Up @@ -6684,6 +6691,7 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::compare_marked_files))
.when(cx.has_flag::<ProjectPanelUndoRedoFeatureFlag>(), |el| {
el.on_action(cx.listener(Self::undo))
.on_action(cx.listener(Self::redo))
})
.when(!project.is_read_only(cx), |el| {
el.on_action(cx.listener(Self::new_file))
Expand Down
109 changes: 95 additions & 14 deletions crates/project_panel/src/project_panel_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::undo::test::{build_create_operation, build_rename_operation};
use collections::HashSet;
use editor::MultiBufferOffset;
use gpui::{Empty, Entity, TestAppContext, VisualTestContext};
Expand Down Expand Up @@ -1995,7 +1996,7 @@ async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext)
}

#[gpui::test]
async fn test_undo_rename(cx: &mut gpui::TestAppContext) {
async fn test_undo_redo_rename(cx: &mut gpui::TestAppContext) {
init_test(cx);

let fs = FakeFs::new(cx.executor());
Expand Down Expand Up @@ -2054,6 +2055,21 @@ async fn test_undo_rename(cx: &mut gpui::TestAppContext) {
None,
"Renamed file should no longer exist after undo"
);

panel.update_in(cx, |panel, window, cx| {
panel.redo(&Redo, window, cx);
});
cx.run_until_parked();

assert!(
find_project_entry(&panel, "root/renamed.txt", cx).is_some(),
"File should be renamed to renamed.txt after redo"
);
assert_eq!(
find_project_entry(&panel, "root/a.txt", cx),
None,
"Original file should no longer exist after redo"
);
}

#[gpui::test]
Expand Down Expand Up @@ -2504,18 +2520,8 @@ async fn test_undo_batch(cx: &mut gpui::TestAppContext) {
// being provided in the operations.
panel.update(cx, |panel, _cx| {
panel.undo_manager.record_batch(vec![
ProjectPanelOperation::Create {
project_path: ProjectPath {
worktree_id,
path: Arc::from(rel_path("src/main.rs")),
},
},
ProjectPanelOperation::Create {
project_path: ProjectPath {
worktree_id,
path: Arc::from(rel_path("src/")),
},
},
build_create_operation(worktree_id, "src/main.rs"),
build_create_operation(worktree_id, "src/"),
]);
});

Expand Down Expand Up @@ -2543,6 +2549,81 @@ async fn test_undo_batch(cx: &mut gpui::TestAppContext) {
);
}

#[gpui::test]
async fn test_redo_batch(cx: &mut gpui::TestAppContext) {
init_test(cx);

let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"file_c.txt": "",
"file_d.txt": "",
}),
)
.await;

let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
let worktree_id = project.update(cx, |project, cx| {
project.visible_worktrees(cx).next().unwrap().read(cx).id()
});
cx.run_until_parked();

// At the time of writing, only the `ProjectPanelOperation::Rename`
// operation supports redoing. As such, that's what we'll use in the batch
// operation, to ensure that undoing and redoing a batch operation works as
// expected.
panel.update(cx, |panel, _cx| {
panel.undo_manager.record_batch(vec![
build_rename_operation(worktree_id, "file_a.txt", "file_c.txt"),
build_rename_operation(worktree_id, "file_b.txt", "file_d.txt"),
]);
});

// Before proceeding, ensure that both `file_c.txt` as well as `file_d.txt`
// exist in the filesystem, so we can later ensure that undoing renames
// these files and redoing renames again to how the test started.
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/root/file_c.txt")),
PathBuf::from(path!("/root/file_d.txt"))
]
);

panel.update_in(cx, |panel, window, cx| {
panel.undo(&Undo, window, cx);
});
cx.run_until_parked();

assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/root/file_a.txt")),
PathBuf::from(path!("/root/file_b.txt"))
]
);

panel.update_in(cx, |panel, window, cx| {
panel.redo(&Redo, window, cx);
});
cx.run_until_parked();

assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/root/file_c.txt")),
PathBuf::from(path!("/root/file_d.txt"))
]
);
}

#[gpui::test]
async fn test_paste_external_paths(cx: &mut gpui::TestAppContext) {
init_test(cx);
Expand Down Expand Up @@ -9556,7 +9637,7 @@ async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
);
}

fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
pub(crate) fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
let path = rel_path(path);
panel.update_in(cx, |panel, window, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
Expand Down
Loading
Loading