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",
Kay Simmons created
Drag project entry to pane
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(-)
@@ -4277,6 +4277,7 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"context_menu",
+ "drag_and_drop",
"editor",
"futures 0.3.24",
"gpui",
@@ -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();
@@ -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);
}
}
@@ -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 {
@@ -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");
@@ -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);
}
@@ -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" }
@@ -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()
}
@@ -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,
@@ -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");
@@ -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,
+ }),
+ }
}
}
@@ -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);
@@ -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();
@@ -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"),
@@ -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,
};