diff --git a/Cargo.lock b/Cargo.lock index d9da330daa..9e1354c40d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12418,6 +12418,7 @@ name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion 1.1.1", "breadcrumbs", "client", "collections", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 514604ef98..f3990cecee 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -732,7 +732,11 @@ "cmd-end": "terminal::ScrollToBottom", "shift-home": "terminal::ScrollToTop", "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode" + "ctrl-shift-space": "terminal::ToggleViMode", + "ctrl-k up": "pane::SplitUp", + "ctrl-k down": "pane::SplitDown", + "ctrl-k left": "pane::SplitLeft", + "ctrl-k right": "pane::SplitRight" } } ] diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 7467d5dfd4..79e026cb51 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -416,7 +416,6 @@ impl AssistantPanel { ControlFlow::Break(()) }); - pane.set_can_split(false, cx); pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_| true); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 51ad9b9dec..813b212761 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -47,7 +47,7 @@ use workspace::item::{BreadcrumbText, FollowEvent}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, - ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; pub const MAX_TAB_TITLE_LEN: usize = 24; @@ -954,7 +954,7 @@ impl SerializableItem for Editor { workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let serialized_editor = match DB .get_serialized_editor(item_id, workspace_id) @@ -989,7 +989,7 @@ impl SerializableItem for Editor { contents: Some(contents), language, .. - } => cx.spawn(|pane, mut cx| { + } => cx.spawn(|mut cx| { let project = project.clone(); async move { let language = if let Some(language_name) = language { @@ -1019,7 +1019,7 @@ impl SerializableItem for Editor { buffer.set_text(contents, cx); })?; - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); @@ -1046,7 +1046,7 @@ impl SerializableItem for Editor { match project_item { Some(project_item) => { - cx.spawn(|pane, mut cx| async move { + cx.spawn(|mut cx| async move { let (_, project_item) = project_item.await?; let buffer = project_item.downcast::().map_err(|_| { anyhow!("Project item at stored path was not a buffer") @@ -1073,7 +1073,7 @@ impl SerializableItem for Editor { })?; } - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); @@ -1087,7 +1087,7 @@ impl SerializableItem for Editor { let open_by_abs_path = workspace.update(cx, |workspace, cx| { workspace.open_abs_path(abs_path.clone(), false, cx) }); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { let editor = open_by_abs_path?.await?.downcast::().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?; editor.update(&mut cx, |editor, cx| { editor.read_scroll_position_from_db(item_id, workspace_id, cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0776e5c72e..87ee3942dd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1578,7 +1578,7 @@ pub struct AnyDrag { pub view: AnyView, /// The value of the dragged item, to be dropped - pub value: Box, + pub value: Arc, /// This is used to render the dragged item in the same place /// on the original element that the drag was initiated diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 6928ca74ee..909af004a5 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -35,6 +35,7 @@ use std::{ mem, ops::DerefMut, rc::Rc, + sync::Arc, time::Duration, }; use taffy::style::Overflow; @@ -61,6 +62,7 @@ pub struct DragMoveEvent { /// The bounds of this element. pub bounds: Bounds, drag: PhantomData, + dragged_item: Arc, } impl DragMoveEvent { @@ -71,6 +73,11 @@ impl DragMoveEvent { .and_then(|drag| drag.value.downcast_ref::()) .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") } + + /// An item that is about to be dropped. + pub fn dragged_item(&self) -> &dyn Any { + self.dragged_item.as_ref() + } } impl Interactivity { @@ -243,20 +250,20 @@ impl Interactivity { { self.mouse_move_listeners .push(Box::new(move |event, phase, hitbox, cx| { - if phase == DispatchPhase::Capture - && cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.as_ref().type_id() == TypeId::of::()) - { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - }, - cx, - ); + if phase == DispatchPhase::Capture { + if let Some(drag) = &cx.active_drag { + if drag.value.as_ref().type_id() == TypeId::of::() { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + cx, + ); + } + } } })); } @@ -454,7 +461,7 @@ impl Interactivity { "calling on_drag more than once on the same element is not supported" ); self.drag_listener = Some(( - Box::new(value), + Arc::new(value), Box::new(move |value, offset, cx| { constructor(value.downcast_ref().unwrap(), offset, cx).into() }), @@ -1292,7 +1299,7 @@ pub struct Interactivity { pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, pub(crate) can_drop_predicate: Option, pub(crate) click_listeners: Vec, - pub(crate) drag_listener: Option<(Box, DragListener)>, + pub(crate) drag_listener: Option<(Arc, DragListener)>, pub(crate) hover_listener: Option>, pub(crate) tooltip_builder: Option, pub(crate) occlude_mouse: bool, diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 66eb914a30..13a7896a3f 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -385,20 +385,28 @@ impl LineLayoutCache { let mut previous_frame = &mut *self.previous_frame.lock(); let mut current_frame = &mut *self.current_frame.write(); - for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { - if let Some((key, line)) = previous_frame.lines.remove_entry(key) { - current_frame.lines.insert(key, line); + if let Some(cached_keys) = previous_frame + .used_lines + .get(range.start.lines_index..range.end.lines_index) + { + for key in cached_keys { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); } - current_frame.used_lines.push(key.clone()); } - for key in &previous_frame.used_wrapped_lines - [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + if let Some(cached_keys) = previous_frame + .used_wrapped_lines + .get(range.start.wrapped_lines_index..range.end.wrapped_lines_index) { - if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { - current_frame.wrapped_lines.insert(key, line); + for key in cached_keys { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); } - current_frame.used_wrapped_lines.push(key.clone()); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c1c14edba2..902c699cb7 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1752,12 +1752,18 @@ impl<'a> WindowContext<'a> { .iter_mut() .map(|listener| listener.take()), ); - window.next_frame.accessed_element_states.extend( - window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), - ); + if let Some(element_states) = window + .rendered_frame + .accessed_element_states + .get(range.start.accessed_element_states_index..range.end.accessed_element_states_index) + { + window.next_frame.accessed_element_states.extend( + element_states + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + } + window .text_system .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); @@ -3126,7 +3132,7 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = position; if self.active_drag.is_none() { self.active_drag = Some(AnyDrag { - value: Box::new(paths.clone()), + value: Arc::new(paths.clone()), view: self.new_view(|_| paths).into(), cursor_offset: position, }); diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 1d03e77e76..ed87562e64 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -16,7 +16,7 @@ use settings::Settings; use util::paths::PathExt; use workspace::{ item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams}, - ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, + ItemId, ItemSettings, ToolbarItemLocation, Workspace, WorkspaceId, }; const IMAGE_VIEWER_KIND: &str = "ImageView"; @@ -172,9 +172,9 @@ impl SerializableItem for ImageView { _workspace: WeakView, workspace_id: WorkspaceId, item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { - cx.spawn(|_pane, mut cx| async move { + cx.spawn(|mut cx| async move { let image_path = IMAGE_VIEWER .get_image_path(item_id, workspace_id)? .ok_or_else(|| anyhow::anyhow!("No image path found"))?; diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index e57d9d1fc6..7e4a4fe76f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +async-recursion.workspace = true breadcrumbs.workspace = true collections.workspace = true db.workspace = true diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index b8c31e05b0..dd430963d2 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,8 +1,351 @@ use anyhow::Result; +use async_recursion::async_recursion; +use collections::HashSet; +use futures::{stream::FuturesUnordered, StreamExt as _}; +use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView}; +use project::{terminals::TerminalKind, Project}; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use ui::{Pixels, ViewContext, VisualContext as _, WindowContext}; +use util::ResultExt as _; use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; -use workspace::{ItemId, WorkspaceDb, WorkspaceId}; +use workspace::{ + ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, + WorkspaceDb, WorkspaceId, +}; + +use crate::{ + default_working_directory, + terminal_panel::{new_terminal_pane, TerminalPanel}, + TerminalView, +}; + +pub(crate) fn serialize_pane_group( + pane_group: &PaneGroup, + active_pane: &View, + cx: &WindowContext, +) -> SerializedPaneGroup { + build_serialized_pane_group(&pane_group.root, active_pane, cx) +} + +fn build_serialized_pane_group( + pane_group: &Member, + active_pane: &View, + cx: &WindowContext, +) -> SerializedPaneGroup { + match pane_group { + Member::Axis(PaneAxis { + axis, + members, + flexes, + bounding_boxes: _, + }) => SerializedPaneGroup::Group { + axis: SerializedAxis(*axis), + children: members + .iter() + .map(|member| build_serialized_pane_group(member, active_pane, cx)) + .collect::>(), + flexes: Some(flexes.lock().clone()), + }, + Member::Pane(pane_handle) => { + SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx)) + } + } +} + +fn serialize_pane(pane: &View, active: bool, cx: &WindowContext) -> SerializedPane { + let mut items_to_serialize = HashSet::default(); + let pane = pane.read(cx); + let children = pane + .items() + .filter_map(|item| { + let terminal_view = item.act_as::(cx)?; + if terminal_view.read(cx).terminal().read(cx).task().is_some() { + None + } else { + let id = item.item_id().as_u64(); + items_to_serialize.insert(id); + Some(id) + } + }) + .collect::>(); + let active_item = pane + .active_item() + .map(|item| item.item_id().as_u64()) + .filter(|active_id| items_to_serialize.contains(active_id)); + + SerializedPane { + active, + children, + active_item, + } +} + +pub(crate) fn deserialize_terminal_panel( + workspace: WeakView, + project: Model, + database_id: WorkspaceId, + serialized_panel: SerializedTerminalPanel, + cx: &mut WindowContext, +) -> Task>> { + cx.spawn(move |mut cx| async move { + let terminal_panel = workspace.update(&mut cx, |workspace, cx| { + cx.new_view(|cx| { + let mut panel = TerminalPanel::new(workspace, cx); + panel.height = serialized_panel.height.map(|h| h.round()); + panel.width = serialized_panel.width.map(|w| w.round()); + panel + }) + })?; + match &serialized_panel.items { + SerializedItems::NoSplits(item_ids) => { + let items = deserialize_terminal_views( + database_id, + project, + workspace, + item_ids.as_slice(), + &mut cx, + ) + .await; + let active_item = serialized_panel.active_item_id; + terminal_panel.update(&mut cx, |terminal_panel, cx| { + terminal_panel.active_pane.update(cx, |pane, cx| { + populate_pane_items(pane, items, active_item, cx); + }); + })?; + } + SerializedItems::WithSplits(serialized_pane_group) => { + let center_pane = deserialize_pane_group( + workspace, + project, + terminal_panel.clone(), + database_id, + serialized_pane_group, + &mut cx, + ) + .await; + if let Some((center_group, active_pane)) = center_pane { + terminal_panel.update(&mut cx, |terminal_panel, _| { + terminal_panel.center = PaneGroup::with_root(center_group); + terminal_panel.active_pane = + active_pane.unwrap_or_else(|| terminal_panel.center.first_pane()); + })?; + } + } + } + + Ok(terminal_panel) + }) +} + +fn populate_pane_items( + pane: &mut Pane, + items: Vec>, + active_item: Option, + cx: &mut ViewContext<'_, Pane>, +) { + let mut item_index = pane.items_len(); + for item in items { + let activate_item = Some(item.item_id().as_u64()) == active_item; + pane.add_item(Box::new(item), false, false, None, cx); + item_index += 1; + if activate_item { + pane.activate_item(item_index, false, false, cx); + } + } +} + +#[async_recursion(?Send)] +async fn deserialize_pane_group( + workspace: WeakView, + project: Model, + panel: View, + workspace_id: WorkspaceId, + serialized: &SerializedPaneGroup, + cx: &mut AsyncWindowContext, +) -> Option<(Member, Option>)> { + match serialized { + SerializedPaneGroup::Group { + axis, + flexes, + children, + } => { + let mut current_active_pane = None; + let mut members = Vec::new(); + for child in children { + if let Some((new_member, active_pane)) = deserialize_pane_group( + workspace.clone(), + project.clone(), + panel.clone(), + workspace_id, + child, + cx, + ) + .await + { + members.push(new_member); + current_active_pane = current_active_pane.or(active_pane); + } + } + + if members.is_empty() { + return None; + } + + if members.len() == 1 { + return Some((members.remove(0), current_active_pane)); + } + + Some(( + Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())), + current_active_pane, + )) + } + SerializedPaneGroup::Pane(serialized_pane) => { + let active = serialized_pane.active; + let new_items = deserialize_terminal_views( + workspace_id, + project.clone(), + workspace.clone(), + serialized_pane.children.as_slice(), + cx, + ) + .await; + + let pane = panel + .update(cx, |_, cx| { + new_terminal_pane(workspace.clone(), project.clone(), cx) + }) + .log_err()?; + let active_item = serialized_pane.active_item; + pane.update(cx, |pane, cx| { + populate_pane_items(pane, new_items, active_item, cx); + // Avoid blank panes in splits + if pane.items_len() == 0 { + let working_directory = workspace + .update(cx, |workspace, cx| default_working_directory(workspace, cx)) + .ok() + .flatten(); + let kind = TerminalKind::Shell(working_directory); + let window = cx.window_handle(); + let terminal = project + .update(cx, |project, cx| project.create_terminal(kind, window, cx)) + .log_err()?; + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new( + terminal.clone(), + workspace.clone(), + Some(workspace_id), + cx, + ) + })); + pane.add_item(terminal_view, true, false, None, cx); + } + Some(()) + }) + .ok() + .flatten()?; + Some((Member::Pane(pane.clone()), active.then_some(pane))) + } + } +} + +async fn deserialize_terminal_views( + workspace_id: WorkspaceId, + project: Model, + workspace: WeakView, + item_ids: &[u64], + cx: &mut AsyncWindowContext, +) -> Vec> { + let mut items = Vec::with_capacity(item_ids.len()); + let mut deserialized_items = item_ids + .iter() + .map(|item_id| { + cx.update(|cx| { + TerminalView::deserialize( + project.clone(), + workspace.clone(), + workspace_id, + *item_id, + cx, + ) + }) + .unwrap_or_else(|e| Task::ready(Err(e.context("no window present")))) + }) + .collect::>(); + while let Some(item) = deserialized_items.next().await { + if let Some(item) = item.log_err() { + items.push(item); + } + } + items +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct SerializedTerminalPanel { + pub items: SerializedItems, + // A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced. + pub active_item_id: Option, + pub width: Option, + pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum SerializedItems { + // The data stored before terminal splits were introduced. + NoSplits(Vec), + WithSplits(SerializedPaneGroup), +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) enum SerializedPaneGroup { + Pane(SerializedPane), + Group { + axis: SerializedAxis, + flexes: Option>, + children: Vec, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct SerializedPane { + pub active: bool, + pub children: Vec, + pub active_item: Option, +} + +#[derive(Debug)] +pub(crate) struct SerializedAxis(pub Axis); + +impl Serialize for SerializedAxis { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + Axis::Horizontal => serializer.serialize_str("horizontal"), + Axis::Vertical => serializer.serialize_str("vertical"), + } + } +} + +impl<'de> Deserialize<'de> for SerializedAxis { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "horizontal" => Ok(SerializedAxis(Axis::Horizontal)), + "vertical" => Ok(SerializedAxis(Axis::Vertical)), + invalid => Err(serde::de::Error::custom(format!( + "Invalid axis value: '{invalid}'" + ))), + } + } +} define_connection! { pub static ref TERMINAL_DB: TerminalDb = diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ee10e924f4..38b2eda676 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,19 +1,24 @@ -use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; +use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; -use crate::{default_working_directory, TerminalView}; +use crate::{ + default_working_directory, + persistence::{ + deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel, + }, + TerminalView, +}; use breadcrumbs::Breadcrumbs; -use collections::{HashMap, HashSet}; +use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; use gpui::{ - actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter, + actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter, ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; -use project::{terminals::TerminalKind, Fs, ProjectEntryId}; +use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; -use serde::{Deserialize, Serialize}; use settings::Settings; use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId}; use terminal::{ @@ -21,16 +26,18 @@ use terminal::{ Terminal, }; use ui::{ - h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable, - Tooltip, + div, h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, InteractiveElement, + PopoverMenu, Selectable, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::SerializableItem, - pane, + move_item, pane, ui::IconName, - DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace, + ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab, + ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SwapPaneInDirection, ToggleZoom, + Workspace, }; use anyhow::Result; @@ -60,14 +67,14 @@ pub fn init(cx: &mut AppContext) { } pub struct TerminalPanel { - pane: View, + pub(crate) active_pane: View, + pub(crate) center: PaneGroup, fs: Arc, workspace: WeakView, - width: Option, - height: Option, + pub(crate) width: Option, + pub(crate) height: Option, pending_serialization: Task>, pending_terminals_to_add: usize, - _subscriptions: Vec, deferred_tasks: HashMap>, enabled: bool, assistant_enabled: bool, @@ -75,85 +82,14 @@ pub struct TerminalPanel { } impl TerminalPanel { - fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let pane = cx.new_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - Default::default(), - None, - NewTerminal.boxed_clone(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_| true); - - let is_local = workspace.project().read(cx).is_local(); - let workspace = workspace.weak_handle(); - pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { - if let Some(tab) = dropped_item.downcast_ref::() { - let item = if &tab.pane == cx.view() { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - if item.downcast::().is_some() { - return ControlFlow::Continue(()); - } else if let Some(project_path) = item.project_path(cx) { - if let Some(entry_path) = workspace - .update(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .absolute_path(&project_path, cx) - }) - .log_err() - .flatten() - { - add_paths_to_terminal(pane, &[entry_path], cx); - } - } - } - } else if let Some(&entry_id) = dropped_item.downcast_ref::() { - if let Some(entry_path) = workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - project - .path_for_entry(entry_id, cx) - .and_then(|project_path| project.absolute_path(&project_path, cx)) - }) - .log_err() - .flatten() - { - add_paths_to_terminal(pane, &[entry_path], cx); - } - } else if is_local { - if let Some(paths) = dropped_item.downcast_ref::() { - add_paths_to_terminal(pane, paths.paths(), cx); - } - } - - ControlFlow::Break(()) - }); - let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); - let breadcrumbs = cx.new_view(|_| Breadcrumbs::new()); - pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(buffer_search_bar, cx); - toolbar.add_item(breadcrumbs, cx); - }); - pane - }); - let subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - let project = workspace.project().read(cx); - let enabled = project.supports_terminal(cx); - let this = Self { - pane, + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let project = workspace.project(); + let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), cx); + let center = PaneGroup::new(pane.clone()); + let enabled = project.read(cx).supports_terminal(cx); + let terminal_panel = Self { + center, + active_pane: pane, fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), @@ -161,20 +97,19 @@ impl TerminalPanel { height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), - _subscriptions: subscriptions, enabled, assistant_enabled: false, assistant_tab_bar_button: None, }; - this.apply_tab_bar_buttons(cx); - this + terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); + terminal_panel } pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext) { self.assistant_enabled = enabled; if enabled { let focus_handle = self - .pane + .active_pane .read(cx) .active_item() .map(|item| item.focus_handle(cx)) @@ -186,12 +121,14 @@ impl TerminalPanel { } else { self.assistant_tab_bar_button = None; } - self.apply_tab_bar_buttons(cx); + for pane in self.center.panes() { + self.apply_tab_bar_buttons(pane, cx); + } } - fn apply_tab_bar_buttons(&self, cx: &mut ViewContext) { + fn apply_tab_bar_buttons(&self, terminal_pane: &View, cx: &mut ViewContext) { let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); - self.pane.update(cx, |pane, cx| { + terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, cx| { if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { return (None, None); @@ -268,80 +205,45 @@ impl TerminalPanel { .log_err() .flatten(); - let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { - let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx)); - let items = if let Some((serialized_panel, database_id)) = - serialized_panel.as_ref().zip(workspace.database_id()) - { - panel.update(cx, |panel, cx| { - cx.notify(); - panel.height = serialized_panel.height.map(|h| h.round()); - panel.width = serialized_panel.width.map(|w| w.round()); - panel.pane.update(cx, |_, cx| { - serialized_panel - .items - .iter() - .map(|item_id| { - TerminalView::deserialize( - workspace.project().clone(), - workspace.weak_handle(), - database_id, - *item_id, - cx, - ) - }) - .collect::>() - }) - }) - } else { - Vec::new() - }; - let pane = panel.read(cx).pane.clone(); - (panel, pane, items) - })?; + let terminal_panel = workspace + .update(&mut cx, |workspace, cx| { + match serialized_panel.zip(workspace.database_id()) { + Some((serialized_panel, database_id)) => deserialize_terminal_panel( + workspace.weak_handle(), + workspace.project().clone(), + database_id, + serialized_panel, + cx, + ), + None => Task::ready(Ok(cx.new_view(|cx| TerminalPanel::new(workspace, cx)))), + } + })? + .await?; if let Some(workspace) = workspace.upgrade() { - panel - .update(&mut cx, |panel, cx| { - panel._subscriptions.push(cx.subscribe( - &workspace, - |terminal_panel, _, e, cx| { - if let workspace::Event::SpawnTask(spawn_in_terminal) = e { - terminal_panel.spawn_task(spawn_in_terminal, cx); - }; - }, - )) + terminal_panel + .update(&mut cx, |_, cx| { + cx.subscribe(&workspace, |terminal_panel, _, e, cx| { + if let workspace::Event::SpawnTask(spawn_in_terminal) = e { + terminal_panel.spawn_task(spawn_in_terminal, cx); + }; + }) + .detach(); }) .ok(); } - let pane = pane.downgrade(); - let items = futures::future::join_all(items).await; - let mut alive_item_ids = Vec::new(); - pane.update(&mut cx, |pane, cx| { - let active_item_id = serialized_panel - .as_ref() - .and_then(|panel| panel.active_item_id); - let mut active_ix = None; - for item in items { - if let Some(item) = item.log_err() { - let item_id = item.entity_id().as_u64(); - pane.add_item(Box::new(item), false, false, None, cx); - alive_item_ids.push(item_id as ItemId); - if Some(item_id) == active_item_id { - active_ix = Some(pane.items_len() - 1); - } - } - } - - if let Some(active_ix) = active_ix { - pane.activate_item(active_ix, false, false, cx) - } - })?; - // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. if let Some(workspace) = workspace.upgrade() { let cleanup_task = workspace.update(&mut cx, |workspace, cx| { + let alive_item_ids = terminal_panel + .read(cx) + .center + .panes() + .into_iter() + .flat_map(|pane| pane.read(cx).items()) + .map(|item| item.item_id().as_u64() as ItemId) + .collect(); workspace .database_id() .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx)) @@ -351,33 +253,92 @@ impl TerminalPanel { } } - Ok(panel) + Ok(terminal_panel) } fn handle_pane_event( &mut self, - _pane: View, + pane: View, event: &pane::Event, cx: &mut ViewContext, ) { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemovedItem { .. } => self.serialize(cx), - pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), + pane::Event::Remove { focus_on_pane } => { + let pane_count_before_removal = self.center.panes().len(); + let _removal_result = self.center.remove(&pane); + if pane_count_before_removal == 1 { + cx.emit(PanelEvent::Close); + } else { + if let Some(focus_on_pane) = + focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) + { + focus_on_pane.focus_handle(cx).focus(cx); + } + } + } pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { - let pane = self.pane.clone(); - workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) + workspace.update(cx, |workspace, cx| { + item.added_to_pane(workspace, pane.clone(), cx) + }) } } + pane::Event::Split(direction) => { + let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { + return; + }; + self.center.split(&pane, &new_pane, *direction).log_err(); + } + pane::Event::Focus => { + self.active_pane = pane.clone(); + } _ => {} } } + fn new_pane_with_cloned_active_terminal( + &mut self, + cx: &mut ViewContext, + ) -> Option> { + let workspace = self.workspace.clone().upgrade()?; + let project = workspace.read(cx).project().clone(); + let working_directory = self + .active_pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .and_then(|terminal_view| { + terminal_view + .read(cx) + .terminal() + .read(cx) + .working_directory() + }) + .or_else(|| default_working_directory(workspace.read(cx), cx)); + let kind = TerminalKind::Shell(working_directory); + let window = cx.window_handle(); + let terminal = project + .update(cx, |project, cx| project.create_terminal(kind, window, cx)) + .log_err()?; + let database_id = workspace.read(cx).database_id(); + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new(terminal.clone(), self.workspace.clone(), database_id, cx) + })); + let pane = new_terminal_pane(self.workspace.clone(), project, cx); + self.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, cx); + }); + cx.focus_view(&pane); + + Some(pane) + } + pub fn open_terminal( workspace: &mut Workspace, action: &workspace::OpenTerminal, @@ -494,7 +455,7 @@ impl TerminalPanel { .detach_and_log_err(cx); return; } - let (existing_item_index, existing_terminal) = terminals_for_task + let (existing_item_index, task_pane, existing_terminal) = terminals_for_task .last() .expect("covered no terminals case above") .clone(); @@ -503,7 +464,13 @@ impl TerminalPanel { !use_new_terminal, "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" ); - self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx); + self.replace_terminal( + spawn_task, + task_pane, + existing_item_index, + existing_terminal, + cx, + ); } else { self.deferred_tasks.insert( spawn_in_terminal.id.clone(), @@ -518,6 +485,7 @@ impl TerminalPanel { } else { terminal_panel.replace_terminal( spawn_task, + task_pane, existing_item_index, existing_terminal, cx, @@ -562,25 +530,36 @@ impl TerminalPanel { &self, label: &str, cx: &mut AppContext, - ) -> Vec<(usize, View)> { - self.pane - .read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.full_label == label { - Some((index, terminal_view)) - } else { - None - } + ) -> Vec<(usize, View, View)> { + self.center + .panes() + .into_iter() + .flat_map(|pane| { + pane.read(cx) + .items() + .enumerate() + .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) + .filter_map(|(index, terminal_view)| { + let task_state = terminal_view.read(cx).terminal().read(cx).task()?; + if &task_state.full_label == label { + Some((index, terminal_view)) + } else { + None + } + }) + .map(|(index, terminal_view)| (index, pane.clone(), terminal_view)) }) .collect() } - fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) { - self.pane.update(cx, |pane, cx| { + fn activate_terminal_view( + &self, + pane: &View, + item_index: usize, + focus: bool, + cx: &mut WindowContext, + ) { + pane.update(cx, |pane, cx| { pane.activate_item(item_index, true, focus, cx) }) } @@ -601,7 +580,7 @@ impl TerminalPanel { self.pending_terminals_to_add += 1; cx.spawn(|terminal_panel, mut cx| async move { - let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?; + let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?; let result = workspace.update(&mut cx, |workspace, cx| { let window = cx.window_handle(); let terminal = workspace @@ -640,52 +619,49 @@ impl TerminalPanel { } fn serialize(&mut self, cx: &mut ViewContext) { - let mut items_to_serialize = HashSet::default(); - let items = self - .pane - .read(cx) - .items() - .filter_map(|item| { - let terminal_view = item.act_as::(cx)?; - if terminal_view.read(cx).terminal().read(cx).task().is_some() { - None - } else { - let id = item.item_id().as_u64(); - items_to_serialize.insert(id); - Some(id) - } - }) - .collect::>(); - let active_item_id = self - .pane - .read(cx) - .active_item() - .map(|item| item.item_id().as_u64()) - .filter(|active_id| items_to_serialize.contains(active_id)); let height = self.height; let width = self.width; - self.pending_serialization = cx.background_executor().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - TERMINAL_PANEL_KEY.into(), - serde_json::to_string(&SerializedTerminalPanel { - items, - active_item_id, - height, - width, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); + self.pending_serialization = cx.spawn(|terminal_panel, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + let terminal_panel = terminal_panel.upgrade()?; + let items = terminal_panel + .update(&mut cx, |terminal_panel, cx| { + SerializedItems::WithSplits(serialize_pane_group( + &terminal_panel.center, + &terminal_panel.active_pane, + cx, + )) + }) + .ok()?; + cx.background_executor() + .spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + TERMINAL_PANEL_KEY.into(), + serde_json::to_string(&SerializedTerminalPanel { + items, + active_item_id: None, + height, + width, + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ) + .await; + Some(()) + }); } fn replace_terminal( &self, spawn_task: SpawnInTerminal, + task_pane: View, terminal_item_index: usize, terminal_to_replace: View, cx: &mut ViewContext<'_, Self>, @@ -708,7 +684,7 @@ impl TerminalPanel { match reveal { RevealStrategy::Always => { - self.activate_terminal_view(terminal_item_index, true, cx); + self.activate_terminal_view(&task_pane, terminal_item_index, true, cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -718,7 +694,7 @@ impl TerminalPanel { .detach(); } RevealStrategy::NoFocus => { - self.activate_terminal_view(terminal_item_index, false, cx); + self.activate_terminal_view(&task_pane, terminal_item_index, false, cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -734,7 +710,7 @@ impl TerminalPanel { } fn has_no_terminals(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 + self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 } pub fn assistant_enabled(&self) -> bool { @@ -742,11 +718,149 @@ impl TerminalPanel { } } +pub fn new_terminal_pane( + workspace: WeakView, + project: Model, + cx: &mut ViewContext, +) -> View { + let is_local = project.read(cx).is_local(); + let terminal_panel = cx.view().clone(); + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.clone(), + project.clone(), + Default::default(), + None, + NewTerminal.boxed_clone(), + cx, + ); + pane.set_can_navigate(false, cx); + pane.display_nav_history_buttons(None); + pane.set_should_display_tab_bar(|_| true); + + let terminal_panel_for_split_check = terminal_panel.clone(); + pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| { + if let Some(tab) = dragged_item.downcast_ref::() { + let current_pane = cx.view().clone(); + let can_drag_away = + terminal_panel_for_split_check.update(cx, |terminal_panel, _| { + let current_panes = terminal_panel.center.panes(); + !current_panes.contains(&&tab.pane) + || current_panes.len() > 1 + || (tab.pane != current_pane || pane.items_len() > 1) + }); + if can_drag_away { + let item = if tab.pane == current_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { + return item.downcast::().is_some(); + } + } + } + false + }))); + + let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); + let breadcrumbs = cx.new_view(|_| Breadcrumbs::new()); + pane.toolbar().update(cx, |toolbar, cx| { + toolbar.add_item(buffer_search_bar, cx); + toolbar.add_item(breadcrumbs, cx); + }); + + pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { + if let Some(tab) = dropped_item.downcast_ref::() { + let this_pane = cx.view().clone(); + let belongs_to_this_pane = tab.pane == this_pane; + let item = if belongs_to_this_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { + if item.downcast::().is_some() { + let source = tab.pane.clone(); + let item_id_to_move = item.item_id(); + + let new_pane = pane.drag_split_direction().and_then(|split_direction| { + terminal_panel.update(cx, |terminal_panel, cx| { + let new_pane = + new_terminal_pane(workspace.clone(), project.clone(), cx); + terminal_panel.apply_tab_bar_buttons(&new_pane, cx); + terminal_panel + .center + .split(&this_pane, &new_pane, split_direction) + .log_err()?; + Some(new_pane) + }) + }); + + let destination; + let destination_index; + if let Some(new_pane) = new_pane { + destination_index = new_pane.read(cx).active_item_index(); + destination = new_pane; + } else if belongs_to_this_pane { + return ControlFlow::Break(()); + } else { + destination = cx.view().clone(); + destination_index = pane.active_item_index(); + } + // Destination pane may be the one currently updated, so defer the move. + cx.spawn(|_, mut cx| async move { + cx.update(|cx| { + move_item( + &source, + &destination, + item_id_to_move, + destination_index, + cx, + ); + }) + .ok(); + }) + .detach(); + } else if let Some(project_path) = item.project_path(cx) { + if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } + } + } else if let Some(&entry_id) = dropped_item.downcast_ref::() { + if let Some(entry_path) = project + .read(cx) + .path_for_entry(entry_id, cx) + .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx)) + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } else if is_local { + if let Some(paths) = dropped_item.downcast_ref::() { + add_paths_to_terminal(pane, paths.paths(), cx); + } + } + + ControlFlow::Break(()) + }); + + pane + }); + + cx.subscribe(&pane, TerminalPanel::handle_pane_event) + .detach(); + cx.observe(&pane, |_, _, cx| cx.notify()).detach(); + + pane +} + async fn wait_for_terminals_tasks( - terminals_for_task: Vec<(usize, View)>, + terminals_for_task: Vec<(usize, View, View)>, cx: &mut AsyncWindowContext, ) { - let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| { + let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| { terminal .update(cx, |terminal_view, cx| { terminal_view @@ -781,7 +895,7 @@ impl Render for TerminalPanel { let mut registrar = DivRegistrar::new( |panel, cx| { panel - .pane + .active_pane .read(cx) .toolbar() .read(cx) @@ -790,13 +904,99 @@ impl Render for TerminalPanel { cx, ); BufferSearchBar::register(&mut registrar); - registrar.into_div().size_full().child(self.pane.clone()) + let registrar = registrar.into_div(); + self.workspace + .update(cx, |workspace, cx| { + registrar.size_full().child(self.center.render( + workspace.project(), + &HashMap::default(), + None, + &self.active_pane, + workspace.zoomed_item(), + workspace.app_state(), + cx, + )) + }) + .ok() + .map(|div| { + div.on_action({ + cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| { + if let Some(pane) = terminal_panel.center.find_pane_in_direction( + &terminal_panel.active_pane, + action.0, + cx, + ) { + cx.focus_view(&pane); + } + }) + }) + .on_action( + cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(ix) = panes + .iter() + .position(|pane| **pane == terminal_panel.active_pane) + { + let next_ix = (ix + 1) % panes.len(); + let next_pane = panes[next_ix].clone(); + cx.focus_view(&next_pane); + } + }), + ) + .on_action( + cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(ix) = panes + .iter() + .position(|pane| **pane == terminal_panel.active_pane) + { + let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); + let prev_pane = panes[prev_ix].clone(); + cx.focus_view(&prev_pane); + } + }), + ) + .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { + cx.focus_view(&pane); + } else { + if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + } + } + })) + .on_action(cx.listener( + |terminal_panel, action: &SwapPaneInDirection, cx| { + if let Some(to) = terminal_panel + .center + .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx) + .cloned() + { + terminal_panel + .center + .swap(&terminal_panel.active_pane.clone(), &to); + cx.notify(); + } + }, + )) + }) + .unwrap_or_else(|| div()) } } impl FocusableView for TerminalPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.pane.focus_handle(cx) + self.active_pane.focus_handle(cx) } } @@ -848,11 +1048,12 @@ impl Panel for TerminalPanel { } fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() + self.active_pane.read(cx).is_zoomed() } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + self.active_pane + .update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { @@ -872,7 +1073,12 @@ impl Panel for TerminalPanel { } fn icon_label(&self, cx: &WindowContext) -> Option { - let count = self.pane.read(cx).items_len(); + let count = self + .center + .panes() + .into_iter() + .map(|pane| pane.read(cx).items_len()) + .sum::(); if count == 0 { None } else { @@ -901,7 +1107,7 @@ impl Panel for TerminalPanel { } fn pane(&self) -> Option> { - Some(self.pane.clone()) + Some(self.active_pane.clone()) } } @@ -923,14 +1129,6 @@ impl Render for InlineAssistTabBarButton { } } -#[derive(Serialize, Deserialize)] -struct SerializedTerminalPanel { - items: Vec, - active_item_id: Option, - width: Option, - height: Option, -} - fn retrieve_system_shell() -> Option { #[cfg(not(target_os = "windows"))] { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ad0c7f520d..35ad35a0e1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,8 +33,8 @@ use workspace::{ notifications::NotifyResultExt, register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, - CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, Pane, ToolbarItemLocation, - Workspace, WorkspaceId, + CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, + WorkspaceId, }; use anyhow::Context; @@ -1222,10 +1222,10 @@ impl SerializableItem for TerminalView { workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: workspace::ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let window = cx.window_handle(); - cx.spawn(|pane, mut cx| async move { + cx.spawn(|mut cx| async move { let cwd = cx .update(|cx| { let from_db = TERMINAL_DB @@ -1249,7 +1249,7 @@ impl SerializableItem for TerminalView { let terminal = project.update(&mut cx, |project, cx| { project.create_terminal(TerminalKind::Shell(cwd), window, cx) })??; - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) }) }) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a7bf90dd17..20437145cb 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -315,7 +315,7 @@ pub trait SerializableItem: Item { _workspace: WeakView, _workspace_id: WorkspaceId, _item_id: ItemId, - _cx: &mut ViewContext, + _cx: &mut WindowContext, ) -> Task>>; fn serialize( @@ -1032,7 +1032,7 @@ impl WeakFollowableItemHandle for WeakView { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::{Item, ItemEvent, SerializableItem, TabContentParams}; - use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId}; use gpui::{ AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView, InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext, @@ -1040,6 +1040,7 @@ pub mod test { }; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use std::{any::Any, cell::Cell, path::Path}; + use ui::WindowContext; pub struct TestProjectItem { pub entry_id: Option, @@ -1339,7 +1340,7 @@ pub mod test { _workspace: WeakView, workspace_id: WorkspaceId, _item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx)); Task::ready(Ok(view)) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4eec2f18d1..69485846e9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -291,7 +291,7 @@ pub struct Pane { can_drop_predicate: Option bool>>, custom_drop_handle: Option) -> ControlFlow<(), ()>>>, - can_split: bool, + can_split_predicate: Option) -> bool>>, should_display_tab_bar: Rc) -> bool>, render_tab_bar_buttons: Rc) -> (Option, Option)>, @@ -411,7 +411,7 @@ impl Pane { project, can_drop_predicate, custom_drop_handle: None, - can_split: true, + can_split_predicate: None, should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show), render_tab_bar_buttons: Rc::new(move |pane, cx| { if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { @@ -623,9 +623,13 @@ impl Pane { self.should_display_tab_bar = Rc::new(should_display_tab_bar); } - pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { - self.can_split = can_split; - cx.notify(); + pub fn set_can_split( + &mut self, + can_split_predicate: Option< + Arc) -> bool + 'static>, + >, + ) { + self.can_split_predicate = can_split_predicate; } pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { @@ -2384,8 +2388,18 @@ impl Pane { self.zoomed } - fn handle_drag_move(&mut self, event: &DragMoveEvent, cx: &mut ViewContext) { - if !self.can_split { + fn handle_drag_move( + &mut self, + event: &DragMoveEvent, + cx: &mut ViewContext, + ) { + let can_split_predicate = self.can_split_predicate.take(); + let can_split = match &can_split_predicate { + Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx), + None => false, + }; + self.can_split_predicate = can_split_predicate; + if !can_split { return; } @@ -2679,6 +2693,10 @@ impl Pane { }) .collect() } + + pub fn drag_split_direction(&self) -> Option { + self.drag_split_direction + } } impl FocusableView for Pane { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6f7d1a66b9..4461e58925 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -27,11 +27,11 @@ const VERTICAL_MIN_SIZE: f32 = 100.; /// Single-pane group is a regular pane. #[derive(Clone)] pub struct PaneGroup { - pub(crate) root: Member, + pub root: Member, } impl PaneGroup { - pub(crate) fn with_root(root: Member) -> Self { + pub fn with_root(root: Member) -> Self { Self { root } } @@ -122,7 +122,7 @@ impl PaneGroup { } #[allow(clippy::too_many_arguments)] - pub(crate) fn render( + pub fn render( &self, project: &Model, follower_states: &HashMap, @@ -144,19 +144,51 @@ impl PaneGroup { ) } - pub(crate) fn panes(&self) -> Vec<&View> { + pub fn panes(&self) -> Vec<&View> { let mut panes = Vec::new(); self.root.collect_panes(&mut panes); panes } - pub(crate) fn first_pane(&self) -> View { + pub fn first_pane(&self) -> View { self.root.first_pane() } + + pub fn find_pane_in_direction( + &mut self, + active_pane: &View, + direction: SplitDirection, + cx: &WindowContext, + ) -> Option<&View> { + let bounding_box = self.bounding_box_for_pane(active_pane)?; + let cursor = active_pane.read(cx).pixel_position_of_cursor(cx); + let center = match cursor { + Some(cursor) if bounding_box.contains(&cursor) => cursor, + _ => bounding_box.center(), + }; + + let distance_to_next = crate::HANDLE_HITBOX_SIZE; + + let target = match direction { + SplitDirection::Left => { + Point::new(bounding_box.left() - distance_to_next.into(), center.y) + } + SplitDirection::Right => { + Point::new(bounding_box.right() + distance_to_next.into(), center.y) + } + SplitDirection::Up => { + Point::new(center.x, bounding_box.top() - distance_to_next.into()) + } + SplitDirection::Down => { + Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) + } + }; + self.pane_at_pixel_position(target) + } } -#[derive(Clone)] -pub(crate) enum Member { +#[derive(Debug, Clone)] +pub enum Member { Axis(PaneAxis), Pane(View), } @@ -359,8 +391,8 @@ impl Member { } } -#[derive(Clone)] -pub(crate) struct PaneAxis { +#[derive(Debug, Clone)] +pub struct PaneAxis { pub axis: Axis, pub members: Vec, pub flexes: Arc>>, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 28fd730e60..4687b1decd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -777,7 +777,7 @@ pub struct ViewId { pub id: u64, } -struct FollowerState { +pub struct FollowerState { center_pane: View, dock_pane: Option>, active_view_id: Option, @@ -887,14 +887,16 @@ impl Workspace { let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); let center_pane = cx.new_view(|cx| { - Pane::new( + let mut center_pane = Pane::new( weak_handle.clone(), project.clone(), pane_history_timestamp.clone(), None, NewFile.boxed_clone(), cx, - ) + ); + center_pane.set_can_split(Some(Arc::new(|_, _, _| true))); + center_pane }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); @@ -2464,14 +2466,16 @@ impl Workspace { fn add_pane(&mut self, cx: &mut ViewContext) -> View { let pane = cx.new_view(|cx| { - Pane::new( + let mut pane = Pane::new( self.weak_handle(), self.project.clone(), self.pane_history_timestamp.clone(), None, NewFile.boxed_clone(), cx, - ) + ); + pane.set_can_split(Some(Arc::new(|_, _, _| true))); + pane }); cx.subscribe(&pane, Self::handle_pane_event).detach(); self.panes.push(pane.clone()); @@ -2955,30 +2959,9 @@ impl Workspace { direction: SplitDirection, cx: &WindowContext, ) -> Option> { - let bounding_box = self.center.bounding_box_for_pane(&self.active_pane)?; - let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); - let center = match cursor { - Some(cursor) if bounding_box.contains(&cursor) => cursor, - _ => bounding_box.center(), - }; - - let distance_to_next = pane_group::HANDLE_HITBOX_SIZE; - - let target = match direction { - SplitDirection::Left => { - Point::new(bounding_box.left() - distance_to_next.into(), center.y) - } - SplitDirection::Right => { - Point::new(bounding_box.right() + distance_to_next.into(), center.y) - } - SplitDirection::Up => { - Point::new(center.x, bounding_box.top() - distance_to_next.into()) - } - SplitDirection::Down => { - Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) - } - }; - self.center.pane_at_pixel_position(target).cloned() + self.center + .find_pane_in_direction(&self.active_pane, direction, cx) + .cloned() } pub fn swap_pane_in_direction( @@ -4591,6 +4574,10 @@ impl Workspace { let window = cx.window_handle().downcast::()?; cx.read_window(&window, |workspace, _| workspace).ok() } + + pub fn zoomed_item(&self) -> Option<&AnyWeakView> { + self.zoomed.as_ref() + } } fn leader_border_for_pane(