mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +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 gpui::{
|
||||
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
|
||||
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement, KeyContext, ListSizingBehavior, Model,
|
||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful,
|
||||
Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _,
|
||||
WeakView, WindowContext,
|
||||
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, DragMoveEvent,
|
||||
EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
@ -50,6 +50,7 @@ pub struct ProjectPanel {
|
||||
focus_handle: FocusHandle,
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
||||
last_worktree_root_id: Option<ProjectEntryId>,
|
||||
last_external_paths_drag_over_entry: Option<ProjectEntryId>,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
||||
// Currently selected entry in a file tree
|
||||
@ -260,6 +261,7 @@ impl ProjectPanel {
|
||||
focus_handle,
|
||||
visible_entries: Default::default(),
|
||||
last_worktree_root_id: Default::default(),
|
||||
last_external_paths_drag_over_entry: None,
|
||||
expanded_dir_ids: Default::default(),
|
||||
unfolded_dir_ids: Default::default(),
|
||||
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(
|
||||
&mut self,
|
||||
selections: &DraggedSelection,
|
||||
@ -1949,6 +2027,7 @@ impl ProjectPanel {
|
||||
.canonical_path
|
||||
.as_ref()
|
||||
.map(|f| f.to_string_lossy().to_string());
|
||||
let path = details.path.clone();
|
||||
|
||||
let depth = details.depth;
|
||||
let worktree_id = details.worktree_id;
|
||||
@ -1960,6 +2039,57 @@ impl ProjectPanel {
|
||||
};
|
||||
div()
|
||||
.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| {
|
||||
cx.new_view(|_| DraggedProjectEntryView {
|
||||
details: details.clone(),
|
||||
@ -2257,6 +2387,29 @@ impl Render for ProjectPanel {
|
||||
.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(
|
||||
&mut self,
|
||||
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(
|
||||
&mut self,
|
||||
entry_id: ProjectEntryId,
|
||||
|
Loading…
Reference in New Issue
Block a user