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;
15
16use anyhow::{anyhow, Context, Result};
17use assets::Assets;
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, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
43 SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
44 WindowContext,
45};
46use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
47use language::{LanguageRegistry, Rope};
48use std::{
49 any::TypeId,
50 borrow::Cow,
51 cmp, env,
52 future::Future,
53 path::{Path, PathBuf},
54 str,
55 sync::Arc,
56 time::Duration,
57};
58
59use crate::{
60 notifications::simple_message_notification::MessageNotification,
61 persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
62};
63use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel};
64use lazy_static::lazy_static;
65use notifications::{NotificationHandle, NotifyResultExt};
66pub use pane::*;
67pub use pane_group::*;
68use persistence::{model::SerializedItem, DB};
69pub use persistence::{
70 model::{ItemId, WorkspaceLocation},
71 WorkspaceDb, DB as WORKSPACE_DB,
72};
73use postage::prelude::Stream;
74use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
75use serde::Deserialize;
76use settings::{Autosave, Settings};
77use shared_screen::SharedScreen;
78use status_bar::StatusBar;
79pub use status_bar::StatusItemView;
80use theme::{Theme, ThemeRegistry};
81pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
82use util::{paths, ResultExt};
83
84lazy_static! {
85 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93}
94
95pub trait Modal: View {
96 fn dismiss_on_event(event: &Self::Event) -> bool;
97}
98
99#[derive(Clone, PartialEq)]
100pub struct RemoveWorktreeFromProject(pub WorktreeId);
101
102actions!(
103 workspace,
104 [
105 Open,
106 NewFile,
107 NewWindow,
108 CloseWindow,
109 AddFolderToProject,
110 Unfollow,
111 Save,
112 SaveAs,
113 SaveAll,
114 ActivatePreviousPane,
115 ActivateNextPane,
116 FollowNextCollaborator,
117 ToggleLeftDock,
118 NewTerminal,
119 NewSearch,
120 Feedback,
121 Restart,
122 Welcome
123 ]
124);
125
126actions!(zed, [OpenSettings]);
127
128#[derive(Clone, PartialEq)]
129pub struct OpenPaths {
130 pub paths: Vec<PathBuf>,
131}
132
133#[derive(Clone, Deserialize, PartialEq)]
134pub struct ActivatePane(pub usize);
135
136pub struct Toast {
137 id: usize,
138 msg: Cow<'static, str>,
139 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
140}
141
142impl Toast {
143 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
144 Toast {
145 id,
146 msg: msg.into(),
147 on_click: None,
148 }
149 }
150
151 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
152 where
153 M: Into<Cow<'static, str>>,
154 F: Fn(&mut WindowContext) + 'static,
155 {
156 self.on_click = Some((message.into(), Arc::new(on_click)));
157 self
158 }
159}
160
161impl PartialEq for Toast {
162 fn eq(&self, other: &Self) -> bool {
163 self.id == other.id
164 && self.msg == other.msg
165 && self.on_click.is_some() == other.on_click.is_some()
166 }
167}
168
169impl Clone for Toast {
170 fn clone(&self) -> Self {
171 Toast {
172 id: self.id,
173 msg: self.msg.to_owned(),
174 on_click: self.on_click.clone(),
175 }
176 }
177}
178
179pub type WorkspaceId = i64;
180
181impl_actions!(workspace, [ActivatePane]);
182
183pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
184 pane::init(cx);
185 notifications::init(cx);
186
187 cx.add_global_action({
188 let app_state = Arc::downgrade(&app_state);
189 move |_: &Open, cx: &mut AppContext| {
190 let mut paths = cx.prompt_for_paths(PathPromptOptions {
191 files: true,
192 directories: true,
193 multiple: true,
194 });
195
196 if let Some(app_state) = app_state.upgrade() {
197 cx.spawn(move |mut cx| async move {
198 if let Some(paths) = paths.recv().await.flatten() {
199 cx.update(|cx| {
200 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
201 });
202 }
203 })
204 .detach();
205 }
206 }
207 });
208 cx.add_async_action(Workspace::open);
209
210 cx.add_async_action(Workspace::follow_next_collaborator);
211 cx.add_async_action(Workspace::close);
212 cx.add_global_action(Workspace::close_global);
213 cx.add_global_action(restart);
214 cx.add_async_action(Workspace::save_all);
215 cx.add_action(Workspace::add_folder_to_project);
216 cx.add_action(
217 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
218 let pane = workspace.active_pane().clone();
219 workspace.unfollow(&pane, cx);
220 },
221 );
222 cx.add_action(
223 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
224 workspace.save_active_item(false, cx).detach_and_log_err(cx);
225 },
226 );
227 cx.add_action(
228 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
229 workspace.save_active_item(true, cx).detach_and_log_err(cx);
230 },
231 );
232 cx.add_action(Workspace::toggle_panel);
233 cx.add_action(Workspace::focus_center);
234 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
235 workspace.activate_previous_pane(cx)
236 });
237 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
238 workspace.activate_next_pane(cx)
239 });
240 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
241 workspace.toggle_dock(DockPosition::Left, cx);
242 });
243 cx.add_action(Workspace::activate_pane_at_index);
244
245 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
246 cx.spawn(|workspace, mut cx| async move {
247 let err = install_cli::install_cli(&cx)
248 .await
249 .context("Failed to create CLI symlink");
250
251 workspace.update(&mut cx, |workspace, cx| {
252 if matches!(err, Err(_)) {
253 err.notify_err(workspace, cx);
254 } else {
255 workspace.show_notification(1, cx, |cx| {
256 cx.add_view(|_| {
257 MessageNotification::new("Successfully installed the `zed` binary")
258 })
259 });
260 }
261 })
262 })
263 .detach();
264 });
265
266 cx.add_action(
267 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
268 create_and_open_local_file(&paths::SETTINGS, cx, || {
269 Settings::initial_user_settings_content(&Assets)
270 .as_ref()
271 .into()
272 })
273 .detach_and_log_err(cx);
274 },
275 );
276
277 let client = &app_state.client;
278 client.add_view_request_handler(Workspace::handle_follow);
279 client.add_view_message_handler(Workspace::handle_unfollow);
280 client.add_view_message_handler(Workspace::handle_update_followers);
281}
282
283type ProjectItemBuilders = HashMap<
284 TypeId,
285 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
286>;
287pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
288 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
289 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
290 let item = model.downcast::<I::Item>().unwrap();
291 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
292 });
293 });
294}
295
296type FollowableItemBuilder = fn(
297 ViewHandle<Pane>,
298 ModelHandle<Project>,
299 ViewId,
300 &mut Option<proto::view::Variant>,
301 &mut AppContext,
302) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
303type FollowableItemBuilders = HashMap<
304 TypeId,
305 (
306 FollowableItemBuilder,
307 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
308 ),
309>;
310pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
311 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
312 builders.insert(
313 TypeId::of::<I>(),
314 (
315 |pane, project, id, state, cx| {
316 I::from_state_proto(pane, project, id, state, cx).map(|task| {
317 cx.foreground()
318 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
319 })
320 },
321 |this| Box::new(this.clone().downcast::<I>().unwrap()),
322 ),
323 );
324 });
325}
326
327type ItemDeserializers = HashMap<
328 Arc<str>,
329 fn(
330 ModelHandle<Project>,
331 WeakViewHandle<Workspace>,
332 WorkspaceId,
333 ItemId,
334 &mut ViewContext<Pane>,
335 ) -> Task<Result<Box<dyn ItemHandle>>>,
336>;
337pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
338 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
339 if let Some(serialized_item_kind) = I::serialized_item_kind() {
340 deserializers.insert(
341 Arc::from(serialized_item_kind),
342 |project, workspace, workspace_id, item_id, cx| {
343 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
344 cx.foreground()
345 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
346 },
347 );
348 }
349 });
350}
351
352pub struct AppState {
353 pub languages: Arc<LanguageRegistry>,
354 pub themes: Arc<ThemeRegistry>,
355 pub client: Arc<client::Client>,
356 pub user_store: ModelHandle<client::UserStore>,
357 pub fs: Arc<dyn fs::Fs>,
358 pub build_window_options:
359 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
360 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
361 pub background_actions: BackgroundActions,
362}
363
364impl AppState {
365 #[cfg(any(test, feature = "test-support"))]
366 pub fn test(cx: &mut AppContext) -> Arc<Self> {
367 let settings = Settings::test(cx);
368 cx.set_global(settings);
369
370 let fs = fs::FakeFs::new(cx.background().clone());
371 let languages = Arc::new(LanguageRegistry::test());
372 let http_client = util::http::FakeHttpClient::with_404_response();
373 let client = Client::new(http_client.clone(), cx);
374 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
375 let themes = ThemeRegistry::new((), cx.font_cache().clone());
376 Arc::new(Self {
377 client,
378 themes,
379 fs,
380 languages,
381 user_store,
382 initialize_workspace: |_, _, _| {},
383 build_window_options: |_, _, _| Default::default(),
384 background_actions: || &[],
385 })
386 }
387}
388
389struct DelayedDebouncedEditAction {
390 task: Option<Task<()>>,
391 cancel_channel: Option<oneshot::Sender<()>>,
392}
393
394impl DelayedDebouncedEditAction {
395 fn new() -> DelayedDebouncedEditAction {
396 DelayedDebouncedEditAction {
397 task: None,
398 cancel_channel: None,
399 }
400 }
401
402 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, f: F)
403 where
404 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
405 {
406 if let Some(channel) = self.cancel_channel.take() {
407 _ = channel.send(());
408 }
409
410 let (sender, mut receiver) = oneshot::channel::<()>();
411 self.cancel_channel = Some(sender);
412
413 let previous_task = self.task.take();
414 self.task = Some(cx.spawn(|workspace, mut cx| async move {
415 let mut timer = cx.background().timer(delay).fuse();
416 if let Some(previous_task) = previous_task {
417 previous_task.await;
418 }
419
420 futures::select_biased! {
421 _ = receiver => return,
422 _ = timer => {}
423 }
424
425 if let Some(result) = workspace
426 .update(&mut cx, |workspace, cx| (f)(workspace, cx))
427 .log_err()
428 {
429 result.await.log_err();
430 }
431 }));
432 }
433}
434
435pub enum Event {
436 PaneAdded(ViewHandle<Pane>),
437 ContactRequestedJoin(u64),
438}
439
440pub struct Workspace {
441 weak_self: WeakViewHandle<Self>,
442 remote_entity_subscription: Option<client::Subscription>,
443 modal: Option<AnyViewHandle>,
444 center: PaneGroup,
445 left_dock: ViewHandle<Dock>,
446 bottom_dock: ViewHandle<Dock>,
447 right_dock: ViewHandle<Dock>,
448 panes: Vec<ViewHandle<Pane>>,
449 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
450 active_pane: ViewHandle<Pane>,
451 last_active_center_pane: Option<WeakViewHandle<Pane>>,
452 status_bar: ViewHandle<StatusBar>,
453 titlebar_item: Option<AnyViewHandle>,
454 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
455 project: ModelHandle<Project>,
456 leader_state: LeaderState,
457 follower_states_by_leader: FollowerStatesByLeader,
458 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
459 window_edited: bool,
460 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
461 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
462 database_id: WorkspaceId,
463 app_state: Arc<AppState>,
464 _subscriptions: Vec<Subscription>,
465 _apply_leader_updates: Task<Result<()>>,
466 _observe_current_user: Task<Result<()>>,
467}
468
469#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
470pub struct ViewId {
471 pub creator: PeerId,
472 pub id: u64,
473}
474
475#[derive(Default)]
476struct LeaderState {
477 followers: HashSet<PeerId>,
478}
479
480type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
481
482#[derive(Default)]
483struct FollowerState {
484 active_view_id: Option<ViewId>,
485 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
486}
487
488impl Workspace {
489 pub fn new(
490 serialized_workspace: Option<SerializedWorkspace>,
491 workspace_id: WorkspaceId,
492 project: ModelHandle<Project>,
493 app_state: Arc<AppState>,
494 cx: &mut ViewContext<Self>,
495 ) -> Self {
496 cx.observe(&project, |_, _, cx| cx.notify()).detach();
497 cx.subscribe(&project, move |this, _, event, cx| {
498 match event {
499 project::Event::RemoteIdChanged(remote_id) => {
500 this.update_window_title(cx);
501 this.project_remote_id_changed(*remote_id, cx);
502 }
503
504 project::Event::CollaboratorLeft(peer_id) => {
505 this.collaborator_left(*peer_id, cx);
506 }
507
508 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
509 this.update_window_title(cx);
510 this.serialize_workspace(cx);
511 }
512
513 project::Event::DisconnectedFromHost => {
514 this.update_window_edited(cx);
515 cx.blur();
516 }
517
518 project::Event::Closed => {
519 cx.remove_window();
520 }
521
522 _ => {}
523 }
524 cx.notify()
525 })
526 .detach();
527
528 let weak_handle = cx.weak_handle();
529
530 let center_pane =
531 cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
532 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
533 cx.focus(¢er_pane);
534 cx.emit(Event::PaneAdded(center_pane.clone()));
535
536 let mut current_user = app_state.user_store.read(cx).watch_current_user();
537 let mut connection_status = app_state.client.status();
538 let _observe_current_user = cx.spawn(|this, mut cx| async move {
539 current_user.recv().await;
540 connection_status.recv().await;
541 let mut stream =
542 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
543
544 while stream.recv().await.is_some() {
545 this.update(&mut cx, |_, cx| cx.notify())?;
546 }
547 anyhow::Ok(())
548 });
549
550 // All leader updates are enqueued and then processed in a single task, so
551 // that each asynchronous operation can be run in order.
552 let (leader_updates_tx, mut leader_updates_rx) =
553 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
554 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
555 while let Some((leader_id, update)) = leader_updates_rx.next().await {
556 Self::process_leader_update(&this, leader_id, update, &mut cx)
557 .await
558 .log_err();
559 }
560
561 Ok(())
562 });
563
564 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
565
566 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
567 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
568 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
569 let left_dock_buttons =
570 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
571 let bottom_dock_buttons =
572 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
573 let right_dock_buttons =
574 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
575 let status_bar = cx.add_view(|cx| {
576 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
577 status_bar.add_left_item(left_dock_buttons, cx);
578 status_bar.add_right_item(right_dock_buttons, cx);
579 status_bar.add_right_item(bottom_dock_buttons, cx);
580 status_bar
581 });
582
583 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
584 drag_and_drop.register_container(weak_handle.clone());
585 });
586
587 let mut active_call = None;
588 if cx.has_global::<ModelHandle<ActiveCall>>() {
589 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
590 let mut subscriptions = Vec::new();
591 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
592 active_call = Some((call, subscriptions));
593 }
594
595 let subscriptions = vec![
596 cx.observe_fullscreen(|_, _, cx| cx.notify()),
597 cx.observe_window_activation(Self::on_window_activation_changed),
598 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
599 // Transform fixed bounds to be stored in terms of the containing display
600 if let WindowBounds::Fixed(mut window_bounds) = bounds {
601 if let Some(screen) = cx.platform().screen_by_id(display) {
602 let screen_bounds = screen.bounds();
603 window_bounds
604 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
605 window_bounds
606 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
607 bounds = WindowBounds::Fixed(window_bounds);
608 }
609 }
610
611 cx.background()
612 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
613 .detach_and_log_err(cx);
614 }),
615 cx.observe(&left_dock, |this, _, cx| {
616 this.serialize_workspace(cx);
617 cx.notify();
618 }),
619 cx.observe(&bottom_dock, |this, _, cx| {
620 this.serialize_workspace(cx);
621 cx.notify();
622 }),
623 cx.observe(&right_dock, |this, _, cx| {
624 this.serialize_workspace(cx);
625 cx.notify();
626 }),
627 ];
628
629 let mut this = Workspace {
630 modal: None,
631 weak_self: weak_handle.clone(),
632 center: PaneGroup::new(center_pane.clone()),
633 panes: vec![center_pane.clone()],
634 panes_by_item: Default::default(),
635 active_pane: center_pane.clone(),
636 last_active_center_pane: Some(center_pane.downgrade()),
637 status_bar,
638 titlebar_item: None,
639 notifications: Default::default(),
640 remote_entity_subscription: None,
641 left_dock,
642 bottom_dock,
643 right_dock,
644 project: project.clone(),
645 leader_state: Default::default(),
646 follower_states_by_leader: Default::default(),
647 last_leaders_by_pane: Default::default(),
648 window_edited: false,
649 active_call,
650 database_id: workspace_id,
651 app_state,
652 _observe_current_user,
653 _apply_leader_updates,
654 leader_updates_tx,
655 _subscriptions: subscriptions,
656 };
657 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
658 cx.defer(|this, cx| this.update_window_title(cx));
659
660 if let Some(serialized_workspace) = serialized_workspace {
661 cx.defer(move |_, cx| {
662 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
663 });
664 }
665
666 this
667 }
668
669 fn new_local(
670 abs_paths: Vec<PathBuf>,
671 app_state: Arc<AppState>,
672 requesting_window_id: Option<usize>,
673 cx: &mut AppContext,
674 ) -> Task<(
675 WeakViewHandle<Workspace>,
676 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
677 )> {
678 let project_handle = Project::local(
679 app_state.client.clone(),
680 app_state.user_store.clone(),
681 app_state.languages.clone(),
682 app_state.fs.clone(),
683 cx,
684 );
685
686 cx.spawn(|mut cx| async move {
687 let mut serialized_workspace =
688 persistence::DB.workspace_for_roots(&abs_paths.as_slice());
689
690 let paths_to_open = serialized_workspace
691 .as_ref()
692 .map(|workspace| workspace.location.paths())
693 .unwrap_or(Arc::new(abs_paths));
694
695 // Get project paths for all of the abs_paths
696 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
697 let mut project_paths = Vec::new();
698 for path in paths_to_open.iter() {
699 if let Some((worktree, project_entry)) = cx
700 .update(|cx| {
701 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
702 })
703 .await
704 .log_err()
705 {
706 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
707 project_paths.push(Some(project_entry));
708 } else {
709 project_paths.push(None);
710 }
711 }
712
713 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
714 serialized_workspace.id
715 } else {
716 DB.next_id().await.unwrap_or(0)
717 };
718
719 let window_bounds_override =
720 ZED_WINDOW_POSITION
721 .zip(*ZED_WINDOW_SIZE)
722 .map(|(position, size)| {
723 WindowBounds::Fixed(RectF::new(
724 cx.platform().screens()[0].bounds().origin() + position,
725 size,
726 ))
727 });
728
729 let build_workspace =
730 |cx: &mut ViewContext<Workspace>,
731 serialized_workspace: Option<SerializedWorkspace>| {
732 let mut workspace = Workspace::new(
733 serialized_workspace,
734 workspace_id,
735 project_handle.clone(),
736 app_state.clone(),
737 cx,
738 );
739 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
740 workspace
741 };
742
743 let workspace = requesting_window_id
744 .and_then(|window_id| {
745 cx.update(|cx| {
746 cx.replace_root_view(window_id, |cx| {
747 build_workspace(cx, serialized_workspace.take())
748 })
749 })
750 })
751 .unwrap_or_else(|| {
752 let (bounds, display) = if let Some(bounds) = window_bounds_override {
753 (Some(bounds), None)
754 } else {
755 serialized_workspace
756 .as_ref()
757 .and_then(|serialized_workspace| {
758 let display = serialized_workspace.display?;
759 let mut bounds = serialized_workspace.bounds?;
760
761 // Stored bounds are relative to the containing display.
762 // So convert back to global coordinates if that screen still exists
763 if let WindowBounds::Fixed(mut window_bounds) = bounds {
764 if let Some(screen) = cx.platform().screen_by_id(display) {
765 let screen_bounds = screen.bounds();
766 window_bounds.set_origin_x(
767 window_bounds.origin_x() + screen_bounds.origin_x(),
768 );
769 window_bounds.set_origin_y(
770 window_bounds.origin_y() + screen_bounds.origin_y(),
771 );
772 bounds = WindowBounds::Fixed(window_bounds);
773 } else {
774 // Screen no longer exists. Return none here.
775 return None;
776 }
777 }
778
779 Some((bounds, display))
780 })
781 .unzip()
782 };
783
784 // Use the serialized workspace to construct the new window
785 cx.add_window(
786 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
787 |cx| build_workspace(cx, serialized_workspace),
788 )
789 .1
790 });
791
792 let workspace = workspace.downgrade();
793 notify_if_database_failed(&workspace, &mut cx);
794
795 // Call open path for each of the project paths
796 // (this will bring them to the front if they were in the serialized workspace)
797 debug_assert!(paths_to_open.len() == project_paths.len());
798 let tasks = paths_to_open
799 .iter()
800 .cloned()
801 .zip(project_paths.into_iter())
802 .map(|(abs_path, project_path)| {
803 let workspace = workspace.clone();
804 cx.spawn(|mut cx| {
805 let fs = app_state.fs.clone();
806 async move {
807 let project_path = project_path?;
808 if fs.is_file(&abs_path).await {
809 Some(
810 workspace
811 .update(&mut cx, |workspace, cx| {
812 workspace.open_path(project_path, None, true, cx)
813 })
814 .log_err()?
815 .await,
816 )
817 } else {
818 None
819 }
820 }
821 })
822 });
823
824 let opened_items = futures::future::join_all(tasks.into_iter()).await;
825
826 (workspace, opened_items)
827 })
828 }
829
830 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
831 self.weak_self.clone()
832 }
833
834 pub fn left_dock(&self) -> &ViewHandle<Dock> {
835 &self.left_dock
836 }
837
838 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
839 &self.bottom_dock
840 }
841
842 pub fn right_dock(&self) -> &ViewHandle<Dock> {
843 &self.right_dock
844 }
845
846 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
847 let dock = match panel.position(cx) {
848 DockPosition::Left => &self.left_dock,
849 DockPosition::Bottom => &self.bottom_dock,
850 DockPosition::Right => &self.right_dock,
851 };
852
853 cx.subscribe(&panel, {
854 let mut dock = dock.clone();
855 let mut prev_position = panel.position(cx);
856 move |this, panel, event, cx| {
857 if T::should_change_position_on_event(event) {
858 let new_position = panel.read(cx).position(cx);
859 let mut was_visible = false;
860 let mut size = None;
861 dock.update(cx, |dock, cx| {
862 if new_position.axis() == prev_position.axis() {
863 size = dock.panel_size(&panel);
864 }
865 prev_position = new_position;
866
867 was_visible = dock.is_open()
868 && dock
869 .active_panel()
870 .map_or(false, |active_panel| active_panel.id() == panel.id());
871 dock.remove_panel(&panel, cx);
872 });
873 dock = match panel.read(cx).position(cx) {
874 DockPosition::Left => &this.left_dock,
875 DockPosition::Bottom => &this.bottom_dock,
876 DockPosition::Right => &this.right_dock,
877 }
878 .clone();
879 dock.update(cx, |dock, cx| {
880 dock.add_panel(panel.clone(), cx);
881 if let Some(size) = size {
882 dock.resize_panel(&panel, size);
883 }
884
885 if was_visible {
886 dock.set_open(true, cx);
887 dock.activate_panel(dock.panels_len() - 1, cx);
888 }
889 });
890 }
891 }
892 })
893 .detach();
894
895 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
896 }
897
898 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
899 &self.status_bar
900 }
901
902 pub fn app_state(&self) -> &Arc<AppState> {
903 &self.app_state
904 }
905
906 pub fn user_store(&self) -> &ModelHandle<UserStore> {
907 &self.app_state.user_store
908 }
909
910 pub fn project(&self) -> &ModelHandle<Project> {
911 &self.project
912 }
913
914 pub fn client(&self) -> &Client {
915 &self.app_state.client
916 }
917
918 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
919 self.titlebar_item = Some(item);
920 cx.notify();
921 }
922
923 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
924 self.titlebar_item.clone()
925 }
926
927 /// Call the given callback with a workspace whose project is local.
928 ///
929 /// If the given workspace has a local project, then it will be passed
930 /// to the callback. Otherwise, a new empty window will be created.
931 pub fn with_local_workspace<T, F>(
932 &mut self,
933 cx: &mut ViewContext<Self>,
934 callback: F,
935 ) -> Task<Result<T>>
936 where
937 T: 'static,
938 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
939 {
940 if self.project.read(cx).is_local() {
941 Task::Ready(Some(Ok(callback(self, cx))))
942 } else {
943 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
944 cx.spawn(|_vh, mut cx| async move {
945 let (workspace, _) = task.await;
946 workspace.update(&mut cx, callback)
947 })
948 }
949 }
950
951 pub fn worktrees<'a>(
952 &self,
953 cx: &'a AppContext,
954 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
955 self.project.read(cx).worktrees(cx)
956 }
957
958 pub fn visible_worktrees<'a>(
959 &self,
960 cx: &'a AppContext,
961 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
962 self.project.read(cx).visible_worktrees(cx)
963 }
964
965 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
966 let futures = self
967 .worktrees(cx)
968 .filter_map(|worktree| worktree.read(cx).as_local())
969 .map(|worktree| worktree.scan_complete())
970 .collect::<Vec<_>>();
971 async move {
972 for future in futures {
973 future.await;
974 }
975 }
976 }
977
978 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
979 cx.spawn(|mut cx| async move {
980 let id = cx
981 .window_ids()
982 .into_iter()
983 .find(|&id| cx.window_is_active(id));
984 if let Some(id) = id {
985 //This can only get called when the window's project connection has been lost
986 //so we don't need to prompt the user for anything and instead just close the window
987 cx.remove_window(id);
988 }
989 })
990 .detach();
991 }
992
993 pub fn close(
994 &mut self,
995 _: &CloseWindow,
996 cx: &mut ViewContext<Self>,
997 ) -> Option<Task<Result<()>>> {
998 let window_id = cx.window_id();
999 let prepare = self.prepare_to_close(false, cx);
1000 Some(cx.spawn(|_, mut cx| async move {
1001 if prepare.await? {
1002 cx.remove_window(window_id);
1003 }
1004 Ok(())
1005 }))
1006 }
1007
1008 pub fn prepare_to_close(
1009 &mut self,
1010 quitting: bool,
1011 cx: &mut ViewContext<Self>,
1012 ) -> Task<Result<bool>> {
1013 let active_call = self.active_call().cloned();
1014 let window_id = cx.window_id();
1015
1016 cx.spawn(|this, mut cx| async move {
1017 let workspace_count = cx
1018 .window_ids()
1019 .into_iter()
1020 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1021 .count();
1022
1023 if let Some(active_call) = active_call {
1024 if !quitting
1025 && workspace_count == 1
1026 && active_call.read_with(&cx, |call, _| call.room().is_some())
1027 {
1028 let answer = cx.prompt(
1029 window_id,
1030 PromptLevel::Warning,
1031 "Do you want to leave the current call?",
1032 &["Close window and hang up", "Cancel"],
1033 );
1034
1035 if let Some(mut answer) = answer {
1036 if answer.next().await == Some(1) {
1037 return anyhow::Ok(false);
1038 } else {
1039 active_call
1040 .update(&mut cx, |call, cx| call.hang_up(cx))
1041 .await
1042 .log_err();
1043 }
1044 }
1045 }
1046 }
1047
1048 Ok(this
1049 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1050 .await?)
1051 })
1052 }
1053
1054 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1055 let save_all = self.save_all_internal(false, cx);
1056 Some(cx.foreground().spawn(async move {
1057 save_all.await?;
1058 Ok(())
1059 }))
1060 }
1061
1062 fn save_all_internal(
1063 &mut self,
1064 should_prompt_to_save: bool,
1065 cx: &mut ViewContext<Self>,
1066 ) -> Task<Result<bool>> {
1067 if self.project.read(cx).is_read_only() {
1068 return Task::ready(Ok(true));
1069 }
1070
1071 let dirty_items = self
1072 .panes
1073 .iter()
1074 .flat_map(|pane| {
1075 pane.read(cx).items().filter_map(|item| {
1076 if item.is_dirty(cx) {
1077 Some((pane.downgrade(), item.boxed_clone()))
1078 } else {
1079 None
1080 }
1081 })
1082 })
1083 .collect::<Vec<_>>();
1084
1085 let project = self.project.clone();
1086 cx.spawn(|_, mut cx| async move {
1087 for (pane, item) in dirty_items {
1088 let (singleton, project_entry_ids) =
1089 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1090 if singleton || !project_entry_ids.is_empty() {
1091 if let Some(ix) =
1092 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1093 {
1094 if !Pane::save_item(
1095 project.clone(),
1096 &pane,
1097 ix,
1098 &*item,
1099 should_prompt_to_save,
1100 &mut cx,
1101 )
1102 .await?
1103 {
1104 return Ok(false);
1105 }
1106 }
1107 }
1108 }
1109 Ok(true)
1110 })
1111 }
1112
1113 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1114 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1115 files: true,
1116 directories: true,
1117 multiple: true,
1118 });
1119
1120 Some(cx.spawn(|this, mut cx| async move {
1121 if let Some(paths) = paths.recv().await.flatten() {
1122 if let Some(task) = this
1123 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1124 .log_err()
1125 {
1126 task.await?
1127 }
1128 }
1129 Ok(())
1130 }))
1131 }
1132
1133 pub fn open_workspace_for_paths(
1134 &mut self,
1135 paths: Vec<PathBuf>,
1136 cx: &mut ViewContext<Self>,
1137 ) -> Task<Result<()>> {
1138 let window_id = cx.window_id();
1139 let is_remote = self.project.read(cx).is_remote();
1140 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1141 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1142 let close_task = if is_remote || has_worktree || has_dirty_items {
1143 None
1144 } else {
1145 Some(self.prepare_to_close(false, cx))
1146 };
1147 let app_state = self.app_state.clone();
1148
1149 cx.spawn(|_, mut cx| async move {
1150 let window_id_to_replace = if let Some(close_task) = close_task {
1151 if !close_task.await? {
1152 return Ok(());
1153 }
1154 Some(window_id)
1155 } else {
1156 None
1157 };
1158 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1159 .await?;
1160 Ok(())
1161 })
1162 }
1163
1164 #[allow(clippy::type_complexity)]
1165 pub fn open_paths(
1166 &mut self,
1167 mut abs_paths: Vec<PathBuf>,
1168 visible: bool,
1169 cx: &mut ViewContext<Self>,
1170 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1171 let fs = self.app_state.fs.clone();
1172
1173 // Sort the paths to ensure we add worktrees for parents before their children.
1174 abs_paths.sort_unstable();
1175 cx.spawn(|this, mut cx| async move {
1176 let mut project_paths = Vec::new();
1177 for path in &abs_paths {
1178 if let Some(project_path) = this
1179 .update(&mut cx, |this, cx| {
1180 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1181 })
1182 .log_err()
1183 {
1184 project_paths.push(project_path.await.log_err());
1185 } else {
1186 project_paths.push(None);
1187 }
1188 }
1189
1190 let tasks = abs_paths
1191 .iter()
1192 .cloned()
1193 .zip(project_paths.into_iter())
1194 .map(|(abs_path, project_path)| {
1195 let this = this.clone();
1196 cx.spawn(|mut cx| {
1197 let fs = fs.clone();
1198 async move {
1199 let (_worktree, project_path) = project_path?;
1200 if fs.is_file(&abs_path).await {
1201 Some(
1202 this.update(&mut cx, |this, cx| {
1203 this.open_path(project_path, None, true, cx)
1204 })
1205 .log_err()?
1206 .await,
1207 )
1208 } else {
1209 None
1210 }
1211 }
1212 })
1213 })
1214 .collect::<Vec<_>>();
1215
1216 futures::future::join_all(tasks).await
1217 })
1218 }
1219
1220 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1221 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1222 files: false,
1223 directories: true,
1224 multiple: true,
1225 });
1226 cx.spawn(|this, mut cx| async move {
1227 if let Some(paths) = paths.recv().await.flatten() {
1228 let results = this
1229 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1230 .await;
1231 for result in results.into_iter().flatten() {
1232 result.log_err();
1233 }
1234 }
1235 anyhow::Ok(())
1236 })
1237 .detach_and_log_err(cx);
1238 }
1239
1240 fn project_path_for_path(
1241 project: ModelHandle<Project>,
1242 abs_path: &Path,
1243 visible: bool,
1244 cx: &mut AppContext,
1245 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1246 let entry = project.update(cx, |project, cx| {
1247 project.find_or_create_local_worktree(abs_path, visible, cx)
1248 });
1249 cx.spawn(|cx| async move {
1250 let (worktree, path) = entry.await?;
1251 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1252 Ok((
1253 worktree,
1254 ProjectPath {
1255 worktree_id,
1256 path: path.into(),
1257 },
1258 ))
1259 })
1260 }
1261
1262 /// Returns the modal that was toggled closed if it was open.
1263 pub fn toggle_modal<V, F>(
1264 &mut self,
1265 cx: &mut ViewContext<Self>,
1266 add_view: F,
1267 ) -> Option<ViewHandle<V>>
1268 where
1269 V: 'static + Modal,
1270 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1271 {
1272 cx.notify();
1273 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1274 // it. Otherwise, create a new modal and set it as active.
1275 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1276 if let Some(already_open_modal) = already_open_modal {
1277 cx.focus_self();
1278 Some(already_open_modal)
1279 } else {
1280 let modal = add_view(self, cx);
1281 cx.subscribe(&modal, |this, _, event, cx| {
1282 if V::dismiss_on_event(event) {
1283 this.dismiss_modal(cx);
1284 }
1285 })
1286 .detach();
1287 cx.focus(&modal);
1288 self.modal = Some(modal.into_any());
1289 None
1290 }
1291 }
1292
1293 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1294 self.modal
1295 .as_ref()
1296 .and_then(|modal| modal.clone().downcast::<V>())
1297 }
1298
1299 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1300 if self.modal.take().is_some() {
1301 cx.focus(&self.active_pane);
1302 cx.notify();
1303 }
1304 }
1305
1306 pub fn items<'a>(
1307 &'a self,
1308 cx: &'a AppContext,
1309 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1310 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1311 }
1312
1313 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1314 self.items_of_type(cx).max_by_key(|item| item.id())
1315 }
1316
1317 pub fn items_of_type<'a, T: Item>(
1318 &'a self,
1319 cx: &'a AppContext,
1320 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1321 self.panes
1322 .iter()
1323 .flat_map(|pane| pane.read(cx).items_of_type())
1324 }
1325
1326 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1327 self.active_pane().read(cx).active_item()
1328 }
1329
1330 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1331 self.active_item(cx).and_then(|item| item.project_path(cx))
1332 }
1333
1334 pub fn save_active_item(
1335 &mut self,
1336 force_name_change: bool,
1337 cx: &mut ViewContext<Self>,
1338 ) -> Task<Result<()>> {
1339 let project = self.project.clone();
1340 if let Some(item) = self.active_item(cx) {
1341 if !force_name_change && item.can_save(cx) {
1342 if item.has_conflict(cx) {
1343 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1344
1345 let mut answer = cx.prompt(
1346 PromptLevel::Warning,
1347 CONFLICT_MESSAGE,
1348 &["Overwrite", "Cancel"],
1349 );
1350 cx.spawn(|this, mut cx| async move {
1351 let answer = answer.recv().await;
1352 if answer == Some(0) {
1353 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1354 .await?;
1355 }
1356 Ok(())
1357 })
1358 } else {
1359 item.save(self.project.clone(), cx)
1360 }
1361 } else if item.is_singleton(cx) {
1362 let worktree = self.worktrees(cx).next();
1363 let start_abs_path = worktree
1364 .and_then(|w| w.read(cx).as_local())
1365 .map_or(Path::new(""), |w| w.abs_path())
1366 .to_path_buf();
1367 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1368 cx.spawn(|this, mut cx| async move {
1369 if let Some(abs_path) = abs_path.recv().await.flatten() {
1370 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1371 .await?;
1372 }
1373 Ok(())
1374 })
1375 } else {
1376 Task::ready(Ok(()))
1377 }
1378 } else {
1379 Task::ready(Ok(()))
1380 }
1381 }
1382
1383 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1384 let dock = match dock_side {
1385 DockPosition::Left => &mut self.left_dock,
1386 DockPosition::Bottom => &mut self.bottom_dock,
1387 DockPosition::Right => &mut self.right_dock,
1388 };
1389 dock.update(cx, |dock, cx| {
1390 let open = !dock.is_open();
1391 dock.set_open(open, cx);
1392 });
1393
1394 self.serialize_workspace(cx);
1395
1396 cx.focus_self();
1397 cx.notify();
1398 }
1399
1400 pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
1401 let dock = match action.dock_position {
1402 DockPosition::Left => &mut self.left_dock,
1403 DockPosition::Bottom => &mut self.bottom_dock,
1404 DockPosition::Right => &mut self.right_dock,
1405 };
1406 let active_item = dock.update(cx, move |dock, cx| {
1407 if dock.is_open() && dock.active_panel_index() == action.panel_index {
1408 dock.set_open(false, cx);
1409 None
1410 } else {
1411 dock.set_open(true, cx);
1412 dock.activate_panel(action.panel_index, cx);
1413 dock.active_panel().cloned()
1414 }
1415 });
1416
1417 if let Some(active_item) = active_item {
1418 if active_item.is_focused(cx) {
1419 cx.focus_self();
1420 } else {
1421 cx.focus(active_item.as_any());
1422 }
1423 } else {
1424 cx.focus_self();
1425 }
1426
1427 self.serialize_workspace(cx);
1428
1429 cx.notify();
1430 }
1431
1432 pub fn toggle_panel_focus(
1433 &mut self,
1434 dock_position: DockPosition,
1435 panel_index: usize,
1436 cx: &mut ViewContext<Self>,
1437 ) {
1438 let dock = match dock_position {
1439 DockPosition::Left => &mut self.left_dock,
1440 DockPosition::Bottom => &mut self.bottom_dock,
1441 DockPosition::Right => &mut self.right_dock,
1442 };
1443 let active_item = dock.update(cx, |dock, cx| {
1444 dock.set_open(true, cx);
1445 dock.activate_panel(panel_index, cx);
1446 dock.active_panel().cloned()
1447 });
1448 if let Some(active_item) = active_item {
1449 if active_item.is_focused(cx) {
1450 cx.focus_self();
1451 } else {
1452 cx.focus(active_item.as_any());
1453 }
1454 }
1455
1456 self.serialize_workspace(cx);
1457
1458 cx.notify();
1459 }
1460
1461 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1462 cx.focus_self();
1463 cx.notify();
1464 }
1465
1466 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1467 let pane =
1468 cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
1469 cx.subscribe(&pane, Self::handle_pane_event).detach();
1470 self.panes.push(pane.clone());
1471 cx.focus(&pane);
1472 cx.emit(Event::PaneAdded(pane.clone()));
1473 pane
1474 }
1475
1476 pub fn add_item_to_center(
1477 &mut self,
1478 item: Box<dyn ItemHandle>,
1479 cx: &mut ViewContext<Self>,
1480 ) -> bool {
1481 if let Some(center_pane) = self.last_active_center_pane.clone() {
1482 if let Some(center_pane) = center_pane.upgrade(cx) {
1483 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1484 true
1485 } else {
1486 false
1487 }
1488 } else {
1489 false
1490 }
1491 }
1492
1493 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1494 let active_pane = self.active_pane().clone();
1495 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1496 }
1497
1498 pub fn open_path(
1499 &mut self,
1500 path: impl Into<ProjectPath>,
1501 pane: Option<WeakViewHandle<Pane>>,
1502 focus_item: bool,
1503 cx: &mut ViewContext<Self>,
1504 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1505 let pane = pane.unwrap_or_else(|| {
1506 self.last_active_center_pane.clone().unwrap_or_else(|| {
1507 self.panes
1508 .first()
1509 .expect("There must be an active pane")
1510 .downgrade()
1511 })
1512 });
1513
1514 let task = self.load_path(path.into(), cx);
1515 cx.spawn(|this, mut cx| async move {
1516 let (project_entry_id, build_item) = task.await?;
1517 let pane = pane
1518 .upgrade(&cx)
1519 .ok_or_else(|| anyhow!("pane was closed"))?;
1520 this.update(&mut cx, |this, cx| {
1521 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1522 })
1523 })
1524 }
1525
1526 pub(crate) fn load_path(
1527 &mut self,
1528 path: ProjectPath,
1529 cx: &mut ViewContext<Self>,
1530 ) -> Task<
1531 Result<(
1532 ProjectEntryId,
1533 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1534 )>,
1535 > {
1536 let project = self.project().clone();
1537 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1538 cx.spawn(|_, mut cx| async move {
1539 let (project_entry_id, project_item) = project_item.await?;
1540 let build_item = cx.update(|cx| {
1541 cx.default_global::<ProjectItemBuilders>()
1542 .get(&project_item.model_type())
1543 .ok_or_else(|| anyhow!("no item builder for project item"))
1544 .cloned()
1545 })?;
1546 let build_item =
1547 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1548 Ok((project_entry_id, build_item))
1549 })
1550 }
1551
1552 pub fn open_project_item<T>(
1553 &mut self,
1554 project_item: ModelHandle<T::Item>,
1555 cx: &mut ViewContext<Self>,
1556 ) -> ViewHandle<T>
1557 where
1558 T: ProjectItem,
1559 {
1560 use project::Item as _;
1561
1562 let entry_id = project_item.read(cx).entry_id(cx);
1563 if let Some(item) = entry_id
1564 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1565 .and_then(|item| item.downcast())
1566 {
1567 self.activate_item(&item, cx);
1568 return item;
1569 }
1570
1571 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1572 self.add_item(Box::new(item.clone()), cx);
1573 item
1574 }
1575
1576 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1577 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1578 let pane = self.active_pane.clone();
1579 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1580 }
1581 }
1582
1583 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1584 let result = self.panes.iter().find_map(|pane| {
1585 pane.read(cx)
1586 .index_for_item(item)
1587 .map(|ix| (pane.clone(), ix))
1588 });
1589 if let Some((pane, ix)) = result {
1590 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1591 true
1592 } else {
1593 false
1594 }
1595 }
1596
1597 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1598 let panes = self.center.panes();
1599 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1600 cx.focus(&pane);
1601 } else {
1602 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1603 }
1604 }
1605
1606 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1607 let panes = self.center.panes();
1608 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1609 let next_ix = (ix + 1) % panes.len();
1610 let next_pane = panes[next_ix].clone();
1611 cx.focus(&next_pane);
1612 }
1613 }
1614
1615 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1616 let panes = self.center.panes();
1617 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1618 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1619 let prev_pane = panes[prev_ix].clone();
1620 cx.focus(&prev_pane);
1621 }
1622 }
1623
1624 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1625 if self.active_pane != pane {
1626 self.active_pane
1627 .update(cx, |pane, cx| pane.set_active(false, cx));
1628 self.active_pane = pane.clone();
1629 self.active_pane
1630 .update(cx, |pane, cx| pane.set_active(true, cx));
1631 self.status_bar.update(cx, |status_bar, cx| {
1632 status_bar.set_active_pane(&self.active_pane, cx);
1633 });
1634 self.active_item_path_changed(cx);
1635 self.last_active_center_pane = Some(pane.downgrade());
1636 cx.notify();
1637 }
1638
1639 self.update_followers(
1640 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1641 id: self.active_item(cx).and_then(|item| {
1642 item.to_followable_item_handle(cx)?
1643 .remote_id(&self.app_state.client, cx)
1644 .map(|id| id.to_proto())
1645 }),
1646 leader_id: self.leader_for_pane(&pane),
1647 }),
1648 cx,
1649 );
1650 }
1651
1652 fn handle_pane_event(
1653 &mut self,
1654 pane: ViewHandle<Pane>,
1655 event: &pane::Event,
1656 cx: &mut ViewContext<Self>,
1657 ) {
1658 match event {
1659 pane::Event::Split(direction) => {
1660 self.split_pane(pane, *direction, cx);
1661 }
1662 pane::Event::Remove => self.remove_pane(pane, cx),
1663 pane::Event::ActivateItem { local } => {
1664 if *local {
1665 self.unfollow(&pane, cx);
1666 }
1667 if &pane == self.active_pane() {
1668 self.active_item_path_changed(cx);
1669 }
1670 }
1671 pane::Event::ChangeItemTitle => {
1672 if pane == self.active_pane {
1673 self.active_item_path_changed(cx);
1674 }
1675 self.update_window_edited(cx);
1676 }
1677 pane::Event::RemoveItem { item_id } => {
1678 self.update_window_edited(cx);
1679 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1680 if entry.get().id() == pane.id() {
1681 entry.remove();
1682 }
1683 }
1684 }
1685 pane::Event::Focus => {
1686 self.handle_pane_focused(pane.clone(), cx);
1687 }
1688 }
1689
1690 self.serialize_workspace(cx);
1691 }
1692
1693 pub fn split_pane(
1694 &mut self,
1695 pane: ViewHandle<Pane>,
1696 direction: SplitDirection,
1697 cx: &mut ViewContext<Self>,
1698 ) -> Option<ViewHandle<Pane>> {
1699 let item = pane.read(cx).active_item()?;
1700 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1701 let new_pane = self.add_pane(cx);
1702 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1703 self.center.split(&pane, &new_pane, direction).unwrap();
1704 Some(new_pane)
1705 } else {
1706 None
1707 };
1708 cx.notify();
1709 maybe_pane_handle
1710 }
1711
1712 pub fn split_pane_with_item(
1713 &mut self,
1714 pane_to_split: WeakViewHandle<Pane>,
1715 split_direction: SplitDirection,
1716 from: WeakViewHandle<Pane>,
1717 item_id_to_move: usize,
1718 cx: &mut ViewContext<Self>,
1719 ) {
1720 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1721 let Some(from) = from.upgrade(cx) else { return; };
1722
1723 let new_pane = self.add_pane(cx);
1724 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1725 self.center
1726 .split(&pane_to_split, &new_pane, split_direction)
1727 .unwrap();
1728 cx.notify();
1729 }
1730
1731 pub fn split_pane_with_project_entry(
1732 &mut self,
1733 pane_to_split: WeakViewHandle<Pane>,
1734 split_direction: SplitDirection,
1735 project_entry: ProjectEntryId,
1736 cx: &mut ViewContext<Self>,
1737 ) -> Option<Task<Result<()>>> {
1738 let pane_to_split = pane_to_split.upgrade(cx)?;
1739 let new_pane = self.add_pane(cx);
1740 self.center
1741 .split(&pane_to_split, &new_pane, split_direction)
1742 .unwrap();
1743
1744 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1745 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1746 Some(cx.foreground().spawn(async move {
1747 task.await?;
1748 Ok(())
1749 }))
1750 }
1751
1752 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1753 if self.center.remove(&pane).unwrap() {
1754 self.force_remove_pane(&pane, cx);
1755 self.unfollow(&pane, cx);
1756 self.last_leaders_by_pane.remove(&pane.downgrade());
1757 for removed_item in pane.read(cx).items() {
1758 self.panes_by_item.remove(&removed_item.id());
1759 }
1760
1761 cx.notify();
1762 } else {
1763 self.active_item_path_changed(cx);
1764 }
1765 }
1766
1767 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1768 &self.panes
1769 }
1770
1771 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1772 &self.active_pane
1773 }
1774
1775 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1776 if let Some(remote_id) = remote_id {
1777 self.remote_entity_subscription = Some(
1778 self.app_state
1779 .client
1780 .add_view_for_remote_entity(remote_id, cx),
1781 );
1782 } else {
1783 self.remote_entity_subscription.take();
1784 }
1785 }
1786
1787 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1788 self.leader_state.followers.remove(&peer_id);
1789 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1790 for state in states_by_pane.into_values() {
1791 for item in state.items_by_leader_view_id.into_values() {
1792 item.set_leader_replica_id(None, cx);
1793 }
1794 }
1795 }
1796 cx.notify();
1797 }
1798
1799 pub fn toggle_follow(
1800 &mut self,
1801 leader_id: PeerId,
1802 cx: &mut ViewContext<Self>,
1803 ) -> Option<Task<Result<()>>> {
1804 let pane = self.active_pane().clone();
1805
1806 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1807 if leader_id == prev_leader_id {
1808 return None;
1809 }
1810 }
1811
1812 self.last_leaders_by_pane
1813 .insert(pane.downgrade(), leader_id);
1814 self.follower_states_by_leader
1815 .entry(leader_id)
1816 .or_default()
1817 .insert(pane.clone(), Default::default());
1818 cx.notify();
1819
1820 let project_id = self.project.read(cx).remote_id()?;
1821 let request = self.app_state.client.request(proto::Follow {
1822 project_id,
1823 leader_id: Some(leader_id),
1824 });
1825
1826 Some(cx.spawn(|this, mut cx| async move {
1827 let response = request.await?;
1828 this.update(&mut cx, |this, _| {
1829 let state = this
1830 .follower_states_by_leader
1831 .get_mut(&leader_id)
1832 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1833 .ok_or_else(|| anyhow!("following interrupted"))?;
1834 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1835 Some(ViewId::from_proto(active_view_id)?)
1836 } else {
1837 None
1838 };
1839 Ok::<_, anyhow::Error>(())
1840 })??;
1841 Self::add_views_from_leader(
1842 this.clone(),
1843 leader_id,
1844 vec![pane],
1845 response.views,
1846 &mut cx,
1847 )
1848 .await?;
1849 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1850 Ok(())
1851 }))
1852 }
1853
1854 pub fn follow_next_collaborator(
1855 &mut self,
1856 _: &FollowNextCollaborator,
1857 cx: &mut ViewContext<Self>,
1858 ) -> Option<Task<Result<()>>> {
1859 let collaborators = self.project.read(cx).collaborators();
1860 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1861 let mut collaborators = collaborators.keys().copied();
1862 for peer_id in collaborators.by_ref() {
1863 if peer_id == leader_id {
1864 break;
1865 }
1866 }
1867 collaborators.next()
1868 } else if let Some(last_leader_id) =
1869 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1870 {
1871 if collaborators.contains_key(last_leader_id) {
1872 Some(*last_leader_id)
1873 } else {
1874 None
1875 }
1876 } else {
1877 None
1878 };
1879
1880 next_leader_id
1881 .or_else(|| collaborators.keys().copied().next())
1882 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1883 }
1884
1885 pub fn unfollow(
1886 &mut self,
1887 pane: &ViewHandle<Pane>,
1888 cx: &mut ViewContext<Self>,
1889 ) -> Option<PeerId> {
1890 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1891 let leader_id = *leader_id;
1892 if let Some(state) = states_by_pane.remove(pane) {
1893 for (_, item) in state.items_by_leader_view_id {
1894 item.set_leader_replica_id(None, cx);
1895 }
1896
1897 if states_by_pane.is_empty() {
1898 self.follower_states_by_leader.remove(&leader_id);
1899 if let Some(project_id) = self.project.read(cx).remote_id() {
1900 self.app_state
1901 .client
1902 .send(proto::Unfollow {
1903 project_id,
1904 leader_id: Some(leader_id),
1905 })
1906 .log_err();
1907 }
1908 }
1909
1910 cx.notify();
1911 return Some(leader_id);
1912 }
1913 }
1914 None
1915 }
1916
1917 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1918 self.follower_states_by_leader.contains_key(&peer_id)
1919 }
1920
1921 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
1922 self.leader_state.followers.contains(&peer_id)
1923 }
1924
1925 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1926 // TODO: There should be a better system in place for this
1927 // (https://github.com/zed-industries/zed/issues/1290)
1928 let is_fullscreen = cx.window_is_fullscreen();
1929 let container_theme = if is_fullscreen {
1930 let mut container_theme = theme.workspace.titlebar.container;
1931 container_theme.padding.left = container_theme.padding.right;
1932 container_theme
1933 } else {
1934 theme.workspace.titlebar.container
1935 };
1936
1937 enum TitleBar {}
1938 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
1939 Stack::new()
1940 .with_children(
1941 self.titlebar_item
1942 .as_ref()
1943 .map(|item| ChildView::new(item, cx)),
1944 )
1945 .contained()
1946 .with_style(container_theme)
1947 })
1948 .on_click(MouseButton::Left, |event, _, cx| {
1949 if event.click_count == 2 {
1950 cx.zoom_window();
1951 }
1952 })
1953 .constrained()
1954 .with_height(theme.workspace.titlebar.height)
1955 .into_any_named("titlebar")
1956 }
1957
1958 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1959 let active_entry = self.active_project_path(cx);
1960 self.project
1961 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1962 self.update_window_title(cx);
1963 }
1964
1965 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1966 let project = self.project().read(cx);
1967 let mut title = String::new();
1968
1969 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1970 let filename = path
1971 .path
1972 .file_name()
1973 .map(|s| s.to_string_lossy())
1974 .or_else(|| {
1975 Some(Cow::Borrowed(
1976 project
1977 .worktree_for_id(path.worktree_id, cx)?
1978 .read(cx)
1979 .root_name(),
1980 ))
1981 });
1982
1983 if let Some(filename) = filename {
1984 title.push_str(filename.as_ref());
1985 title.push_str(" β ");
1986 }
1987 }
1988
1989 for (i, name) in project.worktree_root_names(cx).enumerate() {
1990 if i > 0 {
1991 title.push_str(", ");
1992 }
1993 title.push_str(name);
1994 }
1995
1996 if title.is_empty() {
1997 title = "empty project".to_string();
1998 }
1999
2000 if project.is_remote() {
2001 title.push_str(" β");
2002 } else if project.is_shared() {
2003 title.push_str(" β");
2004 }
2005
2006 cx.set_window_title(&title);
2007 }
2008
2009 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2010 let is_edited = !self.project.read(cx).is_read_only()
2011 && self
2012 .items(cx)
2013 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2014 if is_edited != self.window_edited {
2015 self.window_edited = is_edited;
2016 cx.set_window_edited(self.window_edited)
2017 }
2018 }
2019
2020 fn render_disconnected_overlay(
2021 &self,
2022 cx: &mut ViewContext<Workspace>,
2023 ) -> Option<AnyElement<Workspace>> {
2024 if self.project.read(cx).is_read_only() {
2025 enum DisconnectedOverlay {}
2026 Some(
2027 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2028 let theme = &cx.global::<Settings>().theme;
2029 Label::new(
2030 "Your connection to the remote project has been lost.",
2031 theme.workspace.disconnected_overlay.text.clone(),
2032 )
2033 .aligned()
2034 .contained()
2035 .with_style(theme.workspace.disconnected_overlay.container)
2036 })
2037 .with_cursor_style(CursorStyle::Arrow)
2038 .capture_all()
2039 .into_any_named("disconnected overlay"),
2040 )
2041 } else {
2042 None
2043 }
2044 }
2045
2046 fn render_notifications(
2047 &self,
2048 theme: &theme::Workspace,
2049 cx: &AppContext,
2050 ) -> Option<AnyElement<Workspace>> {
2051 if self.notifications.is_empty() {
2052 None
2053 } else {
2054 Some(
2055 Flex::column()
2056 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2057 ChildView::new(notification.as_any(), cx)
2058 .contained()
2059 .with_style(theme.notification)
2060 }))
2061 .constrained()
2062 .with_width(theme.notifications.width)
2063 .contained()
2064 .with_style(theme.notifications.container)
2065 .aligned()
2066 .bottom()
2067 .right()
2068 .into_any(),
2069 )
2070 }
2071 }
2072
2073 // RPC handlers
2074
2075 async fn handle_follow(
2076 this: WeakViewHandle<Self>,
2077 envelope: TypedEnvelope<proto::Follow>,
2078 _: Arc<Client>,
2079 mut cx: AsyncAppContext,
2080 ) -> Result<proto::FollowResponse> {
2081 this.update(&mut cx, |this, cx| {
2082 let client = &this.app_state.client;
2083 this.leader_state
2084 .followers
2085 .insert(envelope.original_sender_id()?);
2086
2087 let active_view_id = this.active_item(cx).and_then(|i| {
2088 Some(
2089 i.to_followable_item_handle(cx)?
2090 .remote_id(client, cx)?
2091 .to_proto(),
2092 )
2093 });
2094
2095 cx.notify();
2096
2097 Ok(proto::FollowResponse {
2098 active_view_id,
2099 views: this
2100 .panes()
2101 .iter()
2102 .flat_map(|pane| {
2103 let leader_id = this.leader_for_pane(pane);
2104 pane.read(cx).items().filter_map({
2105 let cx = &cx;
2106 move |item| {
2107 let item = item.to_followable_item_handle(cx)?;
2108 let id = item.remote_id(client, cx)?.to_proto();
2109 let variant = item.to_state_proto(cx)?;
2110 Some(proto::View {
2111 id: Some(id),
2112 leader_id,
2113 variant: Some(variant),
2114 })
2115 }
2116 })
2117 })
2118 .collect(),
2119 })
2120 })?
2121 }
2122
2123 async fn handle_unfollow(
2124 this: WeakViewHandle<Self>,
2125 envelope: TypedEnvelope<proto::Unfollow>,
2126 _: Arc<Client>,
2127 mut cx: AsyncAppContext,
2128 ) -> Result<()> {
2129 this.update(&mut cx, |this, cx| {
2130 this.leader_state
2131 .followers
2132 .remove(&envelope.original_sender_id()?);
2133 cx.notify();
2134 Ok(())
2135 })?
2136 }
2137
2138 async fn handle_update_followers(
2139 this: WeakViewHandle<Self>,
2140 envelope: TypedEnvelope<proto::UpdateFollowers>,
2141 _: Arc<Client>,
2142 cx: AsyncAppContext,
2143 ) -> Result<()> {
2144 let leader_id = envelope.original_sender_id()?;
2145 this.read_with(&cx, |this, _| {
2146 this.leader_updates_tx
2147 .unbounded_send((leader_id, envelope.payload))
2148 })??;
2149 Ok(())
2150 }
2151
2152 async fn process_leader_update(
2153 this: &WeakViewHandle<Self>,
2154 leader_id: PeerId,
2155 update: proto::UpdateFollowers,
2156 cx: &mut AsyncAppContext,
2157 ) -> Result<()> {
2158 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2159 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2160 this.update(cx, |this, _| {
2161 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2162 for state in state.values_mut() {
2163 state.active_view_id =
2164 if let Some(active_view_id) = update_active_view.id.clone() {
2165 Some(ViewId::from_proto(active_view_id)?)
2166 } else {
2167 None
2168 };
2169 }
2170 }
2171 anyhow::Ok(())
2172 })??;
2173 }
2174 proto::update_followers::Variant::UpdateView(update_view) => {
2175 let variant = update_view
2176 .variant
2177 .ok_or_else(|| anyhow!("missing update view variant"))?;
2178 let id = update_view
2179 .id
2180 .ok_or_else(|| anyhow!("missing update view id"))?;
2181 let mut tasks = Vec::new();
2182 this.update(cx, |this, cx| {
2183 let project = this.project.clone();
2184 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2185 for state in state.values_mut() {
2186 let view_id = ViewId::from_proto(id.clone())?;
2187 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2188 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2189 }
2190 }
2191 }
2192 anyhow::Ok(())
2193 })??;
2194 try_join_all(tasks).await.log_err();
2195 }
2196 proto::update_followers::Variant::CreateView(view) => {
2197 let panes = this.read_with(cx, |this, _| {
2198 this.follower_states_by_leader
2199 .get(&leader_id)
2200 .into_iter()
2201 .flat_map(|states_by_pane| states_by_pane.keys())
2202 .cloned()
2203 .collect()
2204 })?;
2205 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2206 }
2207 }
2208 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2209 Ok(())
2210 }
2211
2212 async fn add_views_from_leader(
2213 this: WeakViewHandle<Self>,
2214 leader_id: PeerId,
2215 panes: Vec<ViewHandle<Pane>>,
2216 views: Vec<proto::View>,
2217 cx: &mut AsyncAppContext,
2218 ) -> Result<()> {
2219 let project = this.read_with(cx, |this, _| this.project.clone())?;
2220 let replica_id = project
2221 .read_with(cx, |project, _| {
2222 project
2223 .collaborators()
2224 .get(&leader_id)
2225 .map(|c| c.replica_id)
2226 })
2227 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2228
2229 let item_builders = cx.update(|cx| {
2230 cx.default_global::<FollowableItemBuilders>()
2231 .values()
2232 .map(|b| b.0)
2233 .collect::<Vec<_>>()
2234 });
2235
2236 let mut item_tasks_by_pane = HashMap::default();
2237 for pane in panes {
2238 let mut item_tasks = Vec::new();
2239 let mut leader_view_ids = Vec::new();
2240 for view in &views {
2241 let Some(id) = &view.id else { continue };
2242 let id = ViewId::from_proto(id.clone())?;
2243 let mut variant = view.variant.clone();
2244 if variant.is_none() {
2245 Err(anyhow!("missing variant"))?;
2246 }
2247 for build_item in &item_builders {
2248 let task = cx.update(|cx| {
2249 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2250 });
2251 if let Some(task) = task {
2252 item_tasks.push(task);
2253 leader_view_ids.push(id);
2254 break;
2255 } else {
2256 assert!(variant.is_some());
2257 }
2258 }
2259 }
2260
2261 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2262 }
2263
2264 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2265 let items = futures::future::try_join_all(item_tasks).await?;
2266 this.update(cx, |this, cx| {
2267 let state = this
2268 .follower_states_by_leader
2269 .get_mut(&leader_id)?
2270 .get_mut(&pane)?;
2271
2272 for (id, item) in leader_view_ids.into_iter().zip(items) {
2273 item.set_leader_replica_id(Some(replica_id), cx);
2274 state.items_by_leader_view_id.insert(id, item);
2275 }
2276
2277 Some(())
2278 })?;
2279 }
2280 Ok(())
2281 }
2282
2283 fn update_followers(
2284 &self,
2285 update: proto::update_followers::Variant,
2286 cx: &AppContext,
2287 ) -> Option<()> {
2288 let project_id = self.project.read(cx).remote_id()?;
2289 if !self.leader_state.followers.is_empty() {
2290 self.app_state
2291 .client
2292 .send(proto::UpdateFollowers {
2293 project_id,
2294 follower_ids: self.leader_state.followers.iter().copied().collect(),
2295 variant: Some(update),
2296 })
2297 .log_err();
2298 }
2299 None
2300 }
2301
2302 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2303 self.follower_states_by_leader
2304 .iter()
2305 .find_map(|(leader_id, state)| {
2306 if state.contains_key(pane) {
2307 Some(*leader_id)
2308 } else {
2309 None
2310 }
2311 })
2312 }
2313
2314 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2315 cx.notify();
2316
2317 let call = self.active_call()?;
2318 let room = call.read(cx).room()?.read(cx);
2319 let participant = room.remote_participant_for_peer_id(leader_id)?;
2320 let mut items_to_activate = Vec::new();
2321 match participant.location {
2322 call::ParticipantLocation::SharedProject { project_id } => {
2323 if Some(project_id) == self.project.read(cx).remote_id() {
2324 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2325 if let Some(item) = state
2326 .active_view_id
2327 .and_then(|id| state.items_by_leader_view_id.get(&id))
2328 {
2329 items_to_activate.push((pane.clone(), item.boxed_clone()));
2330 } else {
2331 if let Some(shared_screen) =
2332 self.shared_screen_for_peer(leader_id, pane, cx)
2333 {
2334 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2335 }
2336 }
2337 }
2338 }
2339 }
2340 call::ParticipantLocation::UnsharedProject => {}
2341 call::ParticipantLocation::External => {
2342 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2343 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2344 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2345 }
2346 }
2347 }
2348 }
2349
2350 for (pane, item) in items_to_activate {
2351 let pane_was_focused = pane.read(cx).has_focus();
2352 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2353 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2354 } else {
2355 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2356 }
2357
2358 if pane_was_focused {
2359 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2360 }
2361 }
2362
2363 None
2364 }
2365
2366 fn shared_screen_for_peer(
2367 &self,
2368 peer_id: PeerId,
2369 pane: &ViewHandle<Pane>,
2370 cx: &mut ViewContext<Self>,
2371 ) -> Option<ViewHandle<SharedScreen>> {
2372 let call = self.active_call()?;
2373 let room = call.read(cx).room()?.read(cx);
2374 let participant = room.remote_participant_for_peer_id(peer_id)?;
2375 let track = participant.tracks.values().next()?.clone();
2376 let user = participant.user.clone();
2377
2378 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2379 if item.read(cx).peer_id == peer_id {
2380 return Some(item);
2381 }
2382 }
2383
2384 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2385 }
2386
2387 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2388 if active {
2389 cx.background()
2390 .spawn(persistence::DB.update_timestamp(self.database_id()))
2391 .detach();
2392 } else {
2393 for pane in &self.panes {
2394 pane.update(cx, |pane, cx| {
2395 if let Some(item) = pane.active_item() {
2396 item.workspace_deactivated(cx);
2397 }
2398 if matches!(
2399 cx.global::<Settings>().autosave,
2400 Autosave::OnWindowChange | Autosave::OnFocusChange
2401 ) {
2402 for item in pane.items() {
2403 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2404 .detach_and_log_err(cx);
2405 }
2406 }
2407 });
2408 }
2409 }
2410 }
2411
2412 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2413 self.active_call.as_ref().map(|(call, _)| call)
2414 }
2415
2416 fn on_active_call_event(
2417 &mut self,
2418 _: ModelHandle<ActiveCall>,
2419 event: &call::room::Event,
2420 cx: &mut ViewContext<Self>,
2421 ) {
2422 match event {
2423 call::room::Event::ParticipantLocationChanged { participant_id }
2424 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2425 self.leader_updated(*participant_id, cx);
2426 }
2427 _ => {}
2428 }
2429 }
2430
2431 pub fn database_id(&self) -> WorkspaceId {
2432 self.database_id
2433 }
2434
2435 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2436 let project = self.project().read(cx);
2437
2438 if project.is_local() {
2439 Some(
2440 project
2441 .visible_worktrees(cx)
2442 .map(|worktree| worktree.read(cx).abs_path())
2443 .collect::<Vec<_>>()
2444 .into(),
2445 )
2446 } else {
2447 None
2448 }
2449 }
2450
2451 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2452 match member {
2453 Member::Axis(PaneAxis { members, .. }) => {
2454 for child in members.iter() {
2455 self.remove_panes(child.clone(), cx)
2456 }
2457 }
2458 Member::Pane(pane) => {
2459 self.force_remove_pane(&pane, cx);
2460 }
2461 }
2462 }
2463
2464 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2465 self.panes.retain(|p| p != pane);
2466 cx.focus(self.panes.last().unwrap());
2467 if self.last_active_center_pane == Some(pane.downgrade()) {
2468 self.last_active_center_pane = None;
2469 }
2470 cx.notify();
2471 }
2472
2473 fn serialize_workspace(&self, cx: &AppContext) {
2474 fn serialize_pane_handle(
2475 pane_handle: &ViewHandle<Pane>,
2476 cx: &AppContext,
2477 ) -> SerializedPane {
2478 let (items, active) = {
2479 let pane = pane_handle.read(cx);
2480 let active_item_id = pane.active_item().map(|item| item.id());
2481 (
2482 pane.items()
2483 .filter_map(|item_handle| {
2484 Some(SerializedItem {
2485 kind: Arc::from(item_handle.serialized_item_kind()?),
2486 item_id: item_handle.id(),
2487 active: Some(item_handle.id()) == active_item_id,
2488 })
2489 })
2490 .collect::<Vec<_>>(),
2491 pane.is_active(),
2492 )
2493 };
2494
2495 SerializedPane::new(items, active)
2496 }
2497
2498 fn build_serialized_pane_group(
2499 pane_group: &Member,
2500 cx: &AppContext,
2501 ) -> SerializedPaneGroup {
2502 match pane_group {
2503 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2504 axis: *axis,
2505 children: members
2506 .iter()
2507 .map(|member| build_serialized_pane_group(member, cx))
2508 .collect::<Vec<_>>(),
2509 },
2510 Member::Pane(pane_handle) => {
2511 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2512 }
2513 }
2514 }
2515
2516 if let Some(location) = self.location(cx) {
2517 // Load bearing special case:
2518 // - with_local_workspace() relies on this to not have other stuff open
2519 // when you open your log
2520 if !location.paths().is_empty() {
2521 let center_group = build_serialized_pane_group(&self.center.root, cx);
2522
2523 let serialized_workspace = SerializedWorkspace {
2524 id: self.database_id,
2525 location,
2526 center_group,
2527 left_sidebar_open: self.left_dock.read(cx).is_open(),
2528 bounds: Default::default(),
2529 display: Default::default(),
2530 };
2531
2532 cx.background()
2533 .spawn(persistence::DB.save_workspace(serialized_workspace))
2534 .detach();
2535 }
2536 }
2537 }
2538
2539 fn load_from_serialized_workspace(
2540 workspace: WeakViewHandle<Workspace>,
2541 serialized_workspace: SerializedWorkspace,
2542 cx: &mut AppContext,
2543 ) {
2544 cx.spawn(|mut cx| async move {
2545 let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
2546 (
2547 workspace.project().clone(),
2548 workspace.last_active_center_pane.clone(),
2549 )
2550 })?;
2551
2552 // Traverse the splits tree and add to things
2553 let center_group = serialized_workspace
2554 .center_group
2555 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2556 .await;
2557
2558 // Remove old panes from workspace panes list
2559 workspace.update(&mut cx, |workspace, cx| {
2560 if let Some((center_group, active_pane)) = center_group {
2561 workspace.remove_panes(workspace.center.root.clone(), cx);
2562
2563 // Swap workspace center group
2564 workspace.center = PaneGroup::with_root(center_group);
2565
2566 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2567 cx.focus_self();
2568
2569 if let Some(active_pane) = active_pane {
2570 cx.focus(&active_pane);
2571 } else {
2572 cx.focus(workspace.panes.last().unwrap());
2573 }
2574 } else {
2575 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2576 if let Some(old_center_handle) = old_center_handle {
2577 cx.focus(&old_center_handle)
2578 } else {
2579 cx.focus_self()
2580 }
2581 }
2582
2583 if workspace.left_dock().read(cx).is_open()
2584 != serialized_workspace.left_sidebar_open
2585 {
2586 workspace.toggle_dock(DockPosition::Left, cx);
2587 }
2588
2589 cx.notify();
2590 })?;
2591
2592 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2593 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2594 anyhow::Ok(())
2595 })
2596 .detach_and_log_err(cx);
2597 }
2598
2599 #[cfg(any(test, feature = "test-support"))]
2600 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2601 let app_state = Arc::new(AppState {
2602 languages: project.read(cx).languages().clone(),
2603 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2604 client: project.read(cx).client(),
2605 user_store: project.read(cx).user_store(),
2606 fs: project.read(cx).fs().clone(),
2607 build_window_options: |_, _, _| Default::default(),
2608 initialize_workspace: |_, _, _| {},
2609 background_actions: || &[],
2610 });
2611 Self::new(None, 0, project, app_state, cx)
2612 }
2613}
2614
2615fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2616 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2617
2618 workspace
2619 .update(cx, |workspace, cx| {
2620 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2621 workspace.show_notification_once(0, cx, |cx| {
2622 cx.add_view(|_| {
2623 MessageNotification::new("Failed to load any database file.")
2624 .with_click_message("Click to let us know about this error")
2625 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2626 })
2627 });
2628 } else {
2629 let backup_path = (*db::BACKUP_DB_PATH).read();
2630 if let Some(backup_path) = backup_path.clone() {
2631 workspace.show_notification_once(0, cx, move |cx| {
2632 cx.add_view(move |_| {
2633 MessageNotification::new(format!(
2634 "Database file was corrupted. Old database backed up to {}",
2635 backup_path.display()
2636 ))
2637 .with_click_message("Click to show old database in finder")
2638 .on_click(move |cx| {
2639 cx.platform().open_url(&backup_path.to_string_lossy())
2640 })
2641 })
2642 });
2643 }
2644 }
2645 })
2646 .log_err();
2647}
2648
2649impl Entity for Workspace {
2650 type Event = Event;
2651}
2652
2653impl View for Workspace {
2654 fn ui_name() -> &'static str {
2655 "Workspace"
2656 }
2657
2658 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2659 let theme = cx.global::<Settings>().theme.clone();
2660 Stack::new()
2661 .with_child(
2662 Flex::column()
2663 .with_child(self.render_titlebar(&theme, cx))
2664 .with_child(
2665 Stack::new()
2666 .with_child({
2667 let project = self.project.clone();
2668 Flex::row()
2669 .with_children(
2670 if self.left_dock.read(cx).active_panel().is_some() {
2671 Some(
2672 ChildView::new(&self.left_dock, cx)
2673 .constrained()
2674 .dynamically(|constraint, _, cx| {
2675 SizeConstraint::new(
2676 Vector2F::new(20., constraint.min.y()),
2677 Vector2F::new(
2678 cx.window_size().x() * 0.8,
2679 constraint.max.y(),
2680 ),
2681 )
2682 }),
2683 )
2684 } else {
2685 None
2686 },
2687 )
2688 .with_child(
2689 Flex::column()
2690 .with_child(
2691 FlexItem::new(self.center.render(
2692 &project,
2693 &theme,
2694 &self.follower_states_by_leader,
2695 self.active_call(),
2696 self.active_pane(),
2697 &self.app_state,
2698 cx,
2699 ))
2700 .flex(1., true),
2701 )
2702 .with_children(
2703 if self
2704 .bottom_dock
2705 .read(cx)
2706 .active_panel()
2707 .is_some()
2708 {
2709 Some(ChildView::new(&self.bottom_dock, cx))
2710 } else {
2711 None
2712 },
2713 )
2714 .flex(1., true),
2715 )
2716 .with_children(
2717 if self.right_dock.read(cx).active_panel().is_some() {
2718 Some(
2719 ChildView::new(&self.right_dock, cx)
2720 .constrained()
2721 .dynamically(|constraint, _, cx| {
2722 SizeConstraint::new(
2723 Vector2F::new(20., constraint.min.y()),
2724 Vector2F::new(
2725 cx.window_size().x() * 0.8,
2726 constraint.max.y(),
2727 ),
2728 )
2729 }),
2730 )
2731 } else {
2732 None
2733 },
2734 )
2735 })
2736 .with_child(Overlay::new(
2737 Stack::new()
2738 .with_children(self.modal.as_ref().map(|modal| {
2739 ChildView::new(modal, cx)
2740 .contained()
2741 .with_style(theme.workspace.modal)
2742 .aligned()
2743 .top()
2744 }))
2745 .with_children(self.render_notifications(&theme.workspace, cx)),
2746 ))
2747 .flex(1.0, true),
2748 )
2749 .with_child(ChildView::new(&self.status_bar, cx))
2750 .contained()
2751 .with_background_color(theme.workspace.background),
2752 )
2753 .with_children(DragAndDrop::render(cx))
2754 .with_children(self.render_disconnected_overlay(cx))
2755 .into_any_named("workspace")
2756 }
2757
2758 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2759 if cx.is_self_focused() {
2760 cx.focus(&self.active_pane);
2761 }
2762 }
2763}
2764
2765impl ViewId {
2766 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2767 Ok(Self {
2768 creator: message
2769 .creator
2770 .ok_or_else(|| anyhow!("creator is missing"))?,
2771 id: message.id,
2772 })
2773 }
2774
2775 pub(crate) fn to_proto(&self) -> proto::ViewId {
2776 proto::ViewId {
2777 creator: Some(self.creator),
2778 id: self.id,
2779 }
2780 }
2781}
2782
2783pub trait WorkspaceHandle {
2784 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2785}
2786
2787impl WorkspaceHandle for ViewHandle<Workspace> {
2788 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2789 self.read(cx)
2790 .worktrees(cx)
2791 .flat_map(|worktree| {
2792 let worktree_id = worktree.read(cx).id();
2793 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2794 worktree_id,
2795 path: f.path.clone(),
2796 })
2797 })
2798 .collect::<Vec<_>>()
2799 }
2800}
2801
2802impl std::fmt::Debug for OpenPaths {
2803 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2804 f.debug_struct("OpenPaths")
2805 .field("paths", &self.paths)
2806 .finish()
2807 }
2808}
2809
2810pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2811
2812pub fn activate_workspace_for_project(
2813 cx: &mut AsyncAppContext,
2814 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2815) -> Option<WeakViewHandle<Workspace>> {
2816 for window_id in cx.window_ids() {
2817 let handle = cx
2818 .update_window(window_id, |cx| {
2819 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2820 let project = workspace_handle.read(cx).project.clone();
2821 if project.update(cx, &predicate) {
2822 cx.activate_window();
2823 return Some(workspace_handle.clone());
2824 }
2825 }
2826 None
2827 })
2828 .flatten();
2829
2830 if let Some(handle) = handle {
2831 return Some(handle.downgrade());
2832 }
2833 }
2834 None
2835}
2836
2837pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2838 DB.last_workspace().await.log_err().flatten()
2839}
2840
2841#[allow(clippy::type_complexity)]
2842pub fn open_paths(
2843 abs_paths: &[PathBuf],
2844 app_state: &Arc<AppState>,
2845 requesting_window_id: Option<usize>,
2846 cx: &mut AppContext,
2847) -> Task<
2848 Result<(
2849 WeakViewHandle<Workspace>,
2850 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2851 )>,
2852> {
2853 log::info!("open paths {:?}", abs_paths);
2854
2855 let app_state = app_state.clone();
2856 let abs_paths = abs_paths.to_vec();
2857 cx.spawn(|mut cx| async move {
2858 // Open paths in existing workspace if possible
2859 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
2860 project.contains_paths(&abs_paths, cx)
2861 });
2862
2863 if let Some(existing) = existing {
2864 Ok((
2865 existing.clone(),
2866 existing
2867 .update(&mut cx, |workspace, cx| {
2868 workspace.open_paths(abs_paths, true, cx)
2869 })?
2870 .await,
2871 ))
2872 } else {
2873 let contains_directory =
2874 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2875 .await
2876 .contains(&false);
2877
2878 cx.update(|cx| {
2879 let task =
2880 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2881
2882 cx.spawn(|mut cx| async move {
2883 let (workspace, items) = task.await;
2884
2885 workspace.update(&mut cx, |workspace, cx| {
2886 if contains_directory {
2887 workspace.toggle_dock(DockPosition::Left, cx);
2888 }
2889 })?;
2890
2891 anyhow::Ok((workspace, items))
2892 })
2893 })
2894 .await
2895 }
2896 })
2897}
2898
2899pub fn open_new(
2900 app_state: &Arc<AppState>,
2901 cx: &mut AppContext,
2902 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2903) -> Task<()> {
2904 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2905 cx.spawn(|mut cx| async move {
2906 let (workspace, opened_paths) = task.await;
2907
2908 workspace
2909 .update(&mut cx, |workspace, cx| {
2910 if opened_paths.is_empty() {
2911 init(workspace, cx)
2912 }
2913 })
2914 .log_err();
2915 })
2916}
2917
2918pub fn create_and_open_local_file(
2919 path: &'static Path,
2920 cx: &mut ViewContext<Workspace>,
2921 default_content: impl 'static + Send + FnOnce() -> Rope,
2922) -> Task<Result<Box<dyn ItemHandle>>> {
2923 cx.spawn(|workspace, mut cx| async move {
2924 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
2925 if !fs.is_file(path).await {
2926 fs.create_file(path, Default::default()).await?;
2927 fs.save(path, &default_content(), Default::default())
2928 .await?;
2929 }
2930
2931 let mut items = workspace
2932 .update(&mut cx, |workspace, cx| {
2933 workspace.with_local_workspace(cx, |workspace, cx| {
2934 workspace.open_paths(vec![path.to_path_buf()], false, cx)
2935 })
2936 })?
2937 .await?
2938 .await;
2939
2940 let item = items.pop().flatten();
2941 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
2942 })
2943}
2944
2945pub fn join_remote_project(
2946 project_id: u64,
2947 follow_user_id: u64,
2948 app_state: Arc<AppState>,
2949 cx: &mut AppContext,
2950) -> Task<Result<()>> {
2951 cx.spawn(|mut cx| async move {
2952 let existing_workspace = cx
2953 .window_ids()
2954 .into_iter()
2955 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
2956 .find(|workspace| {
2957 cx.read_window(workspace.window_id(), |cx| {
2958 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
2959 })
2960 .unwrap_or(false)
2961 });
2962
2963 let workspace = if let Some(existing_workspace) = existing_workspace {
2964 existing_workspace.downgrade()
2965 } else {
2966 let active_call = cx.read(ActiveCall::global);
2967 let room = active_call
2968 .read_with(&cx, |call, _| call.room().cloned())
2969 .ok_or_else(|| anyhow!("not in a call"))?;
2970 let project = room
2971 .update(&mut cx, |room, cx| {
2972 room.join_project(
2973 project_id,
2974 app_state.languages.clone(),
2975 app_state.fs.clone(),
2976 cx,
2977 )
2978 })
2979 .await?;
2980
2981 let (_, workspace) = cx.add_window(
2982 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
2983 |cx| {
2984 let mut workspace =
2985 Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
2986 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2987 workspace
2988 },
2989 );
2990 workspace.downgrade()
2991 };
2992
2993 cx.activate_window(workspace.window_id());
2994 cx.platform().activate(true);
2995
2996 workspace.update(&mut cx, |workspace, cx| {
2997 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
2998 let follow_peer_id = room
2999 .read(cx)
3000 .remote_participants()
3001 .iter()
3002 .find(|(_, participant)| participant.user.id == follow_user_id)
3003 .map(|(_, p)| p.peer_id)
3004 .or_else(|| {
3005 // If we couldn't follow the given user, follow the host instead.
3006 let collaborator = workspace
3007 .project()
3008 .read(cx)
3009 .collaborators()
3010 .values()
3011 .find(|collaborator| collaborator.replica_id == 0)?;
3012 Some(collaborator.peer_id)
3013 });
3014
3015 if let Some(follow_peer_id) = follow_peer_id {
3016 if !workspace.is_being_followed(follow_peer_id) {
3017 workspace
3018 .toggle_follow(follow_peer_id, cx)
3019 .map(|follow| follow.detach_and_log_err(cx));
3020 }
3021 }
3022 }
3023 })?;
3024
3025 anyhow::Ok(())
3026 })
3027}
3028
3029pub fn restart(_: &Restart, cx: &mut AppContext) {
3030 let should_confirm = cx.global::<Settings>().confirm_quit;
3031 cx.spawn(|mut cx| async move {
3032 let mut workspaces = cx
3033 .window_ids()
3034 .into_iter()
3035 .filter_map(|window_id| {
3036 Some(
3037 cx.root_view(window_id)?
3038 .clone()
3039 .downcast::<Workspace>()?
3040 .downgrade(),
3041 )
3042 })
3043 .collect::<Vec<_>>();
3044
3045 // If multiple windows have unsaved changes, and need a save prompt,
3046 // prompt in the active window before switching to a different window.
3047 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3048
3049 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3050 let answer = cx.prompt(
3051 workspace.window_id(),
3052 PromptLevel::Info,
3053 "Are you sure you want to restart?",
3054 &["Restart", "Cancel"],
3055 );
3056
3057 if let Some(mut answer) = answer {
3058 let answer = answer.next().await;
3059 if answer != Some(0) {
3060 return Ok(());
3061 }
3062 }
3063 }
3064
3065 // If the user cancels any save prompt, then keep the app open.
3066 for workspace in workspaces {
3067 if !workspace
3068 .update(&mut cx, |workspace, cx| {
3069 workspace.prepare_to_close(true, cx)
3070 })?
3071 .await?
3072 {
3073 return Ok(());
3074 }
3075 }
3076 cx.platform().restart();
3077 anyhow::Ok(())
3078 })
3079 .detach_and_log_err(cx);
3080}
3081
3082fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3083 let mut parts = value.split(',');
3084 let width: usize = parts.next()?.parse().ok()?;
3085 let height: usize = parts.next()?.parse().ok()?;
3086 Some(vec2f(width as f32, height as f32))
3087}
3088
3089#[cfg(test)]
3090mod tests {
3091 use std::{cell::RefCell, rc::Rc};
3092
3093 use crate::{
3094 dock::test::TestPanel,
3095 item::test::{TestItem, TestItemEvent, TestProjectItem},
3096 };
3097
3098 use super::*;
3099 use fs::FakeFs;
3100 use gpui::{executor::Deterministic, TestAppContext};
3101 use project::{Project, ProjectEntryId};
3102 use serde_json::json;
3103
3104 #[gpui::test]
3105 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3106 cx.foreground().forbid_parking();
3107 Settings::test_async(cx);
3108
3109 let fs = FakeFs::new(cx.background());
3110 let project = Project::test(fs, [], cx).await;
3111 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3112
3113 // Adding an item with no ambiguity renders the tab without detail.
3114 let item1 = cx.add_view(window_id, |_| {
3115 let mut item = TestItem::new();
3116 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3117 item
3118 });
3119 workspace.update(cx, |workspace, cx| {
3120 workspace.add_item(Box::new(item1.clone()), cx);
3121 });
3122 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3123
3124 // Adding an item that creates ambiguity increases the level of detail on
3125 // both tabs.
3126 let item2 = cx.add_view(window_id, |_| {
3127 let mut item = TestItem::new();
3128 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3129 item
3130 });
3131 workspace.update(cx, |workspace, cx| {
3132 workspace.add_item(Box::new(item2.clone()), cx);
3133 });
3134 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3135 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3136
3137 // Adding an item that creates ambiguity increases the level of detail only
3138 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3139 // we stop at the highest detail available.
3140 let item3 = cx.add_view(window_id, |_| {
3141 let mut item = TestItem::new();
3142 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3143 item
3144 });
3145 workspace.update(cx, |workspace, cx| {
3146 workspace.add_item(Box::new(item3.clone()), cx);
3147 });
3148 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3149 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3150 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3151 }
3152
3153 #[gpui::test]
3154 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3155 cx.foreground().forbid_parking();
3156 Settings::test_async(cx);
3157 let fs = FakeFs::new(cx.background());
3158 fs.insert_tree(
3159 "/root1",
3160 json!({
3161 "one.txt": "",
3162 "two.txt": "",
3163 }),
3164 )
3165 .await;
3166 fs.insert_tree(
3167 "/root2",
3168 json!({
3169 "three.txt": "",
3170 }),
3171 )
3172 .await;
3173
3174 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3175 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3176 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3177 let worktree_id = project.read_with(cx, |project, cx| {
3178 project.worktrees(cx).next().unwrap().read(cx).id()
3179 });
3180
3181 let item1 = cx.add_view(window_id, |cx| {
3182 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3183 });
3184 let item2 = cx.add_view(window_id, |cx| {
3185 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3186 });
3187
3188 // Add an item to an empty pane
3189 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3190 project.read_with(cx, |project, cx| {
3191 assert_eq!(
3192 project.active_entry(),
3193 project
3194 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3195 .map(|e| e.id)
3196 );
3197 });
3198 assert_eq!(
3199 cx.current_window_title(window_id).as_deref(),
3200 Some("one.txt β root1")
3201 );
3202
3203 // Add a second item to a non-empty pane
3204 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3205 assert_eq!(
3206 cx.current_window_title(window_id).as_deref(),
3207 Some("two.txt β root1")
3208 );
3209 project.read_with(cx, |project, cx| {
3210 assert_eq!(
3211 project.active_entry(),
3212 project
3213 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3214 .map(|e| e.id)
3215 );
3216 });
3217
3218 // Close the active item
3219 pane.update(cx, |pane, cx| {
3220 pane.close_active_item(&Default::default(), cx).unwrap()
3221 })
3222 .await
3223 .unwrap();
3224 assert_eq!(
3225 cx.current_window_title(window_id).as_deref(),
3226 Some("one.txt β root1")
3227 );
3228 project.read_with(cx, |project, cx| {
3229 assert_eq!(
3230 project.active_entry(),
3231 project
3232 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3233 .map(|e| e.id)
3234 );
3235 });
3236
3237 // Add a project folder
3238 project
3239 .update(cx, |project, cx| {
3240 project.find_or_create_local_worktree("/root2", true, cx)
3241 })
3242 .await
3243 .unwrap();
3244 assert_eq!(
3245 cx.current_window_title(window_id).as_deref(),
3246 Some("one.txt β root1, root2")
3247 );
3248
3249 // Remove a project folder
3250 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3251 assert_eq!(
3252 cx.current_window_title(window_id).as_deref(),
3253 Some("one.txt β root2")
3254 );
3255 }
3256
3257 #[gpui::test]
3258 async fn test_close_window(cx: &mut TestAppContext) {
3259 cx.foreground().forbid_parking();
3260 Settings::test_async(cx);
3261 let fs = FakeFs::new(cx.background());
3262 fs.insert_tree("/root", json!({ "one": "" })).await;
3263
3264 let project = Project::test(fs, ["root".as_ref()], cx).await;
3265 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3266
3267 // When there are no dirty items, there's nothing to do.
3268 let item1 = cx.add_view(window_id, |_| TestItem::new());
3269 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3270 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3271 assert!(task.await.unwrap());
3272
3273 // When there are dirty untitled items, prompt to save each one. If the user
3274 // cancels any prompt, then abort.
3275 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3276 let item3 = cx.add_view(window_id, |cx| {
3277 TestItem::new()
3278 .with_dirty(true)
3279 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3280 });
3281 workspace.update(cx, |w, cx| {
3282 w.add_item(Box::new(item2.clone()), cx);
3283 w.add_item(Box::new(item3.clone()), cx);
3284 });
3285 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3286 cx.foreground().run_until_parked();
3287 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3288 cx.foreground().run_until_parked();
3289 assert!(!cx.has_pending_prompt(window_id));
3290 assert!(!task.await.unwrap());
3291 }
3292
3293 #[gpui::test]
3294 async fn test_close_pane_items(cx: &mut TestAppContext) {
3295 cx.foreground().forbid_parking();
3296 Settings::test_async(cx);
3297 let fs = FakeFs::new(cx.background());
3298
3299 let project = Project::test(fs, None, cx).await;
3300 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3301
3302 let item1 = cx.add_view(window_id, |cx| {
3303 TestItem::new()
3304 .with_dirty(true)
3305 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3306 });
3307 let item2 = cx.add_view(window_id, |cx| {
3308 TestItem::new()
3309 .with_dirty(true)
3310 .with_conflict(true)
3311 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3312 });
3313 let item3 = cx.add_view(window_id, |cx| {
3314 TestItem::new()
3315 .with_dirty(true)
3316 .with_conflict(true)
3317 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3318 });
3319 let item4 = cx.add_view(window_id, |cx| {
3320 TestItem::new()
3321 .with_dirty(true)
3322 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3323 });
3324 let pane = workspace.update(cx, |workspace, cx| {
3325 workspace.add_item(Box::new(item1.clone()), cx);
3326 workspace.add_item(Box::new(item2.clone()), cx);
3327 workspace.add_item(Box::new(item3.clone()), cx);
3328 workspace.add_item(Box::new(item4.clone()), cx);
3329 workspace.active_pane().clone()
3330 });
3331
3332 let close_items = pane.update(cx, |pane, cx| {
3333 pane.activate_item(1, true, true, cx);
3334 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3335 let item1_id = item1.id();
3336 let item3_id = item3.id();
3337 let item4_id = item4.id();
3338 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3339 });
3340 cx.foreground().run_until_parked();
3341
3342 // There's a prompt to save item 1.
3343 pane.read_with(cx, |pane, _| {
3344 assert_eq!(pane.items_len(), 4);
3345 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3346 });
3347 assert!(cx.has_pending_prompt(window_id));
3348
3349 // Confirm saving item 1.
3350 cx.simulate_prompt_answer(window_id, 0);
3351 cx.foreground().run_until_parked();
3352
3353 // Item 1 is saved. There's a prompt to save item 3.
3354 pane.read_with(cx, |pane, cx| {
3355 assert_eq!(item1.read(cx).save_count, 1);
3356 assert_eq!(item1.read(cx).save_as_count, 0);
3357 assert_eq!(item1.read(cx).reload_count, 0);
3358 assert_eq!(pane.items_len(), 3);
3359 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3360 });
3361 assert!(cx.has_pending_prompt(window_id));
3362
3363 // Cancel saving item 3.
3364 cx.simulate_prompt_answer(window_id, 1);
3365 cx.foreground().run_until_parked();
3366
3367 // Item 3 is reloaded. There's a prompt to save item 4.
3368 pane.read_with(cx, |pane, cx| {
3369 assert_eq!(item3.read(cx).save_count, 0);
3370 assert_eq!(item3.read(cx).save_as_count, 0);
3371 assert_eq!(item3.read(cx).reload_count, 1);
3372 assert_eq!(pane.items_len(), 2);
3373 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3374 });
3375 assert!(cx.has_pending_prompt(window_id));
3376
3377 // Confirm saving item 4.
3378 cx.simulate_prompt_answer(window_id, 0);
3379 cx.foreground().run_until_parked();
3380
3381 // There's a prompt for a path for item 4.
3382 cx.simulate_new_path_selection(|_| Some(Default::default()));
3383 close_items.await.unwrap();
3384
3385 // The requested items are closed.
3386 pane.read_with(cx, |pane, cx| {
3387 assert_eq!(item4.read(cx).save_count, 0);
3388 assert_eq!(item4.read(cx).save_as_count, 1);
3389 assert_eq!(item4.read(cx).reload_count, 0);
3390 assert_eq!(pane.items_len(), 1);
3391 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3392 });
3393 }
3394
3395 #[gpui::test]
3396 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3397 cx.foreground().forbid_parking();
3398 Settings::test_async(cx);
3399 let fs = FakeFs::new(cx.background());
3400
3401 let project = Project::test(fs, [], cx).await;
3402 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3403
3404 // Create several workspace items with single project entries, and two
3405 // workspace items with multiple project entries.
3406 let single_entry_items = (0..=4)
3407 .map(|project_entry_id| {
3408 cx.add_view(window_id, |cx| {
3409 TestItem::new()
3410 .with_dirty(true)
3411 .with_project_items(&[TestProjectItem::new(
3412 project_entry_id,
3413 &format!("{project_entry_id}.txt"),
3414 cx,
3415 )])
3416 })
3417 })
3418 .collect::<Vec<_>>();
3419 let item_2_3 = cx.add_view(window_id, |cx| {
3420 TestItem::new()
3421 .with_dirty(true)
3422 .with_singleton(false)
3423 .with_project_items(&[
3424 single_entry_items[2].read(cx).project_items[0].clone(),
3425 single_entry_items[3].read(cx).project_items[0].clone(),
3426 ])
3427 });
3428 let item_3_4 = cx.add_view(window_id, |cx| {
3429 TestItem::new()
3430 .with_dirty(true)
3431 .with_singleton(false)
3432 .with_project_items(&[
3433 single_entry_items[3].read(cx).project_items[0].clone(),
3434 single_entry_items[4].read(cx).project_items[0].clone(),
3435 ])
3436 });
3437
3438 // Create two panes that contain the following project entries:
3439 // left pane:
3440 // multi-entry items: (2, 3)
3441 // single-entry items: 0, 1, 2, 3, 4
3442 // right pane:
3443 // single-entry items: 1
3444 // multi-entry items: (3, 4)
3445 let left_pane = workspace.update(cx, |workspace, cx| {
3446 let left_pane = workspace.active_pane().clone();
3447 workspace.add_item(Box::new(item_2_3.clone()), cx);
3448 for item in single_entry_items {
3449 workspace.add_item(Box::new(item), cx);
3450 }
3451 left_pane.update(cx, |pane, cx| {
3452 pane.activate_item(2, true, true, cx);
3453 });
3454
3455 workspace
3456 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3457 .unwrap();
3458
3459 left_pane
3460 });
3461
3462 //Need to cause an effect flush in order to respect new focus
3463 workspace.update(cx, |workspace, cx| {
3464 workspace.add_item(Box::new(item_3_4.clone()), cx);
3465 cx.focus(&left_pane);
3466 });
3467
3468 // When closing all of the items in the left pane, we should be prompted twice:
3469 // once for project entry 0, and once for project entry 2. After those two
3470 // prompts, the task should complete.
3471
3472 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3473 cx.foreground().run_until_parked();
3474 left_pane.read_with(cx, |pane, cx| {
3475 assert_eq!(
3476 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3477 &[ProjectEntryId::from_proto(0)]
3478 );
3479 });
3480 cx.simulate_prompt_answer(window_id, 0);
3481
3482 cx.foreground().run_until_parked();
3483 left_pane.read_with(cx, |pane, cx| {
3484 assert_eq!(
3485 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3486 &[ProjectEntryId::from_proto(2)]
3487 );
3488 });
3489 cx.simulate_prompt_answer(window_id, 0);
3490
3491 cx.foreground().run_until_parked();
3492 close.await.unwrap();
3493 left_pane.read_with(cx, |pane, _| {
3494 assert_eq!(pane.items_len(), 0);
3495 });
3496 }
3497
3498 #[gpui::test]
3499 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3500 deterministic.forbid_parking();
3501
3502 Settings::test_async(cx);
3503 let fs = FakeFs::new(cx.background());
3504
3505 let project = Project::test(fs, [], cx).await;
3506 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3507 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3508
3509 let item = cx.add_view(window_id, |cx| {
3510 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3511 });
3512 let item_id = item.id();
3513 workspace.update(cx, |workspace, cx| {
3514 workspace.add_item(Box::new(item.clone()), cx);
3515 });
3516
3517 // Autosave on window change.
3518 item.update(cx, |item, cx| {
3519 cx.update_global(|settings: &mut Settings, _| {
3520 settings.autosave = Autosave::OnWindowChange;
3521 });
3522 item.is_dirty = true;
3523 });
3524
3525 // Deactivating the window saves the file.
3526 cx.simulate_window_activation(None);
3527 deterministic.run_until_parked();
3528 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3529
3530 // Autosave on focus change.
3531 item.update(cx, |item, cx| {
3532 cx.focus_self();
3533 cx.update_global(|settings: &mut Settings, _| {
3534 settings.autosave = Autosave::OnFocusChange;
3535 });
3536 item.is_dirty = true;
3537 });
3538
3539 // Blurring the item saves the file.
3540 item.update(cx, |_, cx| cx.blur());
3541 deterministic.run_until_parked();
3542 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3543
3544 // Deactivating the window still saves the file.
3545 cx.simulate_window_activation(Some(window_id));
3546 item.update(cx, |item, cx| {
3547 cx.focus_self();
3548 item.is_dirty = true;
3549 });
3550 cx.simulate_window_activation(None);
3551
3552 deterministic.run_until_parked();
3553 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3554
3555 // Autosave after delay.
3556 item.update(cx, |item, cx| {
3557 cx.update_global(|settings: &mut Settings, _| {
3558 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3559 });
3560 item.is_dirty = true;
3561 cx.emit(TestItemEvent::Edit);
3562 });
3563
3564 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3565 deterministic.advance_clock(Duration::from_millis(250));
3566 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3567
3568 // After delay expires, the file is saved.
3569 deterministic.advance_clock(Duration::from_millis(250));
3570 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3571
3572 // Autosave on focus change, ensuring closing the tab counts as such.
3573 item.update(cx, |item, cx| {
3574 cx.update_global(|settings: &mut Settings, _| {
3575 settings.autosave = Autosave::OnFocusChange;
3576 });
3577 item.is_dirty = true;
3578 });
3579
3580 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3581 .await
3582 .unwrap();
3583 assert!(!cx.has_pending_prompt(window_id));
3584 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3585
3586 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3587 workspace.update(cx, |workspace, cx| {
3588 workspace.add_item(Box::new(item.clone()), cx);
3589 });
3590 item.update(cx, |item, cx| {
3591 item.project_items[0].update(cx, |item, _| {
3592 item.entry_id = None;
3593 });
3594 item.is_dirty = true;
3595 cx.blur();
3596 });
3597 deterministic.run_until_parked();
3598 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3599
3600 // Ensure autosave is prevented for deleted files also when closing the buffer.
3601 let _close_items =
3602 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3603 deterministic.run_until_parked();
3604 assert!(cx.has_pending_prompt(window_id));
3605 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3606 }
3607
3608 #[gpui::test]
3609 async fn test_pane_navigation(
3610 deterministic: Arc<Deterministic>,
3611 cx: &mut gpui::TestAppContext,
3612 ) {
3613 deterministic.forbid_parking();
3614 Settings::test_async(cx);
3615 let fs = FakeFs::new(cx.background());
3616
3617 let project = Project::test(fs, [], cx).await;
3618 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3619
3620 let item = cx.add_view(window_id, |cx| {
3621 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3622 });
3623 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3624 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3625 let toolbar_notify_count = Rc::new(RefCell::new(0));
3626
3627 workspace.update(cx, |workspace, cx| {
3628 workspace.add_item(Box::new(item.clone()), cx);
3629 let toolbar_notification_count = toolbar_notify_count.clone();
3630 cx.observe(&toolbar, move |_, _, _| {
3631 *toolbar_notification_count.borrow_mut() += 1
3632 })
3633 .detach();
3634 });
3635
3636 pane.read_with(cx, |pane, _| {
3637 assert!(!pane.can_navigate_backward());
3638 assert!(!pane.can_navigate_forward());
3639 });
3640
3641 item.update(cx, |item, cx| {
3642 item.set_state("one".to_string(), cx);
3643 });
3644
3645 // Toolbar must be notified to re-render the navigation buttons
3646 assert_eq!(*toolbar_notify_count.borrow(), 1);
3647
3648 pane.read_with(cx, |pane, _| {
3649 assert!(pane.can_navigate_backward());
3650 assert!(!pane.can_navigate_forward());
3651 });
3652
3653 workspace
3654 .update(cx, |workspace, cx| {
3655 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3656 })
3657 .await
3658 .unwrap();
3659
3660 assert_eq!(*toolbar_notify_count.borrow(), 3);
3661 pane.read_with(cx, |pane, _| {
3662 assert!(!pane.can_navigate_backward());
3663 assert!(pane.can_navigate_forward());
3664 });
3665 }
3666
3667 #[gpui::test]
3668 async fn test_panels(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3669 deterministic.forbid_parking();
3670 Settings::test_async(cx);
3671 let fs = FakeFs::new(cx.background());
3672
3673 let project = Project::test(fs, [], cx).await;
3674 let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3675
3676 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3677 // Add panel_1 on the left, panel_2 on the right.
3678 let panel_1 = cx.add_view(|_| TestPanel {
3679 position: DockPosition::Left,
3680 });
3681 workspace.add_panel(panel_1.clone(), cx);
3682 workspace
3683 .left_dock()
3684 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3685 let panel_2 = cx.add_view(|_| TestPanel {
3686 position: DockPosition::Right,
3687 });
3688 workspace.add_panel(panel_2.clone(), cx);
3689 workspace
3690 .right_dock()
3691 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3692
3693 let left_dock = workspace.left_dock();
3694 assert_eq!(
3695 left_dock.read(cx).active_panel().unwrap().id(),
3696 panel_1.id()
3697 );
3698 assert_eq!(
3699 left_dock.read(cx).active_panel_size().unwrap(),
3700 panel_1.default_size(cx)
3701 );
3702
3703 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3704 assert_eq!(
3705 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3706 panel_2.id()
3707 );
3708
3709 (panel_1, panel_2)
3710 });
3711
3712 // Move panel_1 to the right
3713 panel_1.update(cx, |panel_1, cx| {
3714 panel_1.set_position(DockPosition::Right, cx)
3715 });
3716
3717 workspace.update(cx, |workspace, cx| {
3718 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
3719 // Since it was the only panel on the left, the left dock should now be closed.
3720 assert!(!workspace.left_dock().read(cx).is_open());
3721 assert!(workspace.left_dock().read(cx).active_panel().is_none());
3722 let right_dock = workspace.right_dock();
3723 assert_eq!(
3724 right_dock.read(cx).active_panel().unwrap().id(),
3725 panel_1.id()
3726 );
3727 assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.);
3728
3729 // Now we move panel_2Β to the left
3730 panel_2.set_position(DockPosition::Left, cx);
3731 });
3732
3733 workspace.update(cx, |workspace, cx| {
3734 // Since panel_2 was not visible on the right, we don't open the left dock.
3735 assert!(!workspace.left_dock().read(cx).is_open());
3736 // And the right dock is unaffected in it's displaying of panel_1
3737 assert!(workspace.right_dock().read(cx).is_open());
3738 assert_eq!(
3739 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3740 panel_1.id()
3741 );
3742 });
3743
3744 // Move panel_1 back to the left
3745 panel_1.update(cx, |panel_1, cx| {
3746 panel_1.set_position(DockPosition::Left, cx)
3747 });
3748
3749 workspace.update(cx, |workspace, cx| {
3750 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
3751 let left_dock = workspace.left_dock();
3752 assert!(left_dock.read(cx).is_open());
3753 assert_eq!(
3754 left_dock.read(cx).active_panel().unwrap().id(),
3755 panel_1.id()
3756 );
3757 assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.);
3758 // And right the dock should be closed as it no longer has any panels.
3759 assert!(!workspace.right_dock().read(cx).is_open());
3760
3761 // Now we move panel_1 to the bottom
3762 panel_1.set_position(DockPosition::Bottom, cx);
3763 });
3764
3765 workspace.update(cx, |workspace, cx| {
3766 // Since panel_1 was visible on the left, we close the left dock.
3767 assert!(!workspace.left_dock().read(cx).is_open());
3768 // The bottom dock is sized based on the panel's default size,
3769 // since the panel orientation changed from vertical to horizontal.
3770 assert_eq!(
3771 workspace
3772 .bottom_dock()
3773 .read(cx)
3774 .active_panel_size()
3775 .unwrap(),
3776 panel_1.default_size(cx),
3777 );
3778 });
3779 }
3780}