Merge pull request #1867 from zed-industries/drag-project-entry-to-pane

Kay Simmons created

Drag project entry to pane

Change summary

Cargo.lock                                         |   1 
crates/collab/src/integration_tests.rs             |  24 
crates/drag_and_drop/src/drag_and_drop.rs          | 209 +++++++++++----
crates/editor/src/editor.rs                        |   2 
crates/editor/src/test/editor_lsp_test_context.rs  |   4 
crates/file_finder/src/file_finder.rs              |   2 
crates/project_panel/Cargo.toml                    |   1 
crates/project_panel/src/project_panel.rs          | 172 ++++++++----
crates/theme/src/theme.rs                          |   1 
crates/vim/src/test/vim_test_context.rs            |   4 
crates/workspace/src/pane/dragged_item_receiver.rs | 103 +++++--
crates/workspace/src/workspace.rs                  |  79 +++++
crates/zed/src/zed.rs                              |  30 +-
styles/src/styleTree/projectPanel.ts               |  17 +
styles/src/styleTree/tabBar.ts                     |   2 
15 files changed, 454 insertions(+), 197 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4277,6 +4277,7 @@ name = "project_panel"
 version = "0.1.0"
 dependencies = [
  "context_menu",
+ "drag_and_drop",
  "editor",
  "futures 0.3.24",
  "gpui",

crates/collab/src/integration_tests.rs 🔗

@@ -909,7 +909,7 @@ async fn test_host_disconnect(
         cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "b.txt"), true, cx)
+            workspace.open_path((worktree_id, "b.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -3705,7 +3705,7 @@ async fn test_collaborating_with_code_actions(
         cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "main.rs"), true, cx)
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
         })
         .await
         .unwrap()
@@ -3926,7 +3926,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
         cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "one.rs"), true, cx)
+            workspace.open_path((worktree_id, "one.rs"), None, true, cx)
         })
         .await
         .unwrap()
@@ -4726,7 +4726,7 @@ async fn test_following(
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), true, cx)
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -4734,7 +4734,7 @@ async fn test_following(
         .unwrap();
     let editor_a2 = workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), true, cx)
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -4745,7 +4745,7 @@ async fn test_following(
     let workspace_b = client_b.build_workspace(&project_b, cx_b);
     let editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), true, cx)
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -5003,7 +5003,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), true, cx)
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -5015,7 +5015,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
     let _editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), true, cx)
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -5066,7 +5066,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
 
     workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "3.txt"), true, cx)
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
         })
         .await
         .unwrap();
@@ -5077,7 +5077,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
     workspace_b
         .update(cx_b, |workspace, cx| {
             assert_eq!(*workspace.active_pane(), pane_b1);
-            workspace.open_path((worktree_id, "4.txt"), true, cx)
+            workspace.open_path((worktree_id, "4.txt"), None, true, cx)
         })
         .await
         .unwrap();
@@ -5178,7 +5178,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     let workspace_a = client_a.build_workspace(&project_a, cx_a);
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), true, cx)
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
         })
         .await
         .unwrap()
@@ -5291,7 +5291,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     // When client B activates a different item in the original pane, it automatically stops following client A.
     workspace_b
         .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), true, cx)
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
         })
         .await
         .unwrap();

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -2,29 +2,44 @@ use std::{any::Any, rc::Rc};
 
 use collections::HashSet;
 use gpui::{
-    elements::{MouseEventHandler, Overlay},
-    geometry::vector::Vector2F,
+    elements::{Empty, MouseEventHandler, Overlay},
+    geometry::{rect::RectF, vector::Vector2F},
     scene::MouseDrag,
     CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
     View, WeakViewHandle,
 };
 
-struct State<V: View> {
-    window_id: usize,
-    position: Vector2F,
-    region_offset: Vector2F,
-    payload: Rc<dyn Any + 'static>,
-    render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+enum State<V: View> {
+    Dragging {
+        window_id: usize,
+        position: Vector2F,
+        region_offset: Vector2F,
+        region: RectF,
+        payload: Rc<dyn Any + 'static>,
+        render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+    },
+    Canceled,
 }
 
 impl<V: View> Clone for State<V> {
     fn clone(&self) -> Self {
-        Self {
-            window_id: self.window_id.clone(),
-            position: self.position.clone(),
-            region_offset: self.region_offset.clone(),
-            payload: self.payload.clone(),
-            render: self.render.clone(),
+        match self {
+            State::Dragging {
+                window_id,
+                position,
+                region_offset,
+                region,
+                payload,
+                render,
+            } => Self::Dragging {
+                window_id: window_id.clone(),
+                position: position.clone(),
+                region_offset: region_offset.clone(),
+                region: region.clone(),
+                payload: payload.clone(),
+                render: render.clone(),
+            },
+            State::Canceled => State::Canceled,
         }
     }
 }
@@ -49,24 +64,27 @@ impl<V: View> DragAndDrop<V> {
     }
 
     pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
-        self.currently_dragged.as_ref().and_then(
-            |State {
-                 position,
-                 payload,
-                 window_id: window_dragged_from,
-                 ..
-             }| {
+        self.currently_dragged.as_ref().and_then(|state| {
+            if let State::Dragging {
+                position,
+                payload,
+                window_id: window_dragged_from,
+                ..
+            } = state
+            {
                 if &window_id != window_dragged_from {
                     return None;
                 }
 
                 payload
-                    .clone()
-                    .downcast::<T>()
-                    .ok()
+                    .is::<T>()
+                    .then(|| payload.clone().downcast::<T>().ok())
+                    .flatten()
                     .map(|payload| (position.clone(), payload))
-            },
-        )
+            } else {
+                None
+            }
+        })
     }
 
     pub fn dragging<T: Any>(
@@ -77,74 +95,135 @@ impl<V: View> DragAndDrop<V> {
     ) {
         let window_id = cx.window_id();
         cx.update_global::<Self, _, _>(|this, cx| {
-            let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
-                previous_state.region_offset
+            this.notify_containers_for_window(window_id, cx);
+
+            if matches!(this.currently_dragged, Some(State::Canceled)) {
+                return;
+            }
+
+            let (region_offset, region) = if let Some(State::Dragging {
+                region_offset,
+                region,
+                ..
+            }) = this.currently_dragged.as_ref()
+            {
+                (*region_offset, *region)
             } else {
-                event.region.origin() - event.prev_mouse_position
+                (
+                    event.region.origin() - event.prev_mouse_position,
+                    event.region,
+                )
             };
 
-            this.currently_dragged = Some(State {
+            this.currently_dragged = Some(State::Dragging {
                 window_id,
                 region_offset,
+                region,
                 position: event.position,
                 payload,
                 render: Rc::new(move |payload, cx| {
                     render(payload.downcast_ref::<T>().unwrap(), cx)
                 }),
             });
-
-            this.notify_containers_for_window(window_id, cx);
         });
     }
 
     pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
-        let currently_dragged = cx.global::<Self>().currently_dragged.clone();
-
-        currently_dragged.and_then(
-            |State {
-                 window_id,
-                 region_offset,
-                 position,
-                 payload,
-                 render,
-             }| {
-                if cx.window_id() != window_id {
-                    return None;
-                }
+        enum DraggedElementHandler {}
+        cx.global::<Self>()
+            .currently_dragged
+            .clone()
+            .and_then(|state| {
+                match state {
+                    State::Dragging {
+                        window_id,
+                        region_offset,
+                        position,
+                        region,
+                        payload,
+                        render,
+                    } => {
+                        if cx.window_id() != window_id {
+                            return None;
+                        }
 
-                let position = position + region_offset;
+                        let position = position + region_offset;
+                        Some(
+                            Overlay::new(
+                                MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
+                                    render(payload, cx)
+                                })
+                                .with_cursor_style(CursorStyle::Arrow)
+                                .on_up(MouseButton::Left, |_, cx| {
+                                    cx.defer(|cx| {
+                                        cx.update_global::<Self, _, _>(|this, cx| {
+                                            this.finish_dragging(cx)
+                                        });
+                                    });
+                                    cx.propagate_event();
+                                })
+                                .on_up_out(MouseButton::Left, |_, cx| {
+                                    cx.defer(|cx| {
+                                        cx.update_global::<Self, _, _>(|this, cx| {
+                                            this.finish_dragging(cx)
+                                        });
+                                    });
+                                })
+                                // Don't block hover events or invalidations
+                                .with_hoverable(false)
+                                .constrained()
+                                .with_width(region.width())
+                                .with_height(region.height())
+                                .boxed(),
+                            )
+                            .with_anchor_position(position)
+                            .boxed(),
+                        )
+                    }
 
-                enum DraggedElementHandler {}
-                Some(
-                    Overlay::new(
-                        MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
-                            render(payload, cx)
+                    State::Canceled => Some(
+                        MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
+                            Empty::new()
+                                .constrained()
+                                .with_width(0.)
+                                .with_height(0.)
+                                .boxed()
                         })
-                        .with_cursor_style(CursorStyle::Arrow)
                         .on_up(MouseButton::Left, |_, cx| {
                             cx.defer(|cx| {
-                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+                                cx.update_global::<Self, _, _>(|this, _| {
+                                    this.currently_dragged = None;
+                                });
                             });
-                            cx.propagate_event();
                         })
                         .on_up_out(MouseButton::Left, |_, cx| {
                             cx.defer(|cx| {
-                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+                                cx.update_global::<Self, _, _>(|this, _| {
+                                    this.currently_dragged = None;
+                                });
                             });
                         })
-                        // Don't block hover events or invalidations
-                        .with_hoverable(false)
                         .boxed(),
-                    )
-                    .with_anchor_position(position)
-                    .boxed(),
-                )
-            },
-        )
+                    ),
+                }
+            })
+    }
+
+    pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
+        if let Some(State::Dragging {
+            payload, window_id, ..
+        }) = &self.currently_dragged
+        {
+            if payload.is::<P>() {
+                let window_id = *window_id;
+                self.currently_dragged = Some(State::Canceled);
+                self.notify_containers_for_window(window_id, cx);
+            }
+        }
     }
 
-    fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
-        if let Some(State { window_id, .. }) = self.currently_dragged.take() {
+    fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
+        if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
             self.notify_containers_for_window(window_id, cx);
         }
     }

crates/editor/src/editor.rs 🔗

@@ -6464,7 +6464,7 @@ impl Editor {
     }
 
     fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
-        let editor = workspace.open_path(action.path.clone(), true, cx);
+        let editor = workspace.open_path(action.path.clone(), None, true, cx);
         let position = action.position;
         let anchor = action.anchor;
         cx.spawn_weak(|_, mut cx| async move {

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -76,7 +76,9 @@ impl<'a> EditorLspTestContext<'a> {
 
         let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
         let item = workspace
-            .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
+            .update(cx, |workspace, cx| {
+                workspace.open_path(file, None, true, cx)
+            })
             .await
             .expect("Could not open test file");
 

crates/file_finder/src/file_finder.rs 🔗

@@ -104,7 +104,7 @@ impl FileFinder {
         match event {
             Event::Selected(project_path) => {
                 workspace
-                    .open_path(project_path.clone(), true, cx)
+                    .open_path(project_path.clone(), None, true, cx)
                     .detach_and_log_err(cx);
                 workspace.dismiss_modal(cx);
             }

crates/project_panel/Cargo.toml 🔗

@@ -9,6 +9,7 @@ doctest = false
 
 [dependencies]
 context_menu = { path = "../context_menu" }
+drag_and_drop = { path = "../drag_and_drop" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 menu = { path = "../menu" }

crates/project_panel/src/project_panel.rs 🔗

@@ -1,12 +1,13 @@
 use context_menu::{ContextMenu, ContextMenuItem};
+use drag_and_drop::{DragAndDrop, Draggable};
 use editor::{Cancel, Editor};
 use futures::stream::StreamExt;
 use gpui::{
     actions,
     anyhow::{anyhow, Result},
     elements::{
-        AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
-        ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
+        AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
+        MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
     impl_internal_actions, keymap,
@@ -25,6 +26,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+use theme::ProjectPanelEntry;
 use unicase::UniCase;
 use workspace::Workspace;
 
@@ -71,8 +73,9 @@ pub enum ClipboardEntry {
 }
 
 #[derive(Debug, PartialEq, Eq)]
-struct EntryDetails {
+pub struct EntryDetails {
     filename: String,
+    path: Arc<Path>,
     depth: usize,
     kind: EntryKind,
     is_ignored: bool,
@@ -220,6 +223,7 @@ impl ProjectPanel {
             this.update_visible_entries(None, cx);
             this
         });
+
         cx.subscribe(&project_panel, {
             let project_panel = project_panel.downgrade();
             move |workspace, _, event, cx| match event {
@@ -235,6 +239,7 @@ impl ProjectPanel {
                                         worktree_id: worktree.read(cx).id(),
                                         path: entry.path.clone(),
                                     },
+                                    None,
                                     focus_opened_item,
                                     cx,
                                 )
@@ -601,6 +606,10 @@ impl ProjectPanel {
                     cx.notify();
                 }
             }
+
+            cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
+                drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
+            })
         }
     }
 
@@ -950,14 +959,15 @@ impl ProjectPanel {
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
             if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
                 let snapshot = worktree.read(cx).snapshot();
+                let root_name = OsStr::new(snapshot.root_name());
                 let expanded_entry_ids = self
                     .expanded_dir_ids
                     .get(&snapshot.id())
                     .map(Vec::as_slice)
                     .unwrap_or(&[]);
-                let root_name = OsStr::new(snapshot.root_name());
-                for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
-                {
+
+                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
+                for entry in &visible_worktree_entries[entry_range] {
                     let mut details = EntryDetails {
                         filename: entry
                             .path
@@ -965,6 +975,7 @@ impl ProjectPanel {
                             .unwrap_or(root_name)
                             .to_string_lossy()
                             .to_string(),
+                        path: entry.path.clone(),
                         depth: entry.path.components().count(),
                         kind: entry.kind,
                         is_ignored: entry.is_ignored,
@@ -978,12 +989,14 @@ impl ProjectPanel {
                             .clipboard_entry
                             .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
                     };
+
                     if let Some(edit_state) = &self.edit_state {
                         let is_edited_entry = if edit_state.is_new_entry {
                             entry.id == NEW_ENTRY_ID
                         } else {
                             entry.id == edit_state.entry_id
                         };
+
                         if is_edited_entry {
                             if let Some(processing_filename) = &edit_state.processing_filename {
                                 details.is_processing = true;
@@ -1005,6 +1018,63 @@ impl ProjectPanel {
         }
     }
 
+    fn render_entry_visual_element<V: View>(
+        details: &EntryDetails,
+        editor: Option<&ViewHandle<Editor>>,
+        padding: f32,
+        row_container_style: ContainerStyle,
+        style: &ProjectPanelEntry,
+        cx: &mut RenderContext<V>,
+    ) -> ElementBox {
+        let kind = details.kind;
+        let show_editor = details.is_editing && !details.is_processing;
+
+        Flex::row()
+            .with_child(
+                ConstrainedBox::new(if kind == EntryKind::Dir {
+                    if details.is_expanded {
+                        Svg::new("icons/chevron_down_8.svg")
+                            .with_color(style.icon_color)
+                            .boxed()
+                    } else {
+                        Svg::new("icons/chevron_right_8.svg")
+                            .with_color(style.icon_color)
+                            .boxed()
+                    }
+                } else {
+                    Empty::new().boxed()
+                })
+                .with_max_width(style.icon_size)
+                .with_max_height(style.icon_size)
+                .aligned()
+                .constrained()
+                .with_width(style.icon_size)
+                .boxed(),
+            )
+            .with_child(if show_editor && editor.is_some() {
+                ChildView::new(editor.unwrap().clone(), cx)
+                    .contained()
+                    .with_margin_left(style.icon_spacing)
+                    .aligned()
+                    .left()
+                    .flex(1.0, true)
+                    .boxed()
+            } else {
+                Label::new(details.filename.clone(), style.text.clone())
+                    .contained()
+                    .with_margin_left(style.icon_spacing)
+                    .aligned()
+                    .left()
+                    .boxed()
+            })
+            .constrained()
+            .with_height(style.height)
+            .contained()
+            .with_style(row_container_style)
+            .with_padding_left(padding)
+            .boxed()
+    }
+
     fn render_entry(
         entry_id: ProjectEntryId,
         details: EntryDetails,
@@ -1013,69 +1083,34 @@ impl ProjectPanel {
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         let kind = details.kind;
-        let show_editor = details.is_editing && !details.is_processing;
-        MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
-            let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
+        let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
 
-            let entry_style = if details.is_cut {
-                &theme.cut_entry
-            } else if details.is_ignored {
-                &theme.ignored_entry
-            } else {
-                &theme.entry
-            };
+        let entry_style = if details.is_cut {
+            &theme.cut_entry
+        } else if details.is_ignored {
+            &theme.ignored_entry
+        } else {
+            &theme.entry
+        };
 
-            let style = entry_style.style_for(state, details.is_selected).clone();
+        let show_editor = details.is_editing && !details.is_processing;
 
+        MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
+            let style = entry_style.style_for(state, details.is_selected).clone();
             let row_container_style = if show_editor {
                 theme.filename_editor.container
             } else {
                 style.container
             };
-            Flex::row()
-                .with_child(
-                    ConstrainedBox::new(if kind == EntryKind::Dir {
-                        if details.is_expanded {
-                            Svg::new("icons/chevron_down_8.svg")
-                                .with_color(style.icon_color)
-                                .boxed()
-                        } else {
-                            Svg::new("icons/chevron_right_8.svg")
-                                .with_color(style.icon_color)
-                                .boxed()
-                        }
-                    } else {
-                        Empty::new().boxed()
-                    })
-                    .with_max_width(style.icon_size)
-                    .with_max_height(style.icon_size)
-                    .aligned()
-                    .constrained()
-                    .with_width(style.icon_size)
-                    .boxed(),
-                )
-                .with_child(if show_editor {
-                    ChildView::new(editor.clone(), cx)
-                        .contained()
-                        .with_margin_left(theme.entry.default.icon_spacing)
-                        .aligned()
-                        .left()
-                        .flex(1.0, true)
-                        .boxed()
-                } else {
-                    Label::new(details.filename, style.text.clone())
-                        .contained()
-                        .with_margin_left(style.icon_spacing)
-                        .aligned()
-                        .left()
-                        .boxed()
-                })
-                .constrained()
-                .with_height(theme.entry.default.height)
-                .contained()
-                .with_style(row_container_style)
-                .with_padding_left(padding)
-                .boxed()
+
+            Self::render_entry_visual_element(
+                &details,
+                Some(editor),
+                padding,
+                row_container_style,
+                &style,
+                cx,
+            )
         })
         .on_click(MouseButton::Left, move |e, cx| {
             if kind == EntryKind::Dir {
@@ -1093,6 +1128,21 @@ impl ProjectPanel {
                 position: e.position,
             })
         })
+        .as_draggable(entry_id, {
+            let row_container_style = theme.dragged_entry.container;
+
+            move |_, cx: &mut RenderContext<Workspace>| {
+                let theme = cx.global::<Settings>().theme.clone();
+                Self::render_entry_visual_element(
+                    &details,
+                    None,
+                    padding,
+                    row_container_style,
+                    &theme.project_panel.dragged_entry,
+                    cx,
+                )
+            }
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }

crates/theme/src/theme.rs 🔗

@@ -326,6 +326,7 @@ pub struct ProjectPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub entry: Interactive<ProjectPanelEntry>,
+    pub dragged_entry: ProjectPanelEntry,
     pub ignored_entry: Interactive<ProjectPanelEntry>,
     pub cut_entry: Interactive<ProjectPanelEntry>,
     pub filename_editor: FieldEditor,

crates/vim/src/test/vim_test_context.rs 🔗

@@ -67,7 +67,9 @@ impl<'a> VimTestContext<'a> {
 
         let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
         let item = workspace
-            .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
+            .update(cx, |workspace, cx| {
+                workspace.open_path(file, None, true, cx)
+            })
             .await
             .expect("Could not open test file");
 

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -7,9 +7,13 @@ use gpui::{
     AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
     WeakViewHandle,
 };
+use project::ProjectEntryId;
 use settings::Settings;
 
-use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
+use crate::{
+    MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
+    Workspace,
+};
 
 use super::DraggedItem;
 
@@ -28,12 +32,18 @@ where
     MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
         // Observing hovered will cause a render when the mouse enters regardless
         // of if mouse position was accessed before
-        let hovered = state.hovered();
-        let drag_position = cx
-            .global::<DragAndDrop<Workspace>>()
-            .currently_dragged::<DraggedItem>(cx.window_id())
-            .filter(|_| hovered)
-            .map(|(drag_position, _)| drag_position);
+        let drag_position = if state.hovered() {
+            cx.global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<DraggedItem>(cx.window_id())
+                .map(|(drag_position, _)| drag_position)
+                .or_else(|| {
+                    cx.global::<DragAndDrop<Workspace>>()
+                        .currently_dragged::<ProjectEntryId>(cx.window_id())
+                        .map(|(drag_position, _)| drag_position)
+                })
+        } else {
+            None
+        };
 
         Stack::new()
             .with_child(render_child(state, cx))
@@ -70,10 +80,14 @@ where
         }
     })
     .on_move(|_, cx| {
-        if cx
-            .global::<DragAndDrop<Workspace>>()
+        let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+
+        if drag_and_drop
             .currently_dragged::<DraggedItem>(cx.window_id())
             .is_some()
+            || drag_and_drop
+                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .is_some()
         {
             cx.notify();
         } else {
@@ -90,30 +104,59 @@ pub fn handle_dropped_item(
     split_margin: Option<f32>,
     cx: &mut EventContext,
 ) {
-    if let Some((_, dragged_item)) = cx
-        .global::<DragAndDrop<Workspace>>()
-        .currently_dragged::<DraggedItem>(cx.window_id)
+    enum Action {
+        Move(WeakViewHandle<Pane>, usize),
+        Open(ProjectEntryId),
+    }
+    let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+    let action = if let Some((_, dragged_item)) =
+        drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id)
     {
-        if let Some(split_direction) = split_margin
-            .and_then(|margin| drop_split_direction(event.position, event.region, margin))
-        {
-            cx.dispatch_action(SplitWithItem {
-                from: dragged_item.pane.clone(),
-                item_id_to_move: dragged_item.item.id(),
-                pane_to_split: pane.clone(),
+        Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
+    } else if let Some((_, project_entry)) =
+        drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id)
+    {
+        Action::Open(*project_entry)
+    } else {
+        return;
+    };
+
+    if let Some(split_direction) =
+        split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
+    {
+        let pane_to_split = pane.clone();
+        match action {
+            Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
+                from,
+                item_id_to_move,
+                pane_to_split,
                 split_direction,
-            });
-        } else if pane != &dragged_item.pane || allow_same_pane {
-            // If no split margin or not close enough to the edge, just move the item
-            cx.dispatch_action(MoveItem {
-                item_id: dragged_item.item.id(),
-                from: dragged_item.pane.clone(),
-                to: pane.clone(),
-                destination_index: index,
-            })
-        }
+            }),
+            Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
+                pane_to_split,
+                split_direction,
+                project_entry,
+            }),
+        };
     } else {
-        cx.propagate_event();
+        match action {
+            Action::Move(from, item_id) => {
+                if pane != &from || allow_same_pane {
+                    cx.dispatch_action(MoveItem {
+                        item_id,
+                        from,
+                        to: pane.clone(),
+                        destination_index: index,
+                    })
+                } else {
+                    cx.propagate_event();
+                }
+            }
+            Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
+                pane: pane.clone(),
+                project_entry,
+            }),
+        }
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -128,12 +128,25 @@ pub struct OpenSharedScreen {
 
 #[derive(Clone, PartialEq)]
 pub struct SplitWithItem {
-    from: WeakViewHandle<Pane>,
     pane_to_split: WeakViewHandle<Pane>,
     split_direction: SplitDirection,
+    from: WeakViewHandle<Pane>,
     item_id_to_move: usize,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct SplitWithProjectEntry {
+    pane_to_split: WeakViewHandle<Pane>,
+    split_direction: SplitDirection,
+    project_entry: ProjectEntryId,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct OpenProjectEntryInPane {
+    pane: WeakViewHandle<Pane>,
+    project_entry: ProjectEntryId,
+}
+
 impl_internal_actions!(
     workspace,
     [
@@ -143,6 +156,8 @@ impl_internal_actions!(
         OpenSharedScreen,
         RemoveWorktreeFromProject,
         SplitWithItem,
+        SplitWithProjectEntry,
+        OpenProjectEntryInPane,
     ]
 );
 impl_actions!(workspace, [ActivatePane]);
@@ -234,6 +249,57 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         },
     );
 
+    cx.add_async_action(
+        |workspace: &mut Workspace,
+         SplitWithProjectEntry {
+             pane_to_split,
+             split_direction,
+             project_entry,
+         }: &_,
+         cx| {
+            pane_to_split.upgrade(cx).and_then(|pane_to_split| {
+                let new_pane = workspace.add_pane(cx);
+                workspace
+                    .center
+                    .split(&pane_to_split, &new_pane, *split_direction)
+                    .unwrap();
+
+                workspace
+                    .project
+                    .read(cx)
+                    .path_for_entry(*project_entry, cx)
+                    .map(|path| {
+                        let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx);
+                        cx.foreground().spawn(async move {
+                            task.await?;
+                            Ok(())
+                        })
+                    })
+            })
+        },
+    );
+
+    cx.add_async_action(
+        |workspace: &mut Workspace,
+         OpenProjectEntryInPane {
+             pane,
+             project_entry,
+         }: &_,
+         cx| {
+            workspace
+                .project
+                .read(cx)
+                .path_for_entry(*project_entry, cx)
+                .map(|path| {
+                    let task = workspace.open_path(path, Some(pane.clone()), true, cx);
+                    cx.foreground().spawn(async move {
+                        task.await?;
+                        Ok(())
+                    })
+                })
+        },
+    );
+
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
@@ -1399,7 +1465,7 @@ impl Workspace {
         mut abs_paths: Vec<PathBuf>,
         visible: bool,
         cx: &mut ViewContext<Self>,
-    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
+    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
         let fs = self.fs.clone();
 
         // Sort the paths to ensure we add worktrees for parents before their children.
@@ -1429,7 +1495,7 @@ impl Workspace {
                             if fs.is_file(&abs_path).await {
                                 Some(
                                     this.update(&mut cx, |this, cx| {
-                                        this.open_path(project_path, true, cx)
+                                        this.open_path(project_path, None, true, cx)
                                     })
                                     .await,
                                 )
@@ -1749,10 +1815,11 @@ impl Workspace {
     pub fn open_path(
         &mut self,
         path: impl Into<ProjectPath>,
+        pane: Option<WeakViewHandle<Pane>>,
         focus_item: bool,
         cx: &mut ViewContext<Self>,
-    ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
-        let pane = self.active_pane().downgrade();
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
         let task = self.load_path(path.into(), cx);
         cx.spawn(|this, mut cx| async move {
             let (project_entry_id, build_item) = task.await?;
@@ -2874,7 +2941,7 @@ pub fn open_paths(
     cx: &mut MutableAppContext,
 ) -> Task<(
     ViewHandle<Workspace>,
-    Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
+    Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
 )> {
     log::info!("open paths {:?}", abs_paths);
 

crates/zed/src/zed.rs 🔗

@@ -818,7 +818,7 @@ mod tests {
 
         // Open the first entry
         let entry_1 = workspace
-            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
             .await
             .unwrap();
         cx.read(|cx| {
@@ -832,7 +832,7 @@ mod tests {
 
         // Open the second entry
         workspace
-            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
             .await
             .unwrap();
         cx.read(|cx| {
@@ -846,7 +846,7 @@ mod tests {
 
         // Open the first entry again. The existing pane item is activated.
         let entry_1b = workspace
-            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
             .await
             .unwrap();
         assert_eq!(entry_1.id(), entry_1b.id());
@@ -864,7 +864,7 @@ mod tests {
         workspace
             .update(cx, |w, cx| {
                 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
-                w.open_path(file2.clone(), true, cx)
+                w.open_path(file2.clone(), None, true, cx)
             })
             .await
             .unwrap();
@@ -883,8 +883,8 @@ mod tests {
         // Open the third entry twice concurrently. Only one pane item is added.
         let (t1, t2) = workspace.update(cx, |w, cx| {
             (
-                w.open_path(file3.clone(), true, cx),
-                w.open_path(file3.clone(), true, cx),
+                w.open_path(file3.clone(), None, true, cx),
+                w.open_path(file3.clone(), None, true, cx),
             )
         });
         t1.await.unwrap();
@@ -1195,7 +1195,7 @@ mod tests {
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
+                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
             })
             .await
             .unwrap();
@@ -1284,7 +1284,7 @@ mod tests {
         let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
 
         workspace
-            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
             .await
             .unwrap();
 
@@ -1359,7 +1359,7 @@ mod tests {
         let file3 = entries[2].clone();
 
         let editor1 = workspace
-            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
             .await
             .unwrap()
             .downcast::<Editor>()
@@ -1370,13 +1370,13 @@ mod tests {
             });
         });
         let editor2 = workspace
-            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
             .await
             .unwrap()
             .downcast::<Editor>()
             .unwrap();
         let editor3 = workspace
-            .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
             .await
             .unwrap()
             .downcast::<Editor>()
@@ -1626,22 +1626,22 @@ mod tests {
         let file4 = entries[3].clone();
 
         let file1_item_id = workspace
-            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
             .await
             .unwrap()
             .id();
         let file2_item_id = workspace
-            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
             .await
             .unwrap()
             .id();
         let file3_item_id = workspace
-            .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
             .await
             .unwrap()
             .id();
         let file4_item_id = workspace
-            .update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
+            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
             .await
             .unwrap()
             .id();

styles/src/styleTree/projectPanel.ts 🔗

@@ -1,14 +1,19 @@
 import { ColorScheme } from "../themes/common/colorScheme";
-import { background, foreground, text } from "./components";
+import { withOpacity } from "../utils/color";
+import { background, border, foreground, text } from "./components";
 
 export default function projectPanel(colorScheme: ColorScheme) {
   let layer = colorScheme.middle;
-
-  let entry = {
+  
+  let baseEntry = {
     height: 24,
     iconColor: foreground(layer, "variant"),
     iconSize: 8,
     iconSpacing: 8,
+  }
+
+  let entry = {
+    ...baseEntry,
     text: text(layer, "mono", "variant", { size: "sm" }),
     hover: {
       background: background(layer, "variant", "hovered"),
@@ -28,6 +33,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
     padding: { left: 12, right: 12, top: 6, bottom: 6 },
     indentWidth: 8,
     entry,
+    draggedEntry: {
+      ...baseEntry,
+      text: text(layer, "mono", "on", { size: "sm" }),
+      background: withOpacity(background(layer, "on"), 0.9),
+      border: border(layer),
+    },
     ignoredEntry: {
       ...entry,
       text: text(layer, "mono", "disabled"),

styles/src/styleTree/tabBar.ts 🔗

@@ -67,7 +67,7 @@ export default function tabBar(colorScheme: ColorScheme) {
 
   const draggedTab = {
     ...activePaneActiveTab,
-    background: withOpacity(tab.background, 0.95),
+    background: withOpacity(tab.background, 0.9),
     border: undefined as any,
     shadow: colorScheme.popoverShadow,
   };