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_read_only() {
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_read_only()
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 let app_state = Arc::new(AppState {
3270 languages: project.read(cx).languages().clone(),
3271 workspace_store,
3272 client,
3273 user_store,
3274 fs: project.read(cx).fs().clone(),
3275 build_window_options: |_, _, _| Default::default(),
3276 node_runtime: FakeNodeRuntime::new(),
3277 });
3278 let workspace = Self::new(0, project, app_state, cx);
3279 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3280 workspace
3281 }
3282
3283 // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3284 // let dock = match position {
3285 // DockPosition::Left => &self.left_dock,
3286 // DockPosition::Right => &self.right_dock,
3287 // DockPosition::Bottom => &self.bottom_dock,
3288 // };
3289 // let active_panel = dock.read(cx).visible_panel()?;
3290 // let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3291 // dock.read(cx).render_placeholder(cx)
3292 // } else {
3293 // ChildView::new(dock, cx).into_any()
3294 // };
3295
3296 // Some(
3297 // element
3298 // .constrained()
3299 // .dynamically(move |constraint, _, cx| match position {
3300 // DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3301 // Vector2F::new(20., constraint.min.y()),
3302 // Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3303 // ),
3304 // DockPosition::Bottom => SizeConstraint::new(
3305 // Vector2F::new(constraint.min.x(), 20.),
3306 // Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3307 // ),
3308 // })
3309 // .into_any(),
3310 // )
3311 // }
3312 // }
3313 pub fn register_action<A: Action>(
3314 &mut self,
3315 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3316 ) -> &mut Self {
3317 let callback = Arc::new(callback);
3318
3319 self.workspace_actions.push(Box::new(move |div, cx| {
3320 let callback = callback.clone();
3321 div.on_action(
3322 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3323 )
3324 }));
3325 self
3326 }
3327
3328 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3329 let mut div = div
3330 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3331 .on_action(cx.listener(Self::close_all_items_and_panes))
3332 .on_action(cx.listener(Self::add_folder_to_project))
3333 .on_action(cx.listener(Self::save_all))
3334 .on_action(cx.listener(Self::open));
3335 for action in self.workspace_actions.iter() {
3336 div = (action)(div, cx)
3337 }
3338 div
3339 }
3340
3341 pub fn active_modal<V: ManagedView + 'static>(
3342 &mut self,
3343 cx: &ViewContext<Self>,
3344 ) -> Option<View<V>> {
3345 self.modal_layer.read(cx).active_modal()
3346 }
3347
3348 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
3349 where
3350 B: FnOnce(&mut ViewContext<V>) -> V,
3351 {
3352 self.modal_layer
3353 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3354 }
3355}
3356
3357fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3358 let display_origin = cx
3359 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3360 .ok()??;
3361 ZED_WINDOW_POSITION
3362 .zip(*ZED_WINDOW_SIZE)
3363 .map(|(position, size)| {
3364 WindowBounds::Fixed(Bounds {
3365 origin: display_origin + position,
3366 size,
3367 })
3368 })
3369}
3370
3371fn open_items(
3372 serialized_workspace: Option<SerializedWorkspace>,
3373 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3374 app_state: Arc<AppState>,
3375 cx: &mut ViewContext<Workspace>,
3376) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3377 let restored_items = serialized_workspace.map(|serialized_workspace| {
3378 Workspace::load_workspace(
3379 serialized_workspace,
3380 project_paths_to_open
3381 .iter()
3382 .map(|(_, project_path)| project_path)
3383 .cloned()
3384 .collect(),
3385 cx,
3386 )
3387 });
3388
3389 cx.spawn(|workspace, mut cx| async move {
3390 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3391
3392 if let Some(restored_items) = restored_items {
3393 let restored_items = restored_items.await?;
3394
3395 let restored_project_paths = restored_items
3396 .iter()
3397 .filter_map(|item| {
3398 cx.update(|_, cx| item.as_ref()?.project_path(cx))
3399 .ok()
3400 .flatten()
3401 })
3402 .collect::<HashSet<_>>();
3403
3404 for restored_item in restored_items {
3405 opened_items.push(restored_item.map(Ok));
3406 }
3407
3408 project_paths_to_open
3409 .iter_mut()
3410 .for_each(|(_, project_path)| {
3411 if let Some(project_path_to_open) = project_path {
3412 if restored_project_paths.contains(project_path_to_open) {
3413 *project_path = None;
3414 }
3415 }
3416 });
3417 } else {
3418 for _ in 0..project_paths_to_open.len() {
3419 opened_items.push(None);
3420 }
3421 }
3422 assert!(opened_items.len() == project_paths_to_open.len());
3423
3424 let tasks =
3425 project_paths_to_open
3426 .into_iter()
3427 .enumerate()
3428 .map(|(i, (abs_path, project_path))| {
3429 let workspace = workspace.clone();
3430 cx.spawn(|mut cx| {
3431 let fs = app_state.fs.clone();
3432 async move {
3433 let file_project_path = project_path?;
3434 if fs.is_file(&abs_path).await {
3435 Some((
3436 i,
3437 workspace
3438 .update(&mut cx, |workspace, cx| {
3439 workspace.open_path(file_project_path, None, true, cx)
3440 })
3441 .log_err()?
3442 .await,
3443 ))
3444 } else {
3445 None
3446 }
3447 }
3448 })
3449 });
3450
3451 let tasks = tasks.collect::<Vec<_>>();
3452
3453 let tasks = futures::future::join_all(tasks.into_iter());
3454 for maybe_opened_path in tasks.await.into_iter() {
3455 if let Some((i, path_open_result)) = maybe_opened_path {
3456 opened_items[i] = Some(path_open_result);
3457 }
3458 }
3459
3460 Ok(opened_items)
3461 })
3462}
3463
3464fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3465 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3466
3467 workspace
3468 .update(cx, |workspace, cx| {
3469 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3470 workspace.show_notification_once(0, cx, |cx| {
3471 cx.new_view(|_| {
3472 MessageNotification::new("Failed to load the database file.")
3473 .with_click_message("Click to let us know about this error")
3474 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3475 })
3476 });
3477 }
3478 })
3479 .log_err();
3480}
3481
3482impl FocusableView for Workspace {
3483 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3484 self.active_pane.focus_handle(cx)
3485 }
3486}
3487
3488#[derive(Clone, Render)]
3489struct DraggedDock(DockPosition);
3490
3491impl Render for Workspace {
3492 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3493 let mut context = KeyContext::default();
3494 context.add("Workspace");
3495
3496 let (ui_font, ui_font_size) = {
3497 let theme_settings = ThemeSettings::get_global(cx);
3498 (
3499 theme_settings.ui_font.family.clone(),
3500 theme_settings.ui_font_size.clone(),
3501 )
3502 };
3503
3504 let theme = cx.theme().clone();
3505 let colors = theme.colors();
3506 cx.set_rem_size(ui_font_size);
3507
3508 self.actions(div(), cx)
3509 .key_context(context)
3510 .relative()
3511 .size_full()
3512 .flex()
3513 .flex_col()
3514 .font(ui_font)
3515 .gap_0()
3516 .justify_start()
3517 .items_start()
3518 .text_color(colors.text)
3519 .bg(colors.background)
3520 .border()
3521 .border_color(colors.border)
3522 .children(self.titlebar_item.clone())
3523 .child(
3524 div()
3525 .id("workspace")
3526 .relative()
3527 .flex_1()
3528 .w_full()
3529 .flex()
3530 .flex_col()
3531 .overflow_hidden()
3532 .border_t()
3533 .border_b()
3534 .border_color(colors.border)
3535 .child(
3536 canvas(cx.listener(|workspace, bounds, _| {
3537 workspace.bounds = *bounds;
3538 }))
3539 .absolute()
3540 .size_full(),
3541 )
3542 .on_drag_move(
3543 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3544 match e.drag(cx).0 {
3545 DockPosition::Left => {
3546 let size = workspace.bounds.left() + e.event.position.x;
3547 workspace.left_dock.update(cx, |left_dock, cx| {
3548 left_dock.resize_active_panel(Some(size), cx);
3549 });
3550 }
3551 DockPosition::Right => {
3552 let size = workspace.bounds.right() - e.event.position.x;
3553 workspace.right_dock.update(cx, |right_dock, cx| {
3554 right_dock.resize_active_panel(Some(size), cx);
3555 });
3556 }
3557 DockPosition::Bottom => {
3558 let size = workspace.bounds.bottom() - e.event.position.y;
3559 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3560 bottom_dock.resize_active_panel(Some(size), cx);
3561 });
3562 }
3563 }
3564 }),
3565 )
3566 .child(self.modal_layer.clone())
3567 .child(
3568 div()
3569 .flex()
3570 .flex_row()
3571 .h_full()
3572 // Left Dock
3573 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3574 || {
3575 div()
3576 .flex()
3577 .flex_none()
3578 .overflow_hidden()
3579 .child(self.left_dock.clone())
3580 },
3581 ))
3582 // Panes
3583 .child(
3584 div()
3585 .flex()
3586 .flex_col()
3587 .flex_1()
3588 .overflow_hidden()
3589 .child(self.center.render(
3590 &self.project,
3591 &self.follower_states,
3592 self.active_call(),
3593 &self.active_pane,
3594 self.zoomed.as_ref(),
3595 &self.app_state,
3596 cx,
3597 ))
3598 .children(
3599 self.zoomed_position
3600 .ne(&Some(DockPosition::Bottom))
3601 .then(|| self.bottom_dock.clone()),
3602 ),
3603 )
3604 // Right Dock
3605 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3606 || {
3607 div()
3608 .flex()
3609 .flex_none()
3610 .overflow_hidden()
3611 .child(self.right_dock.clone())
3612 },
3613 )),
3614 )
3615 .children(self.render_notifications(cx))
3616 .children(self.zoomed.as_ref().and_then(|view| {
3617 let zoomed_view = view.upgrade()?;
3618 let div = div()
3619 .z_index(1)
3620 .absolute()
3621 .overflow_hidden()
3622 .border_color(colors.border)
3623 .bg(colors.background)
3624 .child(zoomed_view)
3625 .inset_0()
3626 .shadow_lg();
3627
3628 Some(match self.zoomed_position {
3629 Some(DockPosition::Left) => div.right_2().border_r(),
3630 Some(DockPosition::Right) => div.left_2().border_l(),
3631 Some(DockPosition::Bottom) => div.top_2().border_t(),
3632 None => div.top_2().bottom_2().left_2().right_2().border(),
3633 })
3634 })),
3635 )
3636 .child(self.status_bar.clone())
3637 .children(if self.project.read(cx).is_read_only() {
3638 Some(DisconnectedOverlay)
3639 } else {
3640 None
3641 })
3642 }
3643}
3644
3645impl WorkspaceStore {
3646 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3647 Self {
3648 workspaces: Default::default(),
3649 followers: Default::default(),
3650 _subscriptions: vec![
3651 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3652 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3653 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3654 ],
3655 client,
3656 }
3657 }
3658
3659 pub fn update_followers(
3660 &self,
3661 project_id: Option<u64>,
3662 update: proto::update_followers::Variant,
3663 cx: &AppContext,
3664 ) -> Option<()> {
3665 if !cx.has_global::<Model<ActiveCall>>() {
3666 return None;
3667 }
3668
3669 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3670 let follower_ids: Vec<_> = self
3671 .followers
3672 .iter()
3673 .filter_map(|follower| {
3674 if follower.project_id == project_id || project_id.is_none() {
3675 Some(follower.peer_id.into())
3676 } else {
3677 None
3678 }
3679 })
3680 .collect();
3681 if follower_ids.is_empty() {
3682 return None;
3683 }
3684 self.client
3685 .send(proto::UpdateFollowers {
3686 room_id,
3687 project_id,
3688 follower_ids,
3689 variant: Some(update),
3690 })
3691 .log_err()
3692 }
3693
3694 pub async fn handle_follow(
3695 this: Model<Self>,
3696 envelope: TypedEnvelope<proto::Follow>,
3697 _: Arc<Client>,
3698 mut cx: AsyncAppContext,
3699 ) -> Result<proto::FollowResponse> {
3700 this.update(&mut cx, |this, cx| {
3701 let follower = Follower {
3702 project_id: envelope.payload.project_id,
3703 peer_id: envelope.original_sender_id()?,
3704 };
3705 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3706
3707 let mut response = proto::FollowResponse::default();
3708 this.workspaces.retain(|workspace| {
3709 workspace
3710 .update(cx, |workspace, cx| {
3711 let handler_response = workspace.handle_follow(follower.project_id, cx);
3712 if response.views.is_empty() {
3713 response.views = handler_response.views;
3714 } else {
3715 response.views.extend_from_slice(&handler_response.views);
3716 }
3717
3718 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3719 if response.active_view_id.is_none()
3720 || Some(workspace.project.downgrade()) == active_project
3721 {
3722 response.active_view_id = Some(active_view_id);
3723 }
3724 }
3725 })
3726 .is_ok()
3727 });
3728
3729 if let Err(ix) = this.followers.binary_search(&follower) {
3730 this.followers.insert(ix, follower);
3731 }
3732
3733 Ok(response)
3734 })?
3735 }
3736
3737 async fn handle_unfollow(
3738 model: Model<Self>,
3739 envelope: TypedEnvelope<proto::Unfollow>,
3740 _: Arc<Client>,
3741 mut cx: AsyncAppContext,
3742 ) -> Result<()> {
3743 model.update(&mut cx, |this, _| {
3744 let follower = Follower {
3745 project_id: envelope.payload.project_id,
3746 peer_id: envelope.original_sender_id()?,
3747 };
3748 if let Ok(ix) = this.followers.binary_search(&follower) {
3749 this.followers.remove(ix);
3750 }
3751 Ok(())
3752 })?
3753 }
3754
3755 async fn handle_update_followers(
3756 this: Model<Self>,
3757 envelope: TypedEnvelope<proto::UpdateFollowers>,
3758 _: Arc<Client>,
3759 mut cx: AsyncAppContext,
3760 ) -> Result<()> {
3761 let leader_id = envelope.original_sender_id()?;
3762 let update = envelope.payload;
3763
3764 this.update(&mut cx, |this, cx| {
3765 this.workspaces.retain(|workspace| {
3766 workspace
3767 .update(cx, |workspace, cx| {
3768 let project_id = workspace.project.read(cx).remote_id();
3769 if update.project_id != project_id && update.project_id.is_some() {
3770 return;
3771 }
3772 workspace.handle_update_followers(leader_id, update.clone(), cx);
3773 })
3774 .is_ok()
3775 });
3776 Ok(())
3777 })?
3778 }
3779}
3780
3781impl ViewId {
3782 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3783 Ok(Self {
3784 creator: message
3785 .creator
3786 .ok_or_else(|| anyhow!("creator is missing"))?,
3787 id: message.id,
3788 })
3789 }
3790
3791 pub(crate) fn to_proto(&self) -> proto::ViewId {
3792 proto::ViewId {
3793 creator: Some(self.creator),
3794 id: self.id,
3795 }
3796 }
3797}
3798
3799pub trait WorkspaceHandle {
3800 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3801}
3802
3803impl WorkspaceHandle for View<Workspace> {
3804 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3805 self.read(cx)
3806 .worktrees(cx)
3807 .flat_map(|worktree| {
3808 let worktree_id = worktree.read(cx).id();
3809 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3810 worktree_id,
3811 path: f.path.clone(),
3812 })
3813 })
3814 .collect::<Vec<_>>()
3815 }
3816}
3817
3818impl std::fmt::Debug for OpenPaths {
3819 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3820 f.debug_struct("OpenPaths")
3821 .field("paths", &self.paths)
3822 .finish()
3823 }
3824}
3825
3826pub fn activate_workspace_for_project(
3827 cx: &mut AppContext,
3828 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3829) -> Option<WindowHandle<Workspace>> {
3830 for window in cx.windows() {
3831 let Some(workspace) = window.downcast::<Workspace>() else {
3832 continue;
3833 };
3834
3835 let predicate = workspace
3836 .update(cx, |workspace, cx| {
3837 let project = workspace.project.read(cx);
3838 if predicate(project, cx) {
3839 cx.activate_window();
3840 true
3841 } else {
3842 false
3843 }
3844 })
3845 .log_err()
3846 .unwrap_or(false);
3847
3848 if predicate {
3849 return Some(workspace);
3850 }
3851 }
3852
3853 None
3854}
3855
3856pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3857 DB.last_workspace().await.log_err().flatten()
3858}
3859
3860async fn join_channel_internal(
3861 channel_id: u64,
3862 app_state: &Arc<AppState>,
3863 requesting_window: Option<WindowHandle<Workspace>>,
3864 active_call: &Model<ActiveCall>,
3865 cx: &mut AsyncAppContext,
3866) -> Result<bool> {
3867 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3868 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3869 return (false, None);
3870 };
3871
3872 let already_in_channel = room.channel_id() == Some(channel_id);
3873 let should_prompt = room.is_sharing_project()
3874 && room.remote_participants().len() > 0
3875 && !already_in_channel;
3876 let open_room = if already_in_channel {
3877 active_call.room().cloned()
3878 } else {
3879 None
3880 };
3881 (should_prompt, open_room)
3882 })?;
3883
3884 if let Some(room) = open_room {
3885 let task = room.update(cx, |room, cx| {
3886 if let Some((project, host)) = room.most_active_project(cx) {
3887 return Some(join_remote_project(project, host, app_state.clone(), cx));
3888 }
3889
3890 None
3891 })?;
3892 if let Some(task) = task {
3893 task.await?;
3894 }
3895 return anyhow::Ok(true);
3896 }
3897
3898 if should_prompt {
3899 if let Some(workspace) = requesting_window {
3900 let answer = workspace.update(cx, |_, cx| {
3901 cx.prompt(
3902 PromptLevel::Warning,
3903 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
3904 &["Yes, Join Channel", "Cancel"],
3905 )
3906 })?.await;
3907
3908 if answer == Ok(1) {
3909 return Ok(false);
3910 }
3911 } else {
3912 return Ok(false); // unreachable!() hopefully
3913 }
3914 }
3915
3916 let client = cx.update(|cx| active_call.read(cx).client())?;
3917
3918 let mut client_status = client.status();
3919
3920 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
3921 'outer: loop {
3922 let Some(status) = client_status.recv().await else {
3923 return Err(anyhow!("error connecting"));
3924 };
3925
3926 match status {
3927 Status::Connecting
3928 | Status::Authenticating
3929 | Status::Reconnecting
3930 | Status::Reauthenticating => continue,
3931 Status::Connected { .. } => break 'outer,
3932 Status::SignedOut => return Err(anyhow!("not signed in")),
3933 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
3934 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
3935 return Err(anyhow!("zed is offline"))
3936 }
3937 }
3938 }
3939
3940 let room = active_call
3941 .update(cx, |active_call, cx| {
3942 active_call.join_channel(channel_id, cx)
3943 })?
3944 .await?;
3945
3946 let Some(room) = room else {
3947 return anyhow::Ok(true);
3948 };
3949
3950 room.update(cx, |room, _| room.room_update_completed())?
3951 .await;
3952
3953 let task = room.update(cx, |room, cx| {
3954 if let Some((project, host)) = room.most_active_project(cx) {
3955 return Some(join_remote_project(project, host, app_state.clone(), cx));
3956 }
3957
3958 None
3959 })?;
3960 if let Some(task) = task {
3961 task.await?;
3962 return anyhow::Ok(true);
3963 }
3964 anyhow::Ok(false)
3965}
3966
3967pub fn join_channel(
3968 channel_id: u64,
3969 app_state: Arc<AppState>,
3970 requesting_window: Option<WindowHandle<Workspace>>,
3971 cx: &mut AppContext,
3972) -> Task<Result<()>> {
3973 let active_call = ActiveCall::global(cx);
3974 cx.spawn(|mut cx| async move {
3975 let result = join_channel_internal(
3976 channel_id,
3977 &app_state,
3978 requesting_window,
3979 &active_call,
3980 &mut cx,
3981 )
3982 .await;
3983
3984 // join channel succeeded, and opened a window
3985 if matches!(result, Ok(true)) {
3986 return anyhow::Ok(());
3987 }
3988
3989 if requesting_window.is_some() {
3990 return anyhow::Ok(());
3991 }
3992
3993 // find an existing workspace to focus and show call controls
3994 let mut active_window = activate_any_workspace_window(&mut cx);
3995 if active_window.is_none() {
3996 // no open workspaces, make one to show the error in (blergh)
3997 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
3998 .await?;
3999 }
4000
4001 active_window = activate_any_workspace_window(&mut cx);
4002 let Some(active_window) = active_window else {
4003 return anyhow::Ok(());
4004 };
4005
4006 if let Err(err) = result {
4007 active_window
4008 .update(&mut cx, |_, cx| {
4009 cx.prompt(
4010 PromptLevel::Critical,
4011 &format!("Failed to join channel: {}", err),
4012 &["Ok"],
4013 )
4014 })?
4015 .await
4016 .ok();
4017 }
4018
4019 // return ok, we showed the error to the user.
4020 return anyhow::Ok(());
4021 })
4022}
4023
4024pub async fn get_any_active_workspace(
4025 app_state: Arc<AppState>,
4026 mut cx: AsyncAppContext,
4027) -> anyhow::Result<WindowHandle<Workspace>> {
4028 // find an existing workspace to focus and show call controls
4029 let active_window = activate_any_workspace_window(&mut cx);
4030 if active_window.is_none() {
4031 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4032 .await?;
4033 }
4034 activate_any_workspace_window(&mut cx)
4035 .context("could not open zed")?
4036 .downcast::<Workspace>()
4037 .context("could not open zed workspace window")
4038}
4039
4040fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
4041 cx.update(|cx| {
4042 for window in cx.windows() {
4043 let is_workspace = window.downcast::<Workspace>().is_some();
4044 if is_workspace {
4045 window.update(cx, |_, cx| cx.activate_window()).ok();
4046 return Some(window);
4047 }
4048 }
4049 None
4050 })
4051 .ok()
4052 .flatten()
4053}
4054
4055#[allow(clippy::type_complexity)]
4056pub fn open_paths(
4057 abs_paths: &[PathBuf],
4058 app_state: &Arc<AppState>,
4059 requesting_window: Option<WindowHandle<Workspace>>,
4060 cx: &mut AppContext,
4061) -> Task<
4062 anyhow::Result<(
4063 WindowHandle<Workspace>,
4064 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4065 )>,
4066> {
4067 let app_state = app_state.clone();
4068 let abs_paths = abs_paths.to_vec();
4069 // Open paths in existing workspace if possible
4070 let existing = activate_workspace_for_project(cx, {
4071 let abs_paths = abs_paths.clone();
4072 move |project, cx| project.contains_paths(&abs_paths, cx)
4073 });
4074 cx.spawn(move |mut cx| async move {
4075 if let Some(existing) = existing {
4076 Ok((
4077 existing.clone(),
4078 existing
4079 .update(&mut cx, |workspace, cx| {
4080 workspace.open_paths(abs_paths, true, cx)
4081 })?
4082 .await,
4083 ))
4084 } else {
4085 cx.update(move |cx| {
4086 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4087 })?
4088 .await
4089 }
4090 })
4091}
4092
4093pub fn open_new(
4094 app_state: &Arc<AppState>,
4095 cx: &mut AppContext,
4096 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4097) -> Task<()> {
4098 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4099 cx.spawn(|mut cx| async move {
4100 if let Some((workspace, opened_paths)) = task.await.log_err() {
4101 workspace
4102 .update(&mut cx, |workspace, cx| {
4103 if opened_paths.is_empty() {
4104 init(workspace, cx)
4105 }
4106 })
4107 .log_err();
4108 }
4109 })
4110}
4111
4112pub fn create_and_open_local_file(
4113 path: &'static Path,
4114 cx: &mut ViewContext<Workspace>,
4115 default_content: impl 'static + Send + FnOnce() -> Rope,
4116) -> Task<Result<Box<dyn ItemHandle>>> {
4117 cx.spawn(|workspace, mut cx| async move {
4118 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4119 if !fs.is_file(path).await {
4120 fs.create_file(path, Default::default()).await?;
4121 fs.save(path, &default_content(), Default::default())
4122 .await?;
4123 }
4124
4125 let mut items = workspace
4126 .update(&mut cx, |workspace, cx| {
4127 workspace.with_local_workspace(cx, |workspace, cx| {
4128 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4129 })
4130 })?
4131 .await?
4132 .await;
4133
4134 let item = items.pop().flatten();
4135 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4136 })
4137}
4138
4139pub fn join_remote_project(
4140 project_id: u64,
4141 follow_user_id: u64,
4142 app_state: Arc<AppState>,
4143 cx: &mut AppContext,
4144) -> Task<Result<()>> {
4145 let windows = cx.windows();
4146 cx.spawn(|mut cx| async move {
4147 let existing_workspace = windows.into_iter().find_map(|window| {
4148 window.downcast::<Workspace>().and_then(|window| {
4149 window
4150 .update(&mut cx, |workspace, cx| {
4151 if workspace.project().read(cx).remote_id() == Some(project_id) {
4152 Some(window)
4153 } else {
4154 None
4155 }
4156 })
4157 .unwrap_or(None)
4158 })
4159 });
4160
4161 let workspace = if let Some(existing_workspace) = existing_workspace {
4162 existing_workspace
4163 } else {
4164 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4165 let room = active_call
4166 .read_with(&cx, |call, _| call.room().cloned())?
4167 .ok_or_else(|| anyhow!("not in a call"))?;
4168 let project = room
4169 .update(&mut cx, |room, cx| {
4170 room.join_project(
4171 project_id,
4172 app_state.languages.clone(),
4173 app_state.fs.clone(),
4174 cx,
4175 )
4176 })?
4177 .await?;
4178
4179 let window_bounds_override = window_bounds_env_override(&cx);
4180 cx.update(|cx| {
4181 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4182 cx.open_window(options, |cx| {
4183 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4184 })
4185 })?
4186 };
4187
4188 workspace.update(&mut cx, |workspace, cx| {
4189 cx.activate(true);
4190 cx.activate_window();
4191
4192 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4193 let follow_peer_id = room
4194 .read(cx)
4195 .remote_participants()
4196 .iter()
4197 .find(|(_, participant)| participant.user.id == follow_user_id)
4198 .map(|(_, p)| p.peer_id)
4199 .or_else(|| {
4200 // If we couldn't follow the given user, follow the host instead.
4201 let collaborator = workspace
4202 .project()
4203 .read(cx)
4204 .collaborators()
4205 .values()
4206 .find(|collaborator| collaborator.replica_id == 0)?;
4207 Some(collaborator.peer_id)
4208 });
4209
4210 if let Some(follow_peer_id) = follow_peer_id {
4211 workspace.follow(follow_peer_id, cx);
4212 }
4213 }
4214 })?;
4215
4216 anyhow::Ok(())
4217 })
4218}
4219
4220pub fn restart(_: &Restart, cx: &mut AppContext) {
4221 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4222 let mut workspace_windows = cx
4223 .windows()
4224 .into_iter()
4225 .filter_map(|window| window.downcast::<Workspace>())
4226 .collect::<Vec<_>>();
4227
4228 // If multiple windows have unsaved changes, and need a save prompt,
4229 // prompt in the active window before switching to a different window.
4230 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4231
4232 let mut prompt = None;
4233 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4234 prompt = window
4235 .update(cx, |_, cx| {
4236 cx.prompt(
4237 PromptLevel::Info,
4238 "Are you sure you want to restart?",
4239 &["Restart", "Cancel"],
4240 )
4241 })
4242 .ok();
4243 }
4244
4245 cx.spawn(|mut cx| async move {
4246 if let Some(prompt) = prompt {
4247 let answer = prompt.await?;
4248 if answer != 0 {
4249 return Ok(());
4250 }
4251 }
4252
4253 // If the user cancels any save prompt, then keep the app open.
4254 for window in workspace_windows {
4255 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4256 workspace.prepare_to_close(true, cx)
4257 }) {
4258 if !should_close.await? {
4259 return Ok(());
4260 }
4261 }
4262 }
4263
4264 cx.update(|cx| cx.restart())
4265 })
4266 .detach_and_log_err(cx);
4267}
4268
4269fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4270 let mut parts = value.split(',');
4271 let x: usize = parts.next()?.parse().ok()?;
4272 let y: usize = parts.next()?.parse().ok()?;
4273 Some(point((x as f64).into(), (y as f64).into()))
4274}
4275
4276fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4277 let mut parts = value.split(',');
4278 let width: usize = parts.next()?.parse().ok()?;
4279 let height: usize = parts.next()?.parse().ok()?;
4280 Some(size((width as f64).into(), (height as f64).into()))
4281}
4282
4283struct DisconnectedOverlay;
4284
4285impl Element for DisconnectedOverlay {
4286 type State = AnyElement;
4287
4288 fn request_layout(
4289 &mut self,
4290 _: Option<Self::State>,
4291 cx: &mut WindowContext,
4292 ) -> (LayoutId, Self::State) {
4293 let mut background = cx.theme().colors().elevated_surface_background;
4294 background.fade_out(0.2);
4295 let mut overlay = div()
4296 .bg(background)
4297 .absolute()
4298 .left_0()
4299 .top_0()
4300 .size_full()
4301 .flex()
4302 .items_center()
4303 .justify_center()
4304 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4305 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4306 .child(Label::new(
4307 "Your connection to the remote project has been lost.",
4308 ))
4309 .into_any();
4310 (overlay.request_layout(cx), overlay)
4311 }
4312
4313 fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
4314 cx.with_z_index(u8::MAX, |cx| {
4315 cx.add_opaque_layer(bounds);
4316 overlay.paint(cx);
4317 })
4318 }
4319}
4320
4321impl IntoElement for DisconnectedOverlay {
4322 type Element = Self;
4323
4324 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4325 None
4326 }
4327
4328 fn into_element(self) -> Self::Element {
4329 self
4330 }
4331}
4332
4333#[cfg(test)]
4334mod tests {
4335 use std::{cell::RefCell, rc::Rc};
4336
4337 use super::*;
4338 use crate::item::{
4339 test::{TestItem, TestProjectItem},
4340 ItemEvent,
4341 };
4342 use fs::FakeFs;
4343 use gpui::TestAppContext;
4344 use project::{Project, ProjectEntryId};
4345 use serde_json::json;
4346 use settings::SettingsStore;
4347
4348 #[gpui::test]
4349 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4350 init_test(cx);
4351
4352 let fs = FakeFs::new(cx.executor());
4353 let project = Project::test(fs, [], cx).await;
4354 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4355
4356 // Adding an item with no ambiguity renders the tab without detail.
4357 let item1 = cx.new_view(|cx| {
4358 let mut item = TestItem::new(cx);
4359 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4360 item
4361 });
4362 workspace.update(cx, |workspace, cx| {
4363 workspace.add_item(Box::new(item1.clone()), cx);
4364 });
4365 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4366
4367 // Adding an item that creates ambiguity increases the level of detail on
4368 // both tabs.
4369 let item2 = cx.new_view(|cx| {
4370 let mut item = TestItem::new(cx);
4371 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4372 item
4373 });
4374 workspace.update(cx, |workspace, cx| {
4375 workspace.add_item(Box::new(item2.clone()), cx);
4376 });
4377 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4378 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4379
4380 // Adding an item that creates ambiguity increases the level of detail only
4381 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4382 // we stop at the highest detail available.
4383 let item3 = cx.new_view(|cx| {
4384 let mut item = TestItem::new(cx);
4385 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4386 item
4387 });
4388 workspace.update(cx, |workspace, cx| {
4389 workspace.add_item(Box::new(item3.clone()), cx);
4390 });
4391 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4392 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4393 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4394 }
4395
4396 #[gpui::test]
4397 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4398 init_test(cx);
4399
4400 let fs = FakeFs::new(cx.executor());
4401 fs.insert_tree(
4402 "/root1",
4403 json!({
4404 "one.txt": "",
4405 "two.txt": "",
4406 }),
4407 )
4408 .await;
4409 fs.insert_tree(
4410 "/root2",
4411 json!({
4412 "three.txt": "",
4413 }),
4414 )
4415 .await;
4416
4417 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4418 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4419 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4420 let worktree_id = project.update(cx, |project, cx| {
4421 project.worktrees().next().unwrap().read(cx).id()
4422 });
4423
4424 let item1 = cx.new_view(|cx| {
4425 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4426 });
4427 let item2 = cx.new_view(|cx| {
4428 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4429 });
4430
4431 // Add an item to an empty pane
4432 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4433 project.update(cx, |project, cx| {
4434 assert_eq!(
4435 project.active_entry(),
4436 project
4437 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4438 .map(|e| e.id)
4439 );
4440 });
4441 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4442
4443 // Add a second item to a non-empty pane
4444 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4445 assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
4446 project.update(cx, |project, cx| {
4447 assert_eq!(
4448 project.active_entry(),
4449 project
4450 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4451 .map(|e| e.id)
4452 );
4453 });
4454
4455 // Close the active item
4456 pane.update(cx, |pane, cx| {
4457 pane.close_active_item(&Default::default(), cx).unwrap()
4458 })
4459 .await
4460 .unwrap();
4461 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4462 project.update(cx, |project, cx| {
4463 assert_eq!(
4464 project.active_entry(),
4465 project
4466 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4467 .map(|e| e.id)
4468 );
4469 });
4470
4471 // Add a project folder
4472 project
4473 .update(cx, |project, cx| {
4474 project.find_or_create_local_worktree("/root2", true, cx)
4475 })
4476 .await
4477 .unwrap();
4478 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
4479
4480 // Remove a project folder
4481 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4482 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
4483 }
4484
4485 #[gpui::test]
4486 async fn test_close_window(cx: &mut TestAppContext) {
4487 init_test(cx);
4488
4489 let fs = FakeFs::new(cx.executor());
4490 fs.insert_tree("/root", json!({ "one": "" })).await;
4491
4492 let project = Project::test(fs, ["root".as_ref()], cx).await;
4493 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4494
4495 // When there are no dirty items, there's nothing to do.
4496 let item1 = cx.new_view(|cx| TestItem::new(cx));
4497 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4498 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4499 assert!(task.await.unwrap());
4500
4501 // When there are dirty untitled items, prompt to save each one. If the user
4502 // cancels any prompt, then abort.
4503 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4504 let item3 = cx.new_view(|cx| {
4505 TestItem::new(cx)
4506 .with_dirty(true)
4507 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4508 });
4509 workspace.update(cx, |w, cx| {
4510 w.add_item(Box::new(item2.clone()), cx);
4511 w.add_item(Box::new(item3.clone()), cx);
4512 });
4513 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4514 cx.executor().run_until_parked();
4515 cx.simulate_prompt_answer(2); // cancel save all
4516 cx.executor().run_until_parked();
4517 cx.simulate_prompt_answer(2); // cancel save all
4518 cx.executor().run_until_parked();
4519 assert!(!cx.has_pending_prompt());
4520 assert!(!task.await.unwrap());
4521 }
4522
4523 #[gpui::test]
4524 async fn test_close_pane_items(cx: &mut TestAppContext) {
4525 init_test(cx);
4526
4527 let fs = FakeFs::new(cx.executor());
4528
4529 let project = Project::test(fs, None, cx).await;
4530 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4531
4532 let item1 = cx.new_view(|cx| {
4533 TestItem::new(cx)
4534 .with_dirty(true)
4535 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4536 });
4537 let item2 = cx.new_view(|cx| {
4538 TestItem::new(cx)
4539 .with_dirty(true)
4540 .with_conflict(true)
4541 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4542 });
4543 let item3 = cx.new_view(|cx| {
4544 TestItem::new(cx)
4545 .with_dirty(true)
4546 .with_conflict(true)
4547 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4548 });
4549 let item4 = cx.new_view(|cx| {
4550 TestItem::new(cx)
4551 .with_dirty(true)
4552 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4553 });
4554 let pane = workspace.update(cx, |workspace, cx| {
4555 workspace.add_item(Box::new(item1.clone()), cx);
4556 workspace.add_item(Box::new(item2.clone()), cx);
4557 workspace.add_item(Box::new(item3.clone()), cx);
4558 workspace.add_item(Box::new(item4.clone()), cx);
4559 workspace.active_pane().clone()
4560 });
4561
4562 let close_items = pane.update(cx, |pane, cx| {
4563 pane.activate_item(1, true, true, cx);
4564 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4565 let item1_id = item1.item_id();
4566 let item3_id = item3.item_id();
4567 let item4_id = item4.item_id();
4568 pane.close_items(cx, SaveIntent::Close, move |id| {
4569 [item1_id, item3_id, item4_id].contains(&id)
4570 })
4571 });
4572 cx.executor().run_until_parked();
4573
4574 assert!(cx.has_pending_prompt());
4575 // Ignore "Save all" prompt
4576 cx.simulate_prompt_answer(2);
4577 cx.executor().run_until_parked();
4578 // There's a prompt to save item 1.
4579 pane.update(cx, |pane, _| {
4580 assert_eq!(pane.items_len(), 4);
4581 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4582 });
4583 // Confirm saving item 1.
4584 cx.simulate_prompt_answer(0);
4585 cx.executor().run_until_parked();
4586
4587 // Item 1 is saved. There's a prompt to save item 3.
4588 pane.update(cx, |pane, cx| {
4589 assert_eq!(item1.read(cx).save_count, 1);
4590 assert_eq!(item1.read(cx).save_as_count, 0);
4591 assert_eq!(item1.read(cx).reload_count, 0);
4592 assert_eq!(pane.items_len(), 3);
4593 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4594 });
4595 assert!(cx.has_pending_prompt());
4596
4597 // Cancel saving item 3.
4598 cx.simulate_prompt_answer(1);
4599 cx.executor().run_until_parked();
4600
4601 // Item 3 is reloaded. There's a prompt to save item 4.
4602 pane.update(cx, |pane, cx| {
4603 assert_eq!(item3.read(cx).save_count, 0);
4604 assert_eq!(item3.read(cx).save_as_count, 0);
4605 assert_eq!(item3.read(cx).reload_count, 1);
4606 assert_eq!(pane.items_len(), 2);
4607 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4608 });
4609 assert!(cx.has_pending_prompt());
4610
4611 // Confirm saving item 4.
4612 cx.simulate_prompt_answer(0);
4613 cx.executor().run_until_parked();
4614
4615 // There's a prompt for a path for item 4.
4616 cx.simulate_new_path_selection(|_| Some(Default::default()));
4617 close_items.await.unwrap();
4618
4619 // The requested items are closed.
4620 pane.update(cx, |pane, cx| {
4621 assert_eq!(item4.read(cx).save_count, 0);
4622 assert_eq!(item4.read(cx).save_as_count, 1);
4623 assert_eq!(item4.read(cx).reload_count, 0);
4624 assert_eq!(pane.items_len(), 1);
4625 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4626 });
4627 }
4628
4629 #[gpui::test]
4630 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4631 init_test(cx);
4632
4633 let fs = FakeFs::new(cx.executor());
4634 let project = Project::test(fs, [], cx).await;
4635 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4636
4637 // Create several workspace items with single project entries, and two
4638 // workspace items with multiple project entries.
4639 let single_entry_items = (0..=4)
4640 .map(|project_entry_id| {
4641 cx.new_view(|cx| {
4642 TestItem::new(cx)
4643 .with_dirty(true)
4644 .with_project_items(&[TestProjectItem::new(
4645 project_entry_id,
4646 &format!("{project_entry_id}.txt"),
4647 cx,
4648 )])
4649 })
4650 })
4651 .collect::<Vec<_>>();
4652 let item_2_3 = cx.new_view(|cx| {
4653 TestItem::new(cx)
4654 .with_dirty(true)
4655 .with_singleton(false)
4656 .with_project_items(&[
4657 single_entry_items[2].read(cx).project_items[0].clone(),
4658 single_entry_items[3].read(cx).project_items[0].clone(),
4659 ])
4660 });
4661 let item_3_4 = cx.new_view(|cx| {
4662 TestItem::new(cx)
4663 .with_dirty(true)
4664 .with_singleton(false)
4665 .with_project_items(&[
4666 single_entry_items[3].read(cx).project_items[0].clone(),
4667 single_entry_items[4].read(cx).project_items[0].clone(),
4668 ])
4669 });
4670
4671 // Create two panes that contain the following project entries:
4672 // left pane:
4673 // multi-entry items: (2, 3)
4674 // single-entry items: 0, 1, 2, 3, 4
4675 // right pane:
4676 // single-entry items: 1
4677 // multi-entry items: (3, 4)
4678 let left_pane = workspace.update(cx, |workspace, cx| {
4679 let left_pane = workspace.active_pane().clone();
4680 workspace.add_item(Box::new(item_2_3.clone()), cx);
4681 for item in single_entry_items {
4682 workspace.add_item(Box::new(item), cx);
4683 }
4684 left_pane.update(cx, |pane, cx| {
4685 pane.activate_item(2, true, true, cx);
4686 });
4687
4688 let right_pane = workspace
4689 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4690 .unwrap();
4691
4692 right_pane.update(cx, |pane, cx| {
4693 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4694 });
4695
4696 left_pane
4697 });
4698
4699 cx.focus_view(&left_pane);
4700
4701 // When closing all of the items in the left pane, we should be prompted twice:
4702 // once for project entry 0, and once for project entry 2. Project entries 1,
4703 // 3, and 4 are all still open in the other paten. After those two
4704 // prompts, the task should complete.
4705
4706 let close = left_pane.update(cx, |pane, cx| {
4707 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4708 });
4709 cx.executor().run_until_parked();
4710
4711 // Discard "Save all" prompt
4712 cx.simulate_prompt_answer(2);
4713
4714 cx.executor().run_until_parked();
4715 left_pane.update(cx, |pane, cx| {
4716 assert_eq!(
4717 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4718 &[ProjectEntryId::from_proto(0)]
4719 );
4720 });
4721 cx.simulate_prompt_answer(0);
4722
4723 cx.executor().run_until_parked();
4724 left_pane.update(cx, |pane, cx| {
4725 assert_eq!(
4726 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4727 &[ProjectEntryId::from_proto(2)]
4728 );
4729 });
4730 cx.simulate_prompt_answer(0);
4731
4732 cx.executor().run_until_parked();
4733 close.await.unwrap();
4734 left_pane.update(cx, |pane, _| {
4735 assert_eq!(pane.items_len(), 0);
4736 });
4737 }
4738
4739 #[gpui::test]
4740 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4741 init_test(cx);
4742
4743 let fs = FakeFs::new(cx.executor());
4744 let project = Project::test(fs, [], cx).await;
4745 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4746 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4747
4748 let item = cx.new_view(|cx| {
4749 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4750 });
4751 let item_id = item.entity_id();
4752 workspace.update(cx, |workspace, cx| {
4753 workspace.add_item(Box::new(item.clone()), cx);
4754 });
4755
4756 // Autosave on window change.
4757 item.update(cx, |item, cx| {
4758 cx.update_global(|settings: &mut SettingsStore, cx| {
4759 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4760 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4761 })
4762 });
4763 item.is_dirty = true;
4764 });
4765
4766 // Deactivating the window saves the file.
4767 cx.simulate_deactivation();
4768 cx.executor().run_until_parked();
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.simulate_activation();
4789 item.update(cx, |item, cx| {
4790 cx.focus_self();
4791 item.is_dirty = true;
4792 });
4793 cx.simulate_deactivation();
4794
4795 cx.executor().run_until_parked();
4796 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4797
4798 // Autosave after delay.
4799 item.update(cx, |item, cx| {
4800 cx.update_global(|settings: &mut SettingsStore, cx| {
4801 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4802 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4803 })
4804 });
4805 item.is_dirty = true;
4806 cx.emit(ItemEvent::Edit);
4807 });
4808
4809 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4810 cx.executor().advance_clock(Duration::from_millis(250));
4811 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4812
4813 // After delay expires, the file is saved.
4814 cx.executor().advance_clock(Duration::from_millis(250));
4815 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4816
4817 // Autosave on focus change, ensuring closing the tab counts as such.
4818 item.update(cx, |item, cx| {
4819 cx.update_global(|settings: &mut SettingsStore, cx| {
4820 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4821 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4822 })
4823 });
4824 item.is_dirty = true;
4825 });
4826
4827 pane.update(cx, |pane, cx| {
4828 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4829 })
4830 .await
4831 .unwrap();
4832 assert!(!cx.has_pending_prompt());
4833 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4834
4835 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4836 workspace.update(cx, |workspace, cx| {
4837 workspace.add_item(Box::new(item.clone()), cx);
4838 });
4839 item.update(cx, |item, cx| {
4840 item.project_items[0].update(cx, |item, _| {
4841 item.entry_id = None;
4842 });
4843 item.is_dirty = true;
4844 cx.blur();
4845 });
4846 cx.run_until_parked();
4847 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4848
4849 // Ensure autosave is prevented for deleted files also when closing the buffer.
4850 let _close_items = pane.update(cx, |pane, cx| {
4851 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4852 });
4853 cx.run_until_parked();
4854 assert!(cx.has_pending_prompt());
4855 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4856 }
4857
4858 #[gpui::test]
4859 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4860 init_test(cx);
4861
4862 let fs = FakeFs::new(cx.executor());
4863
4864 let project = Project::test(fs, [], cx).await;
4865 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4866
4867 let item = cx.new_view(|cx| {
4868 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4869 });
4870 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4871 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4872 let toolbar_notify_count = Rc::new(RefCell::new(0));
4873
4874 workspace.update(cx, |workspace, cx| {
4875 workspace.add_item(Box::new(item.clone()), cx);
4876 let toolbar_notification_count = toolbar_notify_count.clone();
4877 cx.observe(&toolbar, move |_, _, _| {
4878 *toolbar_notification_count.borrow_mut() += 1
4879 })
4880 .detach();
4881 });
4882
4883 pane.update(cx, |pane, _| {
4884 assert!(!pane.can_navigate_backward());
4885 assert!(!pane.can_navigate_forward());
4886 });
4887
4888 item.update(cx, |item, cx| {
4889 item.set_state("one".to_string(), cx);
4890 });
4891
4892 // Toolbar must be notified to re-render the navigation buttons
4893 assert_eq!(*toolbar_notify_count.borrow(), 1);
4894
4895 pane.update(cx, |pane, _| {
4896 assert!(pane.can_navigate_backward());
4897 assert!(!pane.can_navigate_forward());
4898 });
4899
4900 workspace
4901 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4902 .await
4903 .unwrap();
4904
4905 assert_eq!(*toolbar_notify_count.borrow(), 2);
4906 pane.update(cx, |pane, _| {
4907 assert!(!pane.can_navigate_backward());
4908 assert!(pane.can_navigate_forward());
4909 });
4910 }
4911
4912 // #[gpui::test]
4913 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4914 // init_test(cx);
4915 // let fs = FakeFs::new(cx.executor());
4916
4917 // let project = Project::test(fs, [], cx).await;
4918 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4919
4920 // let panel = workspace.update(cx, |workspace, cx| {
4921 // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
4922 // workspace.add_panel(panel.clone(), cx);
4923
4924 // workspace
4925 // .right_dock()
4926 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4927
4928 // panel
4929 // });
4930
4931 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4932 // pane.update(cx, |pane, cx| {
4933 // let item = cx.build_view(|cx| TestItem::new(cx));
4934 // pane.add_item(Box::new(item), true, true, None, cx);
4935 // });
4936
4937 // // Transfer focus from center to panel
4938 // workspace.update(cx, |workspace, cx| {
4939 // workspace.toggle_panel_focus::<TestPanel>(cx);
4940 // });
4941
4942 // workspace.update(cx, |workspace, cx| {
4943 // assert!(workspace.right_dock().read(cx).is_open());
4944 // assert!(!panel.is_zoomed(cx));
4945 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4946 // });
4947
4948 // // Transfer focus from panel to center
4949 // workspace.update(cx, |workspace, cx| {
4950 // workspace.toggle_panel_focus::<TestPanel>(cx);
4951 // });
4952
4953 // workspace.update(cx, |workspace, cx| {
4954 // assert!(workspace.right_dock().read(cx).is_open());
4955 // assert!(!panel.is_zoomed(cx));
4956 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4957 // });
4958
4959 // // Close the dock
4960 // workspace.update(cx, |workspace, cx| {
4961 // workspace.toggle_dock(DockPosition::Right, cx);
4962 // });
4963
4964 // workspace.update(cx, |workspace, cx| {
4965 // assert!(!workspace.right_dock().read(cx).is_open());
4966 // assert!(!panel.is_zoomed(cx));
4967 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
4968 // });
4969
4970 // // Open the dock
4971 // workspace.update(cx, |workspace, cx| {
4972 // workspace.toggle_dock(DockPosition::Right, cx);
4973 // });
4974
4975 // workspace.update(cx, |workspace, cx| {
4976 // assert!(workspace.right_dock().read(cx).is_open());
4977 // assert!(!panel.is_zoomed(cx));
4978 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4979 // });
4980
4981 // // Focus and zoom panel
4982 // panel.update(cx, |panel, cx| {
4983 // cx.focus_self();
4984 // panel.set_zoomed(true, cx)
4985 // });
4986
4987 // workspace.update(cx, |workspace, cx| {
4988 // assert!(workspace.right_dock().read(cx).is_open());
4989 // assert!(panel.is_zoomed(cx));
4990 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4991 // });
4992
4993 // // Transfer focus to the center closes the dock
4994 // workspace.update(cx, |workspace, cx| {
4995 // workspace.toggle_panel_focus::<TestPanel>(cx);
4996 // });
4997
4998 // workspace.update(cx, |workspace, cx| {
4999 // assert!(!workspace.right_dock().read(cx).is_open());
5000 // assert!(panel.is_zoomed(cx));
5001 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5002 // });
5003
5004 // // Transferring focus back to the panel keeps it zoomed
5005 // workspace.update(cx, |workspace, cx| {
5006 // workspace.toggle_panel_focus::<TestPanel>(cx);
5007 // });
5008
5009 // workspace.update(cx, |workspace, cx| {
5010 // assert!(workspace.right_dock().read(cx).is_open());
5011 // assert!(panel.is_zoomed(cx));
5012 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5013 // });
5014
5015 // // Close the dock while it is zoomed
5016 // workspace.update(cx, |workspace, cx| {
5017 // workspace.toggle_dock(DockPosition::Right, cx)
5018 // });
5019
5020 // workspace.update(cx, |workspace, cx| {
5021 // assert!(!workspace.right_dock().read(cx).is_open());
5022 // assert!(panel.is_zoomed(cx));
5023 // assert!(workspace.zoomed.is_none());
5024 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5025 // });
5026
5027 // // Opening the dock, when it's zoomed, retains focus
5028 // workspace.update(cx, |workspace, cx| {
5029 // workspace.toggle_dock(DockPosition::Right, cx)
5030 // });
5031
5032 // workspace.update(cx, |workspace, cx| {
5033 // assert!(workspace.right_dock().read(cx).is_open());
5034 // assert!(panel.is_zoomed(cx));
5035 // assert!(workspace.zoomed.is_some());
5036 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5037 // });
5038
5039 // // Unzoom and close the panel, zoom the active pane.
5040 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5041 // workspace.update(cx, |workspace, cx| {
5042 // workspace.toggle_dock(DockPosition::Right, cx)
5043 // });
5044 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5045
5046 // // Opening a dock unzooms the pane.
5047 // workspace.update(cx, |workspace, cx| {
5048 // workspace.toggle_dock(DockPosition::Right, cx)
5049 // });
5050 // workspace.update(cx, |workspace, cx| {
5051 // let pane = pane.read(cx);
5052 // assert!(!pane.is_zoomed());
5053 // assert!(!pane.focus_handle(cx).is_focused(cx));
5054 // assert!(workspace.right_dock().read(cx).is_open());
5055 // assert!(workspace.zoomed.is_none());
5056 // });
5057 // }
5058
5059 // #[gpui::test]
5060 // async fn test_panels(cx: &mut gpui::TestAppContext) {
5061 // init_test(cx);
5062 // let fs = FakeFs::new(cx.executor());
5063
5064 // let project = Project::test(fs, [], cx).await;
5065 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5066
5067 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5068 // // Add panel_1 on the left, panel_2 on the right.
5069 // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
5070 // workspace.add_panel(panel_1.clone(), cx);
5071 // workspace
5072 // .left_dock()
5073 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5074 // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
5075 // workspace.add_panel(panel_2.clone(), cx);
5076 // workspace
5077 // .right_dock()
5078 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5079
5080 // let left_dock = workspace.left_dock();
5081 // assert_eq!(
5082 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5083 // panel_1.panel_id()
5084 // );
5085 // assert_eq!(
5086 // left_dock.read(cx).active_panel_size(cx).unwrap(),
5087 // panel_1.size(cx)
5088 // );
5089
5090 // left_dock.update(cx, |left_dock, cx| {
5091 // left_dock.resize_active_panel(Some(1337.), cx)
5092 // });
5093 // assert_eq!(
5094 // workspace
5095 // .right_dock()
5096 // .read(cx)
5097 // .visible_panel()
5098 // .unwrap()
5099 // .panel_id(),
5100 // panel_2.panel_id(),
5101 // );
5102
5103 // (panel_1, panel_2)
5104 // });
5105
5106 // // Move panel_1 to the right
5107 // panel_1.update(cx, |panel_1, cx| {
5108 // panel_1.set_position(DockPosition::Right, cx)
5109 // });
5110
5111 // workspace.update(cx, |workspace, cx| {
5112 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5113 // // Since it was the only panel on the left, the left dock should now be closed.
5114 // assert!(!workspace.left_dock().read(cx).is_open());
5115 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5116 // let right_dock = workspace.right_dock();
5117 // assert_eq!(
5118 // right_dock.read(cx).visible_panel().unwrap().panel_id(),
5119 // panel_1.panel_id()
5120 // );
5121 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5122
5123 // // Now we move panel_2Β to the left
5124 // panel_2.set_position(DockPosition::Left, cx);
5125 // });
5126
5127 // workspace.update(cx, |workspace, cx| {
5128 // // Since panel_2 was not visible on the right, we don't open the left dock.
5129 // assert!(!workspace.left_dock().read(cx).is_open());
5130 // // And the right dock is unaffected in it's displaying of panel_1
5131 // assert!(workspace.right_dock().read(cx).is_open());
5132 // assert_eq!(
5133 // workspace
5134 // .right_dock()
5135 // .read(cx)
5136 // .visible_panel()
5137 // .unwrap()
5138 // .panel_id(),
5139 // panel_1.panel_id(),
5140 // );
5141 // });
5142
5143 // // Move panel_1 back to the left
5144 // panel_1.update(cx, |panel_1, cx| {
5145 // panel_1.set_position(DockPosition::Left, cx)
5146 // });
5147
5148 // workspace.update(cx, |workspace, cx| {
5149 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5150 // let left_dock = workspace.left_dock();
5151 // assert!(left_dock.read(cx).is_open());
5152 // assert_eq!(
5153 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5154 // panel_1.panel_id()
5155 // );
5156 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5157 // // And right the dock should be closed as it no longer has any panels.
5158 // assert!(!workspace.right_dock().read(cx).is_open());
5159
5160 // // Now we move panel_1 to the bottom
5161 // panel_1.set_position(DockPosition::Bottom, cx);
5162 // });
5163
5164 // workspace.update(cx, |workspace, cx| {
5165 // // Since panel_1 was visible on the left, we close the left dock.
5166 // assert!(!workspace.left_dock().read(cx).is_open());
5167 // // The bottom dock is sized based on the panel's default size,
5168 // // since the panel orientation changed from vertical to horizontal.
5169 // let bottom_dock = workspace.bottom_dock();
5170 // assert_eq!(
5171 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5172 // panel_1.size(cx),
5173 // );
5174 // // Close bottom dock and move panel_1 back to the left.
5175 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5176 // panel_1.set_position(DockPosition::Left, cx);
5177 // });
5178
5179 // // Emit activated event on panel 1
5180 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5181
5182 // // Now the left dock is open and panel_1 is active and focused.
5183 // workspace.update(cx, |workspace, cx| {
5184 // let left_dock = workspace.left_dock();
5185 // assert!(left_dock.read(cx).is_open());
5186 // assert_eq!(
5187 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5188 // panel_1.panel_id(),
5189 // );
5190 // assert!(panel_1.focus_handle(cx).is_focused(cx));
5191 // });
5192
5193 // // Emit closed event on panel 2, which is not active
5194 // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5195
5196 // // Wo don't close the left dock, because panel_2 wasn't the active panel
5197 // workspace.update(cx, |workspace, cx| {
5198 // let left_dock = workspace.left_dock();
5199 // assert!(left_dock.read(cx).is_open());
5200 // assert_eq!(
5201 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5202 // panel_1.panel_id(),
5203 // );
5204 // });
5205
5206 // // Emitting a ZoomIn event shows the panel as zoomed.
5207 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5208 // workspace.update(cx, |workspace, _| {
5209 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5210 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5211 // });
5212
5213 // // Move panel to another dock while it is zoomed
5214 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5215 // workspace.update(cx, |workspace, _| {
5216 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5217
5218 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5219 // });
5220
5221 // // If focus is transferred to another view that's not a panel or another pane, we still show
5222 // // the panel as zoomed.
5223 // let other_focus_handle = cx.update(|cx| cx.focus_handle());
5224 // cx.update(|cx| cx.focus(&other_focus_handle));
5225 // workspace.update(cx, |workspace, _| {
5226 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5227 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5228 // });
5229
5230 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5231 // workspace.update(cx, |_, cx| cx.focus_self());
5232 // workspace.update(cx, |workspace, _| {
5233 // assert_eq!(workspace.zoomed, None);
5234 // assert_eq!(workspace.zoomed_position, None);
5235 // });
5236
5237 // // If focus is transferred again to another view that's not a panel or a pane, we won't
5238 // // show the panel as zoomed because it wasn't zoomed before.
5239 // cx.update(|cx| cx.focus(&other_focus_handle));
5240 // workspace.update(cx, |workspace, _| {
5241 // assert_eq!(workspace.zoomed, None);
5242 // assert_eq!(workspace.zoomed_position, None);
5243 // });
5244
5245 // // When focus is transferred back to the panel, it is zoomed again.
5246 // panel_1.update(cx, |_, cx| cx.focus_self());
5247 // workspace.update(cx, |workspace, _| {
5248 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5249 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5250 // });
5251
5252 // // Emitting a ZoomOut event unzooms the panel.
5253 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5254 // workspace.update(cx, |workspace, _| {
5255 // assert_eq!(workspace.zoomed, None);
5256 // assert_eq!(workspace.zoomed_position, None);
5257 // });
5258
5259 // // Emit closed event on panel 1, which is active
5260 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5261
5262 // // Now the left dock is closed, because panel_1 was the active panel
5263 // workspace.update(cx, |workspace, cx| {
5264 // let right_dock = workspace.right_dock();
5265 // assert!(!right_dock.read(cx).is_open());
5266 // });
5267 // }
5268
5269 pub fn init_test(cx: &mut TestAppContext) {
5270 cx.update(|cx| {
5271 let settings_store = SettingsStore::test(cx);
5272 cx.set_global(settings_store);
5273 theme::init(theme::LoadThemes::JustBase, cx);
5274 language::init(cx);
5275 crate::init_settings(cx);
5276 Project::init_settings(cx);
5277 });
5278 }
5279}