Merge remote-tracking branch 'origin/main' into paint-context

This commit is contained in:
Nathan Sobo 2023-08-08 18:27:16 -06:00
commit db96fb1307
163 changed files with 9459 additions and 4729 deletions

View file

@ -203,7 +203,7 @@ impl Dock {
pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
self.panel_entries.iter().position(|entry| {
let panel = entry.panel.as_any();
cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
})
}
@ -530,16 +530,15 @@ impl View for PanelButtons {
tooltip_action.as_ref().map(|action| action.boxed_clone());
move |_, this, cx| {
if let Some(tooltip_action) = &tooltip_action {
let window_id = cx.window_id();
let window = cx.window();
let view_id = this.workspace.id();
let tooltip_action = tooltip_action.boxed_clone();
cx.spawn(|_, mut cx| async move {
cx.dispatch_action(
window_id,
window.dispatch_action(
view_id,
&*tooltip_action,
)
.ok();
&mut cx,
);
})
.detach();
}

View file

@ -6,6 +6,7 @@ use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result;
use client::{proto, Client};
use gpui::geometry::vector::Vector2F;
use gpui::AnyWindowHandle;
use gpui::{
fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@ -250,7 +251,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn workspace_deactivated(&self, cx: &mut WindowContext);
fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
fn id(&self) -> usize;
fn window_id(&self) -> usize;
fn window(&self) -> AnyWindowHandle;
fn as_any(&self) -> &AnyViewHandle;
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
@ -280,7 +281,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
pub trait WeakItemHandle {
fn id(&self) -> usize;
fn window_id(&self) -> usize;
fn window(&self) -> AnyWindowHandle;
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
}
@ -542,8 +543,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.id()
}
fn window_id(&self) -> usize {
self.window_id()
fn window(&self) -> AnyWindowHandle {
AnyViewHandle::window(self)
}
fn as_any(&self) -> &AnyViewHandle {
@ -649,8 +650,8 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
self.id()
}
fn window_id(&self) -> usize {
self.window_id()
fn window(&self) -> AnyWindowHandle {
self.window()
}
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {

View file

@ -746,6 +746,10 @@ impl Pane {
_: &CloseAllItems,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if self.items.is_empty() {
return None;
}
Some(self.close_items(cx, move |_| true))
}
@ -1913,8 +1917,8 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
gpui::platform::MouseButton::Left,
move |_, _: &mut V, cx| {
let window_id = cx.window_id();
cx.app_context().focus(window_id, Some(child_view_id))
let window = cx.window();
cx.app_context().focus(window, Some(child_view_id))
},
),
);
@ -1968,7 +1972,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
@ -1983,7 +1988,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@ -2061,7 +2067,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@ -2137,7 +2144,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// singleton view
@ -2205,7 +2213,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", false, cx);
@ -2252,7 +2261,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@ -2272,7 +2282,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", true, cx);
@ -2295,7 +2306,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@ -2315,7 +2327,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@ -2335,7 +2348,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
add_labeled_item(&pane, "A", false, cx);

View file

@ -28,11 +28,11 @@ where
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
drag_and_drop
.currently_dragged::<DraggedItem>(cx.window_id())
.currently_dragged::<DraggedItem>(cx.window())
.map(|(drag_position, _)| drag_position)
.or_else(|| {
drag_and_drop
.currently_dragged::<ProjectEntryId>(cx.window_id())
.currently_dragged::<ProjectEntryId>(cx.window())
.map(|(drag_position, _)| drag_position)
})
} else {
@ -91,10 +91,10 @@ where
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
if drag_and_drop
.currently_dragged::<DraggedItem>(cx.window_id())
.currently_dragged::<DraggedItem>(cx.window())
.is_some()
|| drag_and_drop
.currently_dragged::<ProjectEntryId>(cx.window_id())
.currently_dragged::<ProjectEntryId>(cx.window())
.is_some()
{
cx.notify();
@ -122,11 +122,11 @@ pub fn handle_dropped_item<V: View>(
}
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
let action = if let Some((_, dragged_item)) =
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
{
Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
} else if let Some((_, project_entry)) =
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window())
{
Action::Open(*project_entry)
} else {

View file

@ -584,7 +584,7 @@ impl SplitDirection {
}
mod element {
use std::{cell::RefCell, ops::Range, rc::Rc};
use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
use gpui::{
geometry::{
@ -593,8 +593,9 @@ mod element {
},
json::{self, ToJson},
platform::{CursorStyle, MouseButton},
AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, PaintContext,
RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
scene::MouseDrag,
AnyElement, Axis, CursorRegion, Element, EventContext, LayoutContext, MouseRegion,
PaintContext, RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
};
use crate::{
@ -682,6 +683,96 @@ mod element {
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
fn handle_resize(
flexes: Rc<RefCell<Vec<f32>>>,
axis: Axis,
preceding_ix: usize,
child_start: Vector2F,
drag_bounds: RectF,
) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
let size = move |ix, flexes: &[f32]| {
drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
};
move |drag, workspace: &mut Workspace, cx| {
if drag.end {
// TODO: Clear cascading resize state
return;
}
let min_size = match axis {
Axis::Horizontal => HORIZONTAL_MIN_SIZE,
Axis::Vertical => VERTICAL_MIN_SIZE,
};
let mut flexes = flexes.borrow_mut();
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
return;
}
let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
- size(preceding_ix, flexes.as_slice());
let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
let flex_change = pixel_dx / drag_bounds.length_along(axis);
let current_target_flex = flexes[target_ix] + flex_change;
let next_target_flex =
flexes[(target_ix as isize + next) as usize] - flex_change;
(current_target_flex, next_target_flex)
};
let mut successors = from_fn({
let forward = proposed_current_pixel_change > 0.;
let mut ix_offset = 0;
let len = flexes.len();
move || {
let result = if forward {
(preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
} else {
(preceding_ix as isize - ix_offset as isize >= 0)
.then(|| preceding_ix - ix_offset)
};
ix_offset += 1;
result
}
});
while proposed_current_pixel_change.abs() > 0. {
let Some(current_ix) = successors.next() else {
break;
};
let next_target_size = f32::max(
size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
min_size,
);
let current_target_size = f32::max(
size(current_ix, flexes.as_slice())
+ size(current_ix + 1, flexes.as_slice())
- next_target_size,
min_size,
);
let current_pixel_change =
current_target_size - size(current_ix, flexes.as_slice());
let (current_target_flex, next_target_flex) =
flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
flexes[current_ix] = current_target_flex;
flexes[current_ix + 1] = next_target_flex;
proposed_current_pixel_change -= current_pixel_change;
}
workspace.schedule_serialize(cx);
cx.notify();
}
}
}
impl Extend<AnyElement<Workspace>> for PaneAxisElement {
@ -792,8 +883,7 @@ mod element {
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
if let Some(Some((next_ix, next_child))) = can_resize.then(|| children_iter.peek())
{
if can_resize && children_iter.peek().is_some() {
scene.push_stacking_context(None, None);
let handle_origin = match self.axis {
@ -822,15 +912,6 @@ mod element {
style,
});
let axis = self.axis;
let child_size = child.size();
let next_child_size = next_child.size();
let drag_bounds = visible_bounds.clone();
let flexes = self.flexes.borrow();
let current_flex = flexes[ix];
let next_ix = *next_ix;
let next_flex = flexes[next_ix];
drop(flexes);
enum ResizeHandle {}
let mut mouse_region = MouseRegion::new::<ResizeHandle>(
cx.view_id(),
@ -838,56 +919,16 @@ mod element {
handle_bounds,
);
mouse_region = mouse_region
.on_drag(MouseButton::Left, {
let flexes = self.flexes.clone();
move |drag, workspace: &mut Workspace, cx| {
let min_size = match axis {
Axis::Horizontal => HORIZONTAL_MIN_SIZE,
Axis::Vertical => VERTICAL_MIN_SIZE,
};
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - 1. > child_size.along(axis)
|| min_size - 1. > next_child_size.along(axis)
{
return;
}
let mut current_target_size =
(drag.position - child_start).along(axis);
let proposed_current_pixel_change =
current_target_size - child_size.along(axis);
if proposed_current_pixel_change < 0. {
current_target_size = f32::max(current_target_size, min_size);
} else if proposed_current_pixel_change > 0. {
// TODO: cascade this change to other children if current item is at min size
let next_target_size = f32::max(
next_child_size.along(axis) - proposed_current_pixel_change,
min_size,
);
current_target_size = f32::min(
current_target_size,
child_size.along(axis) + next_child_size.along(axis)
- next_target_size,
);
}
let current_pixel_change =
current_target_size - child_size.along(axis);
let flex_change =
current_pixel_change / drag_bounds.length_along(axis);
let current_target_flex = current_flex + flex_change;
let next_target_flex = next_flex - flex_change;
let mut borrow = flexes.borrow_mut();
*borrow.get_mut(ix).unwrap() = current_target_flex;
*borrow.get_mut(next_ix).unwrap() = next_target_flex;
workspace.schedule_serialize(cx);
cx.notify();
}
})
.on_drag(
MouseButton::Left,
Self::handle_resize(
self.flexes.clone(),
self.axis,
ix,
child_start,
visible_bounds.clone(),
),
)
.on_click(MouseButton::Left, {
let flexes = self.flexes.clone();
move |e, v: &mut Workspace, cx| {

View file

@ -235,7 +235,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
impl PartialEq for Box<dyn SearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window_id() == other.window_id()
self.id() == other.id() && self.window() == other.window()
}
}
@ -259,7 +259,7 @@ impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id() && self.window_id() == other.window_id()
self.id() == other.id() && self.window() == other.window()
}
}
@ -267,6 +267,6 @@ impl Eq for Box<dyn WeakSearchableItemHandle> {}
impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.id(), self.window_id()).hash(state)
(self.id(), self.window().id()).hash(state)
}
}

View file

@ -27,6 +27,7 @@ trait StatusItemViewHandle {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
);
fn ui_name(&self) -> &'static str;
}
pub struct StatusBar {
@ -57,7 +58,6 @@ impl View for StatusBar {
.with_margin_right(theme.item_spacing)
}))
.into_any(),
right: Flex::row()
.with_children(self.right_items.iter().rev().map(|i| {
ChildView::new(i.as_any(), cx)
@ -96,6 +96,56 @@ impl StatusBar {
cx.notify();
}
pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
self.left_items
.iter()
.chain(self.right_items.iter())
.find_map(|item| item.as_any().clone().downcast())
}
pub fn position_of_item<T>(&self) -> Option<usize>
where
T: StatusItemView,
{
for (index, item) in self.left_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index);
}
}
for (index, item) in self.right_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index + self.left_items.len());
}
}
return None;
}
pub fn insert_item_after<T>(
&mut self,
position: usize,
item: ViewHandle<T>,
cx: &mut ViewContext<Self>,
) where
T: 'static + StatusItemView,
{
if position < self.left_items.len() {
self.left_items.insert(position + 1, Box::new(item))
} else {
self.right_items
.insert(position + 1 - self.left_items.len(), Box::new(item))
}
cx.notify()
}
pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
if position < self.left_items.len() {
self.left_items.remove(position);
} else {
self.right_items.remove(position - self.left_items.len());
}
cx.notify();
}
pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
@ -133,6 +183,10 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
this.set_active_pane_item(active_pane_item, cx)
});
}
fn ui_name(&self) -> &'static str {
T::ui_name()
}
}
impl From<&dyn StatusItemViewHandle> for AnyViewHandle {

View file

@ -37,7 +37,7 @@ use gpui::{
},
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
WeakViewHandle, WindowContext,
WeakViewHandle, WindowContext, WindowHandle,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use itertools::Itertools;
@ -122,6 +122,7 @@ actions!(
NewFile,
NewWindow,
CloseWindow,
CloseInactiveTabsAndPanes,
AddFolderToProject,
Unfollow,
Save,
@ -240,6 +241,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_async_action(Workspace::follow_next_collaborator);
cx.add_async_action(Workspace::close);
cx.add_async_action(Workspace::close_inactive_items_and_panes);
cx.add_global_action(Workspace::close_global);
cx.add_global_action(restart);
cx.add_async_action(Workspace::save_all);
@ -747,7 +749,7 @@ impl Workspace {
fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
requesting_window_id: Option<usize>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
) -> Task<(
WeakViewHandle<Workspace>,
@ -791,20 +793,13 @@ impl Workspace {
DB.next_id().await.unwrap_or(0)
};
let workspace = requesting_window_id
.and_then(|window_id| {
cx.update(|cx| {
cx.replace_root_view(window_id, |cx| {
Workspace::new(
workspace_id,
project_handle.clone(),
app_state.clone(),
cx,
)
})
})
})
.unwrap_or_else(|| {
let window = if let Some(window) = requesting_window {
window.replace_root(&mut cx, |cx| {
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
});
window
} else {
{
let window_bounds_override = window_bounds_env_override(&cx);
let (bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None)
@ -850,8 +845,12 @@ impl Workspace {
)
},
)
.1
});
}
};
// We haven't yielded the main thread since obtaining the window handle,
// so the window exists.
let workspace = window.root(&cx).unwrap();
(app_state.initialize_workspace)(
workspace.downgrade(),
@ -862,7 +861,7 @@ impl Workspace {
.await
.log_err();
cx.update_window(workspace.window_id(), |cx| cx.activate_window());
window.update(&mut cx, |cx| cx.activate_window());
let workspace = workspace.downgrade();
notify_if_database_failed(&workspace, &mut cx);
@ -1233,14 +1232,14 @@ impl Workspace {
pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
cx.spawn(|mut cx| async move {
let id = cx
.window_ids()
let window = cx
.windows()
.into_iter()
.find(|&id| cx.window_is_active(id));
if let Some(id) = id {
.find(|window| window.is_active(&cx).unwrap_or(false));
if let Some(window) = window {
//This can only get called when the window's project connection has been lost
//so we don't need to prompt the user for anything and instead just close the window
cx.remove_window(id);
window.remove(&mut cx);
}
})
.detach();
@ -1251,11 +1250,11 @@ impl Workspace {
_: &CloseWindow,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let window_id = cx.window_id();
let window = cx.window();
let prepare = self.prepare_to_close(false, cx);
Some(cx.spawn(|_, mut cx| async move {
if prepare.await? {
cx.remove_window(window_id);
window.remove(&mut cx);
}
Ok(())
}))
@ -1267,13 +1266,13 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
let active_call = self.active_call().cloned();
let window_id = cx.window_id();
let window = cx.window();
cx.spawn(|this, mut cx| async move {
let workspace_count = cx
.window_ids()
.windows()
.into_iter()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.filter(|window| window.root_is::<Workspace>())
.count();
if let Some(active_call) = active_call {
@ -1281,11 +1280,11 @@ impl Workspace {
&& workspace_count == 1
&& active_call.read_with(&cx, |call, _| call.room().is_some())
{
let answer = cx.prompt(
window_id,
let answer = window.prompt(
PromptLevel::Warning,
"Do you want to leave the current call?",
&["Close window and hang up", "Cancel"],
&mut cx,
);
if let Some(mut answer) = answer {
@ -1391,7 +1390,7 @@ impl Workspace {
paths: Vec<PathBuf>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let window_id = cx.window_id();
let window = cx.window().downcast::<Self>();
let is_remote = self.project.read(cx).is_remote();
let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
@ -1403,15 +1402,15 @@ impl Workspace {
let app_state = self.app_state.clone();
cx.spawn(|_, mut cx| async move {
let window_id_to_replace = if let Some(close_task) = close_task {
let window_to_replace = if let Some(close_task) = close_task {
if !close_task.await? {
return Ok(());
}
Some(window_id)
window
} else {
None
};
cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
.await?;
Ok(())
})
@ -1671,6 +1670,45 @@ impl Workspace {
}
}
pub fn close_inactive_items_and_panes(
&mut self,
_: &CloseInactiveTabsAndPanes,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let current_pane = self.active_pane();
let mut tasks = Vec::new();
if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
pane.close_inactive_items(&CloseInactiveItems, cx)
}) {
tasks.push(current_pane_close);
};
for pane in self.panes() {
if pane.id() == current_pane.id() {
continue;
}
if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
pane.close_all_items(&CloseAllItems, cx)
}) {
tasks.push(close_pane_items)
}
}
if tasks.is_empty() {
None
} else {
Some(cx.spawn(|_, _| async move {
for task in tasks {
task.await?
}
Ok(())
}))
}
}
pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
let dock = match dock_side {
DockPosition::Left => &self.left_dock,
@ -3143,7 +3181,7 @@ impl Workspace {
let left_visible = left_dock.is_open();
let left_active_panel = left_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
cx.view_ui_name(panel.as_any().window(), panel.id())?
.to_string(),
)
});
@ -3156,7 +3194,7 @@ impl Workspace {
let right_visible = right_dock.is_open();
let right_active_panel = right_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
cx.view_ui_name(panel.as_any().window(), panel.id())?
.to_string(),
)
});
@ -3169,7 +3207,7 @@ impl Workspace {
let bottom_visible = bottom_dock.is_open();
let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
Some(
cx.view_ui_name(panel.as_any().window_id(), panel.id())?
cx.view_ui_name(panel.as_any().window(), panel.id())?
.to_string(),
)
});
@ -3789,9 +3827,9 @@ pub fn activate_workspace_for_project(
cx: &mut AsyncAppContext,
predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
) -> Option<WeakViewHandle<Workspace>> {
for window_id in cx.window_ids() {
let handle = cx
.update_window(window_id, |cx| {
for window in cx.windows() {
let handle = window
.update(cx, |cx| {
if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
let project = workspace_handle.read(cx).project.clone();
if project.update(cx, &predicate) {
@ -3818,7 +3856,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
requesting_window_id: Option<usize>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AppContext,
) -> Task<
Result<(
@ -3846,7 +3884,7 @@ pub fn open_paths(
} else {
Ok(cx
.update(|cx| {
Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
})
.await)
}
@ -3907,18 +3945,23 @@ pub fn join_remote_project(
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let existing_workspace = cx
.window_ids()
.windows()
.into_iter()
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
.find(|workspace| {
cx.read_window(workspace.window_id(), |cx| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
.find_map(|window| {
window.downcast::<Workspace>().and_then(|window| {
window.read_root_with(&cx, |workspace, cx| {
if workspace.project().read(cx).remote_id() == Some(project_id) {
Some(cx.handle().downgrade())
} else {
None
}
})
})
.unwrap_or(false)
});
})
.flatten();
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace.downgrade()
existing_workspace
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
@ -3936,7 +3979,7 @@ pub fn join_remote_project(
.await?;
let window_bounds_override = window_bounds_env_override(&cx);
let (_, workspace) = cx.add_window(
let window = cx.add_window(
(app_state.build_window_options)(
window_bounds_override,
None,
@ -3944,6 +3987,7 @@ pub fn join_remote_project(
),
|cx| Workspace::new(0, project, app_state.clone(), cx),
);
let workspace = window.root(&cx).unwrap();
(app_state.initialize_workspace)(
workspace.downgrade(),
false,
@ -3956,7 +4000,7 @@ pub fn join_remote_project(
workspace.downgrade()
};
cx.activate_window(workspace.window_id());
workspace.window().activate(&mut cx);
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
@ -3995,29 +4039,22 @@ pub fn join_remote_project(
pub fn restart(_: &Restart, cx: &mut AppContext) {
let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
cx.spawn(|mut cx| async move {
let mut workspaces = cx
.window_ids()
let mut workspace_windows = cx
.windows()
.into_iter()
.filter_map(|window_id| {
Some(
cx.root_view(window_id)?
.clone()
.downcast::<Workspace>()?
.downgrade(),
)
})
.filter_map(|window| window.downcast::<Workspace>())
.collect::<Vec<_>>();
// If multiple windows have unsaved changes, and need a save prompt,
// prompt in the active window before switching to a different window.
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
let answer = cx.prompt(
workspace.window_id(),
if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure you want to restart?",
&["Restart", "Cancel"],
&mut cx,
);
if let Some(mut answer) = answer {
@ -4029,14 +4066,13 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
}
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?
{
return Ok(());
for window in workspace_windows {
if let Some(close) = window.update_root(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
}) {
if !close.await? {
return Ok(());
}
}
}
cx.platform().restart();
@ -4072,10 +4108,11 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
// Adding an item with no ambiguity renders the tab without detail.
let item1 = cx.add_view(window_id, |_| {
let item1 = window.add_view(cx, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
item
@ -4087,7 +4124,7 @@ mod tests {
// Adding an item that creates ambiguity increases the level of detail on
// both tabs.
let item2 = cx.add_view(window_id, |_| {
let item2 = window.add_view(cx, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
item
@ -4101,7 +4138,7 @@ mod tests {
// Adding an item that creates ambiguity increases the level of detail only
// on the ambiguous tabs. In this case, the ambiguity can't be resolved so
// we stop at the highest detail available.
let item3 = cx.add_view(window_id, |_| {
let item3 = window.add_view(cx, |_| {
let mut item = TestItem::new();
item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
item
@ -4136,16 +4173,17 @@ mod tests {
.await;
let project = Project::test(fs, ["root1".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let item1 = cx.add_view(window_id, |cx| {
let item1 = window.add_view(cx, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
});
let item2 = cx.add_view(window_id, |cx| {
let item2 = window.add_view(cx, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
});
@ -4159,17 +4197,11 @@ mod tests {
.map(|e| e.id)
);
});
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root1")
);
assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
// Add a second item to a non-empty pane
workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("two.txt — root1")
);
assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1"));
project.read_with(cx, |project, cx| {
assert_eq!(
project.active_entry(),
@ -4185,10 +4217,7 @@ mod tests {
})
.await
.unwrap();
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root1")
);
assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
project.read_with(cx, |project, cx| {
assert_eq!(
project.active_entry(),
@ -4206,16 +4235,13 @@ mod tests {
.await
.unwrap();
assert_eq!(
cx.current_window_title(window_id).as_deref(),
window.current_title(cx).as_deref(),
Some("one.txt — root1, root2")
);
// Remove a project folder
project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
assert_eq!(
cx.current_window_title(window_id).as_deref(),
Some("one.txt — root2")
);
assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2"));
}
#[gpui::test]
@ -4226,18 +4252,19 @@ mod tests {
fs.insert_tree("/root", json!({ "one": "" })).await;
let project = Project::test(fs, ["root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
// When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(window_id, |_| TestItem::new());
let item1 = window.add_view(cx, |_| TestItem::new());
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
assert!(task.await.unwrap());
// When there are dirty untitled items, prompt to save each one. If the user
// cancels any prompt, then abort.
let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
let item3 = cx.add_view(window_id, |cx| {
let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
let item3 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@ -4248,9 +4275,9 @@ mod tests {
});
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
cx.foreground().run_until_parked();
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
window.simulate_prompt_answer(2, cx); // cancel
cx.foreground().run_until_parked();
assert!(!cx.has_pending_prompt(window_id));
assert!(!window.has_pending_prompt(cx));
assert!(!task.await.unwrap());
}
@ -4261,26 +4288,27 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let item1 = cx.add_view(window_id, |cx| {
let item1 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let item2 = cx.add_view(window_id, |cx| {
let item2 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_conflict(true)
.with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
});
let item3 = cx.add_view(window_id, |cx| {
let item3 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_conflict(true)
.with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
});
let item4 = cx.add_view(window_id, |cx| {
let item4 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new_untitled(cx)])
@ -4308,10 +4336,10 @@ mod tests {
assert_eq!(pane.items_len(), 4);
assert_eq!(pane.active_item().unwrap().id(), item1.id());
});
assert!(cx.has_pending_prompt(window_id));
assert!(window.has_pending_prompt(cx));
// Confirm saving item 1.
cx.simulate_prompt_answer(window_id, 0);
window.simulate_prompt_answer(0, cx);
cx.foreground().run_until_parked();
// Item 1 is saved. There's a prompt to save item 3.
@ -4322,10 +4350,10 @@ mod tests {
assert_eq!(pane.items_len(), 3);
assert_eq!(pane.active_item().unwrap().id(), item3.id());
});
assert!(cx.has_pending_prompt(window_id));
assert!(window.has_pending_prompt(cx));
// Cancel saving item 3.
cx.simulate_prompt_answer(window_id, 1);
window.simulate_prompt_answer(1, cx);
cx.foreground().run_until_parked();
// Item 3 is reloaded. There's a prompt to save item 4.
@ -4336,10 +4364,10 @@ mod tests {
assert_eq!(pane.items_len(), 2);
assert_eq!(pane.active_item().unwrap().id(), item4.id());
});
assert!(cx.has_pending_prompt(window_id));
assert!(window.has_pending_prompt(cx));
// Confirm saving item 4.
cx.simulate_prompt_answer(window_id, 0);
window.simulate_prompt_answer(0, cx);
cx.foreground().run_until_parked();
// There's a prompt for a path for item 4.
@ -4363,13 +4391,14 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// Create several workspace items with single project entries, and two
// workspace items with multiple project entries.
let single_entry_items = (0..=4)
.map(|project_entry_id| {
cx.add_view(window_id, |cx| {
window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_project_items(&[TestProjectItem::new(
@ -4380,7 +4409,7 @@ mod tests {
})
})
.collect::<Vec<_>>();
let item_2_3 = cx.add_view(window_id, |cx| {
let item_2_3 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_singleton(false)
@ -4389,7 +4418,7 @@ mod tests {
single_entry_items[3].read(cx).project_items[0].clone(),
])
});
let item_3_4 = cx.add_view(window_id, |cx| {
let item_3_4 = window.add_view(cx, |cx| {
TestItem::new()
.with_dirty(true)
.with_singleton(false)
@ -4441,7 +4470,7 @@ mod tests {
&[ProjectEntryId::from_proto(0)]
);
});
cx.simulate_prompt_answer(window_id, 0);
window.simulate_prompt_answer(0, cx);
cx.foreground().run_until_parked();
left_pane.read_with(cx, |pane, cx| {
@ -4450,7 +4479,7 @@ mod tests {
&[ProjectEntryId::from_proto(2)]
);
});
cx.simulate_prompt_answer(window_id, 0);
window.simulate_prompt_answer(0, cx);
cx.foreground().run_until_parked();
close.await.unwrap();
@ -4466,10 +4495,11 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let item = cx.add_view(window_id, |cx| {
let item = window.add_view(cx, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let item_id = item.id();
@ -4488,7 +4518,7 @@ mod tests {
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
window.simulate_deactivation(cx);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
@ -4509,12 +4539,12 @@ mod tests {
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
// Deactivating the window still saves the file.
cx.simulate_window_activation(Some(window_id));
window.simulate_activation(cx);
item.update(cx, |item, cx| {
cx.focus_self();
item.is_dirty = true;
});
cx.simulate_window_activation(None);
window.simulate_deactivation(cx);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
@ -4551,7 +4581,7 @@ mod tests {
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
.await
.unwrap();
assert!(!cx.has_pending_prompt(window_id));
assert!(!window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
@ -4572,7 +4602,7 @@ mod tests {
let _close_items =
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
deterministic.run_until_parked();
assert!(cx.has_pending_prompt(window_id));
assert!(window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
}
@ -4583,9 +4613,10 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let item = cx.add_view(window_id, |cx| {
let item = window.add_view(cx, |cx| {
TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
});
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
@ -4636,7 +4667,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let panel = workspace.update(cx, |workspace, cx| {
let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
@ -4783,7 +4815,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
// Add panel_1 on the left, panel_2 on the right.
@ -4938,7 +4971,7 @@ mod tests {
// If focus is transferred to another view that's not a panel or another pane, we still show
// the panel as zoomed.
let focus_receiver = cx.add_view(window_id, |_| EmptyView);
let focus_receiver = window.add_view(cx, |_| EmptyView);
focus_receiver.update(cx, |_, cx| cx.focus_self());
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));