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