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