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;
15mod workspace_settings;
16
17use anyhow::{anyhow, Context, Result};
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, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
43 ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
44 WeakViewHandle, WindowContext,
45};
46use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
47use itertools::Itertools;
48use language::{LanguageRegistry, Rope};
49use std::{
50 any::TypeId,
51 borrow::Cow,
52 cmp, env,
53 future::Future,
54 path::{Path, PathBuf},
55 rc::Rc,
56 str,
57 sync::{atomic::AtomicUsize, Arc},
58 time::Duration,
59};
60
61use crate::{
62 notifications::{simple_message_notification::MessageNotification, NotificationTracker},
63 persistence::model::{
64 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
65 },
66};
67use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
68use lazy_static::lazy_static;
69use notifications::{NotificationHandle, NotifyResultExt};
70pub use pane::*;
71pub use pane_group::*;
72use persistence::{model::SerializedItem, DB};
73pub use persistence::{
74 model::{ItemId, WorkspaceLocation},
75 WorkspaceDb, DB as WORKSPACE_DB,
76};
77use postage::prelude::Stream;
78use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
79use serde::Deserialize;
80use shared_screen::SharedScreen;
81use status_bar::StatusBar;
82pub use status_bar::StatusItemView;
83use theme::{Theme, ThemeSettings};
84pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
85use util::{async_iife, ResultExt};
86pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
87
88lazy_static! {
89 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
94 .ok()
95 .as_deref()
96 .and_then(parse_pixel_position_env_var);
97}
98
99pub trait Modal: View {
100 fn dismiss_on_event(event: &Self::Event) -> bool;
101}
102
103#[derive(Clone, PartialEq)]
104pub struct RemoveWorktreeFromProject(pub WorktreeId);
105
106actions!(
107 workspace,
108 [
109 Open,
110 NewFile,
111 NewWindow,
112 CloseWindow,
113 AddFolderToProject,
114 Unfollow,
115 Save,
116 SaveAs,
117 SaveAll,
118 ActivatePreviousPane,
119 ActivateNextPane,
120 FollowNextCollaborator,
121 NewTerminal,
122 NewCenterTerminal,
123 ToggleTerminalFocus,
124 NewSearch,
125 Feedback,
126 Restart,
127 Welcome,
128 ToggleZoom,
129 ToggleLeftDock,
130 ToggleRightDock,
131 ToggleBottomDock,
132 ]
133);
134
135#[derive(Clone, PartialEq)]
136pub struct OpenPaths {
137 pub paths: Vec<PathBuf>,
138}
139
140#[derive(Clone, Deserialize, PartialEq)]
141pub struct ActivatePane(pub usize);
142
143#[derive(Deserialize)]
144pub struct Toast {
145 id: usize,
146 msg: Cow<'static, str>,
147 #[serde(skip)]
148 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
149}
150
151impl Toast {
152 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
153 Toast {
154 id,
155 msg: msg.into(),
156 on_click: None,
157 }
158 }
159
160 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
161 where
162 M: Into<Cow<'static, str>>,
163 F: Fn(&mut WindowContext) + 'static,
164 {
165 self.on_click = Some((message.into(), Arc::new(on_click)));
166 self
167 }
168}
169
170impl PartialEq for Toast {
171 fn eq(&self, other: &Self) -> bool {
172 self.id == other.id
173 && self.msg == other.msg
174 && self.on_click.is_some() == other.on_click.is_some()
175 }
176}
177
178impl Clone for Toast {
179 fn clone(&self) -> Self {
180 Toast {
181 id: self.id,
182 msg: self.msg.to_owned(),
183 on_click: self.on_click.clone(),
184 }
185 }
186}
187
188impl_actions!(workspace, [ActivatePane, Toast]);
189
190pub type WorkspaceId = i64;
191
192pub fn init_settings(cx: &mut AppContext) {
193 settings::register::<WorkspaceSettings>(cx);
194}
195
196pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
197 init_settings(cx);
198 pane::init(cx);
199 notifications::init(cx);
200
201 cx.add_global_action({
202 let app_state = Arc::downgrade(&app_state);
203 move |_: &Open, cx: &mut AppContext| {
204 let mut paths = cx.prompt_for_paths(PathPromptOptions {
205 files: true,
206 directories: true,
207 multiple: true,
208 });
209
210 if let Some(app_state) = app_state.upgrade() {
211 cx.spawn(move |mut cx| async move {
212 if let Some(paths) = paths.recv().await.flatten() {
213 cx.update(|cx| {
214 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
215 });
216 }
217 })
218 .detach();
219 }
220 }
221 });
222 cx.add_async_action(Workspace::open);
223
224 cx.add_async_action(Workspace::follow_next_collaborator);
225 cx.add_async_action(Workspace::close);
226 cx.add_global_action(Workspace::close_global);
227 cx.add_global_action(restart);
228 cx.add_async_action(Workspace::save_all);
229 cx.add_action(Workspace::add_folder_to_project);
230 cx.add_action(
231 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
232 let pane = workspace.active_pane().clone();
233 workspace.unfollow(&pane, cx);
234 },
235 );
236 cx.add_action(
237 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
238 workspace.save_active_item(false, cx).detach_and_log_err(cx);
239 },
240 );
241 cx.add_action(
242 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
243 workspace.save_active_item(true, cx).detach_and_log_err(cx);
244 },
245 );
246 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
247 workspace.activate_previous_pane(cx)
248 });
249 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
250 workspace.activate_next_pane(cx)
251 });
252 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
253 workspace.toggle_dock(DockPosition::Left, cx);
254 });
255 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
256 workspace.toggle_dock(DockPosition::Right, cx);
257 });
258 cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
259 workspace.toggle_dock(DockPosition::Bottom, cx);
260 });
261 cx.add_action(Workspace::activate_pane_at_index);
262 cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
263 workspace.reopen_closed_item(cx).detach();
264 });
265 cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
266 workspace
267 .go_back(workspace.active_pane().downgrade(), cx)
268 .detach();
269 });
270 cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
271 workspace
272 .go_forward(workspace.active_pane().downgrade(), cx)
273 .detach();
274 });
275
276 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
277 cx.spawn(|workspace, mut cx| async move {
278 let err = install_cli::install_cli(&cx)
279 .await
280 .context("Failed to create CLI symlink");
281
282 workspace.update(&mut cx, |workspace, cx| {
283 if matches!(err, Err(_)) {
284 err.notify_err(workspace, cx);
285 } else {
286 workspace.show_notification(1, cx, |cx| {
287 cx.add_view(|_| {
288 MessageNotification::new("Successfully installed the `zed` binary")
289 })
290 });
291 }
292 })
293 })
294 .detach();
295 });
296
297 let client = &app_state.client;
298 client.add_view_request_handler(Workspace::handle_follow);
299 client.add_view_message_handler(Workspace::handle_unfollow);
300 client.add_view_message_handler(Workspace::handle_update_followers);
301}
302
303type ProjectItemBuilders = HashMap<
304 TypeId,
305 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
306>;
307pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
308 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
309 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
310 let item = model.downcast::<I::Item>().unwrap();
311 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
312 });
313 });
314}
315
316type FollowableItemBuilder = fn(
317 ViewHandle<Pane>,
318 ModelHandle<Project>,
319 ViewId,
320 &mut Option<proto::view::Variant>,
321 &mut AppContext,
322) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
323type FollowableItemBuilders = HashMap<
324 TypeId,
325 (
326 FollowableItemBuilder,
327 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
328 ),
329>;
330pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
331 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
332 builders.insert(
333 TypeId::of::<I>(),
334 (
335 |pane, project, id, state, cx| {
336 I::from_state_proto(pane, project, id, state, cx).map(|task| {
337 cx.foreground()
338 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
339 })
340 },
341 |this| Box::new(this.clone().downcast::<I>().unwrap()),
342 ),
343 );
344 });
345}
346
347type ItemDeserializers = HashMap<
348 Arc<str>,
349 fn(
350 ModelHandle<Project>,
351 WeakViewHandle<Workspace>,
352 WorkspaceId,
353 ItemId,
354 &mut ViewContext<Pane>,
355 ) -> Task<Result<Box<dyn ItemHandle>>>,
356>;
357pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
358 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
359 if let Some(serialized_item_kind) = I::serialized_item_kind() {
360 deserializers.insert(
361 Arc::from(serialized_item_kind),
362 |project, workspace, workspace_id, item_id, cx| {
363 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
364 cx.foreground()
365 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
366 },
367 );
368 }
369 });
370}
371
372pub struct AppState {
373 pub languages: Arc<LanguageRegistry>,
374 pub client: Arc<client::Client>,
375 pub user_store: ModelHandle<client::UserStore>,
376 pub fs: Arc<dyn fs::Fs>,
377 pub build_window_options:
378 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
379 pub initialize_workspace:
380 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
381 pub background_actions: BackgroundActions,
382}
383
384impl AppState {
385 #[cfg(any(test, feature = "test-support"))]
386 pub fn test(cx: &mut AppContext) -> Arc<Self> {
387 use settings::SettingsStore;
388
389 if !cx.has_global::<SettingsStore>() {
390 cx.set_global(SettingsStore::test(cx));
391 }
392
393 let fs = fs::FakeFs::new(cx.background().clone());
394 let languages = Arc::new(LanguageRegistry::test());
395 let http_client = util::http::FakeHttpClient::with_404_response();
396 let client = Client::new(http_client.clone(), cx);
397 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
398
399 theme::init((), cx);
400 client::init(&client, cx);
401 crate::init_settings(cx);
402
403 Arc::new(Self {
404 client,
405 fs,
406 languages,
407 user_store,
408 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
409 build_window_options: |_, _, _| Default::default(),
410 background_actions: || &[],
411 })
412 }
413}
414
415struct DelayedDebouncedEditAction {
416 task: Option<Task<()>>,
417 cancel_channel: Option<oneshot::Sender<()>>,
418}
419
420impl DelayedDebouncedEditAction {
421 fn new() -> DelayedDebouncedEditAction {
422 DelayedDebouncedEditAction {
423 task: None,
424 cancel_channel: None,
425 }
426 }
427
428 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
429 where
430 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
431 {
432 if let Some(channel) = self.cancel_channel.take() {
433 _ = channel.send(());
434 }
435
436 let (sender, mut receiver) = oneshot::channel::<()>();
437 self.cancel_channel = Some(sender);
438
439 let previous_task = self.task.take();
440 self.task = Some(cx.spawn(|workspace, mut cx| async move {
441 let mut timer = cx.background().timer(delay).fuse();
442 if let Some(previous_task) = previous_task {
443 previous_task.await;
444 }
445
446 futures::select_biased! {
447 _ = receiver => return,
448 _ = timer => {}
449 }
450
451 if let Some(result) = workspace
452 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
453 .log_err()
454 {
455 result.await.log_err();
456 }
457 }));
458 }
459}
460
461pub enum Event {
462 PaneAdded(ViewHandle<Pane>),
463 ContactRequestedJoin(u64),
464}
465
466pub struct Workspace {
467 weak_self: WeakViewHandle<Self>,
468 remote_entity_subscription: Option<client::Subscription>,
469 modal: Option<AnyViewHandle>,
470 zoomed: Option<AnyWeakViewHandle>,
471 zoomed_position: Option<DockPosition>,
472 center: PaneGroup,
473 left_dock: ViewHandle<Dock>,
474 bottom_dock: ViewHandle<Dock>,
475 right_dock: ViewHandle<Dock>,
476 panes: Vec<ViewHandle<Pane>>,
477 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
478 active_pane: ViewHandle<Pane>,
479 last_active_center_pane: Option<WeakViewHandle<Pane>>,
480 status_bar: ViewHandle<StatusBar>,
481 titlebar_item: Option<AnyViewHandle>,
482 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
483 project: ModelHandle<Project>,
484 leader_state: LeaderState,
485 follower_states_by_leader: FollowerStatesByLeader,
486 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
487 window_edited: bool,
488 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
489 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
490 database_id: WorkspaceId,
491 app_state: Arc<AppState>,
492 subscriptions: Vec<Subscription>,
493 _apply_leader_updates: Task<Result<()>>,
494 _observe_current_user: Task<Result<()>>,
495 pane_history_timestamp: Arc<AtomicUsize>,
496}
497
498#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
499pub struct ViewId {
500 pub creator: PeerId,
501 pub id: u64,
502}
503
504#[derive(Default)]
505struct LeaderState {
506 followers: HashSet<PeerId>,
507}
508
509type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
510
511#[derive(Default)]
512struct FollowerState {
513 active_view_id: Option<ViewId>,
514 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
515}
516
517impl Workspace {
518 pub fn new(
519 workspace_id: WorkspaceId,
520 project: ModelHandle<Project>,
521 app_state: Arc<AppState>,
522 cx: &mut ViewContext<Self>,
523 ) -> Self {
524 cx.observe(&project, |_, _, cx| cx.notify()).detach();
525 cx.subscribe(&project, move |this, _, event, cx| {
526 match event {
527 project::Event::RemoteIdChanged(remote_id) => {
528 this.update_window_title(cx);
529 this.project_remote_id_changed(*remote_id, cx);
530 }
531
532 project::Event::CollaboratorLeft(peer_id) => {
533 this.collaborator_left(*peer_id, cx);
534 }
535
536 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
537 this.update_window_title(cx);
538 this.serialize_workspace(cx);
539 }
540
541 project::Event::DisconnectedFromHost => {
542 this.update_window_edited(cx);
543 cx.blur();
544 }
545
546 project::Event::Closed => {
547 cx.remove_window();
548 }
549
550 project::Event::DeletedEntry(entry_id) => {
551 for pane in this.panes.iter() {
552 pane.update(cx, |pane, cx| {
553 pane.handle_deleted_project_item(*entry_id, cx)
554 });
555 }
556 }
557
558 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
559 cx.add_view(|_| MessageNotification::new(message.clone()))
560 }),
561
562 _ => {}
563 }
564 cx.notify()
565 })
566 .detach();
567
568 let weak_handle = cx.weak_handle();
569 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
570
571 let center_pane = cx.add_view(|cx| {
572 Pane::new(
573 weak_handle.clone(),
574 project.clone(),
575 app_state.background_actions,
576 pane_history_timestamp.clone(),
577 cx,
578 )
579 });
580 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
581 cx.focus(¢er_pane);
582 cx.emit(Event::PaneAdded(center_pane.clone()));
583
584 let mut current_user = app_state.user_store.read(cx).watch_current_user();
585 let mut connection_status = app_state.client.status();
586 let _observe_current_user = cx.spawn(|this, mut cx| async move {
587 current_user.recv().await;
588 connection_status.recv().await;
589 let mut stream =
590 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
591
592 while stream.recv().await.is_some() {
593 this.update(&mut cx, |_, cx| cx.notify())?;
594 }
595 anyhow::Ok(())
596 });
597
598 // All leader updates are enqueued and then processed in a single task, so
599 // that each asynchronous operation can be run in order.
600 let (leader_updates_tx, mut leader_updates_rx) =
601 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
602 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
603 while let Some((leader_id, update)) = leader_updates_rx.next().await {
604 Self::process_leader_update(&this, leader_id, update, &mut cx)
605 .await
606 .log_err();
607 }
608
609 Ok(())
610 });
611
612 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
613
614 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
615 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
616 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
617 let left_dock_buttons =
618 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
619 let bottom_dock_buttons =
620 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
621 let right_dock_buttons =
622 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
623 let status_bar = cx.add_view(|cx| {
624 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
625 status_bar.add_left_item(left_dock_buttons, cx);
626 status_bar.add_right_item(right_dock_buttons, cx);
627 status_bar.add_right_item(bottom_dock_buttons, cx);
628 status_bar
629 });
630
631 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
632 drag_and_drop.register_container(weak_handle.clone());
633 });
634
635 let mut active_call = None;
636 if cx.has_global::<ModelHandle<ActiveCall>>() {
637 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
638 let mut subscriptions = Vec::new();
639 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
640 active_call = Some((call, subscriptions));
641 }
642
643 let subscriptions = vec![
644 cx.observe_fullscreen(|_, _, cx| cx.notify()),
645 cx.observe_window_activation(Self::on_window_activation_changed),
646 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
647 // Transform fixed bounds to be stored in terms of the containing display
648 if let WindowBounds::Fixed(mut window_bounds) = bounds {
649 if let Some(screen) = cx.platform().screen_by_id(display) {
650 let screen_bounds = screen.bounds();
651 window_bounds
652 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
653 window_bounds
654 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
655 bounds = WindowBounds::Fixed(window_bounds);
656 }
657 }
658
659 cx.background()
660 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
661 .detach_and_log_err(cx);
662 }),
663 cx.observe(&left_dock, |this, _, cx| {
664 this.serialize_workspace(cx);
665 cx.notify();
666 }),
667 cx.observe(&bottom_dock, |this, _, cx| {
668 this.serialize_workspace(cx);
669 cx.notify();
670 }),
671 cx.observe(&right_dock, |this, _, cx| {
672 this.serialize_workspace(cx);
673 cx.notify();
674 }),
675 ];
676
677 let mut this = Workspace {
678 weak_self: weak_handle.clone(),
679 modal: None,
680 zoomed: None,
681 zoomed_position: None,
682 center: PaneGroup::new(center_pane.clone()),
683 panes: vec![center_pane.clone()],
684 panes_by_item: Default::default(),
685 active_pane: center_pane.clone(),
686 last_active_center_pane: Some(center_pane.downgrade()),
687 status_bar,
688 titlebar_item: None,
689 notifications: Default::default(),
690 remote_entity_subscription: None,
691 left_dock,
692 bottom_dock,
693 right_dock,
694 project: project.clone(),
695 leader_state: Default::default(),
696 follower_states_by_leader: Default::default(),
697 last_leaders_by_pane: Default::default(),
698 window_edited: false,
699 active_call,
700 database_id: workspace_id,
701 app_state,
702 _observe_current_user,
703 _apply_leader_updates,
704 leader_updates_tx,
705 subscriptions,
706 pane_history_timestamp,
707 };
708 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
709 cx.defer(|this, cx| this.update_window_title(cx));
710 this
711 }
712
713 fn new_local(
714 abs_paths: Vec<PathBuf>,
715 app_state: Arc<AppState>,
716 requesting_window_id: Option<usize>,
717 cx: &mut AppContext,
718 ) -> Task<(
719 WeakViewHandle<Workspace>,
720 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
721 )> {
722 let project_handle = Project::local(
723 app_state.client.clone(),
724 app_state.user_store.clone(),
725 app_state.languages.clone(),
726 app_state.fs.clone(),
727 cx,
728 );
729
730 cx.spawn(|mut cx| async move {
731 let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
732
733 let paths_to_open = Arc::new(abs_paths);
734
735 // Get project paths for all of the abs_paths
736 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
737 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
738 Vec::with_capacity(paths_to_open.len());
739 for path in paths_to_open.iter().cloned() {
740 if let Some((worktree, project_entry)) = cx
741 .update(|cx| {
742 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
743 })
744 .await
745 .log_err()
746 {
747 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
748 project_paths.push((path, Some(project_entry)));
749 } else {
750 project_paths.push((path, None));
751 }
752 }
753
754 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
755 serialized_workspace.id
756 } else {
757 DB.next_id().await.unwrap_or(0)
758 };
759
760 let workspace = requesting_window_id
761 .and_then(|window_id| {
762 cx.update(|cx| {
763 cx.replace_root_view(window_id, |cx| {
764 Workspace::new(
765 workspace_id,
766 project_handle.clone(),
767 app_state.clone(),
768 cx,
769 )
770 })
771 })
772 })
773 .unwrap_or_else(|| {
774 let window_bounds_override = window_bounds_env_override(&cx);
775 let (bounds, display) = if let Some(bounds) = window_bounds_override {
776 (Some(bounds), None)
777 } else {
778 serialized_workspace
779 .as_ref()
780 .and_then(|serialized_workspace| {
781 let display = serialized_workspace.display?;
782 let mut bounds = serialized_workspace.bounds?;
783
784 // Stored bounds are relative to the containing display.
785 // So convert back to global coordinates if that screen still exists
786 if let WindowBounds::Fixed(mut window_bounds) = bounds {
787 if let Some(screen) = cx.platform().screen_by_id(display) {
788 let screen_bounds = screen.bounds();
789 window_bounds.set_origin_x(
790 window_bounds.origin_x() + screen_bounds.origin_x(),
791 );
792 window_bounds.set_origin_y(
793 window_bounds.origin_y() + screen_bounds.origin_y(),
794 );
795 bounds = WindowBounds::Fixed(window_bounds);
796 } else {
797 // Screen no longer exists. Return none here.
798 return None;
799 }
800 }
801
802 Some((bounds, display))
803 })
804 .unzip()
805 };
806
807 // Use the serialized workspace to construct the new window
808 cx.add_window(
809 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
810 |cx| {
811 Workspace::new(
812 workspace_id,
813 project_handle.clone(),
814 app_state.clone(),
815 cx,
816 )
817 },
818 )
819 .1
820 });
821
822 (app_state.initialize_workspace)(
823 workspace.downgrade(),
824 serialized_workspace.is_some(),
825 app_state.clone(),
826 cx.clone(),
827 )
828 .await
829 .log_err();
830
831 cx.update_window(workspace.window_id(), |cx| cx.activate_window());
832
833 let workspace = workspace.downgrade();
834 notify_if_database_failed(&workspace, &mut cx);
835 let opened_items = open_items(
836 serialized_workspace,
837 &workspace,
838 project_paths,
839 app_state,
840 cx,
841 )
842 .await;
843
844 (workspace, opened_items)
845 })
846 }
847
848 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
849 self.weak_self.clone()
850 }
851
852 pub fn left_dock(&self) -> &ViewHandle<Dock> {
853 &self.left_dock
854 }
855
856 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
857 &self.bottom_dock
858 }
859
860 pub fn right_dock(&self) -> &ViewHandle<Dock> {
861 &self.right_dock
862 }
863
864 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
865 let dock = match panel.position(cx) {
866 DockPosition::Left => &self.left_dock,
867 DockPosition::Bottom => &self.bottom_dock,
868 DockPosition::Right => &self.right_dock,
869 };
870
871 self.subscriptions.push(cx.subscribe(&panel, {
872 let mut dock = dock.clone();
873 let mut prev_position = panel.position(cx);
874 move |this, panel, event, cx| {
875 if T::should_change_position_on_event(event) {
876 let new_position = panel.read(cx).position(cx);
877 let mut was_visible = false;
878 dock.update(cx, |dock, cx| {
879 prev_position = new_position;
880
881 was_visible = dock.is_open()
882 && dock
883 .visible_panel()
884 .map_or(false, |active_panel| active_panel.id() == panel.id());
885 dock.remove_panel(&panel, cx);
886 });
887
888 if panel.is_zoomed(cx) {
889 this.zoomed_position = Some(new_position);
890 }
891
892 dock = match panel.read(cx).position(cx) {
893 DockPosition::Left => &this.left_dock,
894 DockPosition::Bottom => &this.bottom_dock,
895 DockPosition::Right => &this.right_dock,
896 }
897 .clone();
898 dock.update(cx, |dock, cx| {
899 dock.add_panel(panel.clone(), cx);
900 if was_visible {
901 dock.set_open(true, cx);
902 dock.activate_panel(dock.panels_len() - 1, cx);
903 }
904 });
905 } else if T::should_zoom_in_on_event(event) {
906 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
907 if panel.has_focus(cx) {
908 this.zoomed = Some(panel.downgrade().into_any());
909 this.zoomed_position = Some(panel.read(cx).position(cx));
910 }
911 } else if T::should_zoom_out_on_event(event) {
912 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
913 if this.zoomed_position == Some(prev_position) {
914 this.zoomed = None;
915 this.zoomed_position = None;
916 }
917 cx.notify();
918 } else if T::is_focus_event(event) {
919 let position = panel.read(cx).position(cx);
920 this.dismiss_zoomed_items_to_reveal(Some(position), cx);
921 if panel.is_zoomed(cx) {
922 this.zoomed = Some(panel.downgrade().into_any());
923 this.zoomed_position = Some(position);
924 } else {
925 this.zoomed = None;
926 this.zoomed_position = None;
927 }
928 this.update_active_view_for_followers(cx);
929 cx.notify();
930 }
931 }
932 }));
933
934 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
935 }
936
937 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
938 &self.status_bar
939 }
940
941 pub fn app_state(&self) -> &Arc<AppState> {
942 &self.app_state
943 }
944
945 pub fn user_store(&self) -> &ModelHandle<UserStore> {
946 &self.app_state.user_store
947 }
948
949 pub fn project(&self) -> &ModelHandle<Project> {
950 &self.project
951 }
952
953 pub fn recent_navigation_history(
954 &self,
955 limit: Option<usize>,
956 cx: &AppContext,
957 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
958 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
959 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
960 for pane in &self.panes {
961 let pane = pane.read(cx);
962 pane.nav_history()
963 .for_each_entry(cx, |entry, (project_path, fs_path)| {
964 if let Some(fs_path) = &fs_path {
965 abs_paths_opened
966 .entry(fs_path.clone())
967 .or_default()
968 .insert(project_path.clone());
969 }
970 let timestamp = entry.timestamp;
971 match history.entry(project_path) {
972 hash_map::Entry::Occupied(mut entry) => {
973 let (_, old_timestamp) = entry.get();
974 if ×tamp > old_timestamp {
975 entry.insert((fs_path, timestamp));
976 }
977 }
978 hash_map::Entry::Vacant(entry) => {
979 entry.insert((fs_path, timestamp));
980 }
981 }
982 });
983 }
984
985 history
986 .into_iter()
987 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
988 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
989 .rev()
990 .filter(|(history_path, abs_path)| {
991 let latest_project_path_opened = abs_path
992 .as_ref()
993 .and_then(|abs_path| abs_paths_opened.get(abs_path))
994 .and_then(|project_paths| {
995 project_paths
996 .iter()
997 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
998 });
999
1000 match latest_project_path_opened {
1001 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1002 None => true,
1003 }
1004 })
1005 .take(limit.unwrap_or(usize::MAX))
1006 .collect()
1007 }
1008
1009 fn navigate_history(
1010 &mut self,
1011 pane: WeakViewHandle<Pane>,
1012 mode: NavigationMode,
1013 cx: &mut ViewContext<Workspace>,
1014 ) -> Task<Result<()>> {
1015 let to_load = if let Some(pane) = pane.upgrade(cx) {
1016 cx.focus(&pane);
1017
1018 pane.update(cx, |pane, cx| {
1019 loop {
1020 // Retrieve the weak item handle from the history.
1021 let entry = pane.nav_history_mut().pop(mode, cx)?;
1022
1023 // If the item is still present in this pane, then activate it.
1024 if let Some(index) = entry
1025 .item
1026 .upgrade(cx)
1027 .and_then(|v| pane.index_for_item(v.as_ref()))
1028 {
1029 let prev_active_item_index = pane.active_item_index();
1030 pane.nav_history_mut().set_mode(mode);
1031 pane.activate_item(index, true, true, cx);
1032 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1033
1034 let mut navigated = prev_active_item_index != pane.active_item_index();
1035 if let Some(data) = entry.data {
1036 navigated |= pane.active_item()?.navigate(data, cx);
1037 }
1038
1039 if navigated {
1040 break None;
1041 }
1042 }
1043 // If the item is no longer present in this pane, then retrieve its
1044 // project path in order to reopen it.
1045 else {
1046 break pane
1047 .nav_history()
1048 .path_for_item(entry.item.id())
1049 .map(|(project_path, _)| (project_path, entry));
1050 }
1051 }
1052 })
1053 } else {
1054 None
1055 };
1056
1057 if let Some((project_path, entry)) = to_load {
1058 // If the item was no longer present, then load it again from its previous path.
1059 let task = self.load_path(project_path, cx);
1060 cx.spawn(|workspace, mut cx| async move {
1061 let task = task.await;
1062 let mut navigated = false;
1063 if let Some((project_entry_id, build_item)) = task.log_err() {
1064 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1065 pane.nav_history_mut().set_mode(mode);
1066 pane.active_item().map(|p| p.id())
1067 })?;
1068
1069 pane.update(&mut cx, |pane, cx| {
1070 let item = pane.open_item(project_entry_id, true, cx, build_item);
1071 navigated |= Some(item.id()) != prev_active_item_id;
1072 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1073 if let Some(data) = entry.data {
1074 navigated |= item.navigate(data, cx);
1075 }
1076 })?;
1077 }
1078
1079 if !navigated {
1080 workspace
1081 .update(&mut cx, |workspace, cx| {
1082 Self::navigate_history(workspace, pane, mode, cx)
1083 })?
1084 .await?;
1085 }
1086
1087 Ok(())
1088 })
1089 } else {
1090 Task::ready(Ok(()))
1091 }
1092 }
1093
1094 pub fn go_back(
1095 &mut self,
1096 pane: WeakViewHandle<Pane>,
1097 cx: &mut ViewContext<Workspace>,
1098 ) -> Task<Result<()>> {
1099 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1100 }
1101
1102 pub fn go_forward(
1103 &mut self,
1104 pane: WeakViewHandle<Pane>,
1105 cx: &mut ViewContext<Workspace>,
1106 ) -> Task<Result<()>> {
1107 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1108 }
1109
1110 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1111 self.navigate_history(
1112 self.active_pane().downgrade(),
1113 NavigationMode::ReopeningClosedItem,
1114 cx,
1115 )
1116 }
1117
1118 pub fn client(&self) -> &Client {
1119 &self.app_state.client
1120 }
1121
1122 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
1123 self.titlebar_item = Some(item);
1124 cx.notify();
1125 }
1126
1127 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
1128 self.titlebar_item.clone()
1129 }
1130
1131 /// Call the given callback with a workspace whose project is local.
1132 ///
1133 /// If the given workspace has a local project, then it will be passed
1134 /// to the callback. Otherwise, a new empty window will be created.
1135 pub fn with_local_workspace<T, F>(
1136 &mut self,
1137 cx: &mut ViewContext<Self>,
1138 callback: F,
1139 ) -> Task<Result<T>>
1140 where
1141 T: 'static,
1142 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1143 {
1144 if self.project.read(cx).is_local() {
1145 Task::Ready(Some(Ok(callback(self, cx))))
1146 } else {
1147 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1148 cx.spawn(|_vh, mut cx| async move {
1149 let (workspace, _) = task.await;
1150 workspace.update(&mut cx, callback)
1151 })
1152 }
1153 }
1154
1155 pub fn worktrees<'a>(
1156 &self,
1157 cx: &'a AppContext,
1158 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1159 self.project.read(cx).worktrees(cx)
1160 }
1161
1162 pub fn visible_worktrees<'a>(
1163 &self,
1164 cx: &'a AppContext,
1165 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
1166 self.project.read(cx).visible_worktrees(cx)
1167 }
1168
1169 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1170 let futures = self
1171 .worktrees(cx)
1172 .filter_map(|worktree| worktree.read(cx).as_local())
1173 .map(|worktree| worktree.scan_complete())
1174 .collect::<Vec<_>>();
1175 async move {
1176 for future in futures {
1177 future.await;
1178 }
1179 }
1180 }
1181
1182 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1183 cx.spawn(|mut cx| async move {
1184 let id = cx
1185 .window_ids()
1186 .into_iter()
1187 .find(|&id| cx.window_is_active(id));
1188 if let Some(id) = id {
1189 //This can only get called when the window's project connection has been lost
1190 //so we don't need to prompt the user for anything and instead just close the window
1191 cx.remove_window(id);
1192 }
1193 })
1194 .detach();
1195 }
1196
1197 pub fn close(
1198 &mut self,
1199 _: &CloseWindow,
1200 cx: &mut ViewContext<Self>,
1201 ) -> Option<Task<Result<()>>> {
1202 let window_id = cx.window_id();
1203 let prepare = self.prepare_to_close(false, cx);
1204 Some(cx.spawn(|_, mut cx| async move {
1205 if prepare.await? {
1206 cx.remove_window(window_id);
1207 }
1208 Ok(())
1209 }))
1210 }
1211
1212 pub fn prepare_to_close(
1213 &mut self,
1214 quitting: bool,
1215 cx: &mut ViewContext<Self>,
1216 ) -> Task<Result<bool>> {
1217 let active_call = self.active_call().cloned();
1218 let window_id = cx.window_id();
1219
1220 cx.spawn(|this, mut cx| async move {
1221 let workspace_count = cx
1222 .window_ids()
1223 .into_iter()
1224 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1225 .count();
1226
1227 if let Some(active_call) = active_call {
1228 if !quitting
1229 && workspace_count == 1
1230 && active_call.read_with(&cx, |call, _| call.room().is_some())
1231 {
1232 let answer = cx.prompt(
1233 window_id,
1234 PromptLevel::Warning,
1235 "Do you want to leave the current call?",
1236 &["Close window and hang up", "Cancel"],
1237 );
1238
1239 if let Some(mut answer) = answer {
1240 if answer.next().await == Some(1) {
1241 return anyhow::Ok(false);
1242 } else {
1243 active_call
1244 .update(&mut cx, |call, cx| call.hang_up(cx))
1245 .await
1246 .log_err();
1247 }
1248 }
1249 }
1250 }
1251
1252 Ok(this
1253 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1254 .await?)
1255 })
1256 }
1257
1258 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1259 let save_all = self.save_all_internal(false, cx);
1260 Some(cx.foreground().spawn(async move {
1261 save_all.await?;
1262 Ok(())
1263 }))
1264 }
1265
1266 fn save_all_internal(
1267 &mut self,
1268 should_prompt_to_save: bool,
1269 cx: &mut ViewContext<Self>,
1270 ) -> Task<Result<bool>> {
1271 if self.project.read(cx).is_read_only() {
1272 return Task::ready(Ok(true));
1273 }
1274
1275 let dirty_items = self
1276 .panes
1277 .iter()
1278 .flat_map(|pane| {
1279 pane.read(cx).items().filter_map(|item| {
1280 if item.is_dirty(cx) {
1281 Some((pane.downgrade(), item.boxed_clone()))
1282 } else {
1283 None
1284 }
1285 })
1286 })
1287 .collect::<Vec<_>>();
1288
1289 let project = self.project.clone();
1290 cx.spawn(|_, mut cx| async move {
1291 for (pane, item) in dirty_items {
1292 let (singleton, project_entry_ids) =
1293 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1294 if singleton || !project_entry_ids.is_empty() {
1295 if let Some(ix) =
1296 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1297 {
1298 if !Pane::save_item(
1299 project.clone(),
1300 &pane,
1301 ix,
1302 &*item,
1303 should_prompt_to_save,
1304 &mut cx,
1305 )
1306 .await?
1307 {
1308 return Ok(false);
1309 }
1310 }
1311 }
1312 }
1313 Ok(true)
1314 })
1315 }
1316
1317 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1318 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1319 files: true,
1320 directories: true,
1321 multiple: true,
1322 });
1323
1324 Some(cx.spawn(|this, mut cx| async move {
1325 if let Some(paths) = paths.recv().await.flatten() {
1326 if let Some(task) = this
1327 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1328 .log_err()
1329 {
1330 task.await?
1331 }
1332 }
1333 Ok(())
1334 }))
1335 }
1336
1337 pub fn open_workspace_for_paths(
1338 &mut self,
1339 paths: Vec<PathBuf>,
1340 cx: &mut ViewContext<Self>,
1341 ) -> Task<Result<()>> {
1342 let window_id = cx.window_id();
1343 let is_remote = self.project.read(cx).is_remote();
1344 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1345 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1346 let close_task = if is_remote || has_worktree || has_dirty_items {
1347 None
1348 } else {
1349 Some(self.prepare_to_close(false, cx))
1350 };
1351 let app_state = self.app_state.clone();
1352
1353 cx.spawn(|_, mut cx| async move {
1354 let window_id_to_replace = if let Some(close_task) = close_task {
1355 if !close_task.await? {
1356 return Ok(());
1357 }
1358 Some(window_id)
1359 } else {
1360 None
1361 };
1362 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1363 .await?;
1364 Ok(())
1365 })
1366 }
1367
1368 #[allow(clippy::type_complexity)]
1369 pub fn open_paths(
1370 &mut self,
1371 mut abs_paths: Vec<PathBuf>,
1372 visible: bool,
1373 cx: &mut ViewContext<Self>,
1374 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1375 log::info!("open paths {:?}", abs_paths);
1376
1377 let fs = self.app_state.fs.clone();
1378
1379 // Sort the paths to ensure we add worktrees for parents before their children.
1380 abs_paths.sort_unstable();
1381 cx.spawn(|this, mut cx| async move {
1382 let mut project_paths = Vec::new();
1383 for path in &abs_paths {
1384 if let Some(project_path) = this
1385 .update(&mut cx, |this, cx| {
1386 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1387 })
1388 .log_err()
1389 {
1390 project_paths.push(project_path.await.log_err());
1391 } else {
1392 project_paths.push(None);
1393 }
1394 }
1395
1396 let tasks = abs_paths
1397 .iter()
1398 .cloned()
1399 .zip(project_paths.into_iter())
1400 .map(|(abs_path, project_path)| {
1401 let this = this.clone();
1402 cx.spawn(|mut cx| {
1403 let fs = fs.clone();
1404 async move {
1405 let (_worktree, project_path) = project_path?;
1406 if fs.is_file(&abs_path).await {
1407 Some(
1408 this.update(&mut cx, |this, cx| {
1409 this.open_path(project_path, None, true, cx)
1410 })
1411 .log_err()?
1412 .await,
1413 )
1414 } else {
1415 None
1416 }
1417 }
1418 })
1419 })
1420 .collect::<Vec<_>>();
1421
1422 futures::future::join_all(tasks).await
1423 })
1424 }
1425
1426 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1427 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1428 files: false,
1429 directories: true,
1430 multiple: true,
1431 });
1432 cx.spawn(|this, mut cx| async move {
1433 if let Some(paths) = paths.recv().await.flatten() {
1434 let results = this
1435 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1436 .await;
1437 for result in results.into_iter().flatten() {
1438 result.log_err();
1439 }
1440 }
1441 anyhow::Ok(())
1442 })
1443 .detach_and_log_err(cx);
1444 }
1445
1446 fn project_path_for_path(
1447 project: ModelHandle<Project>,
1448 abs_path: &Path,
1449 visible: bool,
1450 cx: &mut AppContext,
1451 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1452 let entry = project.update(cx, |project, cx| {
1453 project.find_or_create_local_worktree(abs_path, visible, cx)
1454 });
1455 cx.spawn(|cx| async move {
1456 let (worktree, path) = entry.await?;
1457 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1458 Ok((
1459 worktree,
1460 ProjectPath {
1461 worktree_id,
1462 path: path.into(),
1463 },
1464 ))
1465 })
1466 }
1467
1468 /// Returns the modal that was toggled closed if it was open.
1469 pub fn toggle_modal<V, F>(
1470 &mut self,
1471 cx: &mut ViewContext<Self>,
1472 add_view: F,
1473 ) -> Option<ViewHandle<V>>
1474 where
1475 V: 'static + Modal,
1476 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1477 {
1478 cx.notify();
1479 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1480 // it. Otherwise, create a new modal and set it as active.
1481 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1482 if let Some(already_open_modal) = already_open_modal {
1483 cx.focus_self();
1484 Some(already_open_modal)
1485 } else {
1486 let modal = add_view(self, cx);
1487 cx.subscribe(&modal, |this, _, event, cx| {
1488 if V::dismiss_on_event(event) {
1489 this.dismiss_modal(cx);
1490 }
1491 })
1492 .detach();
1493 cx.focus(&modal);
1494 self.modal = Some(modal.into_any());
1495 None
1496 }
1497 }
1498
1499 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1500 self.modal
1501 .as_ref()
1502 .and_then(|modal| modal.clone().downcast::<V>())
1503 }
1504
1505 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1506 if self.modal.take().is_some() {
1507 cx.focus(&self.active_pane);
1508 cx.notify();
1509 }
1510 }
1511
1512 pub fn items<'a>(
1513 &'a self,
1514 cx: &'a AppContext,
1515 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1516 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1517 }
1518
1519 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1520 self.items_of_type(cx).max_by_key(|item| item.id())
1521 }
1522
1523 pub fn items_of_type<'a, T: Item>(
1524 &'a self,
1525 cx: &'a AppContext,
1526 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1527 self.panes
1528 .iter()
1529 .flat_map(|pane| pane.read(cx).items_of_type())
1530 }
1531
1532 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1533 self.active_pane().read(cx).active_item()
1534 }
1535
1536 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1537 self.active_item(cx).and_then(|item| item.project_path(cx))
1538 }
1539
1540 pub fn save_active_item(
1541 &mut self,
1542 force_name_change: bool,
1543 cx: &mut ViewContext<Self>,
1544 ) -> Task<Result<()>> {
1545 let project = self.project.clone();
1546 if let Some(item) = self.active_item(cx) {
1547 if !force_name_change && item.can_save(cx) {
1548 if item.has_conflict(cx) {
1549 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1550
1551 let mut answer = cx.prompt(
1552 PromptLevel::Warning,
1553 CONFLICT_MESSAGE,
1554 &["Overwrite", "Cancel"],
1555 );
1556 cx.spawn(|this, mut cx| async move {
1557 let answer = answer.recv().await;
1558 if answer == Some(0) {
1559 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1560 .await?;
1561 }
1562 Ok(())
1563 })
1564 } else {
1565 item.save(self.project.clone(), cx)
1566 }
1567 } else if item.is_singleton(cx) {
1568 let worktree = self.worktrees(cx).next();
1569 let start_abs_path = worktree
1570 .and_then(|w| w.read(cx).as_local())
1571 .map_or(Path::new(""), |w| w.abs_path())
1572 .to_path_buf();
1573 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1574 cx.spawn(|this, mut cx| async move {
1575 if let Some(abs_path) = abs_path.recv().await.flatten() {
1576 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1577 .await?;
1578 }
1579 Ok(())
1580 })
1581 } else {
1582 Task::ready(Ok(()))
1583 }
1584 } else {
1585 Task::ready(Ok(()))
1586 }
1587 }
1588
1589 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1590 let dock = match dock_side {
1591 DockPosition::Left => &self.left_dock,
1592 DockPosition::Bottom => &self.bottom_dock,
1593 DockPosition::Right => &self.right_dock,
1594 };
1595 let mut focus_center = false;
1596 let mut reveal_dock = false;
1597 dock.update(cx, |dock, cx| {
1598 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1599 let was_visible = dock.is_open() && !other_is_zoomed;
1600 dock.set_open(!was_visible, cx);
1601
1602 if let Some(active_panel) = dock.active_panel() {
1603 if was_visible {
1604 if active_panel.has_focus(cx) {
1605 focus_center = true;
1606 }
1607 } else {
1608 cx.focus(active_panel.as_any());
1609 reveal_dock = true;
1610 }
1611 }
1612 });
1613
1614 if reveal_dock {
1615 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1616 }
1617
1618 if focus_center {
1619 cx.focus_self();
1620 }
1621
1622 cx.notify();
1623 self.serialize_workspace(cx);
1624 }
1625
1626 /// Transfer focus to the panel of the given type.
1627 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
1628 self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
1629 .as_any()
1630 .clone()
1631 .downcast()
1632 }
1633
1634 /// Focus the panel of the given type if it isn't already focused. If it is
1635 /// already focused, then transfer focus back to the workspace center.
1636 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1637 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
1638 }
1639
1640 /// Focus or unfocus the given panel type, depending on the given callback.
1641 fn focus_or_unfocus_panel<T: Panel>(
1642 &mut self,
1643 cx: &mut ViewContext<Self>,
1644 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1645 ) -> Option<Rc<dyn PanelHandle>> {
1646 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1647 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1648 let mut focus_center = false;
1649 let mut reveal_dock = false;
1650 let panel = dock.update(cx, |dock, cx| {
1651 dock.activate_panel(panel_index, cx);
1652
1653 let panel = dock.active_panel().cloned();
1654 if let Some(panel) = panel.as_ref() {
1655 if should_focus(&**panel, cx) {
1656 dock.set_open(true, cx);
1657 cx.focus(panel.as_any());
1658 reveal_dock = true;
1659 } else {
1660 // if panel.is_zoomed(cx) {
1661 // dock.set_open(false, cx);
1662 // }
1663 focus_center = true;
1664 }
1665 }
1666 panel
1667 });
1668
1669 if focus_center {
1670 cx.focus_self();
1671 }
1672
1673 self.serialize_workspace(cx);
1674 cx.notify();
1675 return panel;
1676 }
1677 }
1678 None
1679 }
1680
1681 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
1682 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1683 let dock = dock.read(cx);
1684 if let Some(panel) = dock.panel::<T>() {
1685 return Some(panel);
1686 }
1687 }
1688 None
1689 }
1690
1691 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1692 for pane in &self.panes {
1693 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1694 }
1695
1696 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1697 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1698 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1699 self.zoomed = None;
1700 self.zoomed_position = None;
1701
1702 cx.notify();
1703 }
1704
1705 fn dismiss_zoomed_items_to_reveal(
1706 &mut self,
1707 dock_to_reveal: Option<DockPosition>,
1708 cx: &mut ViewContext<Self>,
1709 ) {
1710 // If a center pane is zoomed, unzoom it.
1711 for pane in &self.panes {
1712 if pane != &self.active_pane || dock_to_reveal.is_some() {
1713 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1714 }
1715 }
1716
1717 // If another dock is zoomed, hide it.
1718 let mut focus_center = false;
1719 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1720 dock.update(cx, |dock, cx| {
1721 if Some(dock.position()) != dock_to_reveal {
1722 if let Some(panel) = dock.active_panel() {
1723 if panel.is_zoomed(cx) {
1724 focus_center |= panel.has_focus(cx);
1725 dock.set_open(false, cx);
1726 }
1727 }
1728 }
1729 });
1730 }
1731
1732 if focus_center {
1733 cx.focus_self();
1734 }
1735
1736 if self.zoomed_position != dock_to_reveal {
1737 self.zoomed = None;
1738 self.zoomed_position = None;
1739 }
1740
1741 cx.notify();
1742 }
1743
1744 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1745 let pane = cx.add_view(|cx| {
1746 Pane::new(
1747 self.weak_handle(),
1748 self.project.clone(),
1749 self.app_state.background_actions,
1750 self.pane_history_timestamp.clone(),
1751 cx,
1752 )
1753 });
1754 cx.subscribe(&pane, Self::handle_pane_event).detach();
1755 self.panes.push(pane.clone());
1756 cx.focus(&pane);
1757 cx.emit(Event::PaneAdded(pane.clone()));
1758 pane
1759 }
1760
1761 pub fn add_item_to_center(
1762 &mut self,
1763 item: Box<dyn ItemHandle>,
1764 cx: &mut ViewContext<Self>,
1765 ) -> bool {
1766 if let Some(center_pane) = self.last_active_center_pane.clone() {
1767 if let Some(center_pane) = center_pane.upgrade(cx) {
1768 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1769 true
1770 } else {
1771 false
1772 }
1773 } else {
1774 false
1775 }
1776 }
1777
1778 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1779 self.active_pane
1780 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1781 }
1782
1783 pub fn open_abs_path(
1784 &mut self,
1785 abs_path: PathBuf,
1786 visible: bool,
1787 cx: &mut ViewContext<Self>,
1788 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1789 cx.spawn(|workspace, mut cx| async move {
1790 let open_paths_task_result = workspace
1791 .update(&mut cx, |workspace, cx| {
1792 workspace.open_paths(vec![abs_path.clone()], visible, cx)
1793 })
1794 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1795 .await;
1796 anyhow::ensure!(
1797 open_paths_task_result.len() == 1,
1798 "open abs path {abs_path:?} task returned incorrect number of results"
1799 );
1800 match open_paths_task_result
1801 .into_iter()
1802 .next()
1803 .expect("ensured single task result")
1804 {
1805 Some(open_result) => {
1806 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1807 }
1808 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1809 }
1810 })
1811 }
1812
1813 pub fn open_path(
1814 &mut self,
1815 path: impl Into<ProjectPath>,
1816 pane: Option<WeakViewHandle<Pane>>,
1817 focus_item: bool,
1818 cx: &mut ViewContext<Self>,
1819 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1820 let pane = pane.unwrap_or_else(|| {
1821 self.last_active_center_pane.clone().unwrap_or_else(|| {
1822 self.panes
1823 .first()
1824 .expect("There must be an active pane")
1825 .downgrade()
1826 })
1827 });
1828
1829 let task = self.load_path(path.into(), cx);
1830 cx.spawn(|_, mut cx| async move {
1831 let (project_entry_id, build_item) = task.await?;
1832 pane.update(&mut cx, |pane, cx| {
1833 pane.open_item(project_entry_id, focus_item, cx, build_item)
1834 })
1835 })
1836 }
1837
1838 pub(crate) fn load_path(
1839 &mut self,
1840 path: ProjectPath,
1841 cx: &mut ViewContext<Self>,
1842 ) -> Task<
1843 Result<(
1844 ProjectEntryId,
1845 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1846 )>,
1847 > {
1848 let project = self.project().clone();
1849 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1850 cx.spawn(|_, mut cx| async move {
1851 let (project_entry_id, project_item) = project_item.await?;
1852 let build_item = cx.update(|cx| {
1853 cx.default_global::<ProjectItemBuilders>()
1854 .get(&project_item.model_type())
1855 .ok_or_else(|| anyhow!("no item builder for project item"))
1856 .cloned()
1857 })?;
1858 let build_item =
1859 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1860 Ok((project_entry_id, build_item))
1861 })
1862 }
1863
1864 pub fn open_project_item<T>(
1865 &mut self,
1866 project_item: ModelHandle<T::Item>,
1867 cx: &mut ViewContext<Self>,
1868 ) -> ViewHandle<T>
1869 where
1870 T: ProjectItem,
1871 {
1872 use project::Item as _;
1873
1874 let entry_id = project_item.read(cx).entry_id(cx);
1875 if let Some(item) = entry_id
1876 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1877 .and_then(|item| item.downcast())
1878 {
1879 self.activate_item(&item, cx);
1880 return item;
1881 }
1882
1883 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1884 self.add_item(Box::new(item.clone()), cx);
1885 item
1886 }
1887
1888 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1889 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1890 self.active_pane.update(cx, |pane, cx| {
1891 pane.add_item(Box::new(shared_screen), false, true, None, cx)
1892 });
1893 }
1894 }
1895
1896 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1897 let result = self.panes.iter().find_map(|pane| {
1898 pane.read(cx)
1899 .index_for_item(item)
1900 .map(|ix| (pane.clone(), ix))
1901 });
1902 if let Some((pane, ix)) = result {
1903 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1904 true
1905 } else {
1906 false
1907 }
1908 }
1909
1910 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1911 let panes = self.center.panes();
1912 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1913 cx.focus(&pane);
1914 } else {
1915 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1916 }
1917 }
1918
1919 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1920 let panes = self.center.panes();
1921 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1922 let next_ix = (ix + 1) % panes.len();
1923 let next_pane = panes[next_ix].clone();
1924 cx.focus(&next_pane);
1925 }
1926 }
1927
1928 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1929 let panes = self.center.panes();
1930 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1931 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1932 let prev_pane = panes[prev_ix].clone();
1933 cx.focus(&prev_pane);
1934 }
1935 }
1936
1937 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1938 if self.active_pane != pane {
1939 self.active_pane = pane.clone();
1940 self.status_bar.update(cx, |status_bar, cx| {
1941 status_bar.set_active_pane(&self.active_pane, cx);
1942 });
1943 self.active_item_path_changed(cx);
1944 self.last_active_center_pane = Some(pane.downgrade());
1945 }
1946
1947 self.dismiss_zoomed_items_to_reveal(None, cx);
1948 if pane.read(cx).is_zoomed() {
1949 self.zoomed = Some(pane.downgrade().into_any());
1950 } else {
1951 self.zoomed = None;
1952 }
1953 self.zoomed_position = None;
1954 self.update_active_view_for_followers(cx);
1955
1956 cx.notify();
1957 }
1958
1959 fn handle_pane_event(
1960 &mut self,
1961 pane: ViewHandle<Pane>,
1962 event: &pane::Event,
1963 cx: &mut ViewContext<Self>,
1964 ) {
1965 match event {
1966 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
1967 pane::Event::Split(direction) => {
1968 self.split_pane(pane, *direction, cx);
1969 }
1970 pane::Event::Remove => self.remove_pane(pane, cx),
1971 pane::Event::ActivateItem { local } => {
1972 if *local {
1973 self.unfollow(&pane, cx);
1974 }
1975 if &pane == self.active_pane() {
1976 self.active_item_path_changed(cx);
1977 }
1978 }
1979 pane::Event::ChangeItemTitle => {
1980 if pane == self.active_pane {
1981 self.active_item_path_changed(cx);
1982 }
1983 self.update_window_edited(cx);
1984 }
1985 pane::Event::RemoveItem { item_id } => {
1986 self.update_window_edited(cx);
1987 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1988 if entry.get().id() == pane.id() {
1989 entry.remove();
1990 }
1991 }
1992 }
1993 pane::Event::Focus => {
1994 self.handle_pane_focused(pane.clone(), cx);
1995 }
1996 pane::Event::ZoomIn => {
1997 if pane == self.active_pane {
1998 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1999 if pane.read(cx).has_focus() {
2000 self.zoomed = Some(pane.downgrade().into_any());
2001 self.zoomed_position = None;
2002 }
2003 cx.notify();
2004 }
2005 }
2006 pane::Event::ZoomOut => {
2007 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2008 if self.zoomed_position.is_none() {
2009 self.zoomed = None;
2010 }
2011 cx.notify();
2012 }
2013 }
2014
2015 self.serialize_workspace(cx);
2016 }
2017
2018 pub fn split_pane(
2019 &mut self,
2020 pane: ViewHandle<Pane>,
2021 direction: SplitDirection,
2022 cx: &mut ViewContext<Self>,
2023 ) -> Option<ViewHandle<Pane>> {
2024 let item = pane.read(cx).active_item()?;
2025 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2026 let new_pane = self.add_pane(cx);
2027 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2028 self.center.split(&pane, &new_pane, direction).unwrap();
2029 Some(new_pane)
2030 } else {
2031 None
2032 };
2033 cx.notify();
2034 maybe_pane_handle
2035 }
2036
2037 pub fn split_pane_with_item(
2038 &mut self,
2039 pane_to_split: WeakViewHandle<Pane>,
2040 split_direction: SplitDirection,
2041 from: WeakViewHandle<Pane>,
2042 item_id_to_move: usize,
2043 cx: &mut ViewContext<Self>,
2044 ) {
2045 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
2046 let Some(from) = from.upgrade(cx) else { return; };
2047
2048 let new_pane = self.add_pane(cx);
2049 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2050 self.center
2051 .split(&pane_to_split, &new_pane, split_direction)
2052 .unwrap();
2053 cx.notify();
2054 }
2055
2056 pub fn split_pane_with_project_entry(
2057 &mut self,
2058 pane_to_split: WeakViewHandle<Pane>,
2059 split_direction: SplitDirection,
2060 project_entry: ProjectEntryId,
2061 cx: &mut ViewContext<Self>,
2062 ) -> Option<Task<Result<()>>> {
2063 let pane_to_split = pane_to_split.upgrade(cx)?;
2064 let new_pane = self.add_pane(cx);
2065 self.center
2066 .split(&pane_to_split, &new_pane, split_direction)
2067 .unwrap();
2068
2069 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2070 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2071 Some(cx.foreground().spawn(async move {
2072 task.await?;
2073 Ok(())
2074 }))
2075 }
2076
2077 pub fn move_item(
2078 &mut self,
2079 source: ViewHandle<Pane>,
2080 destination: ViewHandle<Pane>,
2081 item_id_to_move: usize,
2082 destination_index: usize,
2083 cx: &mut ViewContext<Self>,
2084 ) {
2085 let item_to_move = source
2086 .read(cx)
2087 .items()
2088 .enumerate()
2089 .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
2090
2091 if item_to_move.is_none() {
2092 log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
2093 return;
2094 }
2095 let (item_ix, item_handle) = item_to_move.unwrap();
2096 let item_handle = item_handle.clone();
2097
2098 if source != destination {
2099 // Close item from previous pane
2100 source.update(cx, |source, cx| {
2101 source.remove_item(item_ix, false, cx);
2102 });
2103 }
2104
2105 // This automatically removes duplicate items in the pane
2106 destination.update(cx, |destination, cx| {
2107 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2108 cx.focus_self();
2109 });
2110 }
2111
2112 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
2113 if self.center.remove(&pane).unwrap() {
2114 self.force_remove_pane(&pane, cx);
2115 self.unfollow(&pane, cx);
2116 self.last_leaders_by_pane.remove(&pane.downgrade());
2117 for removed_item in pane.read(cx).items() {
2118 self.panes_by_item.remove(&removed_item.id());
2119 }
2120
2121 cx.notify();
2122 } else {
2123 self.active_item_path_changed(cx);
2124 }
2125 }
2126
2127 pub fn panes(&self) -> &[ViewHandle<Pane>] {
2128 &self.panes
2129 }
2130
2131 pub fn active_pane(&self) -> &ViewHandle<Pane> {
2132 &self.active_pane
2133 }
2134
2135 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
2136 if let Some(remote_id) = remote_id {
2137 self.remote_entity_subscription = Some(
2138 self.app_state
2139 .client
2140 .add_view_for_remote_entity(remote_id, cx),
2141 );
2142 } else {
2143 self.remote_entity_subscription.take();
2144 }
2145 }
2146
2147 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2148 self.leader_state.followers.remove(&peer_id);
2149 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
2150 for state in states_by_pane.into_values() {
2151 for item in state.items_by_leader_view_id.into_values() {
2152 item.set_leader_replica_id(None, cx);
2153 }
2154 }
2155 }
2156 cx.notify();
2157 }
2158
2159 pub fn toggle_follow(
2160 &mut self,
2161 leader_id: PeerId,
2162 cx: &mut ViewContext<Self>,
2163 ) -> Option<Task<Result<()>>> {
2164 let pane = self.active_pane().clone();
2165
2166 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
2167 if leader_id == prev_leader_id {
2168 return None;
2169 }
2170 }
2171
2172 self.last_leaders_by_pane
2173 .insert(pane.downgrade(), leader_id);
2174 self.follower_states_by_leader
2175 .entry(leader_id)
2176 .or_default()
2177 .insert(pane.clone(), Default::default());
2178 cx.notify();
2179
2180 let project_id = self.project.read(cx).remote_id()?;
2181 let request = self.app_state.client.request(proto::Follow {
2182 project_id,
2183 leader_id: Some(leader_id),
2184 });
2185
2186 Some(cx.spawn(|this, mut cx| async move {
2187 let response = request.await?;
2188 this.update(&mut cx, |this, _| {
2189 let state = this
2190 .follower_states_by_leader
2191 .get_mut(&leader_id)
2192 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
2193 .ok_or_else(|| anyhow!("following interrupted"))?;
2194 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2195 Some(ViewId::from_proto(active_view_id)?)
2196 } else {
2197 None
2198 };
2199 Ok::<_, anyhow::Error>(())
2200 })??;
2201 Self::add_views_from_leader(
2202 this.clone(),
2203 leader_id,
2204 vec![pane],
2205 response.views,
2206 &mut cx,
2207 )
2208 .await?;
2209 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2210 Ok(())
2211 }))
2212 }
2213
2214 pub fn follow_next_collaborator(
2215 &mut self,
2216 _: &FollowNextCollaborator,
2217 cx: &mut ViewContext<Self>,
2218 ) -> Option<Task<Result<()>>> {
2219 let collaborators = self.project.read(cx).collaborators();
2220 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2221 let mut collaborators = collaborators.keys().copied();
2222 for peer_id in collaborators.by_ref() {
2223 if peer_id == leader_id {
2224 break;
2225 }
2226 }
2227 collaborators.next()
2228 } else if let Some(last_leader_id) =
2229 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2230 {
2231 if collaborators.contains_key(last_leader_id) {
2232 Some(*last_leader_id)
2233 } else {
2234 None
2235 }
2236 } else {
2237 None
2238 };
2239
2240 next_leader_id
2241 .or_else(|| collaborators.keys().copied().next())
2242 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
2243 }
2244
2245 pub fn unfollow(
2246 &mut self,
2247 pane: &ViewHandle<Pane>,
2248 cx: &mut ViewContext<Self>,
2249 ) -> Option<PeerId> {
2250 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
2251 let leader_id = *leader_id;
2252 if let Some(state) = states_by_pane.remove(pane) {
2253 for (_, item) in state.items_by_leader_view_id {
2254 item.set_leader_replica_id(None, cx);
2255 }
2256
2257 if states_by_pane.is_empty() {
2258 self.follower_states_by_leader.remove(&leader_id);
2259 if let Some(project_id) = self.project.read(cx).remote_id() {
2260 self.app_state
2261 .client
2262 .send(proto::Unfollow {
2263 project_id,
2264 leader_id: Some(leader_id),
2265 })
2266 .log_err();
2267 }
2268 }
2269
2270 cx.notify();
2271 return Some(leader_id);
2272 }
2273 }
2274 None
2275 }
2276
2277 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2278 self.follower_states_by_leader.contains_key(&peer_id)
2279 }
2280
2281 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2282 self.leader_state.followers.contains(&peer_id)
2283 }
2284
2285 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2286 // TODO: There should be a better system in place for this
2287 // (https://github.com/zed-industries/zed/issues/1290)
2288 let is_fullscreen = cx.window_is_fullscreen();
2289 let container_theme = if is_fullscreen {
2290 let mut container_theme = theme.workspace.titlebar.container;
2291 container_theme.padding.left = container_theme.padding.right;
2292 container_theme
2293 } else {
2294 theme.workspace.titlebar.container
2295 };
2296
2297 enum TitleBar {}
2298 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2299 Stack::new()
2300 .with_children(
2301 self.titlebar_item
2302 .as_ref()
2303 .map(|item| ChildView::new(item, cx)),
2304 )
2305 .contained()
2306 .with_style(container_theme)
2307 })
2308 .on_click(MouseButton::Left, |event, _, cx| {
2309 if event.click_count == 2 {
2310 cx.zoom_window();
2311 }
2312 })
2313 .constrained()
2314 .with_height(theme.workspace.titlebar.height)
2315 .into_any_named("titlebar")
2316 }
2317
2318 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2319 let active_entry = self.active_project_path(cx);
2320 self.project
2321 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2322 self.update_window_title(cx);
2323 }
2324
2325 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2326 let project = self.project().read(cx);
2327 let mut title = String::new();
2328
2329 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2330 let filename = path
2331 .path
2332 .file_name()
2333 .map(|s| s.to_string_lossy())
2334 .or_else(|| {
2335 Some(Cow::Borrowed(
2336 project
2337 .worktree_for_id(path.worktree_id, cx)?
2338 .read(cx)
2339 .root_name(),
2340 ))
2341 });
2342
2343 if let Some(filename) = filename {
2344 title.push_str(filename.as_ref());
2345 title.push_str(" β ");
2346 }
2347 }
2348
2349 for (i, name) in project.worktree_root_names(cx).enumerate() {
2350 if i > 0 {
2351 title.push_str(", ");
2352 }
2353 title.push_str(name);
2354 }
2355
2356 if title.is_empty() {
2357 title = "empty project".to_string();
2358 }
2359
2360 if project.is_remote() {
2361 title.push_str(" β");
2362 } else if project.is_shared() {
2363 title.push_str(" β");
2364 }
2365
2366 cx.set_window_title(&title);
2367 }
2368
2369 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2370 let is_edited = !self.project.read(cx).is_read_only()
2371 && self
2372 .items(cx)
2373 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2374 if is_edited != self.window_edited {
2375 self.window_edited = is_edited;
2376 cx.set_window_edited(self.window_edited)
2377 }
2378 }
2379
2380 fn render_disconnected_overlay(
2381 &self,
2382 cx: &mut ViewContext<Workspace>,
2383 ) -> Option<AnyElement<Workspace>> {
2384 if self.project.read(cx).is_read_only() {
2385 enum DisconnectedOverlay {}
2386 Some(
2387 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2388 let theme = &theme::current(cx);
2389 Label::new(
2390 "Your connection to the remote project has been lost.",
2391 theme.workspace.disconnected_overlay.text.clone(),
2392 )
2393 .aligned()
2394 .contained()
2395 .with_style(theme.workspace.disconnected_overlay.container)
2396 })
2397 .with_cursor_style(CursorStyle::Arrow)
2398 .capture_all()
2399 .into_any_named("disconnected overlay"),
2400 )
2401 } else {
2402 None
2403 }
2404 }
2405
2406 fn render_notifications(
2407 &self,
2408 theme: &theme::Workspace,
2409 cx: &AppContext,
2410 ) -> Option<AnyElement<Workspace>> {
2411 if self.notifications.is_empty() {
2412 None
2413 } else {
2414 Some(
2415 Flex::column()
2416 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2417 ChildView::new(notification.as_any(), cx)
2418 .contained()
2419 .with_style(theme.notification)
2420 }))
2421 .constrained()
2422 .with_width(theme.notifications.width)
2423 .contained()
2424 .with_style(theme.notifications.container)
2425 .aligned()
2426 .bottom()
2427 .right()
2428 .into_any(),
2429 )
2430 }
2431 }
2432
2433 // RPC handlers
2434
2435 async fn handle_follow(
2436 this: WeakViewHandle<Self>,
2437 envelope: TypedEnvelope<proto::Follow>,
2438 _: Arc<Client>,
2439 mut cx: AsyncAppContext,
2440 ) -> Result<proto::FollowResponse> {
2441 this.update(&mut cx, |this, cx| {
2442 let client = &this.app_state.client;
2443 this.leader_state
2444 .followers
2445 .insert(envelope.original_sender_id()?);
2446
2447 let active_view_id = this.active_item(cx).and_then(|i| {
2448 Some(
2449 i.to_followable_item_handle(cx)?
2450 .remote_id(client, cx)?
2451 .to_proto(),
2452 )
2453 });
2454
2455 cx.notify();
2456
2457 Ok(proto::FollowResponse {
2458 active_view_id,
2459 views: this
2460 .panes()
2461 .iter()
2462 .flat_map(|pane| {
2463 let leader_id = this.leader_for_pane(pane);
2464 pane.read(cx).items().filter_map({
2465 let cx = &cx;
2466 move |item| {
2467 let item = item.to_followable_item_handle(cx)?;
2468 let id = item.remote_id(client, cx)?.to_proto();
2469 let variant = item.to_state_proto(cx)?;
2470 Some(proto::View {
2471 id: Some(id),
2472 leader_id,
2473 variant: Some(variant),
2474 })
2475 }
2476 })
2477 })
2478 .collect(),
2479 })
2480 })?
2481 }
2482
2483 async fn handle_unfollow(
2484 this: WeakViewHandle<Self>,
2485 envelope: TypedEnvelope<proto::Unfollow>,
2486 _: Arc<Client>,
2487 mut cx: AsyncAppContext,
2488 ) -> Result<()> {
2489 this.update(&mut cx, |this, cx| {
2490 this.leader_state
2491 .followers
2492 .remove(&envelope.original_sender_id()?);
2493 cx.notify();
2494 Ok(())
2495 })?
2496 }
2497
2498 async fn handle_update_followers(
2499 this: WeakViewHandle<Self>,
2500 envelope: TypedEnvelope<proto::UpdateFollowers>,
2501 _: Arc<Client>,
2502 cx: AsyncAppContext,
2503 ) -> Result<()> {
2504 let leader_id = envelope.original_sender_id()?;
2505 this.read_with(&cx, |this, _| {
2506 this.leader_updates_tx
2507 .unbounded_send((leader_id, envelope.payload))
2508 })??;
2509 Ok(())
2510 }
2511
2512 async fn process_leader_update(
2513 this: &WeakViewHandle<Self>,
2514 leader_id: PeerId,
2515 update: proto::UpdateFollowers,
2516 cx: &mut AsyncAppContext,
2517 ) -> Result<()> {
2518 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2519 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2520 this.update(cx, |this, _| {
2521 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2522 for state in state.values_mut() {
2523 state.active_view_id =
2524 if let Some(active_view_id) = update_active_view.id.clone() {
2525 Some(ViewId::from_proto(active_view_id)?)
2526 } else {
2527 None
2528 };
2529 }
2530 }
2531 anyhow::Ok(())
2532 })??;
2533 }
2534 proto::update_followers::Variant::UpdateView(update_view) => {
2535 let variant = update_view
2536 .variant
2537 .ok_or_else(|| anyhow!("missing update view variant"))?;
2538 let id = update_view
2539 .id
2540 .ok_or_else(|| anyhow!("missing update view id"))?;
2541 let mut tasks = Vec::new();
2542 this.update(cx, |this, cx| {
2543 let project = this.project.clone();
2544 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2545 for state in state.values_mut() {
2546 let view_id = ViewId::from_proto(id.clone())?;
2547 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2548 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2549 }
2550 }
2551 }
2552 anyhow::Ok(())
2553 })??;
2554 try_join_all(tasks).await.log_err();
2555 }
2556 proto::update_followers::Variant::CreateView(view) => {
2557 let panes = this.read_with(cx, |this, _| {
2558 this.follower_states_by_leader
2559 .get(&leader_id)
2560 .into_iter()
2561 .flat_map(|states_by_pane| states_by_pane.keys())
2562 .cloned()
2563 .collect()
2564 })?;
2565 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2566 }
2567 }
2568 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2569 Ok(())
2570 }
2571
2572 async fn add_views_from_leader(
2573 this: WeakViewHandle<Self>,
2574 leader_id: PeerId,
2575 panes: Vec<ViewHandle<Pane>>,
2576 views: Vec<proto::View>,
2577 cx: &mut AsyncAppContext,
2578 ) -> Result<()> {
2579 let project = this.read_with(cx, |this, _| this.project.clone())?;
2580 let replica_id = project
2581 .read_with(cx, |project, _| {
2582 project
2583 .collaborators()
2584 .get(&leader_id)
2585 .map(|c| c.replica_id)
2586 })
2587 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2588
2589 let item_builders = cx.update(|cx| {
2590 cx.default_global::<FollowableItemBuilders>()
2591 .values()
2592 .map(|b| b.0)
2593 .collect::<Vec<_>>()
2594 });
2595
2596 let mut item_tasks_by_pane = HashMap::default();
2597 for pane in panes {
2598 let mut item_tasks = Vec::new();
2599 let mut leader_view_ids = Vec::new();
2600 for view in &views {
2601 let Some(id) = &view.id else { continue };
2602 let id = ViewId::from_proto(id.clone())?;
2603 let mut variant = view.variant.clone();
2604 if variant.is_none() {
2605 Err(anyhow!("missing variant"))?;
2606 }
2607 for build_item in &item_builders {
2608 let task = cx.update(|cx| {
2609 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2610 });
2611 if let Some(task) = task {
2612 item_tasks.push(task);
2613 leader_view_ids.push(id);
2614 break;
2615 } else {
2616 assert!(variant.is_some());
2617 }
2618 }
2619 }
2620
2621 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2622 }
2623
2624 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2625 let items = futures::future::try_join_all(item_tasks).await?;
2626 this.update(cx, |this, cx| {
2627 let state = this
2628 .follower_states_by_leader
2629 .get_mut(&leader_id)?
2630 .get_mut(&pane)?;
2631
2632 for (id, item) in leader_view_ids.into_iter().zip(items) {
2633 item.set_leader_replica_id(Some(replica_id), cx);
2634 state.items_by_leader_view_id.insert(id, item);
2635 }
2636
2637 Some(())
2638 })?;
2639 }
2640 Ok(())
2641 }
2642
2643 fn update_active_view_for_followers(&self, cx: &AppContext) {
2644 if self.active_pane.read(cx).has_focus() {
2645 self.update_followers(
2646 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2647 id: self.active_item(cx).and_then(|item| {
2648 item.to_followable_item_handle(cx)?
2649 .remote_id(&self.app_state.client, cx)
2650 .map(|id| id.to_proto())
2651 }),
2652 leader_id: self.leader_for_pane(&self.active_pane),
2653 }),
2654 cx,
2655 );
2656 } else {
2657 self.update_followers(
2658 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
2659 id: None,
2660 leader_id: None,
2661 }),
2662 cx,
2663 );
2664 }
2665 }
2666
2667 fn update_followers(
2668 &self,
2669 update: proto::update_followers::Variant,
2670 cx: &AppContext,
2671 ) -> Option<()> {
2672 let project_id = self.project.read(cx).remote_id()?;
2673 if !self.leader_state.followers.is_empty() {
2674 self.app_state
2675 .client
2676 .send(proto::UpdateFollowers {
2677 project_id,
2678 follower_ids: self.leader_state.followers.iter().copied().collect(),
2679 variant: Some(update),
2680 })
2681 .log_err();
2682 }
2683 None
2684 }
2685
2686 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2687 self.follower_states_by_leader
2688 .iter()
2689 .find_map(|(leader_id, state)| {
2690 if state.contains_key(pane) {
2691 Some(*leader_id)
2692 } else {
2693 None
2694 }
2695 })
2696 }
2697
2698 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2699 cx.notify();
2700
2701 let call = self.active_call()?;
2702 let room = call.read(cx).room()?.read(cx);
2703 let participant = room.remote_participant_for_peer_id(leader_id)?;
2704 let mut items_to_activate = Vec::new();
2705 match participant.location {
2706 call::ParticipantLocation::SharedProject { project_id } => {
2707 if Some(project_id) == self.project.read(cx).remote_id() {
2708 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2709 if let Some(item) = state
2710 .active_view_id
2711 .and_then(|id| state.items_by_leader_view_id.get(&id))
2712 {
2713 items_to_activate.push((pane.clone(), item.boxed_clone()));
2714 } else if let Some(shared_screen) =
2715 self.shared_screen_for_peer(leader_id, pane, cx)
2716 {
2717 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2718 }
2719 }
2720 }
2721 }
2722 call::ParticipantLocation::UnsharedProject => {}
2723 call::ParticipantLocation::External => {
2724 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2725 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2726 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2727 }
2728 }
2729 }
2730 }
2731
2732 for (pane, item) in items_to_activate {
2733 let pane_was_focused = pane.read(cx).has_focus();
2734 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2735 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2736 } else {
2737 pane.update(cx, |pane, cx| {
2738 pane.add_item(item.boxed_clone(), false, false, None, cx)
2739 });
2740 }
2741
2742 if pane_was_focused {
2743 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2744 }
2745 }
2746
2747 None
2748 }
2749
2750 fn shared_screen_for_peer(
2751 &self,
2752 peer_id: PeerId,
2753 pane: &ViewHandle<Pane>,
2754 cx: &mut ViewContext<Self>,
2755 ) -> Option<ViewHandle<SharedScreen>> {
2756 let call = self.active_call()?;
2757 let room = call.read(cx).room()?.read(cx);
2758 let participant = room.remote_participant_for_peer_id(peer_id)?;
2759 let track = participant.tracks.values().next()?.clone();
2760 let user = participant.user.clone();
2761
2762 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2763 if item.read(cx).peer_id == peer_id {
2764 return Some(item);
2765 }
2766 }
2767
2768 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2769 }
2770
2771 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2772 if active {
2773 cx.background()
2774 .spawn(persistence::DB.update_timestamp(self.database_id()))
2775 .detach();
2776 } else {
2777 for pane in &self.panes {
2778 pane.update(cx, |pane, cx| {
2779 if let Some(item) = pane.active_item() {
2780 item.workspace_deactivated(cx);
2781 }
2782 if matches!(
2783 settings::get::<WorkspaceSettings>(cx).autosave,
2784 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2785 ) {
2786 for item in pane.items() {
2787 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2788 .detach_and_log_err(cx);
2789 }
2790 }
2791 });
2792 }
2793 }
2794 }
2795
2796 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2797 self.active_call.as_ref().map(|(call, _)| call)
2798 }
2799
2800 fn on_active_call_event(
2801 &mut self,
2802 _: ModelHandle<ActiveCall>,
2803 event: &call::room::Event,
2804 cx: &mut ViewContext<Self>,
2805 ) {
2806 match event {
2807 call::room::Event::ParticipantLocationChanged { participant_id }
2808 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2809 self.leader_updated(*participant_id, cx);
2810 }
2811 _ => {}
2812 }
2813 }
2814
2815 pub fn database_id(&self) -> WorkspaceId {
2816 self.database_id
2817 }
2818
2819 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2820 let project = self.project().read(cx);
2821
2822 if project.is_local() {
2823 Some(
2824 project
2825 .visible_worktrees(cx)
2826 .map(|worktree| worktree.read(cx).abs_path())
2827 .collect::<Vec<_>>()
2828 .into(),
2829 )
2830 } else {
2831 None
2832 }
2833 }
2834
2835 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2836 match member {
2837 Member::Axis(PaneAxis { members, .. }) => {
2838 for child in members.iter() {
2839 self.remove_panes(child.clone(), cx)
2840 }
2841 }
2842 Member::Pane(pane) => {
2843 self.force_remove_pane(&pane, cx);
2844 }
2845 }
2846 }
2847
2848 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2849 self.panes.retain(|p| p != pane);
2850 cx.focus(self.panes.last().unwrap());
2851 if self.last_active_center_pane == Some(pane.downgrade()) {
2852 self.last_active_center_pane = None;
2853 }
2854 cx.notify();
2855 }
2856
2857 fn serialize_workspace(&self, cx: &ViewContext<Self>) {
2858 fn serialize_pane_handle(
2859 pane_handle: &ViewHandle<Pane>,
2860 cx: &AppContext,
2861 ) -> SerializedPane {
2862 let (items, active) = {
2863 let pane = pane_handle.read(cx);
2864 let active_item_id = pane.active_item().map(|item| item.id());
2865 (
2866 pane.items()
2867 .filter_map(|item_handle| {
2868 Some(SerializedItem {
2869 kind: Arc::from(item_handle.serialized_item_kind()?),
2870 item_id: item_handle.id(),
2871 active: Some(item_handle.id()) == active_item_id,
2872 })
2873 })
2874 .collect::<Vec<_>>(),
2875 pane.has_focus(),
2876 )
2877 };
2878
2879 SerializedPane::new(items, active)
2880 }
2881
2882 fn build_serialized_pane_group(
2883 pane_group: &Member,
2884 cx: &AppContext,
2885 ) -> SerializedPaneGroup {
2886 match pane_group {
2887 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2888 axis: *axis,
2889 children: members
2890 .iter()
2891 .map(|member| build_serialized_pane_group(member, cx))
2892 .collect::<Vec<_>>(),
2893 },
2894 Member::Pane(pane_handle) => {
2895 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2896 }
2897 }
2898 }
2899
2900 fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
2901 let left_dock = this.left_dock.read(cx);
2902 let left_visible = left_dock.is_open();
2903 let left_active_panel = left_dock.visible_panel().and_then(|panel| {
2904 Some(
2905 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2906 .to_string(),
2907 )
2908 });
2909 let left_dock_zoom = left_dock
2910 .visible_panel()
2911 .map(|panel| panel.is_zoomed(cx))
2912 .unwrap_or(false);
2913
2914 let right_dock = this.right_dock.read(cx);
2915 let right_visible = right_dock.is_open();
2916 let right_active_panel = right_dock.visible_panel().and_then(|panel| {
2917 Some(
2918 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2919 .to_string(),
2920 )
2921 });
2922 let right_dock_zoom = right_dock
2923 .visible_panel()
2924 .map(|panel| panel.is_zoomed(cx))
2925 .unwrap_or(false);
2926
2927 let bottom_dock = this.bottom_dock.read(cx);
2928 let bottom_visible = bottom_dock.is_open();
2929 let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
2930 Some(
2931 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2932 .to_string(),
2933 )
2934 });
2935 let bottom_dock_zoom = bottom_dock
2936 .visible_panel()
2937 .map(|panel| panel.is_zoomed(cx))
2938 .unwrap_or(false);
2939
2940 DockStructure {
2941 left: DockData {
2942 visible: left_visible,
2943 active_panel: left_active_panel,
2944 zoom: left_dock_zoom,
2945 },
2946 right: DockData {
2947 visible: right_visible,
2948 active_panel: right_active_panel,
2949 zoom: right_dock_zoom,
2950 },
2951 bottom: DockData {
2952 visible: bottom_visible,
2953 active_panel: bottom_active_panel,
2954 zoom: bottom_dock_zoom,
2955 },
2956 }
2957 }
2958
2959 if let Some(location) = self.location(cx) {
2960 // Load bearing special case:
2961 // - with_local_workspace() relies on this to not have other stuff open
2962 // when you open your log
2963 if !location.paths().is_empty() {
2964 let center_group = build_serialized_pane_group(&self.center.root, cx);
2965 let docks = build_serialized_docks(self, cx);
2966
2967 let serialized_workspace = SerializedWorkspace {
2968 id: self.database_id,
2969 location,
2970 center_group,
2971 bounds: Default::default(),
2972 display: Default::default(),
2973 docks,
2974 };
2975
2976 cx.background()
2977 .spawn(persistence::DB.save_workspace(serialized_workspace))
2978 .detach();
2979 }
2980 }
2981 }
2982
2983 pub(crate) fn load_workspace(
2984 workspace: WeakViewHandle<Workspace>,
2985 serialized_workspace: SerializedWorkspace,
2986 paths_to_open: Vec<Option<ProjectPath>>,
2987 cx: &mut AppContext,
2988 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2989 cx.spawn(|mut cx| async move {
2990 let result = async_iife! {{
2991 let (project, old_center_pane) =
2992 workspace.read_with(&cx, |workspace, _| {
2993 (
2994 workspace.project().clone(),
2995 workspace.last_active_center_pane.clone(),
2996 )
2997 })?;
2998
2999 let mut center_items = None;
3000 let mut center_group = None;
3001 // Traverse the splits tree and add to things
3002 if let Some((group, active_pane, items)) = serialized_workspace
3003 .center_group
3004 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
3005 .await {
3006 center_items = Some(items);
3007 center_group = Some((group, active_pane))
3008 }
3009
3010 let resulting_list = cx.read(|cx| {
3011 let mut opened_items = center_items
3012 .unwrap_or_default()
3013 .into_iter()
3014 .filter_map(|item| {
3015 let item = item?;
3016 let project_path = item.project_path(cx)?;
3017 Some((project_path, item))
3018 })
3019 .collect::<HashMap<_, _>>();
3020
3021 paths_to_open
3022 .into_iter()
3023 .map(|path_to_open| {
3024 path_to_open.map(|path_to_open| {
3025 Ok(opened_items.remove(&path_to_open))
3026 })
3027 .transpose()
3028 .map(|item| item.flatten())
3029 .transpose()
3030 })
3031 .collect::<Vec<_>>()
3032 });
3033
3034 // Remove old panes from workspace panes list
3035 workspace.update(&mut cx, |workspace, cx| {
3036 if let Some((center_group, active_pane)) = center_group {
3037 workspace.remove_panes(workspace.center.root.clone(), cx);
3038
3039 // Swap workspace center group
3040 workspace.center = PaneGroup::with_root(center_group);
3041
3042 // Change the focus to the workspace first so that we retrigger focus in on the pane.
3043 cx.focus_self();
3044
3045 if let Some(active_pane) = active_pane {
3046 cx.focus(&active_pane);
3047 } else {
3048 cx.focus(workspace.panes.last().unwrap());
3049 }
3050 } else {
3051 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
3052 if let Some(old_center_handle) = old_center_handle {
3053 cx.focus(&old_center_handle)
3054 } else {
3055 cx.focus_self()
3056 }
3057 }
3058
3059 let docks = serialized_workspace.docks;
3060 workspace.left_dock.update(cx, |dock, cx| {
3061 dock.set_open(docks.left.visible, cx);
3062 if let Some(active_panel) = docks.left.active_panel {
3063 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3064 dock.activate_panel(ix, cx);
3065 }
3066 }
3067 dock.active_panel()
3068 .map(|panel| {
3069 panel.set_zoomed(docks.left.zoom, cx)
3070 });
3071 if docks.left.visible && docks.left.zoom {
3072 cx.focus_self()
3073 }
3074 });
3075 // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
3076 workspace.right_dock.update(cx, |dock, cx| {
3077 dock.set_open(docks.right.visible, cx);
3078 if let Some(active_panel) = docks.right.active_panel {
3079 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3080 dock.activate_panel(ix, cx);
3081
3082 }
3083 }
3084 dock.active_panel()
3085 .map(|panel| {
3086 panel.set_zoomed(docks.right.zoom, cx)
3087 });
3088
3089 if docks.right.visible && docks.right.zoom {
3090 cx.focus_self()
3091 }
3092 });
3093 workspace.bottom_dock.update(cx, |dock, cx| {
3094 dock.set_open(docks.bottom.visible, cx);
3095 if let Some(active_panel) = docks.bottom.active_panel {
3096 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
3097 dock.activate_panel(ix, cx);
3098 }
3099 }
3100
3101 dock.active_panel()
3102 .map(|panel| {
3103 panel.set_zoomed(docks.bottom.zoom, cx)
3104 });
3105
3106 if docks.bottom.visible && docks.bottom.zoom {
3107 cx.focus_self()
3108 }
3109 });
3110
3111
3112 cx.notify();
3113 })?;
3114
3115 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3116 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3117
3118 Ok::<_, anyhow::Error>(resulting_list)
3119 }};
3120
3121 result.await.unwrap_or_default()
3122 })
3123 }
3124
3125 #[cfg(any(test, feature = "test-support"))]
3126 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
3127 let app_state = Arc::new(AppState {
3128 languages: project.read(cx).languages().clone(),
3129 client: project.read(cx).client(),
3130 user_store: project.read(cx).user_store(),
3131 fs: project.read(cx).fs().clone(),
3132 build_window_options: |_, _, _| Default::default(),
3133 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
3134 background_actions: || &[],
3135 });
3136 Self::new(0, project, app_state, cx)
3137 }
3138
3139 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
3140 let dock = match position {
3141 DockPosition::Left => &self.left_dock,
3142 DockPosition::Right => &self.right_dock,
3143 DockPosition::Bottom => &self.bottom_dock,
3144 };
3145 let active_panel = dock.read(cx).visible_panel()?;
3146 let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
3147 dock.read(cx).render_placeholder(cx)
3148 } else {
3149 ChildView::new(dock, cx).into_any()
3150 };
3151
3152 Some(
3153 element
3154 .constrained()
3155 .dynamically(move |constraint, _, cx| match position {
3156 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
3157 Vector2F::new(20., constraint.min.y()),
3158 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
3159 ),
3160 DockPosition::Bottom => SizeConstraint::new(
3161 Vector2F::new(constraint.min.x(), 20.),
3162 Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
3163 ),
3164 })
3165 .into_any(),
3166 )
3167 }
3168}
3169
3170fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3171 ZED_WINDOW_POSITION
3172 .zip(*ZED_WINDOW_SIZE)
3173 .map(|(position, size)| {
3174 WindowBounds::Fixed(RectF::new(
3175 cx.platform().screens()[0].bounds().origin() + position,
3176 size,
3177 ))
3178 })
3179}
3180
3181async fn open_items(
3182 serialized_workspace: Option<SerializedWorkspace>,
3183 workspace: &WeakViewHandle<Workspace>,
3184 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3185 app_state: Arc<AppState>,
3186 mut cx: AsyncAppContext,
3187) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
3188 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3189
3190 if let Some(serialized_workspace) = serialized_workspace {
3191 let workspace = workspace.clone();
3192 let restored_items = cx
3193 .update(|cx| {
3194 Workspace::load_workspace(
3195 workspace,
3196 serialized_workspace,
3197 project_paths_to_open
3198 .iter()
3199 .map(|(_, project_path)| project_path)
3200 .cloned()
3201 .collect(),
3202 cx,
3203 )
3204 })
3205 .await;
3206
3207 let restored_project_paths = cx.read(|cx| {
3208 restored_items
3209 .iter()
3210 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
3211 .collect::<HashSet<_>>()
3212 });
3213
3214 opened_items = restored_items;
3215 project_paths_to_open
3216 .iter_mut()
3217 .for_each(|(_, project_path)| {
3218 if let Some(project_path_to_open) = project_path {
3219 if restored_project_paths.contains(project_path_to_open) {
3220 *project_path = None;
3221 }
3222 }
3223 });
3224 } else {
3225 for _ in 0..project_paths_to_open.len() {
3226 opened_items.push(None);
3227 }
3228 }
3229 assert!(opened_items.len() == project_paths_to_open.len());
3230
3231 let tasks =
3232 project_paths_to_open
3233 .into_iter()
3234 .enumerate()
3235 .map(|(i, (abs_path, project_path))| {
3236 let workspace = workspace.clone();
3237 cx.spawn(|mut cx| {
3238 let fs = app_state.fs.clone();
3239 async move {
3240 let file_project_path = project_path?;
3241 if fs.is_file(&abs_path).await {
3242 Some((
3243 i,
3244 workspace
3245 .update(&mut cx, |workspace, cx| {
3246 workspace.open_path(file_project_path, None, true, cx)
3247 })
3248 .log_err()?
3249 .await,
3250 ))
3251 } else {
3252 None
3253 }
3254 }
3255 })
3256 });
3257
3258 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
3259 .await
3260 .into_iter()
3261 {
3262 if let Some((i, path_open_result)) = maybe_opened_path {
3263 opened_items[i] = Some(path_open_result);
3264 }
3265 }
3266
3267 opened_items
3268}
3269
3270fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3271 const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
3272 const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
3273 const MESSAGE_ID: usize = 2;
3274
3275 if workspace
3276 .read_with(cx, |workspace, cx| {
3277 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3278 })
3279 .unwrap_or(false)
3280 {
3281 return;
3282 }
3283
3284 if db::kvp::KEY_VALUE_STORE
3285 .read_kvp(NEW_DOCK_HINT_KEY)
3286 .ok()
3287 .flatten()
3288 .is_some()
3289 {
3290 if !workspace
3291 .read_with(cx, |workspace, cx| {
3292 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
3293 })
3294 .unwrap_or(false)
3295 {
3296 cx.update(|cx| {
3297 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
3298 let entry = tracker
3299 .entry(TypeId::of::<MessageNotification>())
3300 .or_default();
3301 if !entry.contains(&MESSAGE_ID) {
3302 entry.push(MESSAGE_ID);
3303 }
3304 });
3305 });
3306 }
3307
3308 return;
3309 }
3310
3311 cx.spawn(|_| async move {
3312 db::kvp::KEY_VALUE_STORE
3313 .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
3314 .await
3315 .ok();
3316 })
3317 .detach();
3318
3319 workspace
3320 .update(cx, |workspace, cx| {
3321 workspace.show_notification_once(2, cx, |cx| {
3322 cx.add_view(|_| {
3323 MessageNotification::new_element(|text, _| {
3324 Text::new(
3325 "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
3326 text,
3327 )
3328 .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
3329 let code_span_background_color = settings::get::<ThemeSettings>(cx)
3330 .theme
3331 .editor
3332 .document_highlight_read_background;
3333
3334 scene.push_quad(gpui::Quad {
3335 bounds,
3336 background: Some(code_span_background_color),
3337 border: Default::default(),
3338 corner_radius: 2.0,
3339 })
3340 })
3341 .into_any()
3342 })
3343 .with_click_message("Read more about the new panel system")
3344 .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
3345 })
3346 })
3347 })
3348 .ok();
3349}
3350
3351fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
3352 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3353
3354 workspace
3355 .update(cx, |workspace, cx| {
3356 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3357 workspace.show_notification_once(0, cx, |cx| {
3358 cx.add_view(|_| {
3359 MessageNotification::new("Failed to load any database file.")
3360 .with_click_message("Click to let us know about this error")
3361 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
3362 })
3363 });
3364 } else {
3365 let backup_path = (*db::BACKUP_DB_PATH).read();
3366 if let Some(backup_path) = backup_path.clone() {
3367 workspace.show_notification_once(1, cx, move |cx| {
3368 cx.add_view(move |_| {
3369 MessageNotification::new(format!(
3370 "Database file was corrupted. Old database backed up to {}",
3371 backup_path.display()
3372 ))
3373 .with_click_message("Click to show old database in finder")
3374 .on_click(move |cx| {
3375 cx.platform().open_url(&backup_path.to_string_lossy())
3376 })
3377 })
3378 });
3379 }
3380 }
3381 })
3382 .log_err();
3383}
3384
3385impl Entity for Workspace {
3386 type Event = Event;
3387}
3388
3389impl View for Workspace {
3390 fn ui_name() -> &'static str {
3391 "Workspace"
3392 }
3393
3394 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3395 let theme = theme::current(cx).clone();
3396 Stack::new()
3397 .with_child(
3398 Flex::column()
3399 .with_child(self.render_titlebar(&theme, cx))
3400 .with_child(
3401 Stack::new()
3402 .with_child({
3403 let project = self.project.clone();
3404 Flex::row()
3405 .with_children(self.render_dock(DockPosition::Left, cx))
3406 .with_child(
3407 Flex::column()
3408 .with_child(
3409 FlexItem::new(
3410 self.center.render(
3411 &project,
3412 &theme,
3413 &self.follower_states_by_leader,
3414 self.active_call(),
3415 self.active_pane(),
3416 self.zoomed
3417 .as_ref()
3418 .and_then(|zoomed| zoomed.upgrade(cx))
3419 .as_ref(),
3420 &self.app_state,
3421 cx,
3422 ),
3423 )
3424 .flex(1., true),
3425 )
3426 .with_children(
3427 self.render_dock(DockPosition::Bottom, cx),
3428 )
3429 .flex(1., true),
3430 )
3431 .with_children(self.render_dock(DockPosition::Right, cx))
3432 })
3433 .with_child(Overlay::new(
3434 Stack::new()
3435 .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3436 enum ZoomBackground {}
3437 let zoomed = zoomed.upgrade(cx)?;
3438
3439 let mut foreground_style =
3440 theme.workspace.zoomed_pane_foreground;
3441 if let Some(zoomed_dock_position) = self.zoomed_position {
3442 foreground_style =
3443 theme.workspace.zoomed_panel_foreground;
3444 let margin = foreground_style.margin.top;
3445 let border = foreground_style.border.top;
3446
3447 // Only include a margin and border on the opposite side.
3448 foreground_style.margin.top = 0.;
3449 foreground_style.margin.left = 0.;
3450 foreground_style.margin.bottom = 0.;
3451 foreground_style.margin.right = 0.;
3452 foreground_style.border.top = false;
3453 foreground_style.border.left = false;
3454 foreground_style.border.bottom = false;
3455 foreground_style.border.right = false;
3456 match zoomed_dock_position {
3457 DockPosition::Left => {
3458 foreground_style.margin.right = margin;
3459 foreground_style.border.right = border;
3460 }
3461 DockPosition::Right => {
3462 foreground_style.margin.left = margin;
3463 foreground_style.border.left = border;
3464 }
3465 DockPosition::Bottom => {
3466 foreground_style.margin.top = margin;
3467 foreground_style.border.top = border;
3468 }
3469 }
3470 }
3471
3472 Some(
3473 ChildView::new(&zoomed, cx)
3474 .contained()
3475 .with_style(foreground_style)
3476 .aligned()
3477 .contained()
3478 .with_style(theme.workspace.zoomed_background)
3479 .mouse::<ZoomBackground>(0)
3480 .capture_all()
3481 .on_down(
3482 MouseButton::Left,
3483 |_, this: &mut Self, cx| {
3484 this.zoom_out(cx);
3485 },
3486 ),
3487 )
3488 }))
3489 .with_children(self.modal.as_ref().map(|modal| {
3490 ChildView::new(modal, cx)
3491 .contained()
3492 .with_style(theme.workspace.modal)
3493 .aligned()
3494 .top()
3495 }))
3496 .with_children(self.render_notifications(&theme.workspace, cx)),
3497 ))
3498 .flex(1.0, true),
3499 )
3500 .with_child(ChildView::new(&self.status_bar, cx))
3501 .contained()
3502 .with_background_color(theme.workspace.background),
3503 )
3504 .with_children(DragAndDrop::render(cx))
3505 .with_children(self.render_disconnected_overlay(cx))
3506 .into_any_named("workspace")
3507 }
3508
3509 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3510 if cx.is_self_focused() {
3511 cx.focus(&self.active_pane);
3512 }
3513 }
3514}
3515
3516impl ViewId {
3517 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3518 Ok(Self {
3519 creator: message
3520 .creator
3521 .ok_or_else(|| anyhow!("creator is missing"))?,
3522 id: message.id,
3523 })
3524 }
3525
3526 pub(crate) fn to_proto(&self) -> proto::ViewId {
3527 proto::ViewId {
3528 creator: Some(self.creator),
3529 id: self.id,
3530 }
3531 }
3532}
3533
3534pub trait WorkspaceHandle {
3535 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3536}
3537
3538impl WorkspaceHandle for ViewHandle<Workspace> {
3539 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3540 self.read(cx)
3541 .worktrees(cx)
3542 .flat_map(|worktree| {
3543 let worktree_id = worktree.read(cx).id();
3544 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3545 worktree_id,
3546 path: f.path.clone(),
3547 })
3548 })
3549 .collect::<Vec<_>>()
3550 }
3551}
3552
3553impl std::fmt::Debug for OpenPaths {
3554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3555 f.debug_struct("OpenPaths")
3556 .field("paths", &self.paths)
3557 .finish()
3558 }
3559}
3560
3561pub struct WorkspaceCreated(pub WeakViewHandle<Workspace>);
3562
3563pub fn activate_workspace_for_project(
3564 cx: &mut AsyncAppContext,
3565 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3566) -> Option<WeakViewHandle<Workspace>> {
3567 for window_id in cx.window_ids() {
3568 let handle = cx
3569 .update_window(window_id, |cx| {
3570 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3571 let project = workspace_handle.read(cx).project.clone();
3572 if project.update(cx, &predicate) {
3573 cx.activate_window();
3574 return Some(workspace_handle.clone());
3575 }
3576 }
3577 None
3578 })
3579 .flatten();
3580
3581 if let Some(handle) = handle {
3582 return Some(handle.downgrade());
3583 }
3584 }
3585 None
3586}
3587
3588pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3589 DB.last_workspace().await.log_err().flatten()
3590}
3591
3592#[allow(clippy::type_complexity)]
3593pub fn open_paths(
3594 abs_paths: &[PathBuf],
3595 app_state: &Arc<AppState>,
3596 requesting_window_id: Option<usize>,
3597 cx: &mut AppContext,
3598) -> Task<
3599 Result<(
3600 WeakViewHandle<Workspace>,
3601 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3602 )>,
3603> {
3604 let app_state = app_state.clone();
3605 let abs_paths = abs_paths.to_vec();
3606 cx.spawn(|mut cx| async move {
3607 // Open paths in existing workspace if possible
3608 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3609 project.contains_paths(&abs_paths, cx)
3610 });
3611
3612 if let Some(existing) = existing {
3613 Ok((
3614 existing.clone(),
3615 existing
3616 .update(&mut cx, |workspace, cx| {
3617 workspace.open_paths(abs_paths, true, cx)
3618 })?
3619 .await,
3620 ))
3621 } else {
3622 Ok(cx
3623 .update(|cx| {
3624 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3625 })
3626 .await)
3627 }
3628 })
3629}
3630
3631pub fn open_new(
3632 app_state: &Arc<AppState>,
3633 cx: &mut AppContext,
3634 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3635) -> Task<()> {
3636 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3637 cx.spawn(|mut cx| async move {
3638 let (workspace, opened_paths) = task.await;
3639
3640 workspace
3641 .update(&mut cx, |workspace, cx| {
3642 if opened_paths.is_empty() {
3643 init(workspace, cx)
3644 }
3645 })
3646 .log_err();
3647 })
3648}
3649
3650pub fn create_and_open_local_file(
3651 path: &'static Path,
3652 cx: &mut ViewContext<Workspace>,
3653 default_content: impl 'static + Send + FnOnce() -> Rope,
3654) -> Task<Result<Box<dyn ItemHandle>>> {
3655 cx.spawn(|workspace, mut cx| async move {
3656 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3657 if !fs.is_file(path).await {
3658 fs.create_file(path, Default::default()).await?;
3659 fs.save(path, &default_content(), Default::default())
3660 .await?;
3661 }
3662
3663 let mut items = workspace
3664 .update(&mut cx, |workspace, cx| {
3665 workspace.with_local_workspace(cx, |workspace, cx| {
3666 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3667 })
3668 })?
3669 .await?
3670 .await;
3671
3672 let item = items.pop().flatten();
3673 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3674 })
3675}
3676
3677pub fn join_remote_project(
3678 project_id: u64,
3679 follow_user_id: u64,
3680 app_state: Arc<AppState>,
3681 cx: &mut AppContext,
3682) -> Task<Result<()>> {
3683 cx.spawn(|mut cx| async move {
3684 let existing_workspace = cx
3685 .window_ids()
3686 .into_iter()
3687 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3688 .find(|workspace| {
3689 cx.read_window(workspace.window_id(), |cx| {
3690 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3691 })
3692 .unwrap_or(false)
3693 });
3694
3695 let workspace = if let Some(existing_workspace) = existing_workspace {
3696 existing_workspace.downgrade()
3697 } else {
3698 let active_call = cx.read(ActiveCall::global);
3699 let room = active_call
3700 .read_with(&cx, |call, _| call.room().cloned())
3701 .ok_or_else(|| anyhow!("not in a call"))?;
3702 let project = room
3703 .update(&mut cx, |room, cx| {
3704 room.join_project(
3705 project_id,
3706 app_state.languages.clone(),
3707 app_state.fs.clone(),
3708 cx,
3709 )
3710 })
3711 .await?;
3712
3713 let window_bounds_override = window_bounds_env_override(&cx);
3714 let (_, workspace) = cx.add_window(
3715 (app_state.build_window_options)(
3716 window_bounds_override,
3717 None,
3718 cx.platform().as_ref(),
3719 ),
3720 |cx| Workspace::new(0, project, app_state.clone(), cx),
3721 );
3722 (app_state.initialize_workspace)(
3723 workspace.downgrade(),
3724 false,
3725 app_state.clone(),
3726 cx.clone(),
3727 )
3728 .await
3729 .log_err();
3730
3731 workspace.downgrade()
3732 };
3733
3734 cx.activate_window(workspace.window_id());
3735 cx.platform().activate(true);
3736
3737 workspace.update(&mut cx, |workspace, cx| {
3738 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3739 let follow_peer_id = room
3740 .read(cx)
3741 .remote_participants()
3742 .iter()
3743 .find(|(_, participant)| participant.user.id == follow_user_id)
3744 .map(|(_, p)| p.peer_id)
3745 .or_else(|| {
3746 // If we couldn't follow the given user, follow the host instead.
3747 let collaborator = workspace
3748 .project()
3749 .read(cx)
3750 .collaborators()
3751 .values()
3752 .find(|collaborator| collaborator.replica_id == 0)?;
3753 Some(collaborator.peer_id)
3754 });
3755
3756 if let Some(follow_peer_id) = follow_peer_id {
3757 if !workspace.is_being_followed(follow_peer_id) {
3758 workspace
3759 .toggle_follow(follow_peer_id, cx)
3760 .map(|follow| follow.detach_and_log_err(cx));
3761 }
3762 }
3763 }
3764 })?;
3765
3766 anyhow::Ok(())
3767 })
3768}
3769
3770pub fn restart(_: &Restart, cx: &mut AppContext) {
3771 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3772 cx.spawn(|mut cx| async move {
3773 let mut workspaces = cx
3774 .window_ids()
3775 .into_iter()
3776 .filter_map(|window_id| {
3777 Some(
3778 cx.root_view(window_id)?
3779 .clone()
3780 .downcast::<Workspace>()?
3781 .downgrade(),
3782 )
3783 })
3784 .collect::<Vec<_>>();
3785
3786 // If multiple windows have unsaved changes, and need a save prompt,
3787 // prompt in the active window before switching to a different window.
3788 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3789
3790 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3791 let answer = cx.prompt(
3792 workspace.window_id(),
3793 PromptLevel::Info,
3794 "Are you sure you want to restart?",
3795 &["Restart", "Cancel"],
3796 );
3797
3798 if let Some(mut answer) = answer {
3799 let answer = answer.next().await;
3800 if answer != Some(0) {
3801 return Ok(());
3802 }
3803 }
3804 }
3805
3806 // If the user cancels any save prompt, then keep the app open.
3807 for workspace in workspaces {
3808 if !workspace
3809 .update(&mut cx, |workspace, cx| {
3810 workspace.prepare_to_close(true, cx)
3811 })?
3812 .await?
3813 {
3814 return Ok(());
3815 }
3816 }
3817 cx.platform().restart();
3818 anyhow::Ok(())
3819 })
3820 .detach_and_log_err(cx);
3821}
3822
3823fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3824 let mut parts = value.split(',');
3825 let width: usize = parts.next()?.parse().ok()?;
3826 let height: usize = parts.next()?.parse().ok()?;
3827 Some(vec2f(width as f32, height as f32))
3828}
3829
3830#[cfg(test)]
3831mod tests {
3832 use super::*;
3833 use crate::{
3834 dock::test::{TestPanel, TestPanelEvent},
3835 item::test::{TestItem, TestItemEvent, TestProjectItem},
3836 };
3837 use fs::FakeFs;
3838 use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
3839 use project::{Project, ProjectEntryId};
3840 use serde_json::json;
3841 use settings::SettingsStore;
3842 use std::{cell::RefCell, rc::Rc};
3843
3844 #[gpui::test]
3845 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3846 init_test(cx);
3847
3848 let fs = FakeFs::new(cx.background());
3849 let project = Project::test(fs, [], cx).await;
3850 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3851
3852 // Adding an item with no ambiguity renders the tab without detail.
3853 let item1 = cx.add_view(window_id, |_| {
3854 let mut item = TestItem::new();
3855 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3856 item
3857 });
3858 workspace.update(cx, |workspace, cx| {
3859 workspace.add_item(Box::new(item1.clone()), cx);
3860 });
3861 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3862
3863 // Adding an item that creates ambiguity increases the level of detail on
3864 // both tabs.
3865 let item2 = cx.add_view(window_id, |_| {
3866 let mut item = TestItem::new();
3867 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3868 item
3869 });
3870 workspace.update(cx, |workspace, cx| {
3871 workspace.add_item(Box::new(item2.clone()), cx);
3872 });
3873 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3874 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3875
3876 // Adding an item that creates ambiguity increases the level of detail only
3877 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3878 // we stop at the highest detail available.
3879 let item3 = cx.add_view(window_id, |_| {
3880 let mut item = TestItem::new();
3881 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3882 item
3883 });
3884 workspace.update(cx, |workspace, cx| {
3885 workspace.add_item(Box::new(item3.clone()), cx);
3886 });
3887 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3888 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3889 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3890 }
3891
3892 #[gpui::test]
3893 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3894 init_test(cx);
3895
3896 let fs = FakeFs::new(cx.background());
3897 fs.insert_tree(
3898 "/root1",
3899 json!({
3900 "one.txt": "",
3901 "two.txt": "",
3902 }),
3903 )
3904 .await;
3905 fs.insert_tree(
3906 "/root2",
3907 json!({
3908 "three.txt": "",
3909 }),
3910 )
3911 .await;
3912
3913 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3914 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3915 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3916 let worktree_id = project.read_with(cx, |project, cx| {
3917 project.worktrees(cx).next().unwrap().read(cx).id()
3918 });
3919
3920 let item1 = cx.add_view(window_id, |cx| {
3921 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3922 });
3923 let item2 = cx.add_view(window_id, |cx| {
3924 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3925 });
3926
3927 // Add an item to an empty pane
3928 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3929 project.read_with(cx, |project, cx| {
3930 assert_eq!(
3931 project.active_entry(),
3932 project
3933 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3934 .map(|e| e.id)
3935 );
3936 });
3937 assert_eq!(
3938 cx.current_window_title(window_id).as_deref(),
3939 Some("one.txt β root1")
3940 );
3941
3942 // Add a second item to a non-empty pane
3943 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3944 assert_eq!(
3945 cx.current_window_title(window_id).as_deref(),
3946 Some("two.txt β root1")
3947 );
3948 project.read_with(cx, |project, cx| {
3949 assert_eq!(
3950 project.active_entry(),
3951 project
3952 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3953 .map(|e| e.id)
3954 );
3955 });
3956
3957 // Close the active item
3958 pane.update(cx, |pane, cx| {
3959 pane.close_active_item(&Default::default(), cx).unwrap()
3960 })
3961 .await
3962 .unwrap();
3963 assert_eq!(
3964 cx.current_window_title(window_id).as_deref(),
3965 Some("one.txt β root1")
3966 );
3967 project.read_with(cx, |project, cx| {
3968 assert_eq!(
3969 project.active_entry(),
3970 project
3971 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3972 .map(|e| e.id)
3973 );
3974 });
3975
3976 // Add a project folder
3977 project
3978 .update(cx, |project, cx| {
3979 project.find_or_create_local_worktree("/root2", true, cx)
3980 })
3981 .await
3982 .unwrap();
3983 assert_eq!(
3984 cx.current_window_title(window_id).as_deref(),
3985 Some("one.txt β root1, root2")
3986 );
3987
3988 // Remove a project folder
3989 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3990 assert_eq!(
3991 cx.current_window_title(window_id).as_deref(),
3992 Some("one.txt β root2")
3993 );
3994 }
3995
3996 #[gpui::test]
3997 async fn test_close_window(cx: &mut TestAppContext) {
3998 init_test(cx);
3999
4000 let fs = FakeFs::new(cx.background());
4001 fs.insert_tree("/root", json!({ "one": "" })).await;
4002
4003 let project = Project::test(fs, ["root".as_ref()], cx).await;
4004 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4005
4006 // When there are no dirty items, there's nothing to do.
4007 let item1 = cx.add_view(window_id, |_| TestItem::new());
4008 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4009 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4010 assert!(task.await.unwrap());
4011
4012 // When there are dirty untitled items, prompt to save each one. If the user
4013 // cancels any prompt, then abort.
4014 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
4015 let item3 = cx.add_view(window_id, |cx| {
4016 TestItem::new()
4017 .with_dirty(true)
4018 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4019 });
4020 workspace.update(cx, |w, cx| {
4021 w.add_item(Box::new(item2.clone()), cx);
4022 w.add_item(Box::new(item3.clone()), cx);
4023 });
4024 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4025 cx.foreground().run_until_parked();
4026 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
4027 cx.foreground().run_until_parked();
4028 assert!(!cx.has_pending_prompt(window_id));
4029 assert!(!task.await.unwrap());
4030 }
4031
4032 #[gpui::test]
4033 async fn test_close_pane_items(cx: &mut TestAppContext) {
4034 init_test(cx);
4035
4036 let fs = FakeFs::new(cx.background());
4037
4038 let project = Project::test(fs, None, cx).await;
4039 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4040
4041 let item1 = cx.add_view(window_id, |cx| {
4042 TestItem::new()
4043 .with_dirty(true)
4044 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4045 });
4046 let item2 = cx.add_view(window_id, |cx| {
4047 TestItem::new()
4048 .with_dirty(true)
4049 .with_conflict(true)
4050 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4051 });
4052 let item3 = cx.add_view(window_id, |cx| {
4053 TestItem::new()
4054 .with_dirty(true)
4055 .with_conflict(true)
4056 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4057 });
4058 let item4 = cx.add_view(window_id, |cx| {
4059 TestItem::new()
4060 .with_dirty(true)
4061 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4062 });
4063 let pane = workspace.update(cx, |workspace, cx| {
4064 workspace.add_item(Box::new(item1.clone()), cx);
4065 workspace.add_item(Box::new(item2.clone()), cx);
4066 workspace.add_item(Box::new(item3.clone()), cx);
4067 workspace.add_item(Box::new(item4.clone()), cx);
4068 workspace.active_pane().clone()
4069 });
4070
4071 let close_items = pane.update(cx, |pane, cx| {
4072 pane.activate_item(1, true, true, cx);
4073 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4074 let item1_id = item1.id();
4075 let item3_id = item3.id();
4076 let item4_id = item4.id();
4077 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
4078 });
4079 cx.foreground().run_until_parked();
4080
4081 // There's a prompt to save item 1.
4082 pane.read_with(cx, |pane, _| {
4083 assert_eq!(pane.items_len(), 4);
4084 assert_eq!(pane.active_item().unwrap().id(), item1.id());
4085 });
4086 assert!(cx.has_pending_prompt(window_id));
4087
4088 // Confirm saving item 1.
4089 cx.simulate_prompt_answer(window_id, 0);
4090 cx.foreground().run_until_parked();
4091
4092 // Item 1 is saved. There's a prompt to save item 3.
4093 pane.read_with(cx, |pane, cx| {
4094 assert_eq!(item1.read(cx).save_count, 1);
4095 assert_eq!(item1.read(cx).save_as_count, 0);
4096 assert_eq!(item1.read(cx).reload_count, 0);
4097 assert_eq!(pane.items_len(), 3);
4098 assert_eq!(pane.active_item().unwrap().id(), item3.id());
4099 });
4100 assert!(cx.has_pending_prompt(window_id));
4101
4102 // Cancel saving item 3.
4103 cx.simulate_prompt_answer(window_id, 1);
4104 cx.foreground().run_until_parked();
4105
4106 // Item 3 is reloaded. There's a prompt to save item 4.
4107 pane.read_with(cx, |pane, cx| {
4108 assert_eq!(item3.read(cx).save_count, 0);
4109 assert_eq!(item3.read(cx).save_as_count, 0);
4110 assert_eq!(item3.read(cx).reload_count, 1);
4111 assert_eq!(pane.items_len(), 2);
4112 assert_eq!(pane.active_item().unwrap().id(), item4.id());
4113 });
4114 assert!(cx.has_pending_prompt(window_id));
4115
4116 // Confirm saving item 4.
4117 cx.simulate_prompt_answer(window_id, 0);
4118 cx.foreground().run_until_parked();
4119
4120 // There's a prompt for a path for item 4.
4121 cx.simulate_new_path_selection(|_| Some(Default::default()));
4122 close_items.await.unwrap();
4123
4124 // The requested items are closed.
4125 pane.read_with(cx, |pane, cx| {
4126 assert_eq!(item4.read(cx).save_count, 0);
4127 assert_eq!(item4.read(cx).save_as_count, 1);
4128 assert_eq!(item4.read(cx).reload_count, 0);
4129 assert_eq!(pane.items_len(), 1);
4130 assert_eq!(pane.active_item().unwrap().id(), item2.id());
4131 });
4132 }
4133
4134 #[gpui::test]
4135 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4136 init_test(cx);
4137
4138 let fs = FakeFs::new(cx.background());
4139
4140 let project = Project::test(fs, [], cx).await;
4141 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4142
4143 // Create several workspace items with single project entries, and two
4144 // workspace items with multiple project entries.
4145 let single_entry_items = (0..=4)
4146 .map(|project_entry_id| {
4147 cx.add_view(window_id, |cx| {
4148 TestItem::new()
4149 .with_dirty(true)
4150 .with_project_items(&[TestProjectItem::new(
4151 project_entry_id,
4152 &format!("{project_entry_id}.txt"),
4153 cx,
4154 )])
4155 })
4156 })
4157 .collect::<Vec<_>>();
4158 let item_2_3 = cx.add_view(window_id, |cx| {
4159 TestItem::new()
4160 .with_dirty(true)
4161 .with_singleton(false)
4162 .with_project_items(&[
4163 single_entry_items[2].read(cx).project_items[0].clone(),
4164 single_entry_items[3].read(cx).project_items[0].clone(),
4165 ])
4166 });
4167 let item_3_4 = cx.add_view(window_id, |cx| {
4168 TestItem::new()
4169 .with_dirty(true)
4170 .with_singleton(false)
4171 .with_project_items(&[
4172 single_entry_items[3].read(cx).project_items[0].clone(),
4173 single_entry_items[4].read(cx).project_items[0].clone(),
4174 ])
4175 });
4176
4177 // Create two panes that contain the following project entries:
4178 // left pane:
4179 // multi-entry items: (2, 3)
4180 // single-entry items: 0, 1, 2, 3, 4
4181 // right pane:
4182 // single-entry items: 1
4183 // multi-entry items: (3, 4)
4184 let left_pane = workspace.update(cx, |workspace, cx| {
4185 let left_pane = workspace.active_pane().clone();
4186 workspace.add_item(Box::new(item_2_3.clone()), cx);
4187 for item in single_entry_items {
4188 workspace.add_item(Box::new(item), cx);
4189 }
4190 left_pane.update(cx, |pane, cx| {
4191 pane.activate_item(2, true, true, cx);
4192 });
4193
4194 workspace
4195 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
4196 .unwrap();
4197
4198 left_pane
4199 });
4200
4201 //Need to cause an effect flush in order to respect new focus
4202 workspace.update(cx, |workspace, cx| {
4203 workspace.add_item(Box::new(item_3_4.clone()), cx);
4204 cx.focus(&left_pane);
4205 });
4206
4207 // When closing all of the items in the left pane, we should be prompted twice:
4208 // once for project entry 0, and once for project entry 2. After those two
4209 // prompts, the task should complete.
4210
4211 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
4212 cx.foreground().run_until_parked();
4213 left_pane.read_with(cx, |pane, cx| {
4214 assert_eq!(
4215 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4216 &[ProjectEntryId::from_proto(0)]
4217 );
4218 });
4219 cx.simulate_prompt_answer(window_id, 0);
4220
4221 cx.foreground().run_until_parked();
4222 left_pane.read_with(cx, |pane, cx| {
4223 assert_eq!(
4224 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4225 &[ProjectEntryId::from_proto(2)]
4226 );
4227 });
4228 cx.simulate_prompt_answer(window_id, 0);
4229
4230 cx.foreground().run_until_parked();
4231 close.await.unwrap();
4232 left_pane.read_with(cx, |pane, _| {
4233 assert_eq!(pane.items_len(), 0);
4234 });
4235 }
4236
4237 #[gpui::test]
4238 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
4239 init_test(cx);
4240
4241 let fs = FakeFs::new(cx.background());
4242
4243 let project = Project::test(fs, [], cx).await;
4244 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4245 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4246
4247 let item = cx.add_view(window_id, |cx| {
4248 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4249 });
4250 let item_id = item.id();
4251 workspace.update(cx, |workspace, cx| {
4252 workspace.add_item(Box::new(item.clone()), cx);
4253 });
4254
4255 // Autosave on window change.
4256 item.update(cx, |item, cx| {
4257 cx.update_global(|settings: &mut SettingsStore, cx| {
4258 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4259 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4260 })
4261 });
4262 item.is_dirty = true;
4263 });
4264
4265 // Deactivating the window saves the file.
4266 cx.simulate_window_activation(None);
4267 deterministic.run_until_parked();
4268 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
4269
4270 // Autosave on focus change.
4271 item.update(cx, |item, cx| {
4272 cx.focus_self();
4273 cx.update_global(|settings: &mut SettingsStore, cx| {
4274 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4275 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4276 })
4277 });
4278 item.is_dirty = true;
4279 });
4280
4281 // Blurring the item saves the file.
4282 item.update(cx, |_, cx| cx.blur());
4283 deterministic.run_until_parked();
4284 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
4285
4286 // Deactivating the window still saves the file.
4287 cx.simulate_window_activation(Some(window_id));
4288 item.update(cx, |item, cx| {
4289 cx.focus_self();
4290 item.is_dirty = true;
4291 });
4292 cx.simulate_window_activation(None);
4293
4294 deterministic.run_until_parked();
4295 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4296
4297 // Autosave after delay.
4298 item.update(cx, |item, cx| {
4299 cx.update_global(|settings: &mut SettingsStore, cx| {
4300 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4301 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4302 })
4303 });
4304 item.is_dirty = true;
4305 cx.emit(TestItemEvent::Edit);
4306 });
4307
4308 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4309 deterministic.advance_clock(Duration::from_millis(250));
4310 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
4311
4312 // After delay expires, the file is saved.
4313 deterministic.advance_clock(Duration::from_millis(250));
4314 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
4315
4316 // Autosave on focus change, ensuring closing the tab counts as such.
4317 item.update(cx, |item, cx| {
4318 cx.update_global(|settings: &mut SettingsStore, cx| {
4319 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4320 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4321 })
4322 });
4323 item.is_dirty = true;
4324 });
4325
4326 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
4327 .await
4328 .unwrap();
4329 assert!(!cx.has_pending_prompt(window_id));
4330 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4331
4332 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4333 workspace.update(cx, |workspace, cx| {
4334 workspace.add_item(Box::new(item.clone()), cx);
4335 });
4336 item.update(cx, |item, cx| {
4337 item.project_items[0].update(cx, |item, _| {
4338 item.entry_id = None;
4339 });
4340 item.is_dirty = true;
4341 cx.blur();
4342 });
4343 deterministic.run_until_parked();
4344 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4345
4346 // Ensure autosave is prevented for deleted files also when closing the buffer.
4347 let _close_items =
4348 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
4349 deterministic.run_until_parked();
4350 assert!(cx.has_pending_prompt(window_id));
4351 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
4352 }
4353
4354 #[gpui::test]
4355 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4356 init_test(cx);
4357
4358 let fs = FakeFs::new(cx.background());
4359
4360 let project = Project::test(fs, [], cx).await;
4361 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4362
4363 let item = cx.add_view(window_id, |cx| {
4364 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4365 });
4366 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4367 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
4368 let toolbar_notify_count = Rc::new(RefCell::new(0));
4369
4370 workspace.update(cx, |workspace, cx| {
4371 workspace.add_item(Box::new(item.clone()), cx);
4372 let toolbar_notification_count = toolbar_notify_count.clone();
4373 cx.observe(&toolbar, move |_, _, _| {
4374 *toolbar_notification_count.borrow_mut() += 1
4375 })
4376 .detach();
4377 });
4378
4379 pane.read_with(cx, |pane, _| {
4380 assert!(!pane.can_navigate_backward());
4381 assert!(!pane.can_navigate_forward());
4382 });
4383
4384 item.update(cx, |item, cx| {
4385 item.set_state("one".to_string(), cx);
4386 });
4387
4388 // Toolbar must be notified to re-render the navigation buttons
4389 assert_eq!(*toolbar_notify_count.borrow(), 1);
4390
4391 pane.read_with(cx, |pane, _| {
4392 assert!(pane.can_navigate_backward());
4393 assert!(!pane.can_navigate_forward());
4394 });
4395
4396 workspace
4397 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4398 .await
4399 .unwrap();
4400
4401 assert_eq!(*toolbar_notify_count.borrow(), 3);
4402 pane.read_with(cx, |pane, _| {
4403 assert!(!pane.can_navigate_backward());
4404 assert!(pane.can_navigate_forward());
4405 });
4406 }
4407
4408 #[gpui::test]
4409 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4410 init_test(cx);
4411 let fs = FakeFs::new(cx.background());
4412
4413 let project = Project::test(fs, [], cx).await;
4414 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4415
4416 let panel = workspace.update(cx, |workspace, cx| {
4417 let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4418 workspace.add_panel(panel.clone(), cx);
4419
4420 workspace
4421 .right_dock()
4422 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4423
4424 panel
4425 });
4426
4427 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
4428 pane.update(cx, |pane, cx| {
4429 let item = cx.add_view(|_| TestItem::new());
4430 pane.add_item(Box::new(item), true, true, None, cx);
4431 });
4432
4433 // Transfer focus from center to panel
4434 workspace.update(cx, |workspace, cx| {
4435 workspace.toggle_panel_focus::<TestPanel>(cx);
4436 });
4437
4438 workspace.read_with(cx, |workspace, cx| {
4439 assert!(workspace.right_dock().read(cx).is_open());
4440 assert!(!panel.is_zoomed(cx));
4441 assert!(panel.has_focus(cx));
4442 });
4443
4444 // Transfer focus from panel to center
4445 workspace.update(cx, |workspace, cx| {
4446 workspace.toggle_panel_focus::<TestPanel>(cx);
4447 });
4448
4449 workspace.read_with(cx, |workspace, cx| {
4450 assert!(workspace.right_dock().read(cx).is_open());
4451 assert!(!panel.is_zoomed(cx));
4452 assert!(!panel.has_focus(cx));
4453 });
4454
4455 // Close the dock
4456 workspace.update(cx, |workspace, cx| {
4457 workspace.toggle_dock(DockPosition::Right, cx);
4458 });
4459
4460 workspace.read_with(cx, |workspace, cx| {
4461 assert!(!workspace.right_dock().read(cx).is_open());
4462 assert!(!panel.is_zoomed(cx));
4463 assert!(!panel.has_focus(cx));
4464 });
4465
4466 // Open the dock
4467 workspace.update(cx, |workspace, cx| {
4468 workspace.toggle_dock(DockPosition::Right, cx);
4469 });
4470
4471 workspace.read_with(cx, |workspace, cx| {
4472 assert!(workspace.right_dock().read(cx).is_open());
4473 assert!(!panel.is_zoomed(cx));
4474 assert!(panel.has_focus(cx));
4475 });
4476
4477 // Focus and zoom panel
4478 panel.update(cx, |panel, cx| {
4479 cx.focus_self();
4480 panel.set_zoomed(true, cx)
4481 });
4482
4483 workspace.read_with(cx, |workspace, cx| {
4484 assert!(workspace.right_dock().read(cx).is_open());
4485 assert!(panel.is_zoomed(cx));
4486 assert!(panel.has_focus(cx));
4487 });
4488
4489 // Transfer focus to the center closes the dock
4490 workspace.update(cx, |workspace, cx| {
4491 workspace.toggle_panel_focus::<TestPanel>(cx);
4492 });
4493
4494 workspace.read_with(cx, |workspace, cx| {
4495 assert!(!workspace.right_dock().read(cx).is_open());
4496 assert!(panel.is_zoomed(cx));
4497 assert!(!panel.has_focus(cx));
4498 });
4499
4500 // Transferring focus back to the panel keeps it zoomed
4501 workspace.update(cx, |workspace, cx| {
4502 workspace.toggle_panel_focus::<TestPanel>(cx);
4503 });
4504
4505 workspace.read_with(cx, |workspace, cx| {
4506 assert!(workspace.right_dock().read(cx).is_open());
4507 assert!(panel.is_zoomed(cx));
4508 assert!(panel.has_focus(cx));
4509 });
4510
4511 // Close the dock while it is zoomed
4512 workspace.update(cx, |workspace, cx| {
4513 workspace.toggle_dock(DockPosition::Right, cx)
4514 });
4515
4516 workspace.read_with(cx, |workspace, cx| {
4517 assert!(!workspace.right_dock().read(cx).is_open());
4518 assert!(panel.is_zoomed(cx));
4519 assert!(workspace.zoomed.is_none());
4520 assert!(!panel.has_focus(cx));
4521 });
4522
4523 // Opening the dock, when it's zoomed, retains focus
4524 workspace.update(cx, |workspace, cx| {
4525 workspace.toggle_dock(DockPosition::Right, cx)
4526 });
4527
4528 workspace.read_with(cx, |workspace, cx| {
4529 assert!(workspace.right_dock().read(cx).is_open());
4530 assert!(panel.is_zoomed(cx));
4531 assert!(workspace.zoomed.is_some());
4532 assert!(panel.has_focus(cx));
4533 });
4534
4535 // Unzoom and close the panel, zoom the active pane.
4536 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
4537 workspace.update(cx, |workspace, cx| {
4538 workspace.toggle_dock(DockPosition::Right, cx)
4539 });
4540 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
4541
4542 // Opening a dock unzooms the pane.
4543 workspace.update(cx, |workspace, cx| {
4544 workspace.toggle_dock(DockPosition::Right, cx)
4545 });
4546 workspace.read_with(cx, |workspace, cx| {
4547 let pane = pane.read(cx);
4548 assert!(!pane.is_zoomed());
4549 assert!(!pane.has_focus());
4550 assert!(workspace.right_dock().read(cx).is_open());
4551 assert!(workspace.zoomed.is_none());
4552 });
4553 }
4554
4555 #[gpui::test]
4556 async fn test_panels(cx: &mut gpui::TestAppContext) {
4557 init_test(cx);
4558 let fs = FakeFs::new(cx.background());
4559
4560 let project = Project::test(fs, [], cx).await;
4561 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
4562
4563 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
4564 // Add panel_1 on the left, panel_2 on the right.
4565 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
4566 workspace.add_panel(panel_1.clone(), cx);
4567 workspace
4568 .left_dock()
4569 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
4570 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
4571 workspace.add_panel(panel_2.clone(), cx);
4572 workspace
4573 .right_dock()
4574 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4575
4576 let left_dock = workspace.left_dock();
4577 assert_eq!(
4578 left_dock.read(cx).visible_panel().unwrap().id(),
4579 panel_1.id()
4580 );
4581 assert_eq!(
4582 left_dock.read(cx).active_panel_size(cx).unwrap(),
4583 panel_1.size(cx)
4584 );
4585
4586 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
4587 assert_eq!(
4588 workspace
4589 .right_dock()
4590 .read(cx)
4591 .visible_panel()
4592 .unwrap()
4593 .id(),
4594 panel_2.id()
4595 );
4596
4597 (panel_1, panel_2)
4598 });
4599
4600 // Move panel_1 to the right
4601 panel_1.update(cx, |panel_1, cx| {
4602 panel_1.set_position(DockPosition::Right, cx)
4603 });
4604
4605 workspace.update(cx, |workspace, cx| {
4606 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
4607 // Since it was the only panel on the left, the left dock should now be closed.
4608 assert!(!workspace.left_dock().read(cx).is_open());
4609 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
4610 let right_dock = workspace.right_dock();
4611 assert_eq!(
4612 right_dock.read(cx).visible_panel().unwrap().id(),
4613 panel_1.id()
4614 );
4615 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4616
4617 // Now we move panel_2Β to the left
4618 panel_2.set_position(DockPosition::Left, cx);
4619 });
4620
4621 workspace.update(cx, |workspace, cx| {
4622 // Since panel_2 was not visible on the right, we don't open the left dock.
4623 assert!(!workspace.left_dock().read(cx).is_open());
4624 // And the right dock is unaffected in it's displaying of panel_1
4625 assert!(workspace.right_dock().read(cx).is_open());
4626 assert_eq!(
4627 workspace
4628 .right_dock()
4629 .read(cx)
4630 .visible_panel()
4631 .unwrap()
4632 .id(),
4633 panel_1.id()
4634 );
4635 });
4636
4637 // Move panel_1 back to the left
4638 panel_1.update(cx, |panel_1, cx| {
4639 panel_1.set_position(DockPosition::Left, cx)
4640 });
4641
4642 workspace.update(cx, |workspace, cx| {
4643 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4644 let left_dock = workspace.left_dock();
4645 assert!(left_dock.read(cx).is_open());
4646 assert_eq!(
4647 left_dock.read(cx).visible_panel().unwrap().id(),
4648 panel_1.id()
4649 );
4650 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4651 // And right the dock should be closed as it no longer has any panels.
4652 assert!(!workspace.right_dock().read(cx).is_open());
4653
4654 // Now we move panel_1 to the bottom
4655 panel_1.set_position(DockPosition::Bottom, cx);
4656 });
4657
4658 workspace.update(cx, |workspace, cx| {
4659 // Since panel_1 was visible on the left, we close the left dock.
4660 assert!(!workspace.left_dock().read(cx).is_open());
4661 // The bottom dock is sized based on the panel's default size,
4662 // since the panel orientation changed from vertical to horizontal.
4663 let bottom_dock = workspace.bottom_dock();
4664 assert_eq!(
4665 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4666 panel_1.size(cx),
4667 );
4668 // Close bottom dock and move panel_1 back to the left.
4669 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4670 panel_1.set_position(DockPosition::Left, cx);
4671 });
4672
4673 // Emit activated event on panel 1
4674 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4675
4676 // Now the left dock is open and panel_1 is active and focused.
4677 workspace.read_with(cx, |workspace, cx| {
4678 let left_dock = workspace.left_dock();
4679 assert!(left_dock.read(cx).is_open());
4680 assert_eq!(
4681 left_dock.read(cx).visible_panel().unwrap().id(),
4682 panel_1.id()
4683 );
4684 assert!(panel_1.is_focused(cx));
4685 });
4686
4687 // Emit closed event on panel 2, which is not active
4688 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4689
4690 // Wo don't close the left dock, because panel_2 wasn't the active panel
4691 workspace.read_with(cx, |workspace, cx| {
4692 let left_dock = workspace.left_dock();
4693 assert!(left_dock.read(cx).is_open());
4694 assert_eq!(
4695 left_dock.read(cx).visible_panel().unwrap().id(),
4696 panel_1.id()
4697 );
4698 });
4699
4700 // Emitting a ZoomIn event shows the panel as zoomed.
4701 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4702 workspace.read_with(cx, |workspace, _| {
4703 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4704 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
4705 });
4706
4707 // Move panel to another dock while it is zoomed
4708 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
4709 workspace.read_with(cx, |workspace, _| {
4710 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4711 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4712 });
4713
4714 // If focus is transferred to another view that's not a panel or another pane, we still show
4715 // the panel as zoomed.
4716 let focus_receiver = cx.add_view(window_id, |_| EmptyView);
4717 focus_receiver.update(cx, |_, cx| cx.focus_self());
4718 workspace.read_with(cx, |workspace, _| {
4719 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4720 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4721 });
4722
4723 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4724 workspace.update(cx, |_, cx| cx.focus_self());
4725 workspace.read_with(cx, |workspace, _| {
4726 assert_eq!(workspace.zoomed, None);
4727 assert_eq!(workspace.zoomed_position, None);
4728 });
4729
4730 // If focus is transferred again to another view that's not a panel or a pane, we won't
4731 // show the panel as zoomed because it wasn't zoomed before.
4732 focus_receiver.update(cx, |_, cx| cx.focus_self());
4733 workspace.read_with(cx, |workspace, _| {
4734 assert_eq!(workspace.zoomed, None);
4735 assert_eq!(workspace.zoomed_position, None);
4736 });
4737
4738 // When focus is transferred back to the panel, it is zoomed again.
4739 panel_1.update(cx, |_, cx| cx.focus_self());
4740 workspace.read_with(cx, |workspace, _| {
4741 assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
4742 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
4743 });
4744
4745 // Emitting a ZoomOut event unzooms the panel.
4746 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4747 workspace.read_with(cx, |workspace, _| {
4748 assert_eq!(workspace.zoomed, None);
4749 assert_eq!(workspace.zoomed_position, None);
4750 });
4751
4752 // Emit closed event on panel 1, which is active
4753 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4754
4755 // Now the left dock is closed, because panel_1 was the active panel
4756 workspace.read_with(cx, |workspace, cx| {
4757 let right_dock = workspace.right_dock();
4758 assert!(!right_dock.read(cx).is_open());
4759 });
4760 }
4761
4762 pub fn init_test(cx: &mut TestAppContext) {
4763 cx.foreground().forbid_parking();
4764 cx.update(|cx| {
4765 cx.set_global(SettingsStore::test(cx));
4766 theme::init((), cx);
4767 language::init(cx);
4768 crate::init_settings(cx);
4769 });
4770 }
4771}