diff --git a/Cargo.lock b/Cargo.lock index 137b59a4ff..1cceb9f99c 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a77345270b..0a6c01a691 100644 --- a/crates/collab/src/integration_tests.rs +++ b/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(); diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index bb660c750f..65f5985edf 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/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 { - window_id: usize, - position: Vector2F, - region_offset: Vector2F, - payload: Rc, - render: Rc, &mut RenderContext) -> ElementBox>, +enum State { + Dragging { + window_id: usize, + position: Vector2F, + region_offset: Vector2F, + region: RectF, + payload: Rc, + render: Rc, &mut RenderContext) -> ElementBox>, + }, + Canceled, } impl Clone for State { 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 DragAndDrop { } pub fn currently_dragged(&self, window_id: usize) -> Option<(Vector2F, Rc)> { - 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::() - .ok() + .is::() + .then(|| payload.clone().downcast::().ok()) + .flatten() .map(|payload| (position.clone(), payload)) - }, - ) + } else { + None + } + }) } pub fn dragging( @@ -77,74 +95,135 @@ impl DragAndDrop { ) { let window_id = cx.window_id(); cx.update_global::(|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::().unwrap(), cx) }), }); - - this.notify_containers_for_window(window_id, cx); }); } pub fn render(cx: &mut RenderContext) -> Option { - let currently_dragged = cx.global::().currently_dragged.clone(); + enum DraggedElementHandler {} + cx.global::() + .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; + } - currently_dragged.and_then( - |State { - window_id, - region_offset, - position, - payload, - render, - }| { - if cx.window_id() != window_id { - return None; - } + let position = position + region_offset; + Some( + Overlay::new( + MouseEventHandler::::new(0, cx, |_, cx| { + render(payload, cx) + }) + .with_cursor_style(CursorStyle::Arrow) + .on_up(MouseButton::Left, |_, cx| { + cx.defer(|cx| { + cx.update_global::(|this, cx| { + this.finish_dragging(cx) + }); + }); + cx.propagate_event(); + }) + .on_up_out(MouseButton::Left, |_, cx| { + cx.defer(|cx| { + cx.update_global::(|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(), + ) + } - let position = position + region_offset; - - enum DraggedElementHandler {} - Some( - Overlay::new( - MouseEventHandler::::new(0, cx, |_, cx| { - render(payload, cx) + State::Canceled => Some( + MouseEventHandler::::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::(|this, cx| this.stop_dragging(cx)); + cx.update_global::(|this, _| { + this.currently_dragged = None; + }); }); - cx.propagate_event(); }) .on_up_out(MouseButton::Left, |_, cx| { cx.defer(|cx| { - cx.update_global::(|this, cx| this.stop_dragging(cx)); + cx.update_global::(|this, _| { + this.currently_dragged = None; + }); }); }) - // Don't block hover events or invalidations - .with_hoverable(false) .boxed(), - ) - .with_anchor_position(position) - .boxed(), - ) - }, - ) + ), + } + }) } - fn stop_dragging(&mut self, cx: &mut MutableAppContext) { - if let Some(State { window_id, .. }) = self.currently_dragged.take() { + pub fn cancel_dragging(&mut self, cx: &mut MutableAppContext) { + if let Some(State::Dragging { + payload, window_id, .. + }) = &self.currently_dragged + { + if payload.is::

() { + let window_id = *window_id; + self.currently_dragged = Some(State::Canceled); + self.notify_containers_for_window(window_id, cx); + } + } + } + + 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); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f3d9936673..dd5934f979 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6464,7 +6464,7 @@ impl Editor { } fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext) { - 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 { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b4a4cd7ab8..69205e1991 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/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"); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 06041f8b0b..c6d4a8f121 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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); } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 6d566699fa..8704f57c8c 100644 --- a/crates/project_panel/Cargo.toml +++ b/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" } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 392fd73e03..3eb5d68516 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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, 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, cx| { + drag_and_drop.cancel_dragging::(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( + details: &EntryDetails, + editor: Option<&ViewHandle>, + padding: f32, + row_container_style: ContainerStyle, + style: &ProjectPanelEntry, + cx: &mut RenderContext, + ) -> 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, ) -> ElementBox { let kind = details.kind; + 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 show_editor = details.is_editing && !details.is_processing; + MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { - 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 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| { + let theme = cx.global::().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() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cf7aa6e551..8d2a2df18e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -326,6 +326,7 @@ pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, pub entry: Interactive, + pub dragged_entry: ProjectPanelEntry, pub ignored_entry: Interactive, pub cut_entry: Interactive, pub filename_editor: FieldEditor, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 2fb446d127..1aeba9fd08 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/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"); diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 38613a0376..7a71bfe0e5 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/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::::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::>() - .currently_dragged::(cx.window_id()) - .filter(|_| hovered) - .map(|(drag_position, _)| drag_position); + let drag_position = if state.hovered() { + cx.global::>() + .currently_dragged::(cx.window_id()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + cx.global::>() + .currently_dragged::(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::>() + let drag_and_drop = cx.global::>(); + + if drag_and_drop .currently_dragged::(cx.window_id()) .is_some() + || drag_and_drop + .currently_dragged::(cx.window_id()) + .is_some() { cx.notify(); } else { @@ -90,30 +104,59 @@ pub fn handle_dropped_item( split_margin: Option, cx: &mut EventContext, ) { - if let Some((_, dragged_item)) = cx - .global::>() - .currently_dragged::(cx.window_id) + enum Action { + Move(WeakViewHandle, usize), + Open(ProjectEntryId), + } + let drag_and_drop = cx.global::>(); + let action = if let Some((_, dragged_item)) = + drag_and_drop.currently_dragged::(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(), - 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::Move(dragged_item.pane.clone(), dragged_item.item.id()) + } else if let Some((_, project_entry)) = + drag_and_drop.currently_dragged::(cx.window_id) + { + Action::Open(*project_entry) } else { - cx.propagate_event(); + 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, + }), + Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry { + pane_to_split, + split_direction, + project_entry, + }), + }; + } else { + 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, + }), + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 349217985c..2dbf923484 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -128,12 +128,25 @@ pub struct OpenSharedScreen { #[derive(Clone, PartialEq)] pub struct SplitWithItem { - from: WeakViewHandle, pane_to_split: WeakViewHandle, split_direction: SplitDirection, + from: WeakViewHandle, item_id_to_move: usize, } +#[derive(Clone, PartialEq)] +pub struct SplitWithProjectEntry { + pane_to_split: WeakViewHandle, + split_direction: SplitDirection, + project_entry: ProjectEntryId, +} + +#[derive(Clone, PartialEq)] +pub struct OpenProjectEntryInPane { + pane: WeakViewHandle, + 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, 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, visible: bool, cx: &mut ViewContext, - ) -> Task, Arc>>>> { + ) -> Task, 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, + pane: Option>, focus_item: bool, cx: &mut ViewContext, - ) -> Task, Arc>> { - let pane = self.active_pane().downgrade(); + ) -> Task, 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, - Vec, Arc>>>, + Vec, anyhow::Error>>>, )> { log::info!("open paths {:?}", abs_paths); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 03b881fe1e..bb33109d0d 100644 --- a/crates/zed/src/zed.rs +++ b/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::() @@ -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::() .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::() @@ -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(); diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 729150fbc7..357070e674 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/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"), diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index 2824c43483..fcf78dc73f 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/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, };