1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11mod toolbar;
12mod workspace_settings;
13
14use anyhow::{anyhow, Context as _, Result};
15use call::ActiveCall;
16use client::{
17 proto::{self, PeerId},
18 Client, Status, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
22use futures::{
23 channel::{mpsc, oneshot},
24 future::try_join_all,
25 Future, FutureExt, StreamExt,
26};
27use gpui::{
28 actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
29 AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
30 Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
31 FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
32 ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
33 Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
34 WindowBounds, WindowContext, WindowHandle, WindowOptions,
35};
36use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
37use itertools::Itertools;
38use language::{LanguageRegistry, Rope};
39use lazy_static::lazy_static;
40pub use modal_layer::*;
41use node_runtime::NodeRuntime;
42use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
43pub use pane::*;
44pub use pane_group::*;
45use persistence::DB;
46pub use persistence::{
47 model::{ItemId, SerializedWorkspace, WorkspaceLocation},
48 WorkspaceDb, DB as WORKSPACE_DB,
49};
50use postage::stream::Stream;
51use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
52use serde::Deserialize;
53use settings::Settings;
54use shared_screen::SharedScreen;
55use status_bar::StatusBar;
56pub use status_bar::StatusItemView;
57use std::{
58 any::TypeId,
59 borrow::Cow,
60 cmp, env,
61 path::{Path, PathBuf},
62 sync::{atomic::AtomicUsize, Arc},
63 time::Duration,
64};
65use theme::{ActiveTheme, ThemeSettings};
66pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
67pub use ui;
68use ui::Label;
69use util::ResultExt;
70use uuid::Uuid;
71pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
72
73use crate::persistence::model::{
74 DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup,
75};
76
77lazy_static! {
78 static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
79 .ok()
80 .as_deref()
81 .and_then(parse_pixel_size_env_var);
82 static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
83 .ok()
84 .as_deref()
85 .and_then(parse_pixel_position_env_var);
86}
87
88#[derive(Clone, PartialEq)]
89pub struct RemoveWorktreeFromProject(pub WorktreeId);
90
91actions!(
92 workspace,
93 [
94 Open,
95 NewFile,
96 NewWindow,
97 CloseWindow,
98 CloseInactiveTabsAndPanes,
99 AddFolderToProject,
100 Unfollow,
101 SaveAs,
102 ReloadActiveItem,
103 ActivatePreviousPane,
104 ActivateNextPane,
105 FollowNextCollaborator,
106 NewTerminal,
107 NewCenterTerminal,
108 ToggleTerminalFocus,
109 NewSearch,
110 DeploySearch,
111 Feedback,
112 Restart,
113 Welcome,
114 ToggleZoom,
115 ToggleLeftDock,
116 ToggleRightDock,
117 ToggleBottomDock,
118 CloseAllDocks,
119 ]
120);
121
122#[derive(Clone, PartialEq)]
123pub struct OpenPaths {
124 pub paths: Vec<PathBuf>,
125}
126
127#[derive(Clone, Deserialize, PartialEq)]
128pub struct ActivatePane(pub usize);
129
130#[derive(Clone, Deserialize, PartialEq)]
131pub struct ActivatePaneInDirection(pub SplitDirection);
132
133#[derive(Clone, Deserialize, PartialEq)]
134pub struct SwapPaneInDirection(pub SplitDirection);
135
136#[derive(Clone, Deserialize, PartialEq)]
137pub struct NewFileInDirection(pub SplitDirection);
138
139#[derive(Clone, PartialEq, Debug, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct SaveAll {
142 pub save_intent: Option<SaveIntent>,
143}
144
145#[derive(Clone, PartialEq, Debug, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct Save {
148 pub save_intent: Option<SaveIntent>,
149}
150
151#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
152#[serde(rename_all = "camelCase")]
153pub struct CloseAllItemsAndPanes {
154 pub save_intent: Option<SaveIntent>,
155}
156
157impl_actions!(
158 workspace,
159 [
160 ActivatePane,
161 ActivatePaneInDirection,
162 CloseAllItemsAndPanes,
163 NewFileInDirection,
164 OpenTerminal,
165 Save,
166 SaveAll,
167 SwapPaneInDirection,
168 ]
169);
170
171#[derive(Deserialize)]
172pub struct Toast {
173 id: usize,
174 msg: Cow<'static, str>,
175 #[serde(skip)]
176 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
177}
178
179impl Toast {
180 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
181 Toast {
182 id,
183 msg: msg.into(),
184 on_click: None,
185 }
186 }
187
188 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
189 where
190 M: Into<Cow<'static, str>>,
191 F: Fn(&mut WindowContext) + 'static,
192 {
193 self.on_click = Some((message.into(), Arc::new(on_click)));
194 self
195 }
196}
197
198impl PartialEq for Toast {
199 fn eq(&self, other: &Self) -> bool {
200 self.id == other.id
201 && self.msg == other.msg
202 && self.on_click.is_some() == other.on_click.is_some()
203 }
204}
205
206impl Clone for Toast {
207 fn clone(&self) -> Self {
208 Toast {
209 id: self.id,
210 msg: self.msg.to_owned(),
211 on_click: self.on_click.clone(),
212 }
213 }
214}
215
216#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
217pub struct OpenTerminal {
218 pub working_directory: PathBuf,
219}
220
221pub type WorkspaceId = i64;
222
223pub fn init_settings(cx: &mut AppContext) {
224 WorkspaceSettings::register(cx);
225 ItemSettings::register(cx);
226}
227
228pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
229 init_settings(cx);
230 notifications::init(cx);
231
232 cx.on_action(Workspace::close_global);
233 cx.on_action(restart);
234
235 cx.on_action({
236 let app_state = Arc::downgrade(&app_state);
237 move |_: &Open, cx: &mut AppContext| {
238 let paths = cx.prompt_for_paths(PathPromptOptions {
239 files: true,
240 directories: true,
241 multiple: true,
242 });
243
244 if let Some(app_state) = app_state.upgrade() {
245 cx.spawn(move |cx| async move {
246 if let Some(paths) = paths.await.log_err().flatten() {
247 cx.update(|cx| {
248 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
249 })
250 .ok();
251 }
252 })
253 .detach();
254 }
255 }
256 });
257}
258
259type ProjectItemBuilders =
260 HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
261pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
262 let builders = cx.default_global::<ProjectItemBuilders>();
263 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
264 let item = model.downcast::<I::Item>().unwrap();
265 Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx)))
266 });
267}
268
269type FollowableItemBuilder = fn(
270 View<Pane>,
271 View<Workspace>,
272 ViewId,
273 &mut Option<proto::view::Variant>,
274 &mut WindowContext,
275) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
276type FollowableItemBuilders = HashMap<
277 TypeId,
278 (
279 FollowableItemBuilder,
280 fn(&AnyView) -> Box<dyn FollowableItemHandle>,
281 ),
282>;
283pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
284 let builders = cx.default_global::<FollowableItemBuilders>();
285 builders.insert(
286 TypeId::of::<I>(),
287 (
288 |pane, workspace, id, state, cx| {
289 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
290 cx.foreground_executor()
291 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
292 })
293 },
294 |this| Box::new(this.clone().downcast::<I>().unwrap()),
295 ),
296 );
297}
298
299type ItemDeserializers = HashMap<
300 Arc<str>,
301 fn(
302 Model<Project>,
303 WeakView<Workspace>,
304 WorkspaceId,
305 ItemId,
306 &mut ViewContext<Pane>,
307 ) -> Task<Result<Box<dyn ItemHandle>>>,
308>;
309pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
310 if let Some(serialized_item_kind) = I::serialized_item_kind() {
311 let deserializers = cx.default_global::<ItemDeserializers>();
312 deserializers.insert(
313 Arc::from(serialized_item_kind),
314 |project, workspace, workspace_id, item_id, cx| {
315 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
316 cx.foreground_executor()
317 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
318 },
319 );
320 }
321}
322
323pub struct AppState {
324 pub languages: Arc<LanguageRegistry>,
325 pub client: Arc<Client>,
326 pub user_store: Model<UserStore>,
327 pub workspace_store: Model<WorkspaceStore>,
328 pub fs: Arc<dyn fs::Fs>,
329 pub build_window_options:
330 fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
331 pub node_runtime: Arc<dyn NodeRuntime>,
332}
333
334pub struct WorkspaceStore {
335 workspaces: HashSet<WindowHandle<Workspace>>,
336 followers: Vec<Follower>,
337 client: Arc<Client>,
338 _subscriptions: Vec<client::Subscription>,
339}
340
341#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
342struct Follower {
343 project_id: Option<u64>,
344 peer_id: PeerId,
345}
346
347impl AppState {
348 #[cfg(any(test, feature = "test-support"))]
349 pub fn test(cx: &mut AppContext) -> Arc<Self> {
350 use node_runtime::FakeNodeRuntime;
351 use settings::SettingsStore;
352
353 if !cx.has_global::<SettingsStore>() {
354 let settings_store = SettingsStore::test(cx);
355 cx.set_global(settings_store);
356 }
357
358 let fs = fs::FakeFs::new(cx.background_executor().clone());
359 let languages = Arc::new(LanguageRegistry::test());
360 let http_client = util::http::FakeHttpClient::with_404_response();
361 let client = Client::new(http_client.clone(), cx);
362 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
363 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
364
365 theme::init(theme::LoadThemes::JustBase, cx);
366 client::init(&client, cx);
367 crate::init_settings(cx);
368
369 Arc::new(Self {
370 client,
371 fs,
372 languages,
373 user_store,
374 workspace_store,
375 node_runtime: FakeNodeRuntime::new(),
376 build_window_options: |_, _, _| Default::default(),
377 })
378 }
379}
380
381struct DelayedDebouncedEditAction {
382 task: Option<Task<()>>,
383 cancel_channel: Option<oneshot::Sender<()>>,
384}
385
386impl DelayedDebouncedEditAction {
387 fn new() -> DelayedDebouncedEditAction {
388 DelayedDebouncedEditAction {
389 task: None,
390 cancel_channel: None,
391 }
392 }
393
394 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
395 where
396 F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
397 {
398 if let Some(channel) = self.cancel_channel.take() {
399 _ = channel.send(());
400 }
401
402 let (sender, mut receiver) = oneshot::channel::<()>();
403 self.cancel_channel = Some(sender);
404
405 let previous_task = self.task.take();
406 self.task = Some(cx.spawn(move |workspace, mut cx| async move {
407 let mut timer = cx.background_executor().timer(delay).fuse();
408 if let Some(previous_task) = previous_task {
409 previous_task.await;
410 }
411
412 futures::select_biased! {
413 _ = receiver => return,
414 _ = timer => {}
415 }
416
417 if let Some(result) = workspace
418 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
419 .log_err()
420 {
421 result.await.log_err();
422 }
423 }));
424 }
425}
426
427pub enum Event {
428 PaneAdded(View<Pane>),
429 ContactRequestedJoin(u64),
430 WorkspaceCreated(WeakView<Workspace>),
431}
432
433pub struct Workspace {
434 weak_self: WeakView<Self>,
435 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
436 zoomed: Option<AnyWeakView>,
437 zoomed_position: Option<DockPosition>,
438 center: PaneGroup,
439 left_dock: View<Dock>,
440 bottom_dock: View<Dock>,
441 right_dock: View<Dock>,
442 panes: Vec<View<Pane>>,
443 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
444 active_pane: View<Pane>,
445 last_active_center_pane: Option<WeakView<Pane>>,
446 last_active_view_id: Option<proto::ViewId>,
447 status_bar: View<StatusBar>,
448 modal_layer: View<ModalLayer>,
449 titlebar_item: Option<AnyView>,
450 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
451 project: Model<Project>,
452 follower_states: HashMap<View<Pane>, FollowerState>,
453 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
454 window_edited: bool,
455 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
456 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
457 database_id: WorkspaceId,
458 app_state: Arc<AppState>,
459 _subscriptions: Vec<Subscription>,
460 _apply_leader_updates: Task<Result<()>>,
461 _observe_current_user: Task<Result<()>>,
462 _schedule_serialize: Option<Task<()>>,
463 pane_history_timestamp: Arc<AtomicUsize>,
464 bounds: Bounds<Pixels>,
465}
466
467impl EventEmitter<Event> for Workspace {}
468
469#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
470pub struct ViewId {
471 pub creator: PeerId,
472 pub id: u64,
473}
474
475#[derive(Default)]
476struct FollowerState {
477 leader_id: PeerId,
478 active_view_id: Option<ViewId>,
479 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
480}
481
482impl Workspace {
483 pub fn new(
484 workspace_id: WorkspaceId,
485 project: Model<Project>,
486 app_state: Arc<AppState>,
487 cx: &mut ViewContext<Self>,
488 ) -> Self {
489 cx.observe(&project, |_, _, cx| cx.notify()).detach();
490 cx.subscribe(&project, move |this, _, event, cx| {
491 match event {
492 project::Event::RemoteIdChanged(_) => {
493 this.update_window_title(cx);
494 }
495
496 project::Event::CollaboratorLeft(peer_id) => {
497 this.collaborator_left(*peer_id, cx);
498 }
499
500 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
501 this.update_window_title(cx);
502 this.serialize_workspace(cx);
503 }
504
505 project::Event::DisconnectedFromHost => {
506 this.update_window_edited(cx);
507 cx.disable_focus();
508 }
509
510 project::Event::Closed => {
511 cx.remove_window();
512 }
513
514 project::Event::DeletedEntry(entry_id) => {
515 for pane in this.panes.iter() {
516 pane.update(cx, |pane, cx| {
517 pane.handle_deleted_project_item(*entry_id, cx)
518 });
519 }
520 }
521
522 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
523 cx.new_view(|_| MessageNotification::new(message.clone()))
524 }),
525
526 _ => {}
527 }
528 cx.notify()
529 })
530 .detach();
531
532 cx.on_blur_window(|this, cx| {
533 let focus_handle = this.focus_handle(cx);
534 cx.focus(&focus_handle);
535 })
536 .detach();
537
538 let weak_handle = cx.view().downgrade();
539 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
540
541 let center_pane = cx.new_view(|cx| {
542 Pane::new(
543 weak_handle.clone(),
544 project.clone(),
545 pane_history_timestamp.clone(),
546 None,
547 cx,
548 )
549 });
550 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
551
552 cx.focus_view(¢er_pane);
553 cx.emit(Event::PaneAdded(center_pane.clone()));
554
555 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
556 app_state.workspace_store.update(cx, |store, _| {
557 store.workspaces.insert(window_handle);
558 });
559
560 let mut current_user = app_state.user_store.read(cx).watch_current_user();
561 let mut connection_status = app_state.client.status();
562 let _observe_current_user = cx.spawn(|this, mut cx| async move {
563 current_user.next().await;
564 connection_status.next().await;
565 let mut stream =
566 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
567
568 while stream.recv().await.is_some() {
569 this.update(&mut cx, |_, cx| cx.notify())?;
570 }
571 anyhow::Ok(())
572 });
573
574 // All leader updates are enqueued and then processed in a single task, so
575 // that each asynchronous operation can be run in order.
576 let (leader_updates_tx, mut leader_updates_rx) =
577 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
578 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
579 while let Some((leader_id, update)) = leader_updates_rx.next().await {
580 Self::process_leader_update(&this, leader_id, update, &mut cx)
581 .await
582 .log_err();
583 }
584
585 Ok(())
586 });
587
588 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
589
590 let left_dock = Dock::new(DockPosition::Left, cx);
591 let bottom_dock = Dock::new(DockPosition::Bottom, cx);
592 let right_dock = Dock::new(DockPosition::Right, cx);
593 let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
594 let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
595 let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
596 let status_bar = cx.new_view(|cx| {
597 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
598 status_bar.add_left_item(left_dock_buttons, cx);
599 status_bar.add_right_item(right_dock_buttons, cx);
600 status_bar.add_right_item(bottom_dock_buttons, cx);
601 status_bar
602 });
603
604 let modal_layer = cx.new_view(|_| ModalLayer::new());
605
606 let mut active_call = None;
607 if cx.has_global::<Model<ActiveCall>>() {
608 let call = cx.global::<Model<ActiveCall>>().clone();
609 let mut subscriptions = Vec::new();
610 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
611 active_call = Some((call, subscriptions));
612 }
613
614 let subscriptions = vec![
615 cx.observe_window_activation(Self::on_window_activation_changed),
616 cx.observe_window_bounds(move |_, cx| {
617 if let Some(display) = cx.display() {
618 // Transform fixed bounds to be stored in terms of the containing display
619 let mut bounds = cx.window_bounds();
620 if let WindowBounds::Fixed(window_bounds) = &mut bounds {
621 let display_bounds = display.bounds();
622 window_bounds.origin.x -= display_bounds.origin.x;
623 window_bounds.origin.y -= display_bounds.origin.y;
624 }
625
626 if let Some(display_uuid) = display.uuid().log_err() {
627 cx.background_executor()
628 .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid))
629 .detach_and_log_err(cx);
630 }
631 }
632 cx.notify();
633 }),
634 cx.observe(&left_dock, |this, _, cx| {
635 this.serialize_workspace(cx);
636 cx.notify();
637 }),
638 cx.observe(&bottom_dock, |this, _, cx| {
639 this.serialize_workspace(cx);
640 cx.notify();
641 }),
642 cx.observe(&right_dock, |this, _, cx| {
643 this.serialize_workspace(cx);
644 cx.notify();
645 }),
646 cx.on_release(|this, window, cx| {
647 this.app_state.workspace_store.update(cx, |store, _| {
648 let window = window.downcast::<Self>().unwrap();
649 debug_assert!(store.workspaces.remove(&window));
650 })
651 }),
652 ];
653
654 cx.defer(|this, cx| {
655 this.update_window_title(cx);
656 // todo! @nate - these are useful for testing notifications
657 // this.show_error(
658 // &anyhow::anyhow!("what happens if this message is very very very very very long"),
659 // cx,
660 // );
661
662 // this.show_notification(1, cx, |cx| {
663 // cx.build_view(|_cx| {
664 // simple_message_notification::MessageNotification::new(format!("Error:"))
665 // .with_click_message("click here because!")
666 // })
667 // });
668 });
669 Workspace {
670 weak_self: weak_handle.clone(),
671 zoomed: None,
672 zoomed_position: None,
673 center: PaneGroup::new(center_pane.clone()),
674 panes: vec![center_pane.clone()],
675 panes_by_item: Default::default(),
676 active_pane: center_pane.clone(),
677 last_active_center_pane: Some(center_pane.downgrade()),
678 last_active_view_id: None,
679 status_bar,
680 modal_layer,
681 titlebar_item: None,
682 notifications: Default::default(),
683 left_dock,
684 bottom_dock,
685 right_dock,
686 project: project.clone(),
687 follower_states: Default::default(),
688 last_leaders_by_pane: Default::default(),
689 window_edited: false,
690 active_call,
691 database_id: workspace_id,
692 app_state,
693 _observe_current_user,
694 _apply_leader_updates,
695 _schedule_serialize: None,
696 leader_updates_tx,
697 _subscriptions: subscriptions,
698 pane_history_timestamp,
699 workspace_actions: Default::default(),
700 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
701 bounds: Default::default(),
702 }
703 }
704
705 fn new_local(
706 abs_paths: Vec<PathBuf>,
707 app_state: Arc<AppState>,
708 requesting_window: Option<WindowHandle<Workspace>>,
709 cx: &mut AppContext,
710 ) -> Task<
711 anyhow::Result<(
712 WindowHandle<Workspace>,
713 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
714 )>,
715 > {
716 let project_handle = Project::local(
717 app_state.client.clone(),
718 app_state.node_runtime.clone(),
719 app_state.user_store.clone(),
720 app_state.languages.clone(),
721 app_state.fs.clone(),
722 cx,
723 );
724
725 cx.spawn(|mut cx| async move {
726 let serialized_workspace: Option<SerializedWorkspace> =
727 persistence::DB.workspace_for_roots(&abs_paths.as_slice());
728
729 let paths_to_open = Arc::new(abs_paths);
730
731 // Get project paths for all of the abs_paths
732 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
733 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
734 Vec::with_capacity(paths_to_open.len());
735 for path in paths_to_open.iter().cloned() {
736 if let Some((worktree, project_entry)) = cx
737 .update(|cx| {
738 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
739 })?
740 .await
741 .log_err()
742 {
743 worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
744 project_paths.push((path, Some(project_entry)));
745 } else {
746 project_paths.push((path, None));
747 }
748 }
749
750 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
751 serialized_workspace.id
752 } else {
753 DB.next_id().await.unwrap_or(0)
754 };
755
756 let window = if let Some(window) = requesting_window {
757 cx.update_window(window.into(), |_, cx| {
758 cx.replace_root_view(|cx| {
759 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
760 });
761 })?;
762 window
763 } else {
764 let window_bounds_override = window_bounds_env_override(&cx);
765 let (bounds, display) = if let Some(bounds) = window_bounds_override {
766 (Some(bounds), None)
767 } else {
768 serialized_workspace
769 .as_ref()
770 .and_then(|serialized_workspace| {
771 let serialized_display = serialized_workspace.display?;
772 let mut bounds = serialized_workspace.bounds?;
773
774 // Stored bounds are relative to the containing display.
775 // So convert back to global coordinates if that screen still exists
776 if let WindowBounds::Fixed(mut window_bounds) = bounds {
777 let screen = cx
778 .update(|cx| {
779 cx.displays().into_iter().find(|display| {
780 display.uuid().ok() == Some(serialized_display)
781 })
782 })
783 .ok()??;
784 let screen_bounds = screen.bounds();
785 window_bounds.origin.x += screen_bounds.origin.x;
786 window_bounds.origin.y += screen_bounds.origin.y;
787 bounds = WindowBounds::Fixed(window_bounds);
788 }
789
790 Some((bounds, serialized_display))
791 })
792 .unzip()
793 };
794
795 // Use the serialized workspace to construct the new window
796 let options =
797 cx.update(|cx| (app_state.build_window_options)(bounds, display, cx))?;
798
799 cx.open_window(options, {
800 let app_state = app_state.clone();
801 let workspace_id = workspace_id.clone();
802 let project_handle = project_handle.clone();
803 move |cx| {
804 cx.new_view(|cx| {
805 Workspace::new(workspace_id, project_handle, app_state, cx)
806 })
807 }
808 })?
809 };
810
811 window
812 .update(&mut cx, |_, cx| cx.activate_window())
813 .log_err();
814
815 notify_if_database_failed(window, &mut cx);
816 let opened_items = window
817 .update(&mut cx, |_workspace, cx| {
818 open_items(serialized_workspace, project_paths, app_state, cx)
819 })?
820 .await
821 .unwrap_or_default();
822
823 Ok((window, opened_items))
824 })
825 }
826
827 pub fn weak_handle(&self) -> WeakView<Self> {
828 self.weak_self.clone()
829 }
830
831 pub fn left_dock(&self) -> &View<Dock> {
832 &self.left_dock
833 }
834
835 pub fn bottom_dock(&self) -> &View<Dock> {
836 &self.bottom_dock
837 }
838
839 pub fn right_dock(&self) -> &View<Dock> {
840 &self.right_dock
841 }
842
843 pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
844 let dock = match panel.position(cx) {
845 DockPosition::Left => &self.left_dock,
846 DockPosition::Bottom => &self.bottom_dock,
847 DockPosition::Right => &self.right_dock,
848 };
849
850 dock.update(cx, |dock, cx| {
851 dock.add_panel(panel, self.weak_self.clone(), cx)
852 });
853 }
854
855 pub fn status_bar(&self) -> &View<StatusBar> {
856 &self.status_bar
857 }
858
859 pub fn app_state(&self) -> &Arc<AppState> {
860 &self.app_state
861 }
862
863 pub fn user_store(&self) -> &Model<UserStore> {
864 &self.app_state.user_store
865 }
866
867 pub fn project(&self) -> &Model<Project> {
868 &self.project
869 }
870
871 pub fn recent_navigation_history(
872 &self,
873 limit: Option<usize>,
874 cx: &AppContext,
875 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
876 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
877 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
878 for pane in &self.panes {
879 let pane = pane.read(cx);
880 pane.nav_history()
881 .for_each_entry(cx, |entry, (project_path, fs_path)| {
882 if let Some(fs_path) = &fs_path {
883 abs_paths_opened
884 .entry(fs_path.clone())
885 .or_default()
886 .insert(project_path.clone());
887 }
888 let timestamp = entry.timestamp;
889 match history.entry(project_path) {
890 hash_map::Entry::Occupied(mut entry) => {
891 let (_, old_timestamp) = entry.get();
892 if ×tamp > old_timestamp {
893 entry.insert((fs_path, timestamp));
894 }
895 }
896 hash_map::Entry::Vacant(entry) => {
897 entry.insert((fs_path, timestamp));
898 }
899 }
900 });
901 }
902
903 history
904 .into_iter()
905 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
906 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
907 .rev()
908 .filter(|(history_path, abs_path)| {
909 let latest_project_path_opened = abs_path
910 .as_ref()
911 .and_then(|abs_path| abs_paths_opened.get(abs_path))
912 .and_then(|project_paths| {
913 project_paths
914 .iter()
915 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
916 });
917
918 match latest_project_path_opened {
919 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
920 None => true,
921 }
922 })
923 .take(limit.unwrap_or(usize::MAX))
924 .collect()
925 }
926
927 fn navigate_history(
928 &mut self,
929 pane: WeakView<Pane>,
930 mode: NavigationMode,
931 cx: &mut ViewContext<Workspace>,
932 ) -> Task<Result<()>> {
933 let to_load = if let Some(pane) = pane.upgrade() {
934 // todo!("focus")
935 // cx.focus(&pane);
936
937 pane.update(cx, |pane, cx| {
938 loop {
939 // Retrieve the weak item handle from the history.
940 let entry = pane.nav_history_mut().pop(mode, cx)?;
941
942 // If the item is still present in this pane, then activate it.
943 if let Some(index) = entry
944 .item
945 .upgrade()
946 .and_then(|v| pane.index_for_item(v.as_ref()))
947 {
948 let prev_active_item_index = pane.active_item_index();
949 pane.nav_history_mut().set_mode(mode);
950 pane.activate_item(index, true, true, cx);
951 pane.nav_history_mut().set_mode(NavigationMode::Normal);
952
953 let mut navigated = prev_active_item_index != pane.active_item_index();
954 if let Some(data) = entry.data {
955 navigated |= pane.active_item()?.navigate(data, cx);
956 }
957
958 if navigated {
959 break None;
960 }
961 }
962 // If the item is no longer present in this pane, then retrieve its
963 // project path in order to reopen it.
964 else {
965 break pane
966 .nav_history()
967 .path_for_item(entry.item.id())
968 .map(|(project_path, _)| (project_path, entry));
969 }
970 }
971 })
972 } else {
973 None
974 };
975
976 if let Some((project_path, entry)) = to_load {
977 // If the item was no longer present, then load it again from its previous path.
978 let task = self.load_path(project_path, cx);
979 cx.spawn(|workspace, mut cx| async move {
980 let task = task.await;
981 let mut navigated = false;
982 if let Some((project_entry_id, build_item)) = task.log_err() {
983 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
984 pane.nav_history_mut().set_mode(mode);
985 pane.active_item().map(|p| p.item_id())
986 })?;
987
988 pane.update(&mut cx, |pane, cx| {
989 let item = pane.open_item(project_entry_id, true, cx, build_item);
990 navigated |= Some(item.item_id()) != prev_active_item_id;
991 pane.nav_history_mut().set_mode(NavigationMode::Normal);
992 if let Some(data) = entry.data {
993 navigated |= item.navigate(data, cx);
994 }
995 })?;
996 }
997
998 if !navigated {
999 workspace
1000 .update(&mut cx, |workspace, cx| {
1001 Self::navigate_history(workspace, pane, mode, cx)
1002 })?
1003 .await?;
1004 }
1005
1006 Ok(())
1007 })
1008 } else {
1009 Task::ready(Ok(()))
1010 }
1011 }
1012
1013 pub fn go_back(
1014 &mut self,
1015 pane: WeakView<Pane>,
1016 cx: &mut ViewContext<Workspace>,
1017 ) -> Task<Result<()>> {
1018 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1019 }
1020
1021 pub fn go_forward(
1022 &mut self,
1023 pane: WeakView<Pane>,
1024 cx: &mut ViewContext<Workspace>,
1025 ) -> Task<Result<()>> {
1026 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1027 }
1028
1029 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1030 self.navigate_history(
1031 self.active_pane().downgrade(),
1032 NavigationMode::ReopeningClosedItem,
1033 cx,
1034 )
1035 }
1036
1037 pub fn client(&self) -> &Client {
1038 &self.app_state.client
1039 }
1040
1041 pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1042 self.titlebar_item = Some(item);
1043 cx.notify();
1044 }
1045
1046 pub fn titlebar_item(&self) -> Option<AnyView> {
1047 self.titlebar_item.clone()
1048 }
1049
1050 /// Call the given callback with a workspace whose project is local.
1051 ///
1052 /// If the given workspace has a local project, then it will be passed
1053 /// to the callback. Otherwise, a new empty window will be created.
1054 pub fn with_local_workspace<T, F>(
1055 &mut self,
1056 cx: &mut ViewContext<Self>,
1057 callback: F,
1058 ) -> Task<Result<T>>
1059 where
1060 T: 'static,
1061 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1062 {
1063 if self.project.read(cx).is_local() {
1064 Task::Ready(Some(Ok(callback(self, cx))))
1065 } else {
1066 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1067 cx.spawn(|_vh, mut cx| async move {
1068 let (workspace, _) = task.await?;
1069 workspace.update(&mut cx, callback)
1070 })
1071 }
1072 }
1073
1074 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1075 self.project.read(cx).worktrees()
1076 }
1077
1078 pub fn visible_worktrees<'a>(
1079 &self,
1080 cx: &'a AppContext,
1081 ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1082 self.project.read(cx).visible_worktrees(cx)
1083 }
1084
1085 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1086 let futures = self
1087 .worktrees(cx)
1088 .filter_map(|worktree| worktree.read(cx).as_local())
1089 .map(|worktree| worktree.scan_complete())
1090 .collect::<Vec<_>>();
1091 async move {
1092 for future in futures {
1093 future.await;
1094 }
1095 }
1096 }
1097
1098 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1099 cx.defer(|cx| {
1100 cx.windows().iter().find(|window| {
1101 window
1102 .update(cx, |_, window| {
1103 if window.is_window_active() {
1104 //This can only get called when the window's project connection has been lost
1105 //so we don't need to prompt the user for anything and instead just close the window
1106 window.remove_window();
1107 true
1108 } else {
1109 false
1110 }
1111 })
1112 .unwrap_or(false)
1113 });
1114 });
1115 }
1116
1117 pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1118 let window = cx.window_handle();
1119 let prepare = self.prepare_to_close(false, cx);
1120 cx.spawn(|_, mut cx| async move {
1121 if prepare.await? {
1122 window.update(&mut cx, |_, cx| {
1123 cx.remove_window();
1124 })?;
1125 }
1126 anyhow::Ok(())
1127 })
1128 .detach_and_log_err(cx)
1129 }
1130
1131 pub fn prepare_to_close(
1132 &mut self,
1133 quitting: bool,
1134 cx: &mut ViewContext<Self>,
1135 ) -> Task<Result<bool>> {
1136 //todo!(saveing)
1137 let active_call = self.active_call().cloned();
1138 let window = cx.window_handle();
1139
1140 cx.spawn(|this, mut cx| async move {
1141 let workspace_count = (*cx).update(|cx| {
1142 cx.windows()
1143 .iter()
1144 .filter(|window| window.downcast::<Workspace>().is_some())
1145 .count()
1146 })?;
1147
1148 if let Some(active_call) = active_call {
1149 if !quitting
1150 && workspace_count == 1
1151 && active_call.read_with(&cx, |call, _| call.room().is_some())?
1152 {
1153 let answer = window.update(&mut cx, |_, cx| {
1154 cx.prompt(
1155 PromptLevel::Warning,
1156 "Do you want to leave the current call?",
1157 &["Close window and hang up", "Cancel"],
1158 )
1159 })?;
1160
1161 if answer.await.log_err() == Some(1) {
1162 return anyhow::Ok(false);
1163 } else {
1164 active_call
1165 .update(&mut cx, |call, cx| call.hang_up(cx))?
1166 .await
1167 .log_err();
1168 }
1169 }
1170 }
1171
1172 Ok(this
1173 .update(&mut cx, |this, cx| {
1174 this.save_all_internal(SaveIntent::Close, cx)
1175 })?
1176 .await?)
1177 })
1178 }
1179
1180 fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1181 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1182 .detach_and_log_err(cx);
1183 }
1184
1185 fn save_all_internal(
1186 &mut self,
1187 mut save_intent: SaveIntent,
1188 cx: &mut ViewContext<Self>,
1189 ) -> Task<Result<bool>> {
1190 if self.project.read(cx).is_disconnected() {
1191 return Task::ready(Ok(true));
1192 }
1193 let dirty_items = self
1194 .panes
1195 .iter()
1196 .flat_map(|pane| {
1197 pane.read(cx).items().filter_map(|item| {
1198 if item.is_dirty(cx) {
1199 Some((pane.downgrade(), item.boxed_clone()))
1200 } else {
1201 None
1202 }
1203 })
1204 })
1205 .collect::<Vec<_>>();
1206
1207 let project = self.project.clone();
1208 cx.spawn(|workspace, mut cx| async move {
1209 // Override save mode and display "Save all files" prompt
1210 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1211 let answer = workspace.update(&mut cx, |_, cx| {
1212 let prompt = Pane::file_names_for_prompt(
1213 &mut dirty_items.iter().map(|(_, handle)| handle),
1214 dirty_items.len(),
1215 cx,
1216 );
1217 cx.prompt(
1218 PromptLevel::Warning,
1219 &prompt,
1220 &["Save all", "Discard all", "Cancel"],
1221 )
1222 })?;
1223 match answer.await.log_err() {
1224 Some(0) => save_intent = SaveIntent::SaveAll,
1225 Some(1) => save_intent = SaveIntent::Skip,
1226 _ => {}
1227 }
1228 }
1229 for (pane, item) in dirty_items {
1230 let (singleton, project_entry_ids) =
1231 cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1232 if singleton || !project_entry_ids.is_empty() {
1233 if let Some(ix) =
1234 pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1235 {
1236 if !Pane::save_item(
1237 project.clone(),
1238 &pane,
1239 ix,
1240 &*item,
1241 save_intent,
1242 &mut cx,
1243 )
1244 .await?
1245 {
1246 return Ok(false);
1247 }
1248 }
1249 }
1250 }
1251 Ok(true)
1252 })
1253 }
1254
1255 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
1256 self.client()
1257 .telemetry()
1258 .report_app_event("open project", false, cx);
1259 let paths = cx.prompt_for_paths(PathPromptOptions {
1260 files: true,
1261 directories: true,
1262 multiple: true,
1263 });
1264
1265 cx.spawn(|this, mut cx| async move {
1266 let Some(paths) = paths.await.log_err().flatten() else {
1267 return;
1268 };
1269
1270 if let Some(task) = this
1271 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1272 .log_err()
1273 {
1274 task.await.log_err();
1275 }
1276 })
1277 .detach()
1278 }
1279
1280 pub fn open_workspace_for_paths(
1281 &mut self,
1282 paths: Vec<PathBuf>,
1283 cx: &mut ViewContext<Self>,
1284 ) -> Task<Result<()>> {
1285 let window = cx.window_handle().downcast::<Self>();
1286 let is_remote = self.project.read(cx).is_remote();
1287 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1288 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1289 let close_task = if is_remote || has_worktree || has_dirty_items {
1290 None
1291 } else {
1292 Some(self.prepare_to_close(false, cx))
1293 };
1294 let app_state = self.app_state.clone();
1295
1296 cx.spawn(|_, mut cx| async move {
1297 let window_to_replace = if let Some(close_task) = close_task {
1298 if !close_task.await? {
1299 return Ok(());
1300 }
1301 window
1302 } else {
1303 None
1304 };
1305 cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
1306 .await?;
1307 Ok(())
1308 })
1309 }
1310
1311 #[allow(clippy::type_complexity)]
1312 pub fn open_paths(
1313 &mut self,
1314 mut abs_paths: Vec<PathBuf>,
1315 visible: bool,
1316 cx: &mut ViewContext<Self>,
1317 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1318 log::info!("open paths {abs_paths:?}");
1319
1320 let fs = self.app_state.fs.clone();
1321
1322 // Sort the paths to ensure we add worktrees for parents before their children.
1323 abs_paths.sort_unstable();
1324 cx.spawn(move |this, mut cx| async move {
1325 let mut tasks = Vec::with_capacity(abs_paths.len());
1326 for abs_path in &abs_paths {
1327 let project_path = match this
1328 .update(&mut cx, |this, cx| {
1329 Workspace::project_path_for_path(
1330 this.project.clone(),
1331 abs_path,
1332 visible,
1333 cx,
1334 )
1335 })
1336 .log_err()
1337 {
1338 Some(project_path) => project_path.await.log_err(),
1339 None => None,
1340 };
1341
1342 let this = this.clone();
1343 let abs_path = abs_path.clone();
1344 let fs = fs.clone();
1345 let task = cx.spawn(move |mut cx| async move {
1346 let (worktree, project_path) = project_path?;
1347 if fs.is_file(&abs_path).await {
1348 Some(
1349 this.update(&mut cx, |this, cx| {
1350 this.open_path(project_path, None, true, cx)
1351 })
1352 .log_err()?
1353 .await,
1354 )
1355 } else {
1356 this.update(&mut cx, |workspace, cx| {
1357 let worktree = worktree.read(cx);
1358 let worktree_abs_path = worktree.abs_path();
1359 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1360 worktree.root_entry()
1361 } else {
1362 abs_path
1363 .strip_prefix(worktree_abs_path.as_ref())
1364 .ok()
1365 .and_then(|relative_path| {
1366 worktree.entry_for_path(relative_path)
1367 })
1368 }
1369 .map(|entry| entry.id);
1370 if let Some(entry_id) = entry_id {
1371 workspace.project.update(cx, |_, cx| {
1372 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1373 })
1374 }
1375 })
1376 .log_err()?;
1377 None
1378 }
1379 });
1380 tasks.push(task);
1381 }
1382
1383 futures::future::join_all(tasks).await
1384 })
1385 }
1386
1387 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1388 let paths = cx.prompt_for_paths(PathPromptOptions {
1389 files: false,
1390 directories: true,
1391 multiple: true,
1392 });
1393 cx.spawn(|this, mut cx| async move {
1394 if let Some(paths) = paths.await.log_err().flatten() {
1395 let results = this
1396 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1397 .await;
1398 for result in results.into_iter().flatten() {
1399 result.log_err();
1400 }
1401 }
1402 anyhow::Ok(())
1403 })
1404 .detach_and_log_err(cx);
1405 }
1406
1407 fn project_path_for_path(
1408 project: Model<Project>,
1409 abs_path: &Path,
1410 visible: bool,
1411 cx: &mut AppContext,
1412 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1413 let entry = project.update(cx, |project, cx| {
1414 project.find_or_create_local_worktree(abs_path, visible, cx)
1415 });
1416 cx.spawn(|mut cx| async move {
1417 let (worktree, path) = entry.await?;
1418 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1419 Ok((
1420 worktree,
1421 ProjectPath {
1422 worktree_id,
1423 path: path.into(),
1424 },
1425 ))
1426 })
1427 }
1428
1429 pub fn items<'a>(
1430 &'a self,
1431 cx: &'a AppContext,
1432 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1433 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1434 }
1435
1436 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1437 self.items_of_type(cx).max_by_key(|item| item.item_id())
1438 }
1439
1440 pub fn items_of_type<'a, T: Item>(
1441 &'a self,
1442 cx: &'a AppContext,
1443 ) -> impl 'a + Iterator<Item = View<T>> {
1444 self.panes
1445 .iter()
1446 .flat_map(|pane| pane.read(cx).items_of_type())
1447 }
1448
1449 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1450 self.active_pane().read(cx).active_item()
1451 }
1452
1453 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1454 let item = self.active_item(cx)?;
1455 item.to_any().downcast::<I>().ok()
1456 }
1457
1458 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1459 self.active_item(cx).and_then(|item| item.project_path(cx))
1460 }
1461
1462 pub fn save_active_item(
1463 &mut self,
1464 save_intent: SaveIntent,
1465 cx: &mut ViewContext<Self>,
1466 ) -> Task<Result<()>> {
1467 let project = self.project.clone();
1468 let pane = self.active_pane();
1469 let item_ix = pane.read(cx).active_item_index();
1470 let item = pane.read(cx).active_item();
1471 let pane = pane.downgrade();
1472
1473 cx.spawn(|_, mut cx| async move {
1474 if let Some(item) = item {
1475 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1476 .await
1477 .map(|_| ())
1478 } else {
1479 Ok(())
1480 }
1481 })
1482 }
1483
1484 pub fn close_inactive_items_and_panes(
1485 &mut self,
1486 _: &CloseInactiveTabsAndPanes,
1487 cx: &mut ViewContext<Self>,
1488 ) {
1489 self.close_all_internal(true, SaveIntent::Close, cx)
1490 .map(|task| task.detach_and_log_err(cx));
1491 }
1492
1493 pub fn close_all_items_and_panes(
1494 &mut self,
1495 action: &CloseAllItemsAndPanes,
1496 cx: &mut ViewContext<Self>,
1497 ) {
1498 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1499 .map(|task| task.detach_and_log_err(cx));
1500 }
1501
1502 fn close_all_internal(
1503 &mut self,
1504 retain_active_pane: bool,
1505 save_intent: SaveIntent,
1506 cx: &mut ViewContext<Self>,
1507 ) -> Option<Task<Result<()>>> {
1508 let current_pane = self.active_pane();
1509
1510 let mut tasks = Vec::new();
1511
1512 if retain_active_pane {
1513 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1514 pane.close_inactive_items(&CloseInactiveItems, cx)
1515 }) {
1516 tasks.push(current_pane_close);
1517 };
1518 }
1519
1520 for pane in self.panes() {
1521 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
1522 continue;
1523 }
1524
1525 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1526 pane.close_all_items(
1527 &CloseAllItems {
1528 save_intent: Some(save_intent),
1529 },
1530 cx,
1531 )
1532 }) {
1533 tasks.push(close_pane_items)
1534 }
1535 }
1536
1537 if tasks.is_empty() {
1538 None
1539 } else {
1540 Some(cx.spawn(|_, _| async move {
1541 for task in tasks {
1542 task.await?
1543 }
1544 Ok(())
1545 }))
1546 }
1547 }
1548
1549 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1550 let dock = match dock_side {
1551 DockPosition::Left => &self.left_dock,
1552 DockPosition::Bottom => &self.bottom_dock,
1553 DockPosition::Right => &self.right_dock,
1554 };
1555 let mut focus_center = false;
1556 let mut reveal_dock = false;
1557 dock.update(cx, |dock, cx| {
1558 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1559 let was_visible = dock.is_open() && !other_is_zoomed;
1560 dock.set_open(!was_visible, cx);
1561
1562 if let Some(active_panel) = dock.active_panel() {
1563 if was_visible {
1564 if active_panel.focus_handle(cx).contains_focused(cx) {
1565 focus_center = true;
1566 }
1567 } else {
1568 let focus_handle = &active_panel.focus_handle(cx);
1569 cx.focus(focus_handle);
1570 reveal_dock = true;
1571 }
1572 }
1573 });
1574
1575 if reveal_dock {
1576 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1577 }
1578
1579 if focus_center {
1580 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1581 }
1582
1583 cx.notify();
1584 self.serialize_workspace(cx);
1585 }
1586
1587 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1588 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1589
1590 for dock in docks {
1591 dock.update(cx, |dock, cx| {
1592 dock.set_open(false, cx);
1593 });
1594 }
1595
1596 // todo!("focus")
1597 // cx.focus_self();
1598 cx.notify();
1599 self.serialize_workspace(cx);
1600 }
1601
1602 /// Transfer focus to the panel of the given type.
1603 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
1604 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
1605 panel.to_any().downcast().ok()
1606 }
1607
1608 /// Focus the panel of the given type if it isn't already focused. If it is
1609 /// already focused, then transfer focus back to the workspace center.
1610 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1611 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
1612 !panel.focus_handle(cx).contains_focused(cx)
1613 });
1614 }
1615
1616 /// Focus or unfocus the given panel type, depending on the given callback.
1617 fn focus_or_unfocus_panel<T: Panel>(
1618 &mut self,
1619 cx: &mut ViewContext<Self>,
1620 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1621 ) -> Option<Arc<dyn PanelHandle>> {
1622 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1623 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1624 let mut focus_center = false;
1625 let panel = dock.update(cx, |dock, cx| {
1626 dock.activate_panel(panel_index, cx);
1627
1628 let panel = dock.active_panel().cloned();
1629 if let Some(panel) = panel.as_ref() {
1630 if should_focus(&**panel, cx) {
1631 dock.set_open(true, cx);
1632 panel.focus_handle(cx).focus(cx);
1633 } else {
1634 focus_center = true;
1635 }
1636 }
1637 panel
1638 });
1639
1640 if focus_center {
1641 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1642 }
1643
1644 self.serialize_workspace(cx);
1645 cx.notify();
1646 return panel;
1647 }
1648 }
1649 None
1650 }
1651
1652 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1653 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1654 let dock = dock.read(cx);
1655 if let Some(panel) = dock.panel::<T>() {
1656 return Some(panel);
1657 }
1658 }
1659 None
1660 }
1661
1662 // todo!("implement zoom")
1663 #[allow(unused)]
1664 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1665 for pane in &self.panes {
1666 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1667 }
1668
1669 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1670 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1671 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1672 self.zoomed = None;
1673 self.zoomed_position = None;
1674
1675 cx.notify();
1676 }
1677
1678 // #[cfg(any(test, feature = "test-support"))]
1679 // pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
1680 // self.zoomed.and_then(|view| view.upgrade(cx))
1681 // }
1682
1683 fn dismiss_zoomed_items_to_reveal(
1684 &mut self,
1685 dock_to_reveal: Option<DockPosition>,
1686 cx: &mut ViewContext<Self>,
1687 ) {
1688 // If a center pane is zoomed, unzoom it.
1689 for pane in &self.panes {
1690 if pane != &self.active_pane || dock_to_reveal.is_some() {
1691 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1692 }
1693 }
1694
1695 // If another dock is zoomed, hide it.
1696 let mut focus_center = false;
1697 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1698 dock.update(cx, |dock, cx| {
1699 if Some(dock.position()) != dock_to_reveal {
1700 if let Some(panel) = dock.active_panel() {
1701 if panel.is_zoomed(cx) {
1702 focus_center |= panel.focus_handle(cx).contains_focused(cx);
1703 dock.set_open(false, cx);
1704 }
1705 }
1706 }
1707 });
1708 }
1709
1710 if focus_center {
1711 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1712 }
1713
1714 if self.zoomed_position != dock_to_reveal {
1715 self.zoomed = None;
1716 self.zoomed_position = None;
1717 }
1718
1719 cx.notify();
1720 }
1721
1722 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
1723 let pane = cx.new_view(|cx| {
1724 Pane::new(
1725 self.weak_handle(),
1726 self.project.clone(),
1727 self.pane_history_timestamp.clone(),
1728 None,
1729 cx,
1730 )
1731 });
1732 cx.subscribe(&pane, Self::handle_pane_event).detach();
1733 self.panes.push(pane.clone());
1734 cx.focus_view(&pane);
1735 cx.emit(Event::PaneAdded(pane.clone()));
1736 pane
1737 }
1738
1739 pub fn add_item_to_center(
1740 &mut self,
1741 item: Box<dyn ItemHandle>,
1742 cx: &mut ViewContext<Self>,
1743 ) -> bool {
1744 if let Some(center_pane) = self.last_active_center_pane.clone() {
1745 if let Some(center_pane) = center_pane.upgrade() {
1746 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1747 true
1748 } else {
1749 false
1750 }
1751 } else {
1752 false
1753 }
1754 }
1755
1756 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1757 self.active_pane
1758 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1759 }
1760
1761 pub fn split_item(
1762 &mut self,
1763 split_direction: SplitDirection,
1764 item: Box<dyn ItemHandle>,
1765 cx: &mut ViewContext<Self>,
1766 ) {
1767 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
1768 new_pane.update(cx, move |new_pane, cx| {
1769 new_pane.add_item(item, true, true, None, cx)
1770 })
1771 }
1772
1773 pub fn open_abs_path(
1774 &mut self,
1775 abs_path: PathBuf,
1776 visible: bool,
1777 cx: &mut ViewContext<Self>,
1778 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1779 cx.spawn(|workspace, mut cx| async move {
1780 let open_paths_task_result = workspace
1781 .update(&mut cx, |workspace, cx| {
1782 workspace.open_paths(vec![abs_path.clone()], visible, cx)
1783 })
1784 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1785 .await;
1786 anyhow::ensure!(
1787 open_paths_task_result.len() == 1,
1788 "open abs path {abs_path:?} task returned incorrect number of results"
1789 );
1790 match open_paths_task_result
1791 .into_iter()
1792 .next()
1793 .expect("ensured single task result")
1794 {
1795 Some(open_result) => {
1796 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1797 }
1798 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1799 }
1800 })
1801 }
1802
1803 pub fn split_abs_path(
1804 &mut self,
1805 abs_path: PathBuf,
1806 visible: bool,
1807 cx: &mut ViewContext<Self>,
1808 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1809 let project_path_task =
1810 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
1811 cx.spawn(|this, mut cx| async move {
1812 let (_, path) = project_path_task.await?;
1813 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
1814 .await
1815 })
1816 }
1817
1818 pub fn open_path(
1819 &mut self,
1820 path: impl Into<ProjectPath>,
1821 pane: Option<WeakView<Pane>>,
1822 focus_item: bool,
1823 cx: &mut ViewContext<Self>,
1824 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1825 let pane = pane.unwrap_or_else(|| {
1826 self.last_active_center_pane.clone().unwrap_or_else(|| {
1827 self.panes
1828 .first()
1829 .expect("There must be an active pane")
1830 .downgrade()
1831 })
1832 });
1833
1834 let task = self.load_path(path.into(), cx);
1835 cx.spawn(move |_, mut cx| async move {
1836 let (project_entry_id, build_item) = task.await?;
1837 pane.update(&mut cx, |pane, cx| {
1838 pane.open_item(project_entry_id, focus_item, cx, build_item)
1839 })
1840 })
1841 }
1842
1843 pub fn split_path(
1844 &mut self,
1845 path: impl Into<ProjectPath>,
1846 cx: &mut ViewContext<Self>,
1847 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1848 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
1849 self.panes
1850 .first()
1851 .expect("There must be an active pane")
1852 .downgrade()
1853 });
1854
1855 if let Member::Pane(center_pane) = &self.center.root {
1856 if center_pane.read(cx).items_len() == 0 {
1857 return self.open_path(path, Some(pane), true, cx);
1858 }
1859 }
1860
1861 let task = self.load_path(path.into(), cx);
1862 cx.spawn(|this, mut cx| async move {
1863 let (project_entry_id, build_item) = task.await?;
1864 this.update(&mut cx, move |this, cx| -> Option<_> {
1865 let pane = pane.upgrade()?;
1866 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
1867 new_pane.update(cx, |new_pane, cx| {
1868 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
1869 })
1870 })
1871 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
1872 })
1873 }
1874
1875 fn load_path(
1876 &mut self,
1877 path: ProjectPath,
1878 cx: &mut ViewContext<Self>,
1879 ) -> Task<
1880 Result<(
1881 Option<ProjectEntryId>,
1882 impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1883 )>,
1884 > {
1885 let project = self.project().clone();
1886 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1887 cx.spawn(|_, mut cx| async move {
1888 let (project_entry_id, project_item) = project_item.await?;
1889 let build_item = cx.update(|_, cx| {
1890 cx.default_global::<ProjectItemBuilders>()
1891 .get(&project_item.entity_type())
1892 .ok_or_else(|| anyhow!("no item builder for project item"))
1893 .cloned()
1894 })??;
1895 let build_item =
1896 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1897 Ok((project_entry_id, build_item))
1898 })
1899 }
1900
1901 pub fn open_project_item<T>(
1902 &mut self,
1903 project_item: Model<T::Item>,
1904 cx: &mut ViewContext<Self>,
1905 ) -> View<T>
1906 where
1907 T: ProjectItem,
1908 {
1909 use project::Item as _;
1910
1911 let entry_id = project_item.read(cx).entry_id(cx);
1912 if let Some(item) = entry_id
1913 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1914 .and_then(|item| item.downcast())
1915 {
1916 self.activate_item(&item, cx);
1917 return item;
1918 }
1919
1920 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1921 self.add_item(Box::new(item.clone()), cx);
1922 item
1923 }
1924
1925 pub fn split_project_item<T>(
1926 &mut self,
1927 project_item: Model<T::Item>,
1928 cx: &mut ViewContext<Self>,
1929 ) -> View<T>
1930 where
1931 T: ProjectItem,
1932 {
1933 use project::Item as _;
1934
1935 let entry_id = project_item.read(cx).entry_id(cx);
1936 if let Some(item) = entry_id
1937 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1938 .and_then(|item| item.downcast())
1939 {
1940 self.activate_item(&item, cx);
1941 return item;
1942 }
1943
1944 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1945 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
1946 item
1947 }
1948
1949 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1950 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1951 self.active_pane.update(cx, |pane, cx| {
1952 pane.add_item(Box::new(shared_screen), false, true, None, cx)
1953 });
1954 }
1955 }
1956
1957 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1958 let result = self.panes.iter().find_map(|pane| {
1959 pane.read(cx)
1960 .index_for_item(item)
1961 .map(|ix| (pane.clone(), ix))
1962 });
1963 if let Some((pane, ix)) = result {
1964 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1965 true
1966 } else {
1967 false
1968 }
1969 }
1970
1971 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1972 let panes = self.center.panes();
1973 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1974 cx.focus_view(&pane);
1975 } else {
1976 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
1977 }
1978 }
1979
1980 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1981 let panes = self.center.panes();
1982 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1983 let next_ix = (ix + 1) % panes.len();
1984 let next_pane = panes[next_ix].clone();
1985 cx.focus_view(&next_pane);
1986 }
1987 }
1988
1989 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1990 let panes = self.center.panes();
1991 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1992 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1993 let prev_pane = panes[prev_ix].clone();
1994 cx.focus_view(&prev_pane);
1995 }
1996 }
1997
1998 pub fn activate_pane_in_direction(
1999 &mut self,
2000 direction: SplitDirection,
2001 cx: &mut ViewContext<Self>,
2002 ) {
2003 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2004 cx.focus_view(pane);
2005 }
2006 }
2007
2008 pub fn swap_pane_in_direction(
2009 &mut self,
2010 direction: SplitDirection,
2011 cx: &mut ViewContext<Self>,
2012 ) {
2013 if let Some(to) = self
2014 .find_pane_in_direction(direction, cx)
2015 .map(|pane| pane.clone())
2016 {
2017 self.center.swap(&self.active_pane.clone(), &to);
2018 cx.notify();
2019 }
2020 }
2021
2022 fn find_pane_in_direction(
2023 &mut self,
2024 direction: SplitDirection,
2025 cx: &mut ViewContext<Self>,
2026 ) -> Option<&View<Pane>> {
2027 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2028 return None;
2029 };
2030 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2031 let center = match cursor {
2032 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2033 _ => bounding_box.center(),
2034 };
2035
2036 let distance_to_next = 8.; //todo(pane dividers styling)
2037
2038 let target = match direction {
2039 SplitDirection::Left => {
2040 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2041 }
2042 SplitDirection::Right => {
2043 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2044 }
2045 SplitDirection::Up => {
2046 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2047 }
2048 SplitDirection::Down => {
2049 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2050 }
2051 };
2052 self.center.pane_at_pixel_position(target)
2053 }
2054
2055 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2056 if self.active_pane != pane {
2057 self.active_pane = pane.clone();
2058 self.status_bar.update(cx, |status_bar, cx| {
2059 status_bar.set_active_pane(&self.active_pane, cx);
2060 });
2061 self.active_item_path_changed(cx);
2062 self.last_active_center_pane = Some(pane.downgrade());
2063 }
2064
2065 self.dismiss_zoomed_items_to_reveal(None, cx);
2066 if pane.read(cx).is_zoomed() {
2067 self.zoomed = Some(pane.downgrade().into());
2068 } else {
2069 self.zoomed = None;
2070 }
2071 self.zoomed_position = None;
2072 self.update_active_view_for_followers(cx);
2073
2074 cx.notify();
2075 }
2076
2077 fn handle_pane_event(
2078 &mut self,
2079 pane: View<Pane>,
2080 event: &pane::Event,
2081 cx: &mut ViewContext<Self>,
2082 ) {
2083 match event {
2084 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2085 pane::Event::Split(direction) => {
2086 self.split_and_clone(pane, *direction, cx);
2087 }
2088 pane::Event::Remove => self.remove_pane(pane, cx),
2089 pane::Event::ActivateItem { local } => {
2090 if *local {
2091 self.unfollow(&pane, cx);
2092 }
2093 if &pane == self.active_pane() {
2094 self.active_item_path_changed(cx);
2095 self.update_active_view_for_followers(cx);
2096 }
2097 }
2098 pane::Event::ChangeItemTitle => {
2099 if pane == self.active_pane {
2100 self.active_item_path_changed(cx);
2101 }
2102 self.update_window_edited(cx);
2103 }
2104 pane::Event::RemoveItem { item_id } => {
2105 self.update_window_edited(cx);
2106 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2107 if entry.get().entity_id() == pane.entity_id() {
2108 entry.remove();
2109 }
2110 }
2111 }
2112 pane::Event::Focus => {
2113 self.handle_pane_focused(pane.clone(), cx);
2114 }
2115 pane::Event::ZoomIn => {
2116 if pane == self.active_pane {
2117 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2118 if pane.read(cx).has_focus(cx) {
2119 self.zoomed = Some(pane.downgrade().into());
2120 self.zoomed_position = None;
2121 }
2122 cx.notify();
2123 }
2124 }
2125 pane::Event::ZoomOut => {
2126 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2127 if self.zoomed_position.is_none() {
2128 self.zoomed = None;
2129 }
2130 cx.notify();
2131 }
2132 }
2133
2134 self.serialize_workspace(cx);
2135 }
2136
2137 pub fn split_pane(
2138 &mut self,
2139 pane_to_split: View<Pane>,
2140 split_direction: SplitDirection,
2141 cx: &mut ViewContext<Self>,
2142 ) -> View<Pane> {
2143 let new_pane = self.add_pane(cx);
2144 self.center
2145 .split(&pane_to_split, &new_pane, split_direction)
2146 .unwrap();
2147 cx.notify();
2148 new_pane
2149 }
2150
2151 pub fn split_and_clone(
2152 &mut self,
2153 pane: View<Pane>,
2154 direction: SplitDirection,
2155 cx: &mut ViewContext<Self>,
2156 ) -> Option<View<Pane>> {
2157 let item = pane.read(cx).active_item()?;
2158 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2159 let new_pane = self.add_pane(cx);
2160 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2161 self.center.split(&pane, &new_pane, direction).unwrap();
2162 Some(new_pane)
2163 } else {
2164 None
2165 };
2166 cx.notify();
2167 maybe_pane_handle
2168 }
2169
2170 pub fn split_pane_with_item(
2171 &mut self,
2172 pane_to_split: WeakView<Pane>,
2173 split_direction: SplitDirection,
2174 from: WeakView<Pane>,
2175 item_id_to_move: EntityId,
2176 cx: &mut ViewContext<Self>,
2177 ) {
2178 let Some(pane_to_split) = pane_to_split.upgrade() else {
2179 return;
2180 };
2181 let Some(from) = from.upgrade() else {
2182 return;
2183 };
2184
2185 let new_pane = self.add_pane(cx);
2186 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2187 self.center
2188 .split(&pane_to_split, &new_pane, split_direction)
2189 .unwrap();
2190 cx.notify();
2191 }
2192
2193 pub fn split_pane_with_project_entry(
2194 &mut self,
2195 pane_to_split: WeakView<Pane>,
2196 split_direction: SplitDirection,
2197 project_entry: ProjectEntryId,
2198 cx: &mut ViewContext<Self>,
2199 ) -> Option<Task<Result<()>>> {
2200 let pane_to_split = pane_to_split.upgrade()?;
2201 let new_pane = self.add_pane(cx);
2202 self.center
2203 .split(&pane_to_split, &new_pane, split_direction)
2204 .unwrap();
2205
2206 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2207 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2208 Some(cx.foreground_executor().spawn(async move {
2209 task.await?;
2210 Ok(())
2211 }))
2212 }
2213
2214 pub fn move_item(
2215 &mut self,
2216 source: View<Pane>,
2217 destination: View<Pane>,
2218 item_id_to_move: EntityId,
2219 destination_index: usize,
2220 cx: &mut ViewContext<Self>,
2221 ) {
2222 let item_to_move = source
2223 .read(cx)
2224 .items()
2225 .enumerate()
2226 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move);
2227
2228 if item_to_move.is_none() {
2229 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2230 return;
2231 }
2232 let (item_ix, item_handle) = item_to_move.unwrap();
2233 let item_handle = item_handle.clone();
2234
2235 if source != destination {
2236 // Close item from previous pane
2237 source.update(cx, |source, cx| {
2238 source.remove_item(item_ix, false, cx);
2239 });
2240 }
2241
2242 // This automatically removes duplicate items in the pane
2243 destination.update(cx, |destination, cx| {
2244 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2245 destination.focus(cx)
2246 });
2247 }
2248
2249 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2250 if self.center.remove(&pane).unwrap() {
2251 self.force_remove_pane(&pane, cx);
2252 self.unfollow(&pane, cx);
2253 self.last_leaders_by_pane.remove(&pane.downgrade());
2254 for removed_item in pane.read(cx).items() {
2255 self.panes_by_item.remove(&removed_item.item_id());
2256 }
2257
2258 cx.notify();
2259 } else {
2260 self.active_item_path_changed(cx);
2261 }
2262 }
2263
2264 pub fn panes(&self) -> &[View<Pane>] {
2265 &self.panes
2266 }
2267
2268 pub fn active_pane(&self) -> &View<Pane> {
2269 &self.active_pane
2270 }
2271
2272 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2273 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2274 weak_pane.upgrade()
2275 }
2276
2277 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2278 self.follower_states.retain(|_, state| {
2279 if state.leader_id == peer_id {
2280 for item in state.items_by_leader_view_id.values() {
2281 item.set_leader_peer_id(None, cx);
2282 }
2283 false
2284 } else {
2285 true
2286 }
2287 });
2288 cx.notify();
2289 }
2290
2291 pub fn start_following(
2292 &mut self,
2293 leader_id: PeerId,
2294 cx: &mut ViewContext<Self>,
2295 ) -> Option<Task<Result<()>>> {
2296 let pane = self.active_pane().clone();
2297
2298 self.last_leaders_by_pane
2299 .insert(pane.downgrade(), leader_id);
2300 self.unfollow(&pane, cx);
2301 self.follower_states.insert(
2302 pane.clone(),
2303 FollowerState {
2304 leader_id,
2305 active_view_id: None,
2306 items_by_leader_view_id: Default::default(),
2307 },
2308 );
2309 cx.notify();
2310
2311 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2312 let project_id = self.project.read(cx).remote_id();
2313 let request = self.app_state.client.request(proto::Follow {
2314 room_id,
2315 project_id,
2316 leader_id: Some(leader_id),
2317 });
2318
2319 Some(cx.spawn(|this, mut cx| async move {
2320 let response = request.await?;
2321 this.update(&mut cx, |this, _| {
2322 let state = this
2323 .follower_states
2324 .get_mut(&pane)
2325 .ok_or_else(|| anyhow!("following interrupted"))?;
2326 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2327 Some(ViewId::from_proto(active_view_id)?)
2328 } else {
2329 None
2330 };
2331 Ok::<_, anyhow::Error>(())
2332 })??;
2333 Self::add_views_from_leader(
2334 this.clone(),
2335 leader_id,
2336 vec![pane],
2337 response.views,
2338 &mut cx,
2339 )
2340 .await?;
2341 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2342 Ok(())
2343 }))
2344 }
2345
2346 pub fn follow_next_collaborator(
2347 &mut self,
2348 _: &FollowNextCollaborator,
2349 cx: &mut ViewContext<Self>,
2350 ) {
2351 let collaborators = self.project.read(cx).collaborators();
2352 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2353 let mut collaborators = collaborators.keys().copied();
2354 for peer_id in collaborators.by_ref() {
2355 if peer_id == leader_id {
2356 break;
2357 }
2358 }
2359 collaborators.next()
2360 } else if let Some(last_leader_id) =
2361 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2362 {
2363 if collaborators.contains_key(last_leader_id) {
2364 Some(*last_leader_id)
2365 } else {
2366 None
2367 }
2368 } else {
2369 None
2370 };
2371
2372 let pane = self.active_pane.clone();
2373 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2374 else {
2375 return;
2376 };
2377 if Some(leader_id) == self.unfollow(&pane, cx) {
2378 return;
2379 }
2380 self.start_following(leader_id, cx)
2381 .map(|task| task.detach_and_log_err(cx));
2382 }
2383
2384 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2385 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2386 return;
2387 };
2388 let room = room.read(cx);
2389 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2390 return;
2391 };
2392
2393 let project = self.project.read(cx);
2394
2395 let other_project_id = match remote_participant.location {
2396 call::ParticipantLocation::External => None,
2397 call::ParticipantLocation::UnsharedProject => None,
2398 call::ParticipantLocation::SharedProject { project_id } => {
2399 if Some(project_id) == project.remote_id() {
2400 None
2401 } else {
2402 Some(project_id)
2403 }
2404 }
2405 };
2406
2407 // if they are active in another project, follow there.
2408 if let Some(project_id) = other_project_id {
2409 let app_state = self.app_state.clone();
2410 crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
2411 .detach_and_log_err(cx);
2412 }
2413
2414 // if you're already following, find the right pane and focus it.
2415 for (pane, state) in &self.follower_states {
2416 if leader_id == state.leader_id {
2417 cx.focus_view(pane);
2418 return;
2419 }
2420 }
2421
2422 // Otherwise, follow.
2423 self.start_following(leader_id, cx)
2424 .map(|task| task.detach_and_log_err(cx));
2425 }
2426
2427 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2428 let state = self.follower_states.remove(pane)?;
2429 let leader_id = state.leader_id;
2430 for (_, item) in state.items_by_leader_view_id {
2431 item.set_leader_peer_id(None, cx);
2432 }
2433
2434 if self
2435 .follower_states
2436 .values()
2437 .all(|state| state.leader_id != state.leader_id)
2438 {
2439 let project_id = self.project.read(cx).remote_id();
2440 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2441 self.app_state
2442 .client
2443 .send(proto::Unfollow {
2444 room_id,
2445 project_id,
2446 leader_id: Some(leader_id),
2447 })
2448 .log_err();
2449 }
2450
2451 cx.notify();
2452 Some(leader_id)
2453 }
2454
2455 // pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2456 // self.follower_states
2457 // .values()
2458 // .any(|state| state.leader_id == peer_id)
2459 // }
2460
2461 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2462 let active_entry = self.active_project_path(cx);
2463 self.project
2464 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2465 self.update_window_title(cx);
2466 }
2467
2468 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2469 let project = self.project().read(cx);
2470 let mut title = String::new();
2471
2472 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2473 let filename = path
2474 .path
2475 .file_name()
2476 .map(|s| s.to_string_lossy())
2477 .or_else(|| {
2478 Some(Cow::Borrowed(
2479 project
2480 .worktree_for_id(path.worktree_id, cx)?
2481 .read(cx)
2482 .root_name(),
2483 ))
2484 });
2485
2486 if let Some(filename) = filename {
2487 title.push_str(filename.as_ref());
2488 title.push_str(" β ");
2489 }
2490 }
2491
2492 for (i, name) in project.worktree_root_names(cx).enumerate() {
2493 if i > 0 {
2494 title.push_str(", ");
2495 }
2496 title.push_str(name);
2497 }
2498
2499 if title.is_empty() {
2500 title = "empty project".to_string();
2501 }
2502
2503 if project.is_remote() {
2504 title.push_str(" β");
2505 } else if project.is_shared() {
2506 title.push_str(" β");
2507 }
2508
2509 cx.set_window_title(&title);
2510 }
2511
2512 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2513 let is_edited = !self.project.read(cx).is_disconnected()
2514 && self
2515 .items(cx)
2516 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2517 if is_edited != self.window_edited {
2518 self.window_edited = is_edited;
2519 cx.set_window_edited(self.window_edited)
2520 }
2521 }
2522
2523 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2524 if self.notifications.is_empty() {
2525 None
2526 } else {
2527 Some(
2528 div()
2529 .absolute()
2530 .z_index(100)
2531 .right_3()
2532 .bottom_3()
2533 .w_96()
2534 .h_full()
2535 .flex()
2536 .flex_col()
2537 .justify_end()
2538 .gap_2()
2539 .children(
2540 self.notifications
2541 .iter()
2542 .map(|(_, _, notification)| notification.to_any()),
2543 ),
2544 )
2545 }
2546 }
2547
2548 // RPC handlers
2549
2550 fn handle_follow(
2551 &mut self,
2552 follower_project_id: Option<u64>,
2553 cx: &mut ViewContext<Self>,
2554 ) -> proto::FollowResponse {
2555 let client = &self.app_state.client;
2556 let project_id = self.project.read(cx).remote_id();
2557
2558 let active_view_id = self.active_item(cx).and_then(|i| {
2559 Some(
2560 i.to_followable_item_handle(cx)?
2561 .remote_id(client, cx)?
2562 .to_proto(),
2563 )
2564 });
2565
2566 cx.notify();
2567
2568 self.last_active_view_id = active_view_id.clone();
2569 proto::FollowResponse {
2570 active_view_id,
2571 views: self
2572 .panes()
2573 .iter()
2574 .flat_map(|pane| {
2575 let leader_id = self.leader_for_pane(pane);
2576 pane.read(cx).items().filter_map({
2577 let cx = &cx;
2578 move |item| {
2579 let item = item.to_followable_item_handle(cx)?;
2580 if (project_id.is_none() || project_id != follower_project_id)
2581 && item.is_project_item(cx)
2582 {
2583 return None;
2584 }
2585 let id = item.remote_id(client, cx)?.to_proto();
2586 let variant = item.to_state_proto(cx)?;
2587 Some(proto::View {
2588 id: Some(id),
2589 leader_id,
2590 variant: Some(variant),
2591 })
2592 }
2593 })
2594 })
2595 .collect(),
2596 }
2597 }
2598
2599 fn handle_update_followers(
2600 &mut self,
2601 leader_id: PeerId,
2602 message: proto::UpdateFollowers,
2603 _cx: &mut ViewContext<Self>,
2604 ) {
2605 self.leader_updates_tx
2606 .unbounded_send((leader_id, message))
2607 .ok();
2608 }
2609
2610 async fn process_leader_update(
2611 this: &WeakView<Self>,
2612 leader_id: PeerId,
2613 update: proto::UpdateFollowers,
2614 cx: &mut AsyncWindowContext,
2615 ) -> Result<()> {
2616 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2617 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2618 this.update(cx, |this, _| {
2619 for (_, state) in &mut this.follower_states {
2620 if state.leader_id == leader_id {
2621 state.active_view_id =
2622 if let Some(active_view_id) = update_active_view.id.clone() {
2623 Some(ViewId::from_proto(active_view_id)?)
2624 } else {
2625 None
2626 };
2627 }
2628 }
2629 anyhow::Ok(())
2630 })??;
2631 }
2632 proto::update_followers::Variant::UpdateView(update_view) => {
2633 let variant = update_view
2634 .variant
2635 .ok_or_else(|| anyhow!("missing update view variant"))?;
2636 let id = update_view
2637 .id
2638 .ok_or_else(|| anyhow!("missing update view id"))?;
2639 let mut tasks = Vec::new();
2640 this.update(cx, |this, cx| {
2641 let project = this.project.clone();
2642 for (_, state) in &mut this.follower_states {
2643 if state.leader_id == leader_id {
2644 let view_id = ViewId::from_proto(id.clone())?;
2645 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2646 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2647 }
2648 }
2649 }
2650 anyhow::Ok(())
2651 })??;
2652 try_join_all(tasks).await.log_err();
2653 }
2654 proto::update_followers::Variant::CreateView(view) => {
2655 let panes = this.update(cx, |this, _| {
2656 this.follower_states
2657 .iter()
2658 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2659 .cloned()
2660 .collect()
2661 })?;
2662 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2663 }
2664 }
2665 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2666 Ok(())
2667 }
2668
2669 async fn add_views_from_leader(
2670 this: WeakView<Self>,
2671 leader_id: PeerId,
2672 panes: Vec<View<Pane>>,
2673 views: Vec<proto::View>,
2674 cx: &mut AsyncWindowContext,
2675 ) -> Result<()> {
2676 let this = this.upgrade().context("workspace dropped")?;
2677
2678 let item_builders = cx.update(|_, cx| {
2679 cx.default_global::<FollowableItemBuilders>()
2680 .values()
2681 .map(|b| b.0)
2682 .collect::<Vec<_>>()
2683 })?;
2684
2685 let mut item_tasks_by_pane = HashMap::default();
2686 for pane in panes {
2687 let mut item_tasks = Vec::new();
2688 let mut leader_view_ids = Vec::new();
2689 for view in &views {
2690 let Some(id) = &view.id else { continue };
2691 let id = ViewId::from_proto(id.clone())?;
2692 let mut variant = view.variant.clone();
2693 if variant.is_none() {
2694 Err(anyhow!("missing view variant"))?;
2695 }
2696 for build_item in &item_builders {
2697 let task = cx.update(|_, cx| {
2698 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
2699 })?;
2700 if let Some(task) = task {
2701 item_tasks.push(task);
2702 leader_view_ids.push(id);
2703 break;
2704 } else {
2705 assert!(variant.is_some());
2706 }
2707 }
2708 }
2709
2710 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2711 }
2712
2713 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2714 let items = futures::future::try_join_all(item_tasks).await?;
2715 this.update(cx, |this, cx| {
2716 let state = this.follower_states.get_mut(&pane)?;
2717 for (id, item) in leader_view_ids.into_iter().zip(items) {
2718 item.set_leader_peer_id(Some(leader_id), cx);
2719 state.items_by_leader_view_id.insert(id, item);
2720 }
2721
2722 Some(())
2723 })?;
2724 }
2725 Ok(())
2726 }
2727
2728 fn update_active_view_for_followers(&mut self, cx: &mut ViewContext<Self>) {
2729 let mut is_project_item = true;
2730 let mut update = proto::UpdateActiveView::default();
2731
2732 if let Some(item) = self.active_item(cx) {
2733 if item.focus_handle(cx).contains_focused(cx) {
2734 if let Some(item) = item.to_followable_item_handle(cx) {
2735 is_project_item = item.is_project_item(cx);
2736 update = proto::UpdateActiveView {
2737 id: item
2738 .remote_id(&self.app_state.client, cx)
2739 .map(|id| id.to_proto()),
2740 leader_id: self.leader_for_pane(&self.active_pane),
2741 };
2742 }
2743 }
2744 }
2745
2746 if update.id != self.last_active_view_id {
2747 self.last_active_view_id = update.id.clone();
2748 self.update_followers(
2749 is_project_item,
2750 proto::update_followers::Variant::UpdateActiveView(update),
2751 cx,
2752 );
2753 }
2754 }
2755
2756 fn update_followers(
2757 &self,
2758 project_only: bool,
2759 update: proto::update_followers::Variant,
2760 cx: &mut WindowContext,
2761 ) -> Option<()> {
2762 let project_id = if project_only {
2763 self.project.read(cx).remote_id()
2764 } else {
2765 None
2766 };
2767 self.app_state().workspace_store.update(cx, |store, cx| {
2768 store.update_followers(project_id, update, cx)
2769 })
2770 }
2771
2772 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
2773 self.follower_states.get(pane).map(|state| state.leader_id)
2774 }
2775
2776 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2777 cx.notify();
2778
2779 let call = self.active_call()?;
2780 let room = call.read(cx).room()?.read(cx);
2781 let participant = room.remote_participant_for_peer_id(leader_id)?;
2782 let mut items_to_activate = Vec::new();
2783
2784 let leader_in_this_app;
2785 let leader_in_this_project;
2786 match participant.location {
2787 call::ParticipantLocation::SharedProject { project_id } => {
2788 leader_in_this_app = true;
2789 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
2790 }
2791 call::ParticipantLocation::UnsharedProject => {
2792 leader_in_this_app = true;
2793 leader_in_this_project = false;
2794 }
2795 call::ParticipantLocation::External => {
2796 leader_in_this_app = false;
2797 leader_in_this_project = false;
2798 }
2799 };
2800
2801 for (pane, state) in &self.follower_states {
2802 if state.leader_id != leader_id {
2803 continue;
2804 }
2805 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
2806 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
2807 if leader_in_this_project || !item.is_project_item(cx) {
2808 items_to_activate.push((pane.clone(), item.boxed_clone()));
2809 }
2810 }
2811 continue;
2812 }
2813
2814 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2815 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2816 }
2817 }
2818
2819 for (pane, item) in items_to_activate {
2820 let pane_was_focused = pane.read(cx).has_focus(cx);
2821 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2822 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2823 } else {
2824 pane.update(cx, |pane, cx| {
2825 pane.add_item(item.boxed_clone(), false, false, None, cx)
2826 });
2827 }
2828
2829 if pane_was_focused {
2830 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2831 }
2832 }
2833
2834 None
2835 }
2836
2837 fn shared_screen_for_peer(
2838 &self,
2839 peer_id: PeerId,
2840 pane: &View<Pane>,
2841 cx: &mut ViewContext<Self>,
2842 ) -> Option<View<SharedScreen>> {
2843 let call = self.active_call()?;
2844 let room = call.read(cx).room()?.read(cx);
2845 let participant = room.remote_participant_for_peer_id(peer_id)?;
2846 let track = participant.video_tracks.values().next()?.clone();
2847 let user = participant.user.clone();
2848
2849 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2850 if item.read(cx).peer_id == peer_id {
2851 return Some(item);
2852 }
2853 }
2854
2855 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2856 }
2857
2858 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
2859 if cx.is_window_active() {
2860 self.update_active_view_for_followers(cx);
2861 cx.background_executor()
2862 .spawn(persistence::DB.update_timestamp(self.database_id()))
2863 .detach();
2864 } else {
2865 for pane in &self.panes {
2866 pane.update(cx, |pane, cx| {
2867 if let Some(item) = pane.active_item() {
2868 item.workspace_deactivated(cx);
2869 }
2870 if matches!(
2871 WorkspaceSettings::get_global(cx).autosave,
2872 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2873 ) {
2874 for item in pane.items() {
2875 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2876 .detach_and_log_err(cx);
2877 }
2878 }
2879 });
2880 }
2881 }
2882 }
2883
2884 fn active_call(&self) -> Option<&Model<ActiveCall>> {
2885 self.active_call.as_ref().map(|(call, _)| call)
2886 }
2887
2888 fn on_active_call_event(
2889 &mut self,
2890 _: Model<ActiveCall>,
2891 event: &call::room::Event,
2892 cx: &mut ViewContext<Self>,
2893 ) {
2894 match event {
2895 call::room::Event::ParticipantLocationChanged { participant_id }
2896 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2897 self.leader_updated(*participant_id, cx);
2898 }
2899 _ => {}
2900 }
2901 }
2902
2903 pub fn database_id(&self) -> WorkspaceId {
2904 self.database_id
2905 }
2906
2907 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2908 let project = self.project().read(cx);
2909
2910 if project.is_local() {
2911 Some(
2912 project
2913 .visible_worktrees(cx)
2914 .map(|worktree| worktree.read(cx).abs_path())
2915 .collect::<Vec<_>>()
2916 .into(),
2917 )
2918 } else {
2919 None
2920 }
2921 }
2922
2923 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2924 match member {
2925 Member::Axis(PaneAxis { members, .. }) => {
2926 for child in members.iter() {
2927 self.remove_panes(child.clone(), cx)
2928 }
2929 }
2930 Member::Pane(pane) => {
2931 self.force_remove_pane(&pane, cx);
2932 }
2933 }
2934 }
2935
2936 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
2937 self.panes.retain(|p| p != pane);
2938 self.panes
2939 .last()
2940 .unwrap()
2941 .update(cx, |pane, cx| pane.focus(cx));
2942 if self.last_active_center_pane == Some(pane.downgrade()) {
2943 self.last_active_center_pane = None;
2944 }
2945 cx.notify();
2946 }
2947
2948 #[allow(unused)]
2949 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
2950 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
2951 cx.background_executor()
2952 .timer(Duration::from_millis(100))
2953 .await;
2954 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
2955 .log_err();
2956 }));
2957 }
2958
2959 fn serialize_workspace(&self, cx: &mut ViewContext<Self>) {
2960 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
2961 let (items, active) = {
2962 let pane = pane_handle.read(cx);
2963 let active_item_id = pane.active_item().map(|item| item.item_id());
2964 (
2965 pane.items()
2966 .filter_map(|item_handle| {
2967 Some(SerializedItem {
2968 kind: Arc::from(item_handle.serialized_item_kind()?),
2969 item_id: item_handle.item_id().as_u64(),
2970 active: Some(item_handle.item_id()) == active_item_id,
2971 })
2972 })
2973 .collect::<Vec<_>>(),
2974 pane.has_focus(cx),
2975 )
2976 };
2977
2978 SerializedPane::new(items, active)
2979 }
2980
2981 fn build_serialized_pane_group(
2982 pane_group: &Member,
2983 cx: &WindowContext,
2984 ) -> SerializedPaneGroup {
2985 match pane_group {
2986 Member::Axis(PaneAxis {
2987 axis,
2988 members,
2989 flexes,
2990 bounding_boxes: _,
2991 }) => SerializedPaneGroup::Group {
2992 axis: *axis,
2993 children: members
2994 .iter()
2995 .map(|member| build_serialized_pane_group(member, cx))
2996 .collect::<Vec<_>>(),
2997 flexes: Some(flexes.lock().clone()),
2998 },
2999 Member::Pane(pane_handle) => {
3000 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
3001 }
3002 }
3003 }
3004
3005 fn build_serialized_docks(
3006 this: &Workspace,
3007 cx: &mut ViewContext<Workspace>,
3008 ) -> DockStructure {
3009 let left_dock = this.left_dock.read(cx);
3010 let left_visible = left_dock.is_open();
3011 let left_active_panel = left_dock
3012 .visible_panel()
3013 .and_then(|panel| Some(panel.persistent_name().to_string()));
3014 let left_dock_zoom = left_dock
3015 .visible_panel()
3016 .map(|panel| panel.is_zoomed(cx))
3017 .unwrap_or(false);
3018
3019 let right_dock = this.right_dock.read(cx);
3020 let right_visible = right_dock.is_open();
3021 let right_active_panel = right_dock
3022 .visible_panel()
3023 .and_then(|panel| Some(panel.persistent_name().to_string()));
3024 let right_dock_zoom = right_dock
3025 .visible_panel()
3026 .map(|panel| panel.is_zoomed(cx))
3027 .unwrap_or(false);
3028
3029 let bottom_dock = this.bottom_dock.read(cx);
3030 let bottom_visible = bottom_dock.is_open();
3031 let bottom_active_panel = bottom_dock
3032 .visible_panel()
3033 .and_then(|panel| Some(panel.persistent_name().to_string()));
3034 let bottom_dock_zoom = bottom_dock
3035 .visible_panel()
3036 .map(|panel| panel.is_zoomed(cx))
3037 .unwrap_or(false);
3038
3039 DockStructure {
3040 left: DockData {
3041 visible: left_visible,
3042 active_panel: left_active_panel,
3043 zoom: left_dock_zoom,
3044 },
3045 right: DockData {
3046 visible: right_visible,
3047 active_panel: right_active_panel,
3048 zoom: right_dock_zoom,
3049 },
3050 bottom: DockData {
3051 visible: bottom_visible,
3052 active_panel: bottom_active_panel,
3053 zoom: bottom_dock_zoom,
3054 },
3055 }
3056 }
3057
3058 if let Some(location) = self.location(cx) {
3059 // Load bearing special case:
3060 // - with_local_workspace() relies on this to not have other stuff open
3061 // when you open your log
3062 if !location.paths().is_empty() {
3063 let center_group = build_serialized_pane_group(&self.center.root, cx);
3064 let docks = build_serialized_docks(self, cx);
3065
3066 let serialized_workspace = SerializedWorkspace {
3067 id: self.database_id,
3068 location,
3069 center_group,
3070 bounds: Default::default(),
3071 display: Default::default(),
3072 docks,
3073 };
3074
3075 cx.spawn(|_, _| persistence::DB.save_workspace(serialized_workspace))
3076 .detach();
3077 }
3078 }
3079 }
3080
3081 pub(crate) fn load_workspace(
3082 serialized_workspace: SerializedWorkspace,
3083 paths_to_open: Vec<Option<ProjectPath>>,
3084 cx: &mut ViewContext<Workspace>,
3085 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3086 cx.spawn(|workspace, mut cx| async move {
3087 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3088
3089 let mut center_group = None;
3090 let mut center_items = None;
3091
3092 // Traverse the splits tree and add to things
3093 if let Some((group, active_pane, items)) = serialized_workspace
3094 .center_group
3095 .deserialize(
3096 &project,
3097 serialized_workspace.id,
3098 workspace.clone(),
3099 &mut cx,
3100 )
3101 .await
3102 {
3103 center_items = Some(items);
3104 center_group = Some((group, active_pane))
3105 }
3106
3107 let mut items_by_project_path = cx.update(|_, cx| {
3108 center_items
3109 .unwrap_or_default()
3110 .into_iter()
3111 .filter_map(|item| {
3112 let item = item?;
3113 let project_path = item.project_path(cx)?;
3114 Some((project_path, item))
3115 })
3116 .collect::<HashMap<_, _>>()
3117 })?;
3118
3119 let opened_items = paths_to_open
3120 .into_iter()
3121 .map(|path_to_open| {
3122 path_to_open
3123 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3124 })
3125 .collect::<Vec<_>>();
3126
3127 // Remove old panes from workspace panes list
3128 workspace.update(&mut cx, |workspace, cx| {
3129 if let Some((center_group, active_pane)) = center_group {
3130 workspace.remove_panes(workspace.center.root.clone(), cx);
3131
3132 // Swap workspace center group
3133 workspace.center = PaneGroup::with_root(center_group);
3134 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3135 if let Some(active_pane) = active_pane {
3136 workspace.active_pane = active_pane;
3137 cx.focus_self();
3138 } else {
3139 workspace.active_pane = workspace.center.first_pane().clone();
3140 }
3141 }
3142
3143 let docks = serialized_workspace.docks;
3144 workspace.left_dock.update(cx, |dock, cx| {
3145 dock.set_open(docks.left.visible, cx);
3146 if let Some(active_panel) = docks.left.active_panel {
3147 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3148 dock.activate_panel(ix, cx);
3149 }
3150 }
3151 dock.active_panel()
3152 .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
3153 if docks.left.visible && docks.left.zoom {
3154 cx.focus_self()
3155 }
3156 });
3157 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3158 workspace.right_dock.update(cx, |dock, cx| {
3159 dock.set_open(docks.right.visible, cx);
3160 if let Some(active_panel) = docks.right.active_panel {
3161 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3162 dock.activate_panel(ix, cx);
3163 }
3164 }
3165 dock.active_panel()
3166 .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
3167
3168 if docks.right.visible && docks.right.zoom {
3169 cx.focus_self()
3170 }
3171 });
3172 workspace.bottom_dock.update(cx, |dock, cx| {
3173 dock.set_open(docks.bottom.visible, cx);
3174 if let Some(active_panel) = docks.bottom.active_panel {
3175 if let Some(ix) = dock.panel_index_for_persistent_name(&active_panel, cx) {
3176 dock.activate_panel(ix, cx);
3177 }
3178 }
3179
3180 dock.active_panel()
3181 .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
3182
3183 if docks.bottom.visible && docks.bottom.zoom {
3184 cx.focus_self()
3185 }
3186 });
3187
3188 cx.notify();
3189 })?;
3190
3191 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3192 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3193
3194 Ok(opened_items)
3195 })
3196 }
3197
3198 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3199 self.add_workspace_actions_listeners(div, cx)
3200 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3201 .on_action(cx.listener(Self::close_all_items_and_panes))
3202 .on_action(cx.listener(Self::save_all))
3203 .on_action(cx.listener(Self::add_folder_to_project))
3204 .on_action(cx.listener(Self::follow_next_collaborator))
3205 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3206 let pane = workspace.active_pane().clone();
3207 workspace.unfollow(&pane, cx);
3208 }))
3209 .on_action(cx.listener(|workspace, action: &Save, cx| {
3210 workspace
3211 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3212 .detach_and_log_err(cx);
3213 }))
3214 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3215 workspace
3216 .save_active_item(SaveIntent::SaveAs, cx)
3217 .detach_and_log_err(cx);
3218 }))
3219 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3220 workspace.activate_previous_pane(cx)
3221 }))
3222 .on_action(
3223 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3224 )
3225 .on_action(
3226 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3227 workspace.activate_pane_in_direction(action.0, cx)
3228 }),
3229 )
3230 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3231 workspace.swap_pane_in_direction(action.0, cx)
3232 }))
3233 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3234 this.toggle_dock(DockPosition::Left, cx);
3235 }))
3236 .on_action(
3237 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3238 workspace.toggle_dock(DockPosition::Right, cx);
3239 }),
3240 )
3241 .on_action(
3242 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3243 workspace.toggle_dock(DockPosition::Bottom, cx);
3244 }),
3245 )
3246 .on_action(
3247 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3248 workspace.close_all_docks(cx);
3249 }),
3250 )
3251 .on_action(cx.listener(Workspace::open))
3252 .on_action(cx.listener(Workspace::close_window))
3253 .on_action(cx.listener(Workspace::activate_pane_at_index))
3254 .on_action(
3255 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3256 workspace.reopen_closed_item(cx).detach();
3257 }),
3258 )
3259 }
3260
3261 #[cfg(any(test, feature = "test-support"))]
3262 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3263 use node_runtime::FakeNodeRuntime;
3264
3265 let client = project.read(cx).client();
3266 let user_store = project.read(cx).user_store();
3267
3268 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3269 cx.activate_window();
3270 let app_state = Arc::new(AppState {
3271 languages: project.read(cx).languages().clone(),
3272 workspace_store,
3273 client,
3274 user_store,
3275 fs: project.read(cx).fs().clone(),
3276 build_window_options: |_, _, _| Default::default(),
3277 node_runtime: FakeNodeRuntime::new(),
3278 });
3279 let workspace = Self::new(0, project, app_state, cx);
3280 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3281 workspace
3282 }
3283
3284 // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3285 // let dock = match position {
3286 // DockPosition::Left => &self.left_dock,
3287 // DockPosition::Right => &self.right_dock,
3288 // DockPosition::Bottom => &self.bottom_dock,
3289 // };
3290 // let active_panel = dock.read(cx).visible_panel()?;
3291 // let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3292 // dock.read(cx).render_placeholder(cx)
3293 // } else {
3294 // ChildView::new(dock, cx).into_any()
3295 // };
3296
3297 // Some(
3298 // element
3299 // .constrained()
3300 // .dynamically(move |constraint, _, cx| match position {
3301 // DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3302 // Vector2F::new(20., constraint.min.y()),
3303 // Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3304 // ),
3305 // DockPosition::Bottom => SizeConstraint::new(
3306 // Vector2F::new(constraint.min.x(), 20.),
3307 // Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3308 // ),
3309 // })
3310 // .into_any(),
3311 // )
3312 // }
3313 // }
3314 pub fn register_action<A: Action>(
3315 &mut self,
3316 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3317 ) -> &mut Self {
3318 let callback = Arc::new(callback);
3319
3320 self.workspace_actions.push(Box::new(move |div, cx| {
3321 let callback = callback.clone();
3322 div.on_action(
3323 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3324 )
3325 }));
3326 self
3327 }
3328
3329 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3330 let mut div = div
3331 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3332 .on_action(cx.listener(Self::close_all_items_and_panes))
3333 .on_action(cx.listener(Self::add_folder_to_project))
3334 .on_action(cx.listener(Self::save_all))
3335 .on_action(cx.listener(Self::open));
3336 for action in self.workspace_actions.iter() {
3337 div = (action)(div, cx)
3338 }
3339 div
3340 }
3341
3342 pub fn active_modal<V: ManagedView + 'static>(
3343 &mut self,
3344 cx: &ViewContext<Self>,
3345 ) -> Option<View<V>> {
3346 self.modal_layer.read(cx).active_modal()
3347 }
3348
3349 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
3350 where
3351 B: FnOnce(&mut ViewContext<V>) -> V,
3352 {
3353 self.modal_layer
3354 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3355 }
3356}
3357
3358fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3359 let display_origin = cx
3360 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3361 .ok()??;
3362 ZED_WINDOW_POSITION
3363 .zip(*ZED_WINDOW_SIZE)
3364 .map(|(position, size)| {
3365 WindowBounds::Fixed(Bounds {
3366 origin: display_origin + position,
3367 size,
3368 })
3369 })
3370}
3371
3372fn open_items(
3373 serialized_workspace: Option<SerializedWorkspace>,
3374 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3375 app_state: Arc<AppState>,
3376 cx: &mut ViewContext<Workspace>,
3377) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3378 let restored_items = serialized_workspace.map(|serialized_workspace| {
3379 Workspace::load_workspace(
3380 serialized_workspace,
3381 project_paths_to_open
3382 .iter()
3383 .map(|(_, project_path)| project_path)
3384 .cloned()
3385 .collect(),
3386 cx,
3387 )
3388 });
3389
3390 cx.spawn(|workspace, mut cx| async move {
3391 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3392
3393 if let Some(restored_items) = restored_items {
3394 let restored_items = restored_items.await?;
3395
3396 let restored_project_paths = restored_items
3397 .iter()
3398 .filter_map(|item| {
3399 cx.update(|_, cx| item.as_ref()?.project_path(cx))
3400 .ok()
3401 .flatten()
3402 })
3403 .collect::<HashSet<_>>();
3404
3405 for restored_item in restored_items {
3406 opened_items.push(restored_item.map(Ok));
3407 }
3408
3409 project_paths_to_open
3410 .iter_mut()
3411 .for_each(|(_, project_path)| {
3412 if let Some(project_path_to_open) = project_path {
3413 if restored_project_paths.contains(project_path_to_open) {
3414 *project_path = None;
3415 }
3416 }
3417 });
3418 } else {
3419 for _ in 0..project_paths_to_open.len() {
3420 opened_items.push(None);
3421 }
3422 }
3423 assert!(opened_items.len() == project_paths_to_open.len());
3424
3425 let tasks =
3426 project_paths_to_open
3427 .into_iter()
3428 .enumerate()
3429 .map(|(i, (abs_path, project_path))| {
3430 let workspace = workspace.clone();
3431 cx.spawn(|mut cx| {
3432 let fs = app_state.fs.clone();
3433 async move {
3434 let file_project_path = project_path?;
3435 if fs.is_file(&abs_path).await {
3436 Some((
3437 i,
3438 workspace
3439 .update(&mut cx, |workspace, cx| {
3440 workspace.open_path(file_project_path, None, true, cx)
3441 })
3442 .log_err()?
3443 .await,
3444 ))
3445 } else {
3446 None
3447 }
3448 }
3449 })
3450 });
3451
3452 let tasks = tasks.collect::<Vec<_>>();
3453
3454 let tasks = futures::future::join_all(tasks.into_iter());
3455 for maybe_opened_path in tasks.await.into_iter() {
3456 if let Some((i, path_open_result)) = maybe_opened_path {
3457 opened_items[i] = Some(path_open_result);
3458 }
3459 }
3460
3461 Ok(opened_items)
3462 })
3463}
3464
3465fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3466 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3467
3468 workspace
3469 .update(cx, |workspace, cx| {
3470 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3471 workspace.show_notification_once(0, cx, |cx| {
3472 cx.new_view(|_| {
3473 MessageNotification::new("Failed to load the database file.")
3474 .with_click_message("Click to let us know about this error")
3475 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3476 })
3477 });
3478 }
3479 })
3480 .log_err();
3481}
3482
3483impl FocusableView for Workspace {
3484 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3485 self.active_pane.focus_handle(cx)
3486 }
3487}
3488
3489#[derive(Clone, Render)]
3490struct DraggedDock(DockPosition);
3491
3492impl Render for Workspace {
3493 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3494 let mut context = KeyContext::default();
3495 context.add("Workspace");
3496
3497 let (ui_font, ui_font_size) = {
3498 let theme_settings = ThemeSettings::get_global(cx);
3499 (
3500 theme_settings.ui_font.family.clone(),
3501 theme_settings.ui_font_size.clone(),
3502 )
3503 };
3504
3505 let theme = cx.theme().clone();
3506 let colors = theme.colors();
3507 cx.set_rem_size(ui_font_size);
3508
3509 self.actions(div(), cx)
3510 .key_context(context)
3511 .relative()
3512 .size_full()
3513 .flex()
3514 .flex_col()
3515 .font(ui_font)
3516 .gap_0()
3517 .justify_start()
3518 .items_start()
3519 .text_color(colors.text)
3520 .bg(colors.background)
3521 .border()
3522 .border_color(colors.border)
3523 .children(self.titlebar_item.clone())
3524 .child(
3525 div()
3526 .id("workspace")
3527 .relative()
3528 .flex_1()
3529 .w_full()
3530 .flex()
3531 .flex_col()
3532 .overflow_hidden()
3533 .border_t()
3534 .border_b()
3535 .border_color(colors.border)
3536 .child(
3537 canvas(cx.listener(|workspace, bounds, _| {
3538 workspace.bounds = *bounds;
3539 }))
3540 .absolute()
3541 .size_full(),
3542 )
3543 .on_drag_move(
3544 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3545 match e.drag(cx).0 {
3546 DockPosition::Left => {
3547 let size = workspace.bounds.left() + e.event.position.x;
3548 workspace.left_dock.update(cx, |left_dock, cx| {
3549 left_dock.resize_active_panel(Some(size), cx);
3550 });
3551 }
3552 DockPosition::Right => {
3553 let size = workspace.bounds.right() - e.event.position.x;
3554 workspace.right_dock.update(cx, |right_dock, cx| {
3555 right_dock.resize_active_panel(Some(size), cx);
3556 });
3557 }
3558 DockPosition::Bottom => {
3559 let size = workspace.bounds.bottom() - e.event.position.y;
3560 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3561 bottom_dock.resize_active_panel(Some(size), cx);
3562 });
3563 }
3564 }
3565 }),
3566 )
3567 .child(self.modal_layer.clone())
3568 .child(
3569 div()
3570 .flex()
3571 .flex_row()
3572 .h_full()
3573 // Left Dock
3574 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3575 || {
3576 div()
3577 .flex()
3578 .flex_none()
3579 .overflow_hidden()
3580 .child(self.left_dock.clone())
3581 },
3582 ))
3583 // Panes
3584 .child(
3585 div()
3586 .flex()
3587 .flex_col()
3588 .flex_1()
3589 .overflow_hidden()
3590 .child(self.center.render(
3591 &self.project,
3592 &self.follower_states,
3593 self.active_call(),
3594 &self.active_pane,
3595 self.zoomed.as_ref(),
3596 &self.app_state,
3597 cx,
3598 ))
3599 .children(
3600 self.zoomed_position
3601 .ne(&Some(DockPosition::Bottom))
3602 .then(|| self.bottom_dock.clone()),
3603 ),
3604 )
3605 // Right Dock
3606 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3607 || {
3608 div()
3609 .flex()
3610 .flex_none()
3611 .overflow_hidden()
3612 .child(self.right_dock.clone())
3613 },
3614 )),
3615 )
3616 .children(self.render_notifications(cx))
3617 .children(self.zoomed.as_ref().and_then(|view| {
3618 let zoomed_view = view.upgrade()?;
3619 let div = div()
3620 .z_index(1)
3621 .absolute()
3622 .overflow_hidden()
3623 .border_color(colors.border)
3624 .bg(colors.background)
3625 .child(zoomed_view)
3626 .inset_0()
3627 .shadow_lg();
3628
3629 Some(match self.zoomed_position {
3630 Some(DockPosition::Left) => div.right_2().border_r(),
3631 Some(DockPosition::Right) => div.left_2().border_l(),
3632 Some(DockPosition::Bottom) => div.top_2().border_t(),
3633 None => div.top_2().bottom_2().left_2().right_2().border(),
3634 })
3635 })),
3636 )
3637 .child(self.status_bar.clone())
3638 .children(if self.project.read(cx).is_disconnected() {
3639 Some(DisconnectedOverlay)
3640 } else {
3641 None
3642 })
3643 }
3644}
3645
3646impl WorkspaceStore {
3647 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3648 Self {
3649 workspaces: Default::default(),
3650 followers: Default::default(),
3651 _subscriptions: vec![
3652 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3653 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3654 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3655 ],
3656 client,
3657 }
3658 }
3659
3660 pub fn update_followers(
3661 &self,
3662 project_id: Option<u64>,
3663 update: proto::update_followers::Variant,
3664 cx: &AppContext,
3665 ) -> Option<()> {
3666 if !cx.has_global::<Model<ActiveCall>>() {
3667 return None;
3668 }
3669
3670 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3671 let follower_ids: Vec<_> = self
3672 .followers
3673 .iter()
3674 .filter_map(|follower| {
3675 if follower.project_id == project_id || project_id.is_none() {
3676 Some(follower.peer_id.into())
3677 } else {
3678 None
3679 }
3680 })
3681 .collect();
3682 if follower_ids.is_empty() {
3683 return None;
3684 }
3685 self.client
3686 .send(proto::UpdateFollowers {
3687 room_id,
3688 project_id,
3689 follower_ids,
3690 variant: Some(update),
3691 })
3692 .log_err()
3693 }
3694
3695 pub async fn handle_follow(
3696 this: Model<Self>,
3697 envelope: TypedEnvelope<proto::Follow>,
3698 _: Arc<Client>,
3699 mut cx: AsyncAppContext,
3700 ) -> Result<proto::FollowResponse> {
3701 this.update(&mut cx, |this, cx| {
3702 let follower = Follower {
3703 project_id: envelope.payload.project_id,
3704 peer_id: envelope.original_sender_id()?,
3705 };
3706 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3707
3708 let mut response = proto::FollowResponse::default();
3709 this.workspaces.retain(|workspace| {
3710 workspace
3711 .update(cx, |workspace, cx| {
3712 let handler_response = workspace.handle_follow(follower.project_id, cx);
3713 if response.views.is_empty() {
3714 response.views = handler_response.views;
3715 } else {
3716 response.views.extend_from_slice(&handler_response.views);
3717 }
3718
3719 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3720 if response.active_view_id.is_none()
3721 || Some(workspace.project.downgrade()) == active_project
3722 {
3723 response.active_view_id = Some(active_view_id);
3724 }
3725 }
3726 })
3727 .is_ok()
3728 });
3729
3730 if let Err(ix) = this.followers.binary_search(&follower) {
3731 this.followers.insert(ix, follower);
3732 }
3733
3734 Ok(response)
3735 })?
3736 }
3737
3738 async fn handle_unfollow(
3739 model: Model<Self>,
3740 envelope: TypedEnvelope<proto::Unfollow>,
3741 _: Arc<Client>,
3742 mut cx: AsyncAppContext,
3743 ) -> Result<()> {
3744 model.update(&mut cx, |this, _| {
3745 let follower = Follower {
3746 project_id: envelope.payload.project_id,
3747 peer_id: envelope.original_sender_id()?,
3748 };
3749 if let Ok(ix) = this.followers.binary_search(&follower) {
3750 this.followers.remove(ix);
3751 }
3752 Ok(())
3753 })?
3754 }
3755
3756 async fn handle_update_followers(
3757 this: Model<Self>,
3758 envelope: TypedEnvelope<proto::UpdateFollowers>,
3759 _: Arc<Client>,
3760 mut cx: AsyncAppContext,
3761 ) -> Result<()> {
3762 let leader_id = envelope.original_sender_id()?;
3763 let update = envelope.payload;
3764
3765 this.update(&mut cx, |this, cx| {
3766 this.workspaces.retain(|workspace| {
3767 workspace
3768 .update(cx, |workspace, cx| {
3769 let project_id = workspace.project.read(cx).remote_id();
3770 if update.project_id != project_id && update.project_id.is_some() {
3771 return;
3772 }
3773 workspace.handle_update_followers(leader_id, update.clone(), cx);
3774 })
3775 .is_ok()
3776 });
3777 Ok(())
3778 })?
3779 }
3780}
3781
3782impl ViewId {
3783 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3784 Ok(Self {
3785 creator: message
3786 .creator
3787 .ok_or_else(|| anyhow!("creator is missing"))?,
3788 id: message.id,
3789 })
3790 }
3791
3792 pub(crate) fn to_proto(&self) -> proto::ViewId {
3793 proto::ViewId {
3794 creator: Some(self.creator),
3795 id: self.id,
3796 }
3797 }
3798}
3799
3800pub trait WorkspaceHandle {
3801 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3802}
3803
3804impl WorkspaceHandle for View<Workspace> {
3805 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3806 self.read(cx)
3807 .worktrees(cx)
3808 .flat_map(|worktree| {
3809 let worktree_id = worktree.read(cx).id();
3810 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3811 worktree_id,
3812 path: f.path.clone(),
3813 })
3814 })
3815 .collect::<Vec<_>>()
3816 }
3817}
3818
3819impl std::fmt::Debug for OpenPaths {
3820 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3821 f.debug_struct("OpenPaths")
3822 .field("paths", &self.paths)
3823 .finish()
3824 }
3825}
3826
3827pub fn activate_workspace_for_project(
3828 cx: &mut AppContext,
3829 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3830) -> Option<WindowHandle<Workspace>> {
3831 for window in cx.windows() {
3832 let Some(workspace) = window.downcast::<Workspace>() else {
3833 continue;
3834 };
3835
3836 let predicate = workspace
3837 .update(cx, |workspace, cx| {
3838 let project = workspace.project.read(cx);
3839 if predicate(project, cx) {
3840 cx.activate_window();
3841 true
3842 } else {
3843 false
3844 }
3845 })
3846 .log_err()
3847 .unwrap_or(false);
3848
3849 if predicate {
3850 return Some(workspace);
3851 }
3852 }
3853
3854 None
3855}
3856
3857pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3858 DB.last_workspace().await.log_err().flatten()
3859}
3860
3861async fn join_channel_internal(
3862 channel_id: u64,
3863 app_state: &Arc<AppState>,
3864 requesting_window: Option<WindowHandle<Workspace>>,
3865 active_call: &Model<ActiveCall>,
3866 cx: &mut AsyncAppContext,
3867) -> Result<bool> {
3868 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3869 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3870 return (false, None);
3871 };
3872
3873 let already_in_channel = room.channel_id() == Some(channel_id);
3874 let should_prompt = room.is_sharing_project()
3875 && room.remote_participants().len() > 0
3876 && !already_in_channel;
3877 let open_room = if already_in_channel {
3878 active_call.room().cloned()
3879 } else {
3880 None
3881 };
3882 (should_prompt, open_room)
3883 })?;
3884
3885 if let Some(room) = open_room {
3886 let task = room.update(cx, |room, cx| {
3887 if let Some((project, host)) = room.most_active_project(cx) {
3888 return Some(join_remote_project(project, host, app_state.clone(), cx));
3889 }
3890
3891 None
3892 })?;
3893 if let Some(task) = task {
3894 task.await?;
3895 }
3896 return anyhow::Ok(true);
3897 }
3898
3899 if should_prompt {
3900 if let Some(workspace) = requesting_window {
3901 let answer = workspace.update(cx, |_, cx| {
3902 cx.prompt(
3903 PromptLevel::Warning,
3904 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
3905 &["Yes, Join Channel", "Cancel"],
3906 )
3907 })?.await;
3908
3909 if answer == Ok(1) {
3910 return Ok(false);
3911 }
3912 } else {
3913 return Ok(false); // unreachable!() hopefully
3914 }
3915 }
3916
3917 let client = cx.update(|cx| active_call.read(cx).client())?;
3918
3919 let mut client_status = client.status();
3920
3921 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
3922 'outer: loop {
3923 let Some(status) = client_status.recv().await else {
3924 return Err(anyhow!("error connecting"));
3925 };
3926
3927 match status {
3928 Status::Connecting
3929 | Status::Authenticating
3930 | Status::Reconnecting
3931 | Status::Reauthenticating => continue,
3932 Status::Connected { .. } => break 'outer,
3933 Status::SignedOut => return Err(anyhow!("not signed in")),
3934 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
3935 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
3936 return Err(anyhow!("zed is offline"))
3937 }
3938 }
3939 }
3940
3941 let room = active_call
3942 .update(cx, |active_call, cx| {
3943 active_call.join_channel(channel_id, cx)
3944 })?
3945 .await?;
3946
3947 let Some(room) = room else {
3948 return anyhow::Ok(true);
3949 };
3950
3951 room.update(cx, |room, _| room.room_update_completed())?
3952 .await;
3953
3954 let task = room.update(cx, |room, cx| {
3955 if let Some((project, host)) = room.most_active_project(cx) {
3956 return Some(join_remote_project(project, host, app_state.clone(), cx));
3957 }
3958
3959 None
3960 })?;
3961 if let Some(task) = task {
3962 task.await?;
3963 return anyhow::Ok(true);
3964 }
3965 anyhow::Ok(false)
3966}
3967
3968pub fn join_channel(
3969 channel_id: u64,
3970 app_state: Arc<AppState>,
3971 requesting_window: Option<WindowHandle<Workspace>>,
3972 cx: &mut AppContext,
3973) -> Task<Result<()>> {
3974 let active_call = ActiveCall::global(cx);
3975 cx.spawn(|mut cx| async move {
3976 let result = join_channel_internal(
3977 channel_id,
3978 &app_state,
3979 requesting_window,
3980 &active_call,
3981 &mut cx,
3982 )
3983 .await;
3984
3985 // join channel succeeded, and opened a window
3986 if matches!(result, Ok(true)) {
3987 return anyhow::Ok(());
3988 }
3989
3990 if requesting_window.is_some() {
3991 return anyhow::Ok(());
3992 }
3993
3994 // find an existing workspace to focus and show call controls
3995 let mut active_window = activate_any_workspace_window(&mut cx);
3996 if active_window.is_none() {
3997 // no open workspaces, make one to show the error in (blergh)
3998 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
3999 .await?;
4000 }
4001
4002 active_window = activate_any_workspace_window(&mut cx);
4003 let Some(active_window) = active_window else {
4004 return anyhow::Ok(());
4005 };
4006
4007 if let Err(err) = result {
4008 active_window
4009 .update(&mut cx, |_, cx| {
4010 cx.prompt(
4011 PromptLevel::Critical,
4012 &format!("Failed to join channel: {}", err),
4013 &["Ok"],
4014 )
4015 })?
4016 .await
4017 .ok();
4018 }
4019
4020 // return ok, we showed the error to the user.
4021 return anyhow::Ok(());
4022 })
4023}
4024
4025pub async fn get_any_active_workspace(
4026 app_state: Arc<AppState>,
4027 mut cx: AsyncAppContext,
4028) -> anyhow::Result<WindowHandle<Workspace>> {
4029 // find an existing workspace to focus and show call controls
4030 let active_window = activate_any_workspace_window(&mut cx);
4031 if active_window.is_none() {
4032 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4033 .await?;
4034 }
4035 activate_any_workspace_window(&mut cx)
4036 .context("could not open zed")?
4037 .downcast::<Workspace>()
4038 .context("could not open zed workspace window")
4039}
4040
4041fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
4042 cx.update(|cx| {
4043 for window in cx.windows() {
4044 let is_workspace = window.downcast::<Workspace>().is_some();
4045 if is_workspace {
4046 window.update(cx, |_, cx| cx.activate_window()).ok();
4047 return Some(window);
4048 }
4049 }
4050 None
4051 })
4052 .ok()
4053 .flatten()
4054}
4055
4056#[allow(clippy::type_complexity)]
4057pub fn open_paths(
4058 abs_paths: &[PathBuf],
4059 app_state: &Arc<AppState>,
4060 requesting_window: Option<WindowHandle<Workspace>>,
4061 cx: &mut AppContext,
4062) -> Task<
4063 anyhow::Result<(
4064 WindowHandle<Workspace>,
4065 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4066 )>,
4067> {
4068 let app_state = app_state.clone();
4069 let abs_paths = abs_paths.to_vec();
4070 // Open paths in existing workspace if possible
4071 let existing = activate_workspace_for_project(cx, {
4072 let abs_paths = abs_paths.clone();
4073 move |project, cx| project.contains_paths(&abs_paths, cx)
4074 });
4075 cx.spawn(move |mut cx| async move {
4076 if let Some(existing) = existing {
4077 Ok((
4078 existing.clone(),
4079 existing
4080 .update(&mut cx, |workspace, cx| {
4081 workspace.open_paths(abs_paths, true, cx)
4082 })?
4083 .await,
4084 ))
4085 } else {
4086 cx.update(move |cx| {
4087 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4088 })?
4089 .await
4090 }
4091 })
4092}
4093
4094pub fn open_new(
4095 app_state: &Arc<AppState>,
4096 cx: &mut AppContext,
4097 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4098) -> Task<()> {
4099 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4100 cx.spawn(|mut cx| async move {
4101 if let Some((workspace, opened_paths)) = task.await.log_err() {
4102 workspace
4103 .update(&mut cx, |workspace, cx| {
4104 if opened_paths.is_empty() {
4105 init(workspace, cx)
4106 }
4107 })
4108 .log_err();
4109 }
4110 })
4111}
4112
4113pub fn create_and_open_local_file(
4114 path: &'static Path,
4115 cx: &mut ViewContext<Workspace>,
4116 default_content: impl 'static + Send + FnOnce() -> Rope,
4117) -> Task<Result<Box<dyn ItemHandle>>> {
4118 cx.spawn(|workspace, mut cx| async move {
4119 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4120 if !fs.is_file(path).await {
4121 fs.create_file(path, Default::default()).await?;
4122 fs.save(path, &default_content(), Default::default())
4123 .await?;
4124 }
4125
4126 let mut items = workspace
4127 .update(&mut cx, |workspace, cx| {
4128 workspace.with_local_workspace(cx, |workspace, cx| {
4129 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4130 })
4131 })?
4132 .await?
4133 .await;
4134
4135 let item = items.pop().flatten();
4136 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4137 })
4138}
4139
4140pub fn join_remote_project(
4141 project_id: u64,
4142 follow_user_id: u64,
4143 app_state: Arc<AppState>,
4144 cx: &mut AppContext,
4145) -> Task<Result<()>> {
4146 let windows = cx.windows();
4147 cx.spawn(|mut cx| async move {
4148 let existing_workspace = windows.into_iter().find_map(|window| {
4149 window.downcast::<Workspace>().and_then(|window| {
4150 window
4151 .update(&mut cx, |workspace, cx| {
4152 if workspace.project().read(cx).remote_id() == Some(project_id) {
4153 Some(window)
4154 } else {
4155 None
4156 }
4157 })
4158 .unwrap_or(None)
4159 })
4160 });
4161
4162 let workspace = if let Some(existing_workspace) = existing_workspace {
4163 existing_workspace
4164 } else {
4165 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4166 let room = active_call
4167 .read_with(&cx, |call, _| call.room().cloned())?
4168 .ok_or_else(|| anyhow!("not in a call"))?;
4169 let project = room
4170 .update(&mut cx, |room, cx| {
4171 room.join_project(
4172 project_id,
4173 app_state.languages.clone(),
4174 app_state.fs.clone(),
4175 cx,
4176 )
4177 })?
4178 .await?;
4179
4180 let window_bounds_override = window_bounds_env_override(&cx);
4181 cx.update(|cx| {
4182 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4183 cx.open_window(options, |cx| {
4184 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4185 })
4186 })?
4187 };
4188
4189 workspace.update(&mut cx, |workspace, cx| {
4190 cx.activate(true);
4191 cx.activate_window();
4192
4193 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4194 let follow_peer_id = room
4195 .read(cx)
4196 .remote_participants()
4197 .iter()
4198 .find(|(_, participant)| participant.user.id == follow_user_id)
4199 .map(|(_, p)| p.peer_id)
4200 .or_else(|| {
4201 // If we couldn't follow the given user, follow the host instead.
4202 let collaborator = workspace
4203 .project()
4204 .read(cx)
4205 .collaborators()
4206 .values()
4207 .find(|collaborator| collaborator.replica_id == 0)?;
4208 Some(collaborator.peer_id)
4209 });
4210
4211 if let Some(follow_peer_id) = follow_peer_id {
4212 workspace.follow(follow_peer_id, cx);
4213 }
4214 }
4215 })?;
4216
4217 anyhow::Ok(())
4218 })
4219}
4220
4221pub fn restart(_: &Restart, cx: &mut AppContext) {
4222 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4223 let mut workspace_windows = cx
4224 .windows()
4225 .into_iter()
4226 .filter_map(|window| window.downcast::<Workspace>())
4227 .collect::<Vec<_>>();
4228
4229 // If multiple windows have unsaved changes, and need a save prompt,
4230 // prompt in the active window before switching to a different window.
4231 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4232
4233 let mut prompt = None;
4234 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4235 prompt = window
4236 .update(cx, |_, cx| {
4237 cx.prompt(
4238 PromptLevel::Info,
4239 "Are you sure you want to restart?",
4240 &["Restart", "Cancel"],
4241 )
4242 })
4243 .ok();
4244 }
4245
4246 cx.spawn(|mut cx| async move {
4247 if let Some(prompt) = prompt {
4248 let answer = prompt.await?;
4249 if answer != 0 {
4250 return Ok(());
4251 }
4252 }
4253
4254 // If the user cancels any save prompt, then keep the app open.
4255 for window in workspace_windows {
4256 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4257 workspace.prepare_to_close(true, cx)
4258 }) {
4259 if !should_close.await? {
4260 return Ok(());
4261 }
4262 }
4263 }
4264
4265 cx.update(|cx| cx.restart())
4266 })
4267 .detach_and_log_err(cx);
4268}
4269
4270fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4271 let mut parts = value.split(',');
4272 let x: usize = parts.next()?.parse().ok()?;
4273 let y: usize = parts.next()?.parse().ok()?;
4274 Some(point((x as f64).into(), (y as f64).into()))
4275}
4276
4277fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4278 let mut parts = value.split(',');
4279 let width: usize = parts.next()?.parse().ok()?;
4280 let height: usize = parts.next()?.parse().ok()?;
4281 Some(size((width as f64).into(), (height as f64).into()))
4282}
4283
4284struct DisconnectedOverlay;
4285
4286impl Element for DisconnectedOverlay {
4287 type State = AnyElement;
4288
4289 fn request_layout(
4290 &mut self,
4291 _: Option<Self::State>,
4292 cx: &mut WindowContext,
4293 ) -> (LayoutId, Self::State) {
4294 let mut background = cx.theme().colors().elevated_surface_background;
4295 background.fade_out(0.2);
4296 let mut overlay = div()
4297 .bg(background)
4298 .absolute()
4299 .left_0()
4300 .top_0()
4301 .size_full()
4302 .flex()
4303 .items_center()
4304 .justify_center()
4305 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4306 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4307 .child(Label::new(
4308 "Your connection to the remote project has been lost.",
4309 ))
4310 .into_any();
4311 (overlay.request_layout(cx), overlay)
4312 }
4313
4314 fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
4315 cx.with_z_index(u8::MAX, |cx| {
4316 cx.add_opaque_layer(bounds);
4317 overlay.paint(cx);
4318 })
4319 }
4320}
4321
4322impl IntoElement for DisconnectedOverlay {
4323 type Element = Self;
4324
4325 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4326 None
4327 }
4328
4329 fn into_element(self) -> Self::Element {
4330 self
4331 }
4332}
4333
4334#[cfg(test)]
4335mod tests {
4336 use std::{cell::RefCell, rc::Rc};
4337
4338 use super::*;
4339 use crate::item::{
4340 test::{TestItem, TestProjectItem},
4341 ItemEvent,
4342 };
4343 use fs::FakeFs;
4344 use gpui::TestAppContext;
4345 use project::{Project, ProjectEntryId};
4346 use serde_json::json;
4347 use settings::SettingsStore;
4348
4349 #[gpui::test]
4350 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4351 init_test(cx);
4352
4353 let fs = FakeFs::new(cx.executor());
4354 let project = Project::test(fs, [], cx).await;
4355 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4356
4357 // Adding an item with no ambiguity renders the tab without detail.
4358 let item1 = cx.new_view(|cx| {
4359 let mut item = TestItem::new(cx);
4360 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4361 item
4362 });
4363 workspace.update(cx, |workspace, cx| {
4364 workspace.add_item(Box::new(item1.clone()), cx);
4365 });
4366 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4367
4368 // Adding an item that creates ambiguity increases the level of detail on
4369 // both tabs.
4370 let item2 = cx.new_view(|cx| {
4371 let mut item = TestItem::new(cx);
4372 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4373 item
4374 });
4375 workspace.update(cx, |workspace, cx| {
4376 workspace.add_item(Box::new(item2.clone()), cx);
4377 });
4378 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4379 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4380
4381 // Adding an item that creates ambiguity increases the level of detail only
4382 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4383 // we stop at the highest detail available.
4384 let item3 = cx.new_view(|cx| {
4385 let mut item = TestItem::new(cx);
4386 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4387 item
4388 });
4389 workspace.update(cx, |workspace, cx| {
4390 workspace.add_item(Box::new(item3.clone()), cx);
4391 });
4392 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4393 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4394 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4395 }
4396
4397 #[gpui::test]
4398 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4399 init_test(cx);
4400
4401 let fs = FakeFs::new(cx.executor());
4402 fs.insert_tree(
4403 "/root1",
4404 json!({
4405 "one.txt": "",
4406 "two.txt": "",
4407 }),
4408 )
4409 .await;
4410 fs.insert_tree(
4411 "/root2",
4412 json!({
4413 "three.txt": "",
4414 }),
4415 )
4416 .await;
4417
4418 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4419 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4420 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4421 let worktree_id = project.update(cx, |project, cx| {
4422 project.worktrees().next().unwrap().read(cx).id()
4423 });
4424
4425 let item1 = cx.new_view(|cx| {
4426 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4427 });
4428 let item2 = cx.new_view(|cx| {
4429 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4430 });
4431
4432 // Add an item to an empty pane
4433 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4434 project.update(cx, |project, cx| {
4435 assert_eq!(
4436 project.active_entry(),
4437 project
4438 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4439 .map(|e| e.id)
4440 );
4441 });
4442 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4443
4444 // Add a second item to a non-empty pane
4445 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4446 assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
4447 project.update(cx, |project, cx| {
4448 assert_eq!(
4449 project.active_entry(),
4450 project
4451 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4452 .map(|e| e.id)
4453 );
4454 });
4455
4456 // Close the active item
4457 pane.update(cx, |pane, cx| {
4458 pane.close_active_item(&Default::default(), cx).unwrap()
4459 })
4460 .await
4461 .unwrap();
4462 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4463 project.update(cx, |project, cx| {
4464 assert_eq!(
4465 project.active_entry(),
4466 project
4467 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4468 .map(|e| e.id)
4469 );
4470 });
4471
4472 // Add a project folder
4473 project
4474 .update(cx, |project, cx| {
4475 project.find_or_create_local_worktree("/root2", true, cx)
4476 })
4477 .await
4478 .unwrap();
4479 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
4480
4481 // Remove a project folder
4482 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4483 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
4484 }
4485
4486 #[gpui::test]
4487 async fn test_close_window(cx: &mut TestAppContext) {
4488 init_test(cx);
4489
4490 let fs = FakeFs::new(cx.executor());
4491 fs.insert_tree("/root", json!({ "one": "" })).await;
4492
4493 let project = Project::test(fs, ["root".as_ref()], cx).await;
4494 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4495
4496 // When there are no dirty items, there's nothing to do.
4497 let item1 = cx.new_view(|cx| TestItem::new(cx));
4498 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4499 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4500 assert!(task.await.unwrap());
4501
4502 // When there are dirty untitled items, prompt to save each one. If the user
4503 // cancels any prompt, then abort.
4504 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4505 let item3 = cx.new_view(|cx| {
4506 TestItem::new(cx)
4507 .with_dirty(true)
4508 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4509 });
4510 workspace.update(cx, |w, cx| {
4511 w.add_item(Box::new(item2.clone()), cx);
4512 w.add_item(Box::new(item3.clone()), cx);
4513 });
4514 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4515 cx.executor().run_until_parked();
4516 cx.simulate_prompt_answer(2); // cancel save all
4517 cx.executor().run_until_parked();
4518 cx.simulate_prompt_answer(2); // cancel save all
4519 cx.executor().run_until_parked();
4520 assert!(!cx.has_pending_prompt());
4521 assert!(!task.await.unwrap());
4522 }
4523
4524 #[gpui::test]
4525 async fn test_close_pane_items(cx: &mut TestAppContext) {
4526 init_test(cx);
4527
4528 let fs = FakeFs::new(cx.executor());
4529
4530 let project = Project::test(fs, None, cx).await;
4531 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4532
4533 let item1 = cx.new_view(|cx| {
4534 TestItem::new(cx)
4535 .with_dirty(true)
4536 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4537 });
4538 let item2 = cx.new_view(|cx| {
4539 TestItem::new(cx)
4540 .with_dirty(true)
4541 .with_conflict(true)
4542 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4543 });
4544 let item3 = cx.new_view(|cx| {
4545 TestItem::new(cx)
4546 .with_dirty(true)
4547 .with_conflict(true)
4548 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4549 });
4550 let item4 = cx.new_view(|cx| {
4551 TestItem::new(cx)
4552 .with_dirty(true)
4553 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4554 });
4555 let pane = workspace.update(cx, |workspace, cx| {
4556 workspace.add_item(Box::new(item1.clone()), cx);
4557 workspace.add_item(Box::new(item2.clone()), cx);
4558 workspace.add_item(Box::new(item3.clone()), cx);
4559 workspace.add_item(Box::new(item4.clone()), cx);
4560 workspace.active_pane().clone()
4561 });
4562
4563 let close_items = pane.update(cx, |pane, cx| {
4564 pane.activate_item(1, true, true, cx);
4565 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4566 let item1_id = item1.item_id();
4567 let item3_id = item3.item_id();
4568 let item4_id = item4.item_id();
4569 pane.close_items(cx, SaveIntent::Close, move |id| {
4570 [item1_id, item3_id, item4_id].contains(&id)
4571 })
4572 });
4573 cx.executor().run_until_parked();
4574
4575 assert!(cx.has_pending_prompt());
4576 // Ignore "Save all" prompt
4577 cx.simulate_prompt_answer(2);
4578 cx.executor().run_until_parked();
4579 // There's a prompt to save item 1.
4580 pane.update(cx, |pane, _| {
4581 assert_eq!(pane.items_len(), 4);
4582 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4583 });
4584 // Confirm saving item 1.
4585 cx.simulate_prompt_answer(0);
4586 cx.executor().run_until_parked();
4587
4588 // Item 1 is saved. There's a prompt to save item 3.
4589 pane.update(cx, |pane, cx| {
4590 assert_eq!(item1.read(cx).save_count, 1);
4591 assert_eq!(item1.read(cx).save_as_count, 0);
4592 assert_eq!(item1.read(cx).reload_count, 0);
4593 assert_eq!(pane.items_len(), 3);
4594 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4595 });
4596 assert!(cx.has_pending_prompt());
4597
4598 // Cancel saving item 3.
4599 cx.simulate_prompt_answer(1);
4600 cx.executor().run_until_parked();
4601
4602 // Item 3 is reloaded. There's a prompt to save item 4.
4603 pane.update(cx, |pane, cx| {
4604 assert_eq!(item3.read(cx).save_count, 0);
4605 assert_eq!(item3.read(cx).save_as_count, 0);
4606 assert_eq!(item3.read(cx).reload_count, 1);
4607 assert_eq!(pane.items_len(), 2);
4608 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4609 });
4610 assert!(cx.has_pending_prompt());
4611
4612 // Confirm saving item 4.
4613 cx.simulate_prompt_answer(0);
4614 cx.executor().run_until_parked();
4615
4616 // There's a prompt for a path for item 4.
4617 cx.simulate_new_path_selection(|_| Some(Default::default()));
4618 close_items.await.unwrap();
4619
4620 // The requested items are closed.
4621 pane.update(cx, |pane, cx| {
4622 assert_eq!(item4.read(cx).save_count, 0);
4623 assert_eq!(item4.read(cx).save_as_count, 1);
4624 assert_eq!(item4.read(cx).reload_count, 0);
4625 assert_eq!(pane.items_len(), 1);
4626 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4627 });
4628 }
4629
4630 #[gpui::test]
4631 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4632 init_test(cx);
4633
4634 let fs = FakeFs::new(cx.executor());
4635 let project = Project::test(fs, [], cx).await;
4636 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4637
4638 // Create several workspace items with single project entries, and two
4639 // workspace items with multiple project entries.
4640 let single_entry_items = (0..=4)
4641 .map(|project_entry_id| {
4642 cx.new_view(|cx| {
4643 TestItem::new(cx)
4644 .with_dirty(true)
4645 .with_project_items(&[TestProjectItem::new(
4646 project_entry_id,
4647 &format!("{project_entry_id}.txt"),
4648 cx,
4649 )])
4650 })
4651 })
4652 .collect::<Vec<_>>();
4653 let item_2_3 = cx.new_view(|cx| {
4654 TestItem::new(cx)
4655 .with_dirty(true)
4656 .with_singleton(false)
4657 .with_project_items(&[
4658 single_entry_items[2].read(cx).project_items[0].clone(),
4659 single_entry_items[3].read(cx).project_items[0].clone(),
4660 ])
4661 });
4662 let item_3_4 = cx.new_view(|cx| {
4663 TestItem::new(cx)
4664 .with_dirty(true)
4665 .with_singleton(false)
4666 .with_project_items(&[
4667 single_entry_items[3].read(cx).project_items[0].clone(),
4668 single_entry_items[4].read(cx).project_items[0].clone(),
4669 ])
4670 });
4671
4672 // Create two panes that contain the following project entries:
4673 // left pane:
4674 // multi-entry items: (2, 3)
4675 // single-entry items: 0, 1, 2, 3, 4
4676 // right pane:
4677 // single-entry items: 1
4678 // multi-entry items: (3, 4)
4679 let left_pane = workspace.update(cx, |workspace, cx| {
4680 let left_pane = workspace.active_pane().clone();
4681 workspace.add_item(Box::new(item_2_3.clone()), cx);
4682 for item in single_entry_items {
4683 workspace.add_item(Box::new(item), cx);
4684 }
4685 left_pane.update(cx, |pane, cx| {
4686 pane.activate_item(2, true, true, cx);
4687 });
4688
4689 let right_pane = workspace
4690 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4691 .unwrap();
4692
4693 right_pane.update(cx, |pane, cx| {
4694 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4695 });
4696
4697 left_pane
4698 });
4699
4700 cx.focus_view(&left_pane);
4701
4702 // When closing all of the items in the left pane, we should be prompted twice:
4703 // once for project entry 0, and once for project entry 2. Project entries 1,
4704 // 3, and 4 are all still open in the other paten. After those two
4705 // prompts, the task should complete.
4706
4707 let close = left_pane.update(cx, |pane, cx| {
4708 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4709 });
4710 cx.executor().run_until_parked();
4711
4712 // Discard "Save all" prompt
4713 cx.simulate_prompt_answer(2);
4714
4715 cx.executor().run_until_parked();
4716 left_pane.update(cx, |pane, cx| {
4717 assert_eq!(
4718 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4719 &[ProjectEntryId::from_proto(0)]
4720 );
4721 });
4722 cx.simulate_prompt_answer(0);
4723
4724 cx.executor().run_until_parked();
4725 left_pane.update(cx, |pane, cx| {
4726 assert_eq!(
4727 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4728 &[ProjectEntryId::from_proto(2)]
4729 );
4730 });
4731 cx.simulate_prompt_answer(0);
4732
4733 cx.executor().run_until_parked();
4734 close.await.unwrap();
4735 left_pane.update(cx, |pane, _| {
4736 assert_eq!(pane.items_len(), 0);
4737 });
4738 }
4739
4740 #[gpui::test]
4741 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4742 init_test(cx);
4743
4744 let fs = FakeFs::new(cx.executor());
4745 let project = Project::test(fs, [], cx).await;
4746 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4747 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4748
4749 let item = cx.new_view(|cx| {
4750 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4751 });
4752 let item_id = item.entity_id();
4753 workspace.update(cx, |workspace, cx| {
4754 workspace.add_item(Box::new(item.clone()), cx);
4755 });
4756
4757 // Autosave on window change.
4758 item.update(cx, |item, cx| {
4759 cx.update_global(|settings: &mut SettingsStore, cx| {
4760 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4761 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4762 })
4763 });
4764 item.is_dirty = true;
4765 });
4766
4767 // Deactivating the window saves the file.
4768 cx.deactivate_window();
4769 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4770
4771 // Autosave on focus change.
4772 item.update(cx, |item, cx| {
4773 cx.focus_self();
4774 cx.update_global(|settings: &mut SettingsStore, cx| {
4775 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4776 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4777 })
4778 });
4779 item.is_dirty = true;
4780 });
4781
4782 // Blurring the item saves the file.
4783 item.update(cx, |_, cx| cx.blur());
4784 cx.executor().run_until_parked();
4785 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4786
4787 // Deactivating the window still saves the file.
4788 cx.update(|cx| cx.activate_window());
4789 item.update(cx, |item, cx| {
4790 cx.focus_self();
4791 item.is_dirty = true;
4792 });
4793 cx.deactivate_window();
4794
4795 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4796
4797 // Autosave after delay.
4798 item.update(cx, |item, cx| {
4799 cx.update_global(|settings: &mut SettingsStore, cx| {
4800 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4801 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4802 })
4803 });
4804 item.is_dirty = true;
4805 cx.emit(ItemEvent::Edit);
4806 });
4807
4808 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4809 cx.executor().advance_clock(Duration::from_millis(250));
4810 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4811
4812 // After delay expires, the file is saved.
4813 cx.executor().advance_clock(Duration::from_millis(250));
4814 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4815
4816 // Autosave on focus change, ensuring closing the tab counts as such.
4817 item.update(cx, |item, cx| {
4818 cx.update_global(|settings: &mut SettingsStore, cx| {
4819 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4820 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4821 })
4822 });
4823 item.is_dirty = true;
4824 });
4825
4826 pane.update(cx, |pane, cx| {
4827 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4828 })
4829 .await
4830 .unwrap();
4831 assert!(!cx.has_pending_prompt());
4832 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4833
4834 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4835 workspace.update(cx, |workspace, cx| {
4836 workspace.add_item(Box::new(item.clone()), cx);
4837 });
4838 item.update(cx, |item, cx| {
4839 item.project_items[0].update(cx, |item, _| {
4840 item.entry_id = None;
4841 });
4842 item.is_dirty = true;
4843 cx.blur();
4844 });
4845 cx.run_until_parked();
4846 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4847
4848 // Ensure autosave is prevented for deleted files also when closing the buffer.
4849 let _close_items = pane.update(cx, |pane, cx| {
4850 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4851 });
4852 cx.run_until_parked();
4853 assert!(cx.has_pending_prompt());
4854 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4855 }
4856
4857 #[gpui::test]
4858 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4859 init_test(cx);
4860
4861 let fs = FakeFs::new(cx.executor());
4862
4863 let project = Project::test(fs, [], cx).await;
4864 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4865
4866 let item = cx.new_view(|cx| {
4867 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4868 });
4869 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4870 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4871 let toolbar_notify_count = Rc::new(RefCell::new(0));
4872
4873 workspace.update(cx, |workspace, cx| {
4874 workspace.add_item(Box::new(item.clone()), cx);
4875 let toolbar_notification_count = toolbar_notify_count.clone();
4876 cx.observe(&toolbar, move |_, _, _| {
4877 *toolbar_notification_count.borrow_mut() += 1
4878 })
4879 .detach();
4880 });
4881
4882 pane.update(cx, |pane, _| {
4883 assert!(!pane.can_navigate_backward());
4884 assert!(!pane.can_navigate_forward());
4885 });
4886
4887 item.update(cx, |item, cx| {
4888 item.set_state("one".to_string(), cx);
4889 });
4890
4891 // Toolbar must be notified to re-render the navigation buttons
4892 assert_eq!(*toolbar_notify_count.borrow(), 1);
4893
4894 pane.update(cx, |pane, _| {
4895 assert!(pane.can_navigate_backward());
4896 assert!(!pane.can_navigate_forward());
4897 });
4898
4899 workspace
4900 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4901 .await
4902 .unwrap();
4903
4904 assert_eq!(*toolbar_notify_count.borrow(), 2);
4905 pane.update(cx, |pane, _| {
4906 assert!(!pane.can_navigate_backward());
4907 assert!(pane.can_navigate_forward());
4908 });
4909 }
4910
4911 // #[gpui::test]
4912 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4913 // init_test(cx);
4914 // let fs = FakeFs::new(cx.executor());
4915
4916 // let project = Project::test(fs, [], cx).await;
4917 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4918
4919 // let panel = workspace.update(cx, |workspace, cx| {
4920 // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
4921 // workspace.add_panel(panel.clone(), cx);
4922
4923 // workspace
4924 // .right_dock()
4925 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4926
4927 // panel
4928 // });
4929
4930 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4931 // pane.update(cx, |pane, cx| {
4932 // let item = cx.build_view(|cx| TestItem::new(cx));
4933 // pane.add_item(Box::new(item), true, true, None, cx);
4934 // });
4935
4936 // // Transfer focus from center to panel
4937 // workspace.update(cx, |workspace, cx| {
4938 // workspace.toggle_panel_focus::<TestPanel>(cx);
4939 // });
4940
4941 // workspace.update(cx, |workspace, cx| {
4942 // assert!(workspace.right_dock().read(cx).is_open());
4943 // assert!(!panel.is_zoomed(cx));
4944 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4945 // });
4946
4947 // // Transfer focus from panel to center
4948 // workspace.update(cx, |workspace, cx| {
4949 // workspace.toggle_panel_focus::<TestPanel>(cx);
4950 // });
4951
4952 // workspace.update(cx, |workspace, cx| {
4953 // assert!(workspace.right_dock().read(cx).is_open());
4954 // assert!(!panel.is_zoomed(cx));
4955 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4956 // });
4957
4958 // // Close the dock
4959 // workspace.update(cx, |workspace, cx| {
4960 // workspace.toggle_dock(DockPosition::Right, cx);
4961 // });
4962
4963 // workspace.update(cx, |workspace, cx| {
4964 // assert!(!workspace.right_dock().read(cx).is_open());
4965 // assert!(!panel.is_zoomed(cx));
4966 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4967 // });
4968
4969 // // Open the dock
4970 // workspace.update(cx, |workspace, cx| {
4971 // workspace.toggle_dock(DockPosition::Right, cx);
4972 // });
4973
4974 // workspace.update(cx, |workspace, cx| {
4975 // assert!(workspace.right_dock().read(cx).is_open());
4976 // assert!(!panel.is_zoomed(cx));
4977 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4978 // });
4979
4980 // // Focus and zoom panel
4981 // panel.update(cx, |panel, cx| {
4982 // cx.focus_self();
4983 // panel.set_zoomed(true, cx)
4984 // });
4985
4986 // workspace.update(cx, |workspace, cx| {
4987 // assert!(workspace.right_dock().read(cx).is_open());
4988 // assert!(panel.is_zoomed(cx));
4989 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4990 // });
4991
4992 // // Transfer focus to the center closes the dock
4993 // workspace.update(cx, |workspace, cx| {
4994 // workspace.toggle_panel_focus::<TestPanel>(cx);
4995 // });
4996
4997 // workspace.update(cx, |workspace, cx| {
4998 // assert!(!workspace.right_dock().read(cx).is_open());
4999 // assert!(panel.is_zoomed(cx));
5000 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5001 // });
5002
5003 // // Transferring focus back to the panel keeps it zoomed
5004 // workspace.update(cx, |workspace, cx| {
5005 // workspace.toggle_panel_focus::<TestPanel>(cx);
5006 // });
5007
5008 // workspace.update(cx, |workspace, cx| {
5009 // assert!(workspace.right_dock().read(cx).is_open());
5010 // assert!(panel.is_zoomed(cx));
5011 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5012 // });
5013
5014 // // Close the dock while it is zoomed
5015 // workspace.update(cx, |workspace, cx| {
5016 // workspace.toggle_dock(DockPosition::Right, cx)
5017 // });
5018
5019 // workspace.update(cx, |workspace, cx| {
5020 // assert!(!workspace.right_dock().read(cx).is_open());
5021 // assert!(panel.is_zoomed(cx));
5022 // assert!(workspace.zoomed.is_none());
5023 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5024 // });
5025
5026 // // Opening the dock, when it's zoomed, retains focus
5027 // workspace.update(cx, |workspace, cx| {
5028 // workspace.toggle_dock(DockPosition::Right, cx)
5029 // });
5030
5031 // workspace.update(cx, |workspace, cx| {
5032 // assert!(workspace.right_dock().read(cx).is_open());
5033 // assert!(panel.is_zoomed(cx));
5034 // assert!(workspace.zoomed.is_some());
5035 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5036 // });
5037
5038 // // Unzoom and close the panel, zoom the active pane.
5039 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5040 // workspace.update(cx, |workspace, cx| {
5041 // workspace.toggle_dock(DockPosition::Right, cx)
5042 // });
5043 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5044
5045 // // Opening a dock unzooms the pane.
5046 // workspace.update(cx, |workspace, cx| {
5047 // workspace.toggle_dock(DockPosition::Right, cx)
5048 // });
5049 // workspace.update(cx, |workspace, cx| {
5050 // let pane = pane.read(cx);
5051 // assert!(!pane.is_zoomed());
5052 // assert!(!pane.focus_handle(cx).is_focused(cx));
5053 // assert!(workspace.right_dock().read(cx).is_open());
5054 // assert!(workspace.zoomed.is_none());
5055 // });
5056 // }
5057
5058 // #[gpui::test]
5059 // async fn test_panels(cx: &mut gpui::TestAppContext) {
5060 // init_test(cx);
5061 // let fs = FakeFs::new(cx.executor());
5062
5063 // let project = Project::test(fs, [], cx).await;
5064 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5065
5066 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5067 // // Add panel_1 on the left, panel_2 on the right.
5068 // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
5069 // workspace.add_panel(panel_1.clone(), cx);
5070 // workspace
5071 // .left_dock()
5072 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5073 // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
5074 // workspace.add_panel(panel_2.clone(), cx);
5075 // workspace
5076 // .right_dock()
5077 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5078
5079 // let left_dock = workspace.left_dock();
5080 // assert_eq!(
5081 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5082 // panel_1.panel_id()
5083 // );
5084 // assert_eq!(
5085 // left_dock.read(cx).active_panel_size(cx).unwrap(),
5086 // panel_1.size(cx)
5087 // );
5088
5089 // left_dock.update(cx, |left_dock, cx| {
5090 // left_dock.resize_active_panel(Some(1337.), cx)
5091 // });
5092 // assert_eq!(
5093 // workspace
5094 // .right_dock()
5095 // .read(cx)
5096 // .visible_panel()
5097 // .unwrap()
5098 // .panel_id(),
5099 // panel_2.panel_id(),
5100 // );
5101
5102 // (panel_1, panel_2)
5103 // });
5104
5105 // // Move panel_1 to the right
5106 // panel_1.update(cx, |panel_1, cx| {
5107 // panel_1.set_position(DockPosition::Right, cx)
5108 // });
5109
5110 // workspace.update(cx, |workspace, cx| {
5111 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5112 // // Since it was the only panel on the left, the left dock should now be closed.
5113 // assert!(!workspace.left_dock().read(cx).is_open());
5114 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5115 // let right_dock = workspace.right_dock();
5116 // assert_eq!(
5117 // right_dock.read(cx).visible_panel().unwrap().panel_id(),
5118 // panel_1.panel_id()
5119 // );
5120 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5121
5122 // // Now we move panel_2Β to the left
5123 // panel_2.set_position(DockPosition::Left, cx);
5124 // });
5125
5126 // workspace.update(cx, |workspace, cx| {
5127 // // Since panel_2 was not visible on the right, we don't open the left dock.
5128 // assert!(!workspace.left_dock().read(cx).is_open());
5129 // // And the right dock is unaffected in it's displaying of panel_1
5130 // assert!(workspace.right_dock().read(cx).is_open());
5131 // assert_eq!(
5132 // workspace
5133 // .right_dock()
5134 // .read(cx)
5135 // .visible_panel()
5136 // .unwrap()
5137 // .panel_id(),
5138 // panel_1.panel_id(),
5139 // );
5140 // });
5141
5142 // // Move panel_1 back to the left
5143 // panel_1.update(cx, |panel_1, cx| {
5144 // panel_1.set_position(DockPosition::Left, cx)
5145 // });
5146
5147 // workspace.update(cx, |workspace, cx| {
5148 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5149 // let left_dock = workspace.left_dock();
5150 // assert!(left_dock.read(cx).is_open());
5151 // assert_eq!(
5152 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5153 // panel_1.panel_id()
5154 // );
5155 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5156 // // And right the dock should be closed as it no longer has any panels.
5157 // assert!(!workspace.right_dock().read(cx).is_open());
5158
5159 // // Now we move panel_1 to the bottom
5160 // panel_1.set_position(DockPosition::Bottom, cx);
5161 // });
5162
5163 // workspace.update(cx, |workspace, cx| {
5164 // // Since panel_1 was visible on the left, we close the left dock.
5165 // assert!(!workspace.left_dock().read(cx).is_open());
5166 // // The bottom dock is sized based on the panel's default size,
5167 // // since the panel orientation changed from vertical to horizontal.
5168 // let bottom_dock = workspace.bottom_dock();
5169 // assert_eq!(
5170 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5171 // panel_1.size(cx),
5172 // );
5173 // // Close bottom dock and move panel_1 back to the left.
5174 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5175 // panel_1.set_position(DockPosition::Left, cx);
5176 // });
5177
5178 // // Emit activated event on panel 1
5179 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5180
5181 // // Now the left dock is open and panel_1 is active and focused.
5182 // workspace.update(cx, |workspace, cx| {
5183 // let left_dock = workspace.left_dock();
5184 // assert!(left_dock.read(cx).is_open());
5185 // assert_eq!(
5186 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5187 // panel_1.panel_id(),
5188 // );
5189 // assert!(panel_1.focus_handle(cx).is_focused(cx));
5190 // });
5191
5192 // // Emit closed event on panel 2, which is not active
5193 // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5194
5195 // // Wo don't close the left dock, because panel_2 wasn't the active panel
5196 // workspace.update(cx, |workspace, cx| {
5197 // let left_dock = workspace.left_dock();
5198 // assert!(left_dock.read(cx).is_open());
5199 // assert_eq!(
5200 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5201 // panel_1.panel_id(),
5202 // );
5203 // });
5204
5205 // // Emitting a ZoomIn event shows the panel as zoomed.
5206 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5207 // workspace.update(cx, |workspace, _| {
5208 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5209 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5210 // });
5211
5212 // // Move panel to another dock while it is zoomed
5213 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5214 // workspace.update(cx, |workspace, _| {
5215 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5216
5217 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5218 // });
5219
5220 // // If focus is transferred to another view that's not a panel or another pane, we still show
5221 // // the panel as zoomed.
5222 // let other_focus_handle = cx.update(|cx| cx.focus_handle());
5223 // cx.update(|cx| cx.focus(&other_focus_handle));
5224 // workspace.update(cx, |workspace, _| {
5225 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5226 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5227 // });
5228
5229 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5230 // workspace.update(cx, |_, cx| cx.focus_self());
5231 // workspace.update(cx, |workspace, _| {
5232 // assert_eq!(workspace.zoomed, None);
5233 // assert_eq!(workspace.zoomed_position, None);
5234 // });
5235
5236 // // If focus is transferred again to another view that's not a panel or a pane, we won't
5237 // // show the panel as zoomed because it wasn't zoomed before.
5238 // cx.update(|cx| cx.focus(&other_focus_handle));
5239 // workspace.update(cx, |workspace, _| {
5240 // assert_eq!(workspace.zoomed, None);
5241 // assert_eq!(workspace.zoomed_position, None);
5242 // });
5243
5244 // // When focus is transferred back to the panel, it is zoomed again.
5245 // panel_1.update(cx, |_, cx| cx.focus_self());
5246 // workspace.update(cx, |workspace, _| {
5247 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5248 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5249 // });
5250
5251 // // Emitting a ZoomOut event unzooms the panel.
5252 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5253 // workspace.update(cx, |workspace, _| {
5254 // assert_eq!(workspace.zoomed, None);
5255 // assert_eq!(workspace.zoomed_position, None);
5256 // });
5257
5258 // // Emit closed event on panel 1, which is active
5259 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5260
5261 // // Now the left dock is closed, because panel_1 was the active panel
5262 // workspace.update(cx, |workspace, cx| {
5263 // let right_dock = workspace.right_dock();
5264 // assert!(!right_dock.read(cx).is_open());
5265 // });
5266 // }
5267
5268 pub fn init_test(cx: &mut TestAppContext) {
5269 cx.update(|cx| {
5270 let settings_store = SettingsStore::test(cx);
5271 cx.set_global(settings_store);
5272 theme::init(theme::LoadThemes::JustBase, cx);
5273 language::init(cx);
5274 crate::init_settings(cx);
5275 Project::init_settings(cx);
5276 });
5277 }
5278}