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