Allow splitting the terminal panel (#21238)
Closes https://github.com/zed-industries/zed/issues/4351  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:
parent
4564da2875
commit
d0bafce86b
17 changed files with 953 additions and 348 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -12418,6 +12418,7 @@ name = "terminal_view"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.1.1",
|
||||
"breadcrumbs",
|
||||
"client",
|
||||
"collections",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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"))?;
|
||||
|
|
|
@ -14,6 +14,7 @@ doctest = false
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
|
|
|
@ -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> =
|
||||
|
|
|
@ -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"))]
|
||||
{
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>>>,
|
||||
|
|
|
@ -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(¢er_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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue