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