Allow splitting the terminal panel (#21238)

Closes https://github.com/zed-industries/zed/issues/4351


![it_splits](https://github.com/user-attachments/assets/40de03c9-2173-4441-ba96-8e91537956e0)

Applies the same splitting mechanism, as Zed's central pane has, to the
terminal panel.
Similar navigation, splitting and (de)serialization capabilities are
supported.

Notable caveats:
* zooming keeps the terminal splits' ratio, rather expanding the
terminal pane
* on macOs, central panel is split with `cmd-k up/down/etc.` but `cmd-k`
is a "standard" terminal clearing keybinding on macOS, so terminal panel
splitting is done via `ctrl-k up/down/etc.`
* task terminals are "split" into regular terminals, and also not
persisted (same as currently in the terminal)

Seems ok for the initial version, we can revisit and polish things
later.

Release Notes:

- Added the ability to split the terminal panel
This commit is contained in:
Kirill Bulatov 2024-11-27 20:22:39 +02:00 committed by GitHub
parent 4564da2875
commit d0bafce86b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 953 additions and 348 deletions

1
Cargo.lock generated
View file

@ -12418,6 +12418,7 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.1.1",
"breadcrumbs",
"client",
"collections",

View file

@ -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"
}
}
]

View file

@ -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);

View file

@ -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>,
workspace_id: workspace::WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
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::<Buffer>().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::<Editor>().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);

View file

@ -1578,7 +1578,7 @@ pub struct AnyDrag {
pub view: AnyView,
/// The value of the dragged item, to be dropped
pub value: Box<dyn Any>,
pub value: Arc<dyn Any>,
/// This is used to render the dragged item in the same place
/// on the original element that the drag was initiated

View file

@ -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<T> {
/// The bounds of this element.
pub bounds: Bounds<Pixels>,
drag: PhantomData<T>,
dragged_item: Arc<dyn Any>,
}
impl<T: 'static> DragMoveEvent<T> {
@ -71,6 +73,11 @@ impl<T: 'static> DragMoveEvent<T> {
.and_then(|drag| drag.value.downcast_ref::<T>())
.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::<T>())
{
(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::<T>() {
(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<CanDropPredicate>,
pub(crate) click_listeners: Vec<ClickListener>,
pub(crate) drag_listener: Option<(Box<dyn Any>, DragListener)>,
pub(crate) drag_listener: Option<(Arc<dyn Any>, DragListener)>,
pub(crate) hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
pub(crate) tooltip_builder: Option<TooltipBuilder>,
pub(crate) occlude_mouse: bool,

View file

@ -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());
}
}

View file

@ -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,
});

View file

@ -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>,
workspace_id: WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
cx: &mut WindowContext,
) -> Task<gpui::Result<View<Self>>> {
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"))?;

View file

@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-recursion.workspace = true
breadcrumbs.workspace = true
collections.workspace = true
db.workspace = true

View file

@ -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<Pane>,
cx: &WindowContext,
) -> SerializedPaneGroup {
build_serialized_pane_group(&pane_group.root, active_pane, cx)
}
fn build_serialized_pane_group(
pane_group: &Member,
active_pane: &View<Pane>,
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::<Vec<_>>(),
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<Pane>, 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::<TerminalView>(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::<Vec<_>>();
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<Workspace>,
project: Model<Project>,
database_id: WorkspaceId,
serialized_panel: SerializedTerminalPanel,
cx: &mut WindowContext,
) -> Task<anyhow::Result<View<TerminalPanel>>> {
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<View<TerminalView>>,
active_item: Option<u64>,
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<Workspace>,
project: Model<Project>,
panel: View<TerminalPanel>,
workspace_id: WorkspaceId,
serialized: &SerializedPaneGroup,
cx: &mut AsyncWindowContext,
) -> Option<(Member, Option<View<Pane>>)> {
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<Project>,
workspace: WeakView<Workspace>,
item_ids: &[u64],
cx: &mut AsyncWindowContext,
) -> Vec<View<TerminalView>> {
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::<FuturesUnordered<_>>();
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<u64>,
pub width: Option<Pixels>,
pub height: Option<Pixels>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum SerializedItems {
// The data stored before terminal splits were introduced.
NoSplits(Vec<u64>),
WithSplits(SerializedPaneGroup),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SerializedPaneGroup {
Pane(SerializedPane),
Group {
axis: SerializedAxis,
flexes: Option<Vec<f32>>,
children: Vec<SerializedPaneGroup>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SerializedPane {
pub active: bool,
pub children: Vec<u64>,
pub active_item: Option<u64>,
}
#[derive(Debug)]
pub(crate) struct SerializedAxis(pub Axis);
impl Serialize for SerializedAxis {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<WorkspaceDb> =

View file

@ -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<Pane>,
pub(crate) active_pane: View<Pane>,
pub(crate) center: PaneGroup,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
width: Option<Pixels>,
height: Option<Pixels>,
pub(crate) width: Option<Pixels>,
pub(crate) height: Option<Pixels>,
pending_serialization: Task<Option<()>>,
pending_terminals_to_add: usize,
_subscriptions: Vec<Subscription>,
deferred_tasks: HashMap<TaskId, Task<()>>,
enabled: bool,
assistant_enabled: bool,
@ -75,85 +82,14 @@ pub struct TerminalPanel {
}
impl TerminalPanel {
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> 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::<DraggedTab>() {
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::<TerminalView>().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::<ProjectEntryId>() {
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::<ExternalPaths>() {
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>) -> 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>) {
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<Self>) {
fn apply_tab_bar_buttons(&self, terminal_pane: &View<Pane>, cx: &mut ViewContext<Self>) {
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::<Vec<_>>()
})
})
} 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>,
pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
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<Self>,
) -> Option<View<Pane>> {
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::<TerminalView>())
.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<TerminalView>)> {
self.pane
.read(cx)
.items()
.enumerate()
.filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(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<Pane>, View<TerminalView>)> {
self.center
.panes()
.into_iter()
.flat_map(|pane| {
pane.read(cx)
.items()
.enumerate()
.filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(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<Pane>,
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<Self>) {
let mut items_to_serialize = HashSet::default();
let items = self
.pane
.read(cx)
.items()
.filter_map(|item| {
let terminal_view = item.act_as::<TerminalView>(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::<Vec<_>>();
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<Pane>,
terminal_item_index: usize,
terminal_to_replace: View<TerminalView>,
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<Workspace>,
project: Model<Project>,
cx: &mut ViewContext<TerminalPanel>,
) -> View<Pane> {
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::<DraggedTab>() {
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::<TerminalView>().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::<DraggedTab>() {
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::<TerminalView>().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::<ProjectEntryId>() {
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::<ExternalPaths>() {
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<TerminalView>)>,
terminals_for_task: Vec<(usize, View<Pane>, View<TerminalView>)>,
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>) {
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<Self>) {
@ -872,7 +1073,12 @@ impl Panel for TerminalPanel {
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
let count = self.pane.read(cx).items_len();
let count = self
.center
.panes()
.into_iter()
.map(|pane| pane.read(cx).items_len())
.sum::<usize>();
if count == 0 {
None
} else {
@ -901,7 +1107,7 @@ impl Panel for TerminalPanel {
}
fn pane(&self) -> Option<View<Pane>> {
Some(self.pane.clone())
Some(self.active_pane.clone())
}
}
@ -923,14 +1129,6 @@ impl Render for InlineAssistTabBarButton {
}
}
#[derive(Serialize, Deserialize)]
struct SerializedTerminalPanel {
items: Vec<u64>,
active_item_id: Option<u64>,
width: Option<Pixels>,
height: Option<Pixels>,
}
fn retrieve_system_shell() -> Option<String> {
#[cfg(not(target_os = "windows"))]
{

View file

@ -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>,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
cx: &mut WindowContext,
) -> Task<anyhow::Result<View<Self>>> {
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))
})
})

View file

@ -315,7 +315,7 @@ pub trait SerializableItem: Item {
_workspace: WeakView<Workspace>,
_workspace_id: WorkspaceId,
_item_id: ItemId,
_cx: &mut ViewContext<Pane>,
_cx: &mut WindowContext,
) -> Task<Result<View<Self>>>;
fn serialize(
@ -1032,7 +1032,7 @@ impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
#[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<ProjectEntryId>,
@ -1339,7 +1340,7 @@ pub mod test {
_workspace: WeakView<Workspace>,
workspace_id: WorkspaceId,
_item_id: ItemId,
cx: &mut ViewContext<Pane>,
cx: &mut WindowContext,
) -> Task<anyhow::Result<View<Self>>> {
let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
Task::ready(Ok(view))

View file

@ -291,7 +291,7 @@ pub struct Pane {
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
custom_drop_handle:
Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
can_split: bool,
can_split_predicate: Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool>>,
should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
render_tab_bar_buttons:
Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
@ -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>) {
self.can_split = can_split;
cx.notify();
pub fn set_can_split(
&mut self,
can_split_predicate: Option<
Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool + 'static>,
>,
) {
self.can_split_predicate = can_split_predicate;
}
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
@ -2384,8 +2388,18 @@ impl Pane {
self.zoomed
}
fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
if !self.can_split {
fn handle_drag_move<T: 'static>(
&mut self,
event: &DragMoveEvent<T>,
cx: &mut ViewContext<Self>,
) {
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<SplitDirection> {
self.drag_split_direction
}
}
impl FocusableView for Pane {

View file

@ -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<Project>,
follower_states: &HashMap<PeerId, FollowerState>,
@ -144,19 +144,51 @@ impl PaneGroup {
)
}
pub(crate) fn panes(&self) -> Vec<&View<Pane>> {
pub fn panes(&self) -> Vec<&View<Pane>> {
let mut panes = Vec::new();
self.root.collect_panes(&mut panes);
panes
}
pub(crate) fn first_pane(&self) -> View<Pane> {
pub fn first_pane(&self) -> View<Pane> {
self.root.first_pane()
}
pub fn find_pane_in_direction(
&mut self,
active_pane: &View<Pane>,
direction: SplitDirection,
cx: &WindowContext,
) -> Option<&View<Pane>> {
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<Pane>),
}
@ -359,8 +391,8 @@ impl Member {
}
}
#[derive(Clone)]
pub(crate) struct PaneAxis {
#[derive(Debug, Clone)]
pub struct PaneAxis {
pub axis: Axis,
pub members: Vec<Member>,
pub flexes: Arc<Mutex<Vec<f32>>>,

View file

@ -777,7 +777,7 @@ pub struct ViewId {
pub id: u64,
}
struct FollowerState {
pub struct FollowerState {
center_pane: View<Pane>,
dock_pane: Option<View<Pane>>,
active_view_id: Option<ViewId>,
@ -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(&center_pane, Self::handle_pane_event).detach();
@ -2464,14 +2466,16 @@ impl Workspace {
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
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<View<Pane>> {
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::<Workspace>()?;
cx.read_window(&window, |workspace, _| workspace).ok()
}
pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
self.zoomed.as_ref()
}
}
fn leader_border_for_pane(