mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
project panel: Support dropping files from finder (#12880)
Partially addresses #7386 https://github.com/zed-industries/zed/assets/53836821/fc2e9864-40a8-4ada-ac95-a76a31c44437 Release Notes: - Added support for dropping files from the finder onto the project panel
This commit is contained in:
parent
95c69d0696
commit
38d9ee3731
@ -11,11 +11,11 @@ use collections::{hash_map, BTreeSet, HashMap};
|
|||||||
use git::repository::GitFileStatus;
|
use git::repository::GitFileStatus;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
|
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
|
||||||
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter,
|
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, DragMoveEvent,
|
||||||
FocusHandle, FocusableView, InteractiveElement, KeyContext, ListSizingBehavior, Model,
|
EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext,
|
||||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful,
|
ListSizingBehavior, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||||
Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _,
|
PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
|
||||||
WeakView, WindowContext,
|
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
@ -50,6 +50,7 @@ pub struct ProjectPanel {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
||||||
last_worktree_root_id: Option<ProjectEntryId>,
|
last_worktree_root_id: Option<ProjectEntryId>,
|
||||||
|
last_external_paths_drag_over_entry: Option<ProjectEntryId>,
|
||||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||||
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
||||||
// Currently selected entry in a file tree
|
// Currently selected entry in a file tree
|
||||||
@ -260,6 +261,7 @@ impl ProjectPanel {
|
|||||||
focus_handle,
|
focus_handle,
|
||||||
visible_entries: Default::default(),
|
visible_entries: Default::default(),
|
||||||
last_worktree_root_id: Default::default(),
|
last_worktree_root_id: Default::default(),
|
||||||
|
last_external_paths_drag_over_entry: None,
|
||||||
expanded_dir_ids: Default::default(),
|
expanded_dir_ids: Default::default(),
|
||||||
unfolded_dir_ids: Default::default(),
|
unfolded_dir_ids: Default::default(),
|
||||||
selection: None,
|
selection: None,
|
||||||
@ -1718,6 +1720,82 @@ impl ProjectPanel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drop_external_files(
|
||||||
|
&mut self,
|
||||||
|
paths: &[PathBuf],
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let mut paths: Vec<Arc<Path>> = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| Arc::from(path.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let open_file_after_drop = paths.len() == 1 && paths[0].is_file();
|
||||||
|
|
||||||
|
let Some((target_directory, worktree)) = maybe!({
|
||||||
|
let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?;
|
||||||
|
let entry = worktree.read(cx).entry_for_id(entry_id)?;
|
||||||
|
let path = worktree.read(cx).absolutize(&entry.path).ok()?;
|
||||||
|
let target_directory = if path.is_dir() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
path.parent()?.to_path_buf()
|
||||||
|
};
|
||||||
|
Some((target_directory, worktree))
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut paths_to_replace = Vec::new();
|
||||||
|
for path in &paths {
|
||||||
|
if let Some(name) = path.file_name() {
|
||||||
|
let mut target_path = target_directory.clone();
|
||||||
|
target_path.push(name);
|
||||||
|
if target_path.exists() {
|
||||||
|
paths_to_replace.push((name.to_string_lossy().to_string(), path.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| {
|
||||||
|
async move {
|
||||||
|
for (filename, original_path) in &paths_to_replace {
|
||||||
|
let answer = cx
|
||||||
|
.prompt(
|
||||||
|
PromptLevel::Info,
|
||||||
|
format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(),
|
||||||
|
None,
|
||||||
|
&["Replace", "Cancel"],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if answer == 1 {
|
||||||
|
if let Some(item_idx) = paths.iter().position(|p| p == original_path) {
|
||||||
|
paths.remove(item_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = worktree.update(&mut cx, |worktree, cx| {
|
||||||
|
worktree.copy_external_entries(target_directory, paths, true, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let opened_entries = task.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
if open_file_after_drop && !opened_entries.is_empty() {
|
||||||
|
this.open_entry(opened_entries[0], true, true, false, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn drag_onto(
|
fn drag_onto(
|
||||||
&mut self,
|
&mut self,
|
||||||
selections: &DraggedSelection,
|
selections: &DraggedSelection,
|
||||||
@ -1949,6 +2027,7 @@ impl ProjectPanel {
|
|||||||
.canonical_path
|
.canonical_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|f| f.to_string_lossy().to_string());
|
.map(|f| f.to_string_lossy().to_string());
|
||||||
|
let path = details.path.clone();
|
||||||
|
|
||||||
let depth = details.depth;
|
let depth = details.depth;
|
||||||
let worktree_id = details.worktree_id;
|
let worktree_id = details.worktree_id;
|
||||||
@ -1960,6 +2039,57 @@ impl ProjectPanel {
|
|||||||
};
|
};
|
||||||
div()
|
div()
|
||||||
.id(entry_id.to_proto() as usize)
|
.id(entry_id.to_proto() as usize)
|
||||||
|
.on_drag_move::<ExternalPaths>(cx.listener(
|
||||||
|
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||||
|
if event.bounds.contains(&event.event.position) {
|
||||||
|
if this.last_external_paths_drag_over_entry == Some(entry_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.last_external_paths_drag_over_entry = Some(entry_id);
|
||||||
|
this.marked_entries.clear();
|
||||||
|
|
||||||
|
let Some((worktree, path, entry)) = maybe!({
|
||||||
|
let worktree = this
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(selection.worktree_id, cx)?;
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
let abs_path = worktree.absolutize(&path).log_err()?;
|
||||||
|
let path = if abs_path.is_dir() {
|
||||||
|
path.as_ref()
|
||||||
|
} else {
|
||||||
|
path.parent()?
|
||||||
|
};
|
||||||
|
let entry = worktree.entry_for_path(path)?;
|
||||||
|
Some((worktree, path, entry))
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.marked_entries.insert(SelectedEntry {
|
||||||
|
entry_id: entry.id,
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for entry in worktree.child_entries(path) {
|
||||||
|
this.marked_entries.insert(SelectedEntry {
|
||||||
|
entry_id: entry.id,
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.on_drop(
|
||||||
|
cx.listener(move |this, external_paths: &ExternalPaths, cx| {
|
||||||
|
this.last_external_paths_drag_over_entry = None;
|
||||||
|
this.marked_entries.clear();
|
||||||
|
this.drop_external_files(external_paths.paths(), entry_id, cx);
|
||||||
|
cx.stop_propagation();
|
||||||
|
}),
|
||||||
|
)
|
||||||
.on_drag(dragged_selection, move |selection, cx| {
|
.on_drag(dragged_selection, move |selection, cx| {
|
||||||
cx.new_view(|_| DraggedProjectEntryView {
|
cx.new_view(|_| DraggedProjectEntryView {
|
||||||
details: details.clone(),
|
details: details.clone(),
|
||||||
@ -2257,6 +2387,29 @@ impl Render for ProjectPanel {
|
|||||||
.log_err();
|
.log_err();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
.drag_over::<ExternalPaths>(|style, _, cx| {
|
||||||
|
style.bg(cx.theme().colors().drop_target_background)
|
||||||
|
})
|
||||||
|
.on_drop(
|
||||||
|
cx.listener(move |this, external_paths: &ExternalPaths, cx| {
|
||||||
|
this.last_external_paths_drag_over_entry = None;
|
||||||
|
this.marked_entries.clear();
|
||||||
|
if let Some(task) = this
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_workspace_for_paths(
|
||||||
|
true,
|
||||||
|
external_paths.paths().to_owned(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
task.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
cx.stop_propagation();
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -838,6 +838,23 @@ impl Worktree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn copy_external_entries(
|
||||||
|
&mut self,
|
||||||
|
target_directory: PathBuf,
|
||||||
|
paths: Vec<Arc<Path>>,
|
||||||
|
overwrite_existing_files: bool,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
) -> Task<Result<Vec<ProjectEntryId>>> {
|
||||||
|
match self {
|
||||||
|
Worktree::Local(this) => {
|
||||||
|
this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx)
|
||||||
|
}
|
||||||
|
_ => Task::ready(Err(anyhow!(
|
||||||
|
"Copying external entries is not supported for remote worktrees"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn expand_entry(
|
pub fn expand_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
@ -1579,6 +1596,87 @@ impl LocalWorktree {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn copy_external_entries(
|
||||||
|
&mut self,
|
||||||
|
target_directory: PathBuf,
|
||||||
|
paths: Vec<Arc<Path>>,
|
||||||
|
overwrite_existing_files: bool,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
) -> Task<Result<Vec<ProjectEntryId>>> {
|
||||||
|
let worktree_path = self.abs_path().clone();
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
let paths = paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|source| {
|
||||||
|
let file_name = source.file_name()?;
|
||||||
|
let mut target = target_directory.clone();
|
||||||
|
target.push(file_name);
|
||||||
|
|
||||||
|
// Do not allow copying the same file to itself.
|
||||||
|
if source.as_ref() != target.as_path() {
|
||||||
|
Some((source, target))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let paths_to_refresh = paths
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(|this, cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
for (source, target) in paths {
|
||||||
|
copy_recursive(
|
||||||
|
fs.as_ref(),
|
||||||
|
&source,
|
||||||
|
&target,
|
||||||
|
fs::CopyOptions {
|
||||||
|
overwrite: overwrite_existing_files,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
anyhow!("Failed to copy file from {source:?} to {target:?}")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
let mut refresh = cx.read_model(
|
||||||
|
&this.upgrade().with_context(|| "Dropped worktree")?,
|
||||||
|
|this, _| {
|
||||||
|
Ok::<postage::barrier::Receiver, anyhow::Error>(
|
||||||
|
this.as_local()
|
||||||
|
.with_context(|| "Worktree is not local")?
|
||||||
|
.refresh_entries_for_paths(paths_to_refresh.clone()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)??;
|
||||||
|
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
refresh.next().await;
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
let this = this.upgrade().with_context(|| "Dropped worktree")?;
|
||||||
|
cx.read_model(&this, |this, _| {
|
||||||
|
paths_to_refresh
|
||||||
|
.iter()
|
||||||
|
.filter_map(|path| Some(this.entry_for_path(path)?.id))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn expand_entry(
|
fn expand_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
|
Loading…
Reference in New Issue
Block a user