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