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