1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11mod toolbar;
12mod workspace_settings;
13
14use anyhow::{anyhow, Context as _, Result};
15use call::{call_settings::CallSettings, ActiveCall};
16use client::{
17 proto::{self, ErrorCode, PeerId},
18 ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use derive_more::{Deref, DerefMut};
22use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
23use futures::{
24 channel::{mpsc, oneshot},
25 future::try_join_all,
26 Future, FutureExt, StreamExt,
27};
28use gpui::{
29 actions, canvas, impl_actions, point, size, Action, AnyElement, AnyView, AnyWeakView,
30 AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DragMoveEvent, Entity as _, EntityId,
31 EventEmitter, FocusHandle, FocusableView, Global, GlobalPixels, KeyContext, Keystroke,
32 LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render,
33 Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions,
34};
35use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
36use itertools::Itertools;
37use language::{LanguageRegistry, Rope};
38use lazy_static::lazy_static;
39pub use modal_layer::*;
40use node_runtime::NodeRuntime;
41use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
42pub use pane::*;
43pub use pane_group::*;
44use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
45pub use persistence::{
46 model::{ItemId, WorkspaceLocation},
47 WorkspaceDb, DB as WORKSPACE_DB,
48};
49use postage::stream::Stream;
50use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
51use serde::Deserialize;
52use settings::Settings;
53use shared_screen::SharedScreen;
54use sqlez::{
55 bindable::{Bind, Column, StaticColumnCount},
56 statement::Statement,
57};
58use status_bar::StatusBar;
59pub use status_bar::StatusItemView;
60use std::{
61 any::TypeId,
62 borrow::Cow,
63 cell::RefCell,
64 cmp,
65 collections::hash_map::DefaultHasher,
66 env,
67 hash::{Hash, Hasher},
68 path::{Path, PathBuf},
69 rc::Rc,
70 sync::{atomic::AtomicUsize, Arc, Weak},
71 time::Duration,
72};
73use task::SpawnInTerminal;
74use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
75pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
76pub use ui;
77use ui::{
78 div, Context as _, Div, Element, ElementContext, InteractiveElement as _, IntoElement, Label,
79 ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
80 WindowContext,
81};
82use util::ResultExt;
83use uuid::Uuid;
84pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
85
86use crate::persistence::{
87 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
88 SerializedAxis,
89};
90
91lazy_static! {
92 static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
93 .ok()
94 .as_deref()
95 .and_then(parse_pixel_size_env_var);
96 static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
97 .ok()
98 .as_deref()
99 .and_then(parse_pixel_position_env_var);
100}
101
102#[derive(Clone, PartialEq)]
103pub struct RemoveWorktreeFromProject(pub WorktreeId);
104
105actions!(
106 workspace,
107 [
108 Open,
109 NewFile,
110 NewWindow,
111 CloseWindow,
112 AddFolderToProject,
113 Unfollow,
114 SaveAs,
115 SaveWithoutFormat,
116 ReloadActiveItem,
117 ActivatePreviousPane,
118 ActivateNextPane,
119 FollowNextCollaborator,
120 NewTerminal,
121 NewCenterTerminal,
122 NewSearch,
123 Feedback,
124 Restart,
125 Welcome,
126 ToggleZoom,
127 ToggleLeftDock,
128 ToggleRightDock,
129 ToggleBottomDock,
130 CloseAllDocks,
131 ]
132);
133
134#[derive(Clone, PartialEq)]
135pub struct OpenPaths {
136 pub paths: Vec<PathBuf>,
137}
138
139#[derive(Clone, Deserialize, PartialEq)]
140pub struct ActivatePane(pub usize);
141
142#[derive(Clone, Deserialize, PartialEq)]
143pub struct ActivatePaneInDirection(pub SplitDirection);
144
145#[derive(Clone, Deserialize, PartialEq)]
146pub struct SwapPaneInDirection(pub SplitDirection);
147
148#[derive(Clone, Deserialize, PartialEq)]
149pub struct NewFileInDirection(pub SplitDirection);
150
151#[derive(Clone, PartialEq, Debug, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SaveAll {
154 pub save_intent: Option<SaveIntent>,
155}
156
157#[derive(Clone, PartialEq, Debug, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct Save {
160 pub save_intent: Option<SaveIntent>,
161}
162
163#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
164#[serde(rename_all = "camelCase")]
165pub struct CloseAllItemsAndPanes {
166 pub save_intent: Option<SaveIntent>,
167}
168
169#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
170#[serde(rename_all = "camelCase")]
171pub struct CloseInactiveTabsAndPanes {
172 pub save_intent: Option<SaveIntent>,
173}
174
175#[derive(Clone, Deserialize, PartialEq)]
176pub struct SendKeystrokes(pub String);
177
178impl_actions!(
179 workspace,
180 [
181 ActivatePane,
182 ActivatePaneInDirection,
183 CloseAllItemsAndPanes,
184 CloseInactiveTabsAndPanes,
185 NewFileInDirection,
186 OpenTerminal,
187 Save,
188 SaveAll,
189 SwapPaneInDirection,
190 SendKeystrokes,
191 ]
192);
193
194#[derive(Deserialize)]
195pub struct Toast {
196 id: usize,
197 msg: Cow<'static, str>,
198 #[serde(skip)]
199 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
200}
201
202impl Toast {
203 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
204 Toast {
205 id,
206 msg: msg.into(),
207 on_click: None,
208 }
209 }
210
211 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
212 where
213 M: Into<Cow<'static, str>>,
214 F: Fn(&mut WindowContext) + 'static,
215 {
216 self.on_click = Some((message.into(), Arc::new(on_click)));
217 self
218 }
219}
220
221impl PartialEq for Toast {
222 fn eq(&self, other: &Self) -> bool {
223 self.id == other.id
224 && self.msg == other.msg
225 && self.on_click.is_some() == other.on_click.is_some()
226 }
227}
228
229impl Clone for Toast {
230 fn clone(&self) -> Self {
231 Toast {
232 id: self.id,
233 msg: self.msg.clone(),
234 on_click: self.on_click.clone(),
235 }
236 }
237}
238
239#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
240pub struct OpenTerminal {
241 pub working_directory: PathBuf,
242}
243
244#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
245pub struct WorkspaceId(i64);
246
247impl StaticColumnCount for WorkspaceId {}
248impl Bind for WorkspaceId {
249 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
250 self.0.bind(statement, start_index)
251 }
252}
253impl Column for WorkspaceId {
254 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
255 i64::column(statement, start_index)
256 .map(|(i, next_index)| (Self(i), next_index))
257 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
258 }
259}
260pub fn init_settings(cx: &mut AppContext) {
261 WorkspaceSettings::register(cx);
262 ItemSettings::register(cx);
263}
264
265pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
266 init_settings(cx);
267 notifications::init(cx);
268
269 cx.on_action(Workspace::close_global);
270 cx.on_action(restart);
271
272 cx.on_action({
273 let app_state = Arc::downgrade(&app_state);
274 move |_: &Open, cx: &mut AppContext| {
275 let paths = cx.prompt_for_paths(PathPromptOptions {
276 files: true,
277 directories: true,
278 multiple: true,
279 });
280
281 if let Some(app_state) = app_state.upgrade() {
282 cx.spawn(move |cx| async move {
283 if let Some(paths) = paths.await.log_err().flatten() {
284 cx.update(|cx| {
285 open_paths(&paths, app_state, OpenOptions::default(), cx)
286 .detach_and_log_err(cx)
287 })
288 .ok();
289 }
290 })
291 .detach();
292 }
293 }
294 });
295}
296
297#[derive(Clone, Default, Deref, DerefMut)]
298struct ProjectItemOpeners(Vec<ProjectItemOpener>);
299
300type ProjectItemOpener = fn(
301 &Model<Project>,
302 &ProjectPath,
303 &mut WindowContext,
304)
305 -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
306
307type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
308
309impl Global for ProjectItemOpeners {}
310
311/// Registers a [ProjectItem] for the app. When opening a file, all the registered
312/// items will get a chance to open the file, starting from the project item that
313/// was added last.
314pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
315 let builders = cx.default_global::<ProjectItemOpeners>();
316 builders.push(|project, project_path, cx| {
317 let project_item = <I::Item as project::Item>::try_open(&project, project_path, cx)?;
318 let project = project.clone();
319 Some(cx.spawn(|cx| async move {
320 let project_item = project_item.await?;
321 let project_entry_id: Option<ProjectEntryId> =
322 project_item.read_with(&cx, |item, cx| project::Item::entry_id(item, cx))?;
323 let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
324 Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
325 as Box<dyn ItemHandle>
326 }) as Box<_>;
327 Ok((project_entry_id, build_workspace_item))
328 }))
329 });
330}
331
332type FollowableItemBuilder = fn(
333 View<Pane>,
334 View<Workspace>,
335 ViewId,
336 &mut Option<proto::view::Variant>,
337 &mut WindowContext,
338) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
339
340#[derive(Default, Deref, DerefMut)]
341struct FollowableItemBuilders(
342 HashMap<
343 TypeId,
344 (
345 FollowableItemBuilder,
346 fn(&AnyView) -> Box<dyn FollowableItemHandle>,
347 ),
348 >,
349);
350
351impl Global for FollowableItemBuilders {}
352
353pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
354 let builders = cx.default_global::<FollowableItemBuilders>();
355 builders.insert(
356 TypeId::of::<I>(),
357 (
358 |pane, workspace, id, state, cx| {
359 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
360 cx.foreground_executor()
361 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
362 })
363 },
364 |this| Box::new(this.clone().downcast::<I>().unwrap()),
365 ),
366 );
367}
368
369#[derive(Default, Deref, DerefMut)]
370struct ItemDeserializers(
371 HashMap<
372 Arc<str>,
373 fn(
374 Model<Project>,
375 WeakView<Workspace>,
376 WorkspaceId,
377 ItemId,
378 &mut ViewContext<Pane>,
379 ) -> Task<Result<Box<dyn ItemHandle>>>,
380 >,
381);
382
383impl Global for ItemDeserializers {}
384
385pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
386 if let Some(serialized_item_kind) = I::serialized_item_kind() {
387 let deserializers = cx.default_global::<ItemDeserializers>();
388 deserializers.insert(
389 Arc::from(serialized_item_kind),
390 |project, workspace, workspace_id, item_id, cx| {
391 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
392 cx.foreground_executor()
393 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
394 },
395 );
396 }
397}
398
399pub struct AppState {
400 pub languages: Arc<LanguageRegistry>,
401 pub client: Arc<Client>,
402 pub user_store: Model<UserStore>,
403 pub workspace_store: Model<WorkspaceStore>,
404 pub fs: Arc<dyn fs::Fs>,
405 pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
406 pub node_runtime: Arc<dyn NodeRuntime>,
407}
408
409struct GlobalAppState(Weak<AppState>);
410
411impl Global for GlobalAppState {}
412
413pub struct WorkspaceStore {
414 workspaces: HashSet<WindowHandle<Workspace>>,
415 client: Arc<Client>,
416 _subscriptions: Vec<client::Subscription>,
417}
418
419#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
420struct Follower {
421 project_id: Option<u64>,
422 peer_id: PeerId,
423}
424
425impl AppState {
426 pub fn global(cx: &AppContext) -> Weak<Self> {
427 cx.global::<GlobalAppState>().0.clone()
428 }
429 pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
430 cx.try_global::<GlobalAppState>()
431 .map(|state| state.0.clone())
432 }
433 pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
434 cx.set_global(GlobalAppState(state));
435 }
436
437 #[cfg(any(test, feature = "test-support"))]
438 pub fn test(cx: &mut AppContext) -> Arc<Self> {
439 use node_runtime::FakeNodeRuntime;
440 use settings::SettingsStore;
441 use ui::Context as _;
442
443 if !cx.has_global::<SettingsStore>() {
444 let settings_store = SettingsStore::test(cx);
445 cx.set_global(settings_store);
446 }
447
448 let fs = fs::FakeFs::new(cx.background_executor().clone());
449 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
450 let clock = Arc::new(clock::FakeSystemClock::default());
451 let http_client = util::http::FakeHttpClient::with_404_response();
452 let client = Client::new(clock, http_client.clone(), cx);
453 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
454 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
455
456 theme::init(theme::LoadThemes::JustBase, cx);
457 client::init(&client, cx);
458 crate::init_settings(cx);
459
460 Arc::new(Self {
461 client,
462 fs,
463 languages,
464 user_store,
465 workspace_store,
466 node_runtime: FakeNodeRuntime::new(),
467 build_window_options: |_, _| Default::default(),
468 })
469 }
470}
471
472struct DelayedDebouncedEditAction {
473 task: Option<Task<()>>,
474 cancel_channel: Option<oneshot::Sender<()>>,
475}
476
477impl DelayedDebouncedEditAction {
478 fn new() -> DelayedDebouncedEditAction {
479 DelayedDebouncedEditAction {
480 task: None,
481 cancel_channel: None,
482 }
483 }
484
485 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
486 where
487 F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
488 {
489 if let Some(channel) = self.cancel_channel.take() {
490 _ = channel.send(());
491 }
492
493 let (sender, mut receiver) = oneshot::channel::<()>();
494 self.cancel_channel = Some(sender);
495
496 let previous_task = self.task.take();
497 self.task = Some(cx.spawn(move |workspace, mut cx| async move {
498 let mut timer = cx.background_executor().timer(delay).fuse();
499 if let Some(previous_task) = previous_task {
500 previous_task.await;
501 }
502
503 futures::select_biased! {
504 _ = receiver => return,
505 _ = timer => {}
506 }
507
508 if let Some(result) = workspace
509 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
510 .log_err()
511 {
512 result.await.log_err();
513 }
514 }));
515 }
516}
517
518pub enum Event {
519 PaneAdded(View<Pane>),
520 ContactRequestedJoin(u64),
521 WorkspaceCreated(WeakView<Workspace>),
522 SpawnTask(SpawnInTerminal),
523}
524
525pub enum OpenVisible {
526 All,
527 None,
528 OnlyFiles,
529 OnlyDirectories,
530}
531
532/// Collects everything project-related for a certain window opened.
533/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
534///
535/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
536/// The `Workspace` owns everybody's state and serves as a default, "global context",
537/// that can be used to register a global action to be triggered from any place in the window.
538pub struct Workspace {
539 weak_self: WeakView<Self>,
540 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
541 zoomed: Option<AnyWeakView>,
542 zoomed_position: Option<DockPosition>,
543 center: PaneGroup,
544 left_dock: View<Dock>,
545 bottom_dock: View<Dock>,
546 right_dock: View<Dock>,
547 panes: Vec<View<Pane>>,
548 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
549 active_pane: View<Pane>,
550 last_active_center_pane: Option<WeakView<Pane>>,
551 last_active_view_id: Option<proto::ViewId>,
552 status_bar: View<StatusBar>,
553 modal_layer: View<ModalLayer>,
554 titlebar_item: Option<AnyView>,
555 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
556 project: Model<Project>,
557 follower_states: HashMap<View<Pane>, FollowerState>,
558 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
559 window_edited: bool,
560 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
561 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
562 database_id: WorkspaceId,
563 app_state: Arc<AppState>,
564 dispatching_keystrokes: Rc<RefCell<Vec<Keystroke>>>,
565 _subscriptions: Vec<Subscription>,
566 _apply_leader_updates: Task<Result<()>>,
567 _observe_current_user: Task<Result<()>>,
568 _schedule_serialize: Option<Task<()>>,
569 pane_history_timestamp: Arc<AtomicUsize>,
570 bounds: Bounds<Pixels>,
571}
572
573impl EventEmitter<Event> for Workspace {}
574
575#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
576pub struct ViewId {
577 pub creator: PeerId,
578 pub id: u64,
579}
580
581#[derive(Default)]
582struct FollowerState {
583 leader_id: PeerId,
584 active_view_id: Option<ViewId>,
585 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
586}
587
588impl Workspace {
589 pub fn new(
590 workspace_id: WorkspaceId,
591 project: Model<Project>,
592 app_state: Arc<AppState>,
593 cx: &mut ViewContext<Self>,
594 ) -> Self {
595 cx.observe(&project, |_, _, cx| cx.notify()).detach();
596 cx.subscribe(&project, move |this, _, event, cx| {
597 match event {
598 project::Event::RemoteIdChanged(_) => {
599 this.update_window_title(cx);
600 }
601
602 project::Event::CollaboratorLeft(peer_id) => {
603 this.collaborator_left(*peer_id, cx);
604 }
605
606 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
607 this.update_window_title(cx);
608 let workspace_serialization = this.serialize_workspace(cx);
609 cx.spawn(|workspace, mut cx| async move {
610 workspace_serialization.await;
611 workspace
612 .update(&mut cx, |workspace, cx| {
613 workspace.refresh_recent_documents(cx)
614 })?
615 .await
616 })
617 .detach_and_log_err(cx)
618 }
619
620 project::Event::DisconnectedFromHost => {
621 this.update_window_edited(cx);
622 let panes_to_unfollow: Vec<View<Pane>> =
623 this.follower_states.keys().map(|k| k.clone()).collect();
624 for pane in panes_to_unfollow {
625 this.unfollow(&pane, cx);
626 }
627 cx.disable_focus();
628 }
629
630 project::Event::Closed => {
631 cx.remove_window();
632 }
633
634 project::Event::DeletedEntry(entry_id) => {
635 for pane in this.panes.iter() {
636 pane.update(cx, |pane, cx| {
637 pane.handle_deleted_project_item(*entry_id, cx)
638 });
639 }
640 }
641
642 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
643 cx.new_view(|_| MessageNotification::new(message.clone()))
644 }),
645
646 project::Event::LanguageServerPrompt(request) => {
647 let mut hasher = DefaultHasher::new();
648 request.message.as_str().hash(&mut hasher);
649 let id = hasher.finish();
650
651 this.show_notification(id as usize, cx, |cx| {
652 cx.new_view(|_| notifications::LanguageServerPrompt::new(request.clone()))
653 });
654 }
655
656 _ => {}
657 }
658 cx.notify()
659 })
660 .detach();
661
662 cx.on_focus_lost(|this, cx| {
663 let focus_handle = this.focus_handle(cx);
664 cx.focus(&focus_handle);
665 })
666 .detach();
667
668 let weak_handle = cx.view().downgrade();
669 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
670
671 let center_pane = cx.new_view(|cx| {
672 Pane::new(
673 weak_handle.clone(),
674 project.clone(),
675 pane_history_timestamp.clone(),
676 None,
677 NewFile.boxed_clone(),
678 cx,
679 )
680 });
681 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
682
683 cx.focus_view(¢er_pane);
684 cx.emit(Event::PaneAdded(center_pane.clone()));
685
686 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
687 app_state.workspace_store.update(cx, |store, _| {
688 store.workspaces.insert(window_handle);
689 });
690
691 let mut current_user = app_state.user_store.read(cx).watch_current_user();
692 let mut connection_status = app_state.client.status();
693 let _observe_current_user = cx.spawn(|this, mut cx| async move {
694 current_user.next().await;
695 connection_status.next().await;
696 let mut stream =
697 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
698
699 while stream.recv().await.is_some() {
700 this.update(&mut cx, |_, cx| cx.notify())?;
701 }
702 anyhow::Ok(())
703 });
704
705 // All leader updates are enqueued and then processed in a single task, so
706 // that each asynchronous operation can be run in order.
707 let (leader_updates_tx, mut leader_updates_rx) =
708 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
709 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
710 while let Some((leader_id, update)) = leader_updates_rx.next().await {
711 Self::process_leader_update(&this, leader_id, update, &mut cx)
712 .await
713 .log_err();
714 }
715
716 Ok(())
717 });
718
719 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
720
721 let left_dock = Dock::new(DockPosition::Left, cx);
722 let bottom_dock = Dock::new(DockPosition::Bottom, cx);
723 let right_dock = Dock::new(DockPosition::Right, cx);
724 let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
725 let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
726 let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
727 let status_bar = cx.new_view(|cx| {
728 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
729 status_bar.add_left_item(left_dock_buttons, cx);
730 status_bar.add_right_item(right_dock_buttons, cx);
731 status_bar.add_right_item(bottom_dock_buttons, cx);
732 status_bar
733 });
734
735 let modal_layer = cx.new_view(|_| ModalLayer::new());
736
737 let mut active_call = None;
738 if let Some(call) = ActiveCall::try_global(cx) {
739 let call = call.clone();
740 let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
741 active_call = Some((call, subscriptions));
742 }
743
744 let subscriptions = vec![
745 cx.observe_window_activation(Self::on_window_activation_changed),
746 cx.observe_window_bounds(move |_, cx| {
747 if let Some(display) = cx.display() {
748 // Transform fixed bounds to be stored in terms of the containing display
749 let mut window_bounds = cx.window_bounds();
750 let display_bounds = display.bounds();
751 window_bounds.origin.x -= display_bounds.origin.x;
752 window_bounds.origin.y -= display_bounds.origin.y;
753 let fullscreen = cx.is_fullscreen();
754
755 if let Some(display_uuid) = display.uuid().log_err() {
756 // Only update the window bounds when not full screen,
757 // so we can remember the last non-fullscreen bounds
758 // across restarts
759 if fullscreen {
760 cx.background_executor()
761 .spawn(DB.set_fullscreen(workspace_id, true))
762 .detach_and_log_err(cx);
763 } else if !cx.is_minimized() {
764 cx.background_executor()
765 .spawn(DB.set_fullscreen(workspace_id, false))
766 .detach_and_log_err(cx);
767 cx.background_executor()
768 .spawn(DB.set_window_bounds(
769 workspace_id,
770 SerializedWindowsBounds(window_bounds),
771 display_uuid,
772 ))
773 .detach_and_log_err(cx);
774 }
775 }
776 }
777 cx.notify();
778 }),
779 cx.observe_window_appearance(|_, cx| {
780 let window_appearance = cx.appearance();
781
782 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
783
784 ThemeSettings::reload_current_theme(cx);
785 }),
786 cx.observe(&left_dock, |this, _, cx| {
787 this.serialize_workspace(cx).detach();
788 cx.notify();
789 }),
790 cx.observe(&bottom_dock, |this, _, cx| {
791 this.serialize_workspace(cx).detach();
792 cx.notify();
793 }),
794 cx.observe(&right_dock, |this, _, cx| {
795 this.serialize_workspace(cx).detach();
796 cx.notify();
797 }),
798 cx.on_release(|this, window, cx| {
799 this.app_state.workspace_store.update(cx, |store, _| {
800 let window = window.downcast::<Self>().unwrap();
801 store.workspaces.remove(&window);
802 })
803 }),
804 ];
805
806 cx.defer(|this, cx| {
807 this.update_window_title(cx);
808 });
809 Workspace {
810 weak_self: weak_handle.clone(),
811 zoomed: None,
812 zoomed_position: None,
813 center: PaneGroup::new(center_pane.clone()),
814 panes: vec![center_pane.clone()],
815 panes_by_item: Default::default(),
816 active_pane: center_pane.clone(),
817 last_active_center_pane: Some(center_pane.downgrade()),
818 last_active_view_id: None,
819 status_bar,
820 modal_layer,
821 titlebar_item: None,
822 notifications: Default::default(),
823 left_dock,
824 bottom_dock,
825 right_dock,
826 project: project.clone(),
827 follower_states: Default::default(),
828 last_leaders_by_pane: Default::default(),
829 dispatching_keystrokes: Default::default(),
830 window_edited: false,
831 active_call,
832 database_id: workspace_id,
833 app_state,
834 _observe_current_user,
835 _apply_leader_updates,
836 _schedule_serialize: None,
837 leader_updates_tx,
838 _subscriptions: subscriptions,
839 pane_history_timestamp,
840 workspace_actions: Default::default(),
841 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
842 bounds: Default::default(),
843 }
844 }
845
846 fn new_local(
847 abs_paths: Vec<PathBuf>,
848 app_state: Arc<AppState>,
849 requesting_window: Option<WindowHandle<Workspace>>,
850 cx: &mut AppContext,
851 ) -> Task<
852 anyhow::Result<(
853 WindowHandle<Workspace>,
854 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
855 )>,
856 > {
857 let project_handle = Project::local(
858 app_state.client.clone(),
859 app_state.node_runtime.clone(),
860 app_state.user_store.clone(),
861 app_state.languages.clone(),
862 app_state.fs.clone(),
863 cx,
864 );
865
866 cx.spawn(|mut cx| async move {
867 let serialized_workspace: Option<SerializedWorkspace> =
868 persistence::DB.workspace_for_roots(abs_paths.as_slice());
869
870 let paths_to_open = Arc::new(abs_paths);
871
872 // Get project paths for all of the abs_paths
873 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
874 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
875 Vec::with_capacity(paths_to_open.len());
876 for path in paths_to_open.iter().cloned() {
877 if let Some((worktree, project_entry)) = cx
878 .update(|cx| {
879 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
880 })?
881 .await
882 .log_err()
883 {
884 worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
885 project_paths.push((path, Some(project_entry)));
886 } else {
887 project_paths.push((path, None));
888 }
889 }
890
891 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
892 serialized_workspace.id
893 } else {
894 DB.next_id().await.unwrap_or_else(|_| Default::default())
895 };
896
897 let window = if let Some(window) = requesting_window {
898 cx.update_window(window.into(), |_, cx| {
899 cx.replace_root_view(|cx| {
900 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
901 });
902 })?;
903 window
904 } else {
905 let window_bounds_override = window_bounds_env_override(&cx);
906
907 let (bounds, display, fullscreen) = if let Some(bounds) = window_bounds_override {
908 (Some(bounds), None, false)
909 } else {
910 let restorable_bounds = serialized_workspace
911 .as_ref()
912 .and_then(|workspace| {
913 Some((workspace.display?, workspace.bounds?, workspace.fullscreen))
914 })
915 .or_else(|| {
916 let (display, bounds, fullscreen) = DB.last_window().log_err()?;
917 Some((display?, bounds?.0, fullscreen.unwrap_or(false)))
918 });
919
920 if let Some((serialized_display, mut bounds, fullscreen)) = restorable_bounds {
921 // Stored bounds are relative to the containing display.
922 // So convert back to global coordinates if that screen still exists
923 let screen_bounds = cx
924 .update(|cx| {
925 cx.displays()
926 .into_iter()
927 .find(|display| display.uuid().ok() == Some(serialized_display))
928 })
929 .ok()
930 .flatten()
931 .map(|screen| screen.bounds());
932
933 if let Some(screen_bounds) = screen_bounds {
934 bounds.origin.x += screen_bounds.origin.x;
935 bounds.origin.y += screen_bounds.origin.y;
936 }
937
938 (Some(bounds), Some(serialized_display), fullscreen)
939 } else {
940 (None, None, false)
941 }
942 };
943
944 // Use the serialized workspace to construct the new window
945 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
946 options.bounds = bounds;
947 options.fullscreen = fullscreen;
948 cx.open_window(options, {
949 let app_state = app_state.clone();
950 let project_handle = project_handle.clone();
951 move |cx| {
952 cx.new_view(|cx| {
953 Workspace::new(workspace_id, project_handle, app_state, cx)
954 })
955 }
956 })?
957 };
958
959 notify_if_database_failed(window, &mut cx);
960 let opened_items = window
961 .update(&mut cx, |_workspace, cx| {
962 open_items(serialized_workspace, project_paths, app_state, cx)
963 })?
964 .await
965 .unwrap_or_default();
966
967 window
968 .update(&mut cx, |workspace, cx| {
969 workspace
970 .refresh_recent_documents(cx)
971 .detach_and_log_err(cx);
972 cx.activate_window()
973 })
974 .log_err();
975 Ok((window, opened_items))
976 })
977 }
978
979 pub fn weak_handle(&self) -> WeakView<Self> {
980 self.weak_self.clone()
981 }
982
983 pub fn left_dock(&self) -> &View<Dock> {
984 &self.left_dock
985 }
986
987 pub fn bottom_dock(&self) -> &View<Dock> {
988 &self.bottom_dock
989 }
990
991 pub fn right_dock(&self) -> &View<Dock> {
992 &self.right_dock
993 }
994
995 pub fn is_edited(&self) -> bool {
996 self.window_edited
997 }
998
999 pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut WindowContext) {
1000 let dock = match panel.position(cx) {
1001 DockPosition::Left => &self.left_dock,
1002 DockPosition::Bottom => &self.bottom_dock,
1003 DockPosition::Right => &self.right_dock,
1004 };
1005
1006 dock.update(cx, |dock, cx| {
1007 dock.add_panel(panel, self.weak_self.clone(), cx)
1008 });
1009 }
1010
1011 pub fn status_bar(&self) -> &View<StatusBar> {
1012 &self.status_bar
1013 }
1014
1015 pub fn app_state(&self) -> &Arc<AppState> {
1016 &self.app_state
1017 }
1018
1019 pub fn user_store(&self) -> &Model<UserStore> {
1020 &self.app_state.user_store
1021 }
1022
1023 pub fn project(&self) -> &Model<Project> {
1024 &self.project
1025 }
1026
1027 pub fn recent_navigation_history(
1028 &self,
1029 limit: Option<usize>,
1030 cx: &AppContext,
1031 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1032 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1033 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1034 for pane in &self.panes {
1035 let pane = pane.read(cx);
1036 pane.nav_history()
1037 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1038 if let Some(fs_path) = &fs_path {
1039 abs_paths_opened
1040 .entry(fs_path.clone())
1041 .or_default()
1042 .insert(project_path.clone());
1043 }
1044 let timestamp = entry.timestamp;
1045 match history.entry(project_path) {
1046 hash_map::Entry::Occupied(mut entry) => {
1047 let (_, old_timestamp) = entry.get();
1048 if ×tamp > old_timestamp {
1049 entry.insert((fs_path, timestamp));
1050 }
1051 }
1052 hash_map::Entry::Vacant(entry) => {
1053 entry.insert((fs_path, timestamp));
1054 }
1055 }
1056 });
1057 }
1058
1059 history
1060 .into_iter()
1061 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1062 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1063 .rev()
1064 .filter(|(history_path, abs_path)| {
1065 let latest_project_path_opened = abs_path
1066 .as_ref()
1067 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1068 .and_then(|project_paths| {
1069 project_paths
1070 .iter()
1071 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1072 });
1073
1074 match latest_project_path_opened {
1075 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1076 None => true,
1077 }
1078 })
1079 .take(limit.unwrap_or(usize::MAX))
1080 .collect()
1081 }
1082
1083 fn navigate_history(
1084 &mut self,
1085 pane: WeakView<Pane>,
1086 mode: NavigationMode,
1087 cx: &mut ViewContext<Workspace>,
1088 ) -> Task<Result<()>> {
1089 let to_load = if let Some(pane) = pane.upgrade() {
1090 pane.update(cx, |pane, cx| {
1091 pane.focus(cx);
1092 loop {
1093 // Retrieve the weak item handle from the history.
1094 let entry = pane.nav_history_mut().pop(mode, cx)?;
1095
1096 // If the item is still present in this pane, then activate it.
1097 if let Some(index) = entry
1098 .item
1099 .upgrade()
1100 .and_then(|v| pane.index_for_item(v.as_ref()))
1101 {
1102 let prev_active_item_index = pane.active_item_index();
1103 pane.nav_history_mut().set_mode(mode);
1104 pane.activate_item(index, true, true, cx);
1105 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1106
1107 let mut navigated = prev_active_item_index != pane.active_item_index();
1108 if let Some(data) = entry.data {
1109 navigated |= pane.active_item()?.navigate(data, cx);
1110 }
1111
1112 if navigated {
1113 break None;
1114 }
1115 }
1116 // If the item is no longer present in this pane, then retrieve its
1117 // project path in order to reopen it.
1118 else {
1119 break pane
1120 .nav_history()
1121 .path_for_item(entry.item.id())
1122 .map(|(project_path, _)| (project_path, entry));
1123 }
1124 }
1125 })
1126 } else {
1127 None
1128 };
1129
1130 if let Some((project_path, entry)) = to_load {
1131 // If the item was no longer present, then load it again from its previous path.
1132 let task = self.load_path(project_path, cx);
1133 cx.spawn(|workspace, mut cx| async move {
1134 let task = task.await;
1135 let mut navigated = false;
1136 if let Some((project_entry_id, build_item)) = task.log_err() {
1137 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1138 pane.nav_history_mut().set_mode(mode);
1139 pane.active_item().map(|p| p.item_id())
1140 })?;
1141
1142 pane.update(&mut cx, |pane, cx| {
1143 let item = pane.open_item(project_entry_id, true, cx, build_item);
1144 navigated |= Some(item.item_id()) != prev_active_item_id;
1145 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1146 if let Some(data) = entry.data {
1147 navigated |= item.navigate(data, cx);
1148 }
1149 })?;
1150 }
1151
1152 if !navigated {
1153 workspace
1154 .update(&mut cx, |workspace, cx| {
1155 Self::navigate_history(workspace, pane, mode, cx)
1156 })?
1157 .await?;
1158 }
1159
1160 Ok(())
1161 })
1162 } else {
1163 Task::ready(Ok(()))
1164 }
1165 }
1166
1167 pub fn go_back(
1168 &mut self,
1169 pane: WeakView<Pane>,
1170 cx: &mut ViewContext<Workspace>,
1171 ) -> Task<Result<()>> {
1172 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1173 }
1174
1175 pub fn go_forward(
1176 &mut self,
1177 pane: WeakView<Pane>,
1178 cx: &mut ViewContext<Workspace>,
1179 ) -> Task<Result<()>> {
1180 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1181 }
1182
1183 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1184 self.navigate_history(
1185 self.active_pane().downgrade(),
1186 NavigationMode::ReopeningClosedItem,
1187 cx,
1188 )
1189 }
1190
1191 pub fn client(&self) -> &Arc<Client> {
1192 &self.app_state.client
1193 }
1194
1195 pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1196 self.titlebar_item = Some(item);
1197 cx.notify();
1198 }
1199
1200 pub fn titlebar_item(&self) -> Option<AnyView> {
1201 self.titlebar_item.clone()
1202 }
1203
1204 /// Call the given callback with a workspace whose project is local.
1205 ///
1206 /// If the given workspace has a local project, then it will be passed
1207 /// to the callback. Otherwise, a new empty window will be created.
1208 pub fn with_local_workspace<T, F>(
1209 &mut self,
1210 cx: &mut ViewContext<Self>,
1211 callback: F,
1212 ) -> Task<Result<T>>
1213 where
1214 T: 'static,
1215 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1216 {
1217 if self.project.read(cx).is_local() {
1218 Task::Ready(Some(Ok(callback(self, cx))))
1219 } else {
1220 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1221 cx.spawn(|_vh, mut cx| async move {
1222 let (workspace, _) = task.await?;
1223 workspace.update(&mut cx, callback)
1224 })
1225 }
1226 }
1227
1228 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1229 self.project.read(cx).worktrees()
1230 }
1231
1232 pub fn visible_worktrees<'a>(
1233 &self,
1234 cx: &'a AppContext,
1235 ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1236 self.project.read(cx).visible_worktrees(cx)
1237 }
1238
1239 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1240 let futures = self
1241 .worktrees(cx)
1242 .filter_map(|worktree| worktree.read(cx).as_local())
1243 .map(|worktree| worktree.scan_complete())
1244 .collect::<Vec<_>>();
1245 async move {
1246 for future in futures {
1247 future.await;
1248 }
1249 }
1250 }
1251
1252 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1253 cx.defer(|cx| {
1254 cx.windows().iter().find(|window| {
1255 window
1256 .update(cx, |_, window| {
1257 if window.is_window_active() {
1258 //This can only get called when the window's project connection has been lost
1259 //so we don't need to prompt the user for anything and instead just close the window
1260 window.remove_window();
1261 true
1262 } else {
1263 false
1264 }
1265 })
1266 .unwrap_or(false)
1267 });
1268 });
1269 }
1270
1271 pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1272 let window = cx.window_handle();
1273 let prepare = self.prepare_to_close(false, cx);
1274 cx.spawn(|_, mut cx| async move {
1275 if prepare.await? {
1276 window.update(&mut cx, |_, cx| {
1277 cx.remove_window();
1278 })?;
1279 }
1280 anyhow::Ok(())
1281 })
1282 .detach_and_log_err(cx)
1283 }
1284
1285 pub fn prepare_to_close(
1286 &mut self,
1287 quitting: bool,
1288 cx: &mut ViewContext<Self>,
1289 ) -> Task<Result<bool>> {
1290 let active_call = self.active_call().cloned();
1291 let window = cx.window_handle();
1292
1293 cx.spawn(|this, mut cx| async move {
1294 let workspace_count = (*cx).update(|cx| {
1295 cx.windows()
1296 .iter()
1297 .filter(|window| window.downcast::<Workspace>().is_some())
1298 .count()
1299 })?;
1300
1301 if let Some(active_call) = active_call {
1302 if !quitting
1303 && workspace_count == 1
1304 && active_call.read_with(&cx, |call, _| call.room().is_some())?
1305 {
1306 let answer = window.update(&mut cx, |_, cx| {
1307 cx.prompt(
1308 PromptLevel::Warning,
1309 "Do you want to leave the current call?",
1310 None,
1311 &["Close window and hang up", "Cancel"],
1312 )
1313 })?;
1314
1315 if answer.await.log_err() == Some(1) {
1316 return anyhow::Ok(false);
1317 } else {
1318 active_call
1319 .update(&mut cx, |call, cx| call.hang_up(cx))?
1320 .await
1321 .log_err();
1322 }
1323 }
1324 }
1325
1326 this.update(&mut cx, |this, cx| {
1327 this.save_all_internal(SaveIntent::Close, cx)
1328 })?
1329 .await
1330 })
1331 }
1332
1333 fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1334 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1335 .detach_and_log_err(cx);
1336 }
1337
1338 fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1339 let mut keystrokes: Vec<Keystroke> = action
1340 .0
1341 .split(' ')
1342 .flat_map(|k| Keystroke::parse(k).log_err())
1343 .collect();
1344 keystrokes.reverse();
1345
1346 self.dispatching_keystrokes
1347 .borrow_mut()
1348 .append(&mut keystrokes);
1349
1350 let keystrokes = self.dispatching_keystrokes.clone();
1351 cx.window_context()
1352 .spawn(|mut cx| async move {
1353 // limit to 100 keystrokes to avoid infinite recursion.
1354 for _ in 0..100 {
1355 let Some(keystroke) = keystrokes.borrow_mut().pop() else {
1356 return Ok(());
1357 };
1358 cx.update(|cx| {
1359 let focused = cx.focused();
1360 cx.dispatch_keystroke(keystroke.clone());
1361 if cx.focused() != focused {
1362 // dispatch_keystroke may cause the focus to change.
1363 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1364 // And we need that to happen before the next keystroke to keep vim mode happy...
1365 // (Note that the tests always do this implicitly, so you must manually test with something like:
1366 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1367 // )
1368 cx.draw();
1369 }
1370 })?;
1371 }
1372 keystrokes.borrow_mut().clear();
1373 Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1374 })
1375 .detach_and_log_err(cx);
1376 }
1377
1378 fn save_all_internal(
1379 &mut self,
1380 mut save_intent: SaveIntent,
1381 cx: &mut ViewContext<Self>,
1382 ) -> Task<Result<bool>> {
1383 if self.project.read(cx).is_disconnected() {
1384 return Task::ready(Ok(true));
1385 }
1386 let dirty_items = self
1387 .panes
1388 .iter()
1389 .flat_map(|pane| {
1390 pane.read(cx).items().filter_map(|item| {
1391 if item.is_dirty(cx) {
1392 Some((pane.downgrade(), item.boxed_clone()))
1393 } else {
1394 None
1395 }
1396 })
1397 })
1398 .collect::<Vec<_>>();
1399
1400 let project = self.project.clone();
1401 cx.spawn(|workspace, mut cx| async move {
1402 // Override save mode and display "Save all files" prompt
1403 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1404 let answer = workspace.update(&mut cx, |_, cx| {
1405 let (prompt, detail) = Pane::file_names_for_prompt(
1406 &mut dirty_items.iter().map(|(_, handle)| handle),
1407 dirty_items.len(),
1408 cx,
1409 );
1410 cx.prompt(
1411 PromptLevel::Warning,
1412 &prompt,
1413 Some(&detail),
1414 &["Save all", "Discard all", "Cancel"],
1415 )
1416 })?;
1417 match answer.await.log_err() {
1418 Some(0) => save_intent = SaveIntent::SaveAll,
1419 Some(1) => save_intent = SaveIntent::Skip,
1420 _ => {}
1421 }
1422 }
1423 for (pane, item) in dirty_items {
1424 let (singleton, project_entry_ids) =
1425 cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1426 if singleton || !project_entry_ids.is_empty() {
1427 if let Some(ix) =
1428 pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1429 {
1430 if !Pane::save_item(
1431 project.clone(),
1432 &pane,
1433 ix,
1434 &*item,
1435 save_intent,
1436 &mut cx,
1437 )
1438 .await?
1439 {
1440 return Ok(false);
1441 }
1442 }
1443 }
1444 }
1445 Ok(true)
1446 })
1447 }
1448
1449 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
1450 self.client()
1451 .telemetry()
1452 .report_app_event("open project".to_string());
1453 let paths = cx.prompt_for_paths(PathPromptOptions {
1454 files: true,
1455 directories: true,
1456 multiple: true,
1457 });
1458
1459 cx.spawn(|this, mut cx| async move {
1460 let Some(paths) = paths.await.log_err().flatten() else {
1461 return;
1462 };
1463
1464 if let Some(task) = this
1465 .update(&mut cx, |this, cx| {
1466 this.open_workspace_for_paths(false, paths, cx)
1467 })
1468 .log_err()
1469 {
1470 task.await.log_err();
1471 }
1472 })
1473 .detach()
1474 }
1475
1476 pub fn open_workspace_for_paths(
1477 &mut self,
1478 replace_current_window: bool,
1479 paths: Vec<PathBuf>,
1480 cx: &mut ViewContext<Self>,
1481 ) -> Task<Result<()>> {
1482 let window = cx.window_handle().downcast::<Self>();
1483 let is_remote = self.project.read(cx).is_remote();
1484 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1485 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1486
1487 let window_to_replace = if replace_current_window {
1488 window
1489 } else if is_remote || has_worktree || has_dirty_items {
1490 None
1491 } else {
1492 window
1493 };
1494 let app_state = self.app_state.clone();
1495
1496 cx.spawn(|_, mut cx| async move {
1497 cx.update(|cx| {
1498 open_paths(
1499 &paths,
1500 app_state,
1501 OpenOptions {
1502 replace_window: window_to_replace,
1503 ..Default::default()
1504 },
1505 cx,
1506 )
1507 })?
1508 .await?;
1509 Ok(())
1510 })
1511 }
1512
1513 #[allow(clippy::type_complexity)]
1514 pub fn open_paths(
1515 &mut self,
1516 mut abs_paths: Vec<PathBuf>,
1517 visible: OpenVisible,
1518 pane: Option<WeakView<Pane>>,
1519 cx: &mut ViewContext<Self>,
1520 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1521 log::info!("open paths {abs_paths:?}");
1522
1523 let fs = self.app_state.fs.clone();
1524
1525 // Sort the paths to ensure we add worktrees for parents before their children.
1526 abs_paths.sort_unstable();
1527 cx.spawn(move |this, mut cx| async move {
1528 let mut tasks = Vec::with_capacity(abs_paths.len());
1529
1530 for abs_path in &abs_paths {
1531 let visible = match visible {
1532 OpenVisible::All => Some(true),
1533 OpenVisible::None => Some(false),
1534 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
1535 Some(Some(metadata)) => Some(!metadata.is_dir),
1536 Some(None) => Some(true),
1537 None => None,
1538 },
1539 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
1540 Some(Some(metadata)) => Some(metadata.is_dir),
1541 Some(None) => Some(false),
1542 None => None,
1543 },
1544 };
1545 let project_path = match visible {
1546 Some(visible) => match this
1547 .update(&mut cx, |this, cx| {
1548 Workspace::project_path_for_path(
1549 this.project.clone(),
1550 abs_path,
1551 visible,
1552 cx,
1553 )
1554 })
1555 .log_err()
1556 {
1557 Some(project_path) => project_path.await.log_err(),
1558 None => None,
1559 },
1560 None => None,
1561 };
1562
1563 let this = this.clone();
1564 let abs_path = abs_path.clone();
1565 let fs = fs.clone();
1566 let pane = pane.clone();
1567 let task = cx.spawn(move |mut cx| async move {
1568 let (worktree, project_path) = project_path?;
1569 if fs.is_dir(&abs_path).await {
1570 this.update(&mut cx, |workspace, cx| {
1571 let worktree = worktree.read(cx);
1572 let worktree_abs_path = worktree.abs_path();
1573 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1574 worktree.root_entry()
1575 } else {
1576 abs_path
1577 .strip_prefix(worktree_abs_path.as_ref())
1578 .ok()
1579 .and_then(|relative_path| {
1580 worktree.entry_for_path(relative_path)
1581 })
1582 }
1583 .map(|entry| entry.id);
1584 if let Some(entry_id) = entry_id {
1585 workspace.project.update(cx, |_, cx| {
1586 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1587 })
1588 }
1589 })
1590 .log_err()?;
1591 None
1592 } else {
1593 Some(
1594 this.update(&mut cx, |this, cx| {
1595 this.open_path(project_path, pane, true, cx)
1596 })
1597 .log_err()?
1598 .await,
1599 )
1600 }
1601 });
1602 tasks.push(task);
1603 }
1604
1605 futures::future::join_all(tasks).await
1606 })
1607 }
1608
1609 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1610 let paths = cx.prompt_for_paths(PathPromptOptions {
1611 files: false,
1612 directories: true,
1613 multiple: true,
1614 });
1615 cx.spawn(|this, mut cx| async move {
1616 if let Some(paths) = paths.await.log_err().flatten() {
1617 let results = this
1618 .update(&mut cx, |this, cx| {
1619 this.open_paths(paths, OpenVisible::All, None, cx)
1620 })?
1621 .await;
1622 for result in results.into_iter().flatten() {
1623 result.log_err();
1624 }
1625 }
1626 anyhow::Ok(())
1627 })
1628 .detach_and_log_err(cx);
1629 }
1630
1631 fn project_path_for_path(
1632 project: Model<Project>,
1633 abs_path: &Path,
1634 visible: bool,
1635 cx: &mut AppContext,
1636 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1637 let entry = project.update(cx, |project, cx| {
1638 project.find_or_create_local_worktree(abs_path, visible, cx)
1639 });
1640 cx.spawn(|mut cx| async move {
1641 let (worktree, path) = entry.await?;
1642 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1643 Ok((
1644 worktree,
1645 ProjectPath {
1646 worktree_id,
1647 path: path.into(),
1648 },
1649 ))
1650 })
1651 }
1652
1653 pub fn items<'a>(
1654 &'a self,
1655 cx: &'a AppContext,
1656 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1657 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1658 }
1659
1660 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1661 self.items_of_type(cx).max_by_key(|item| item.item_id())
1662 }
1663
1664 pub fn items_of_type<'a, T: Item>(
1665 &'a self,
1666 cx: &'a AppContext,
1667 ) -> impl 'a + Iterator<Item = View<T>> {
1668 self.panes
1669 .iter()
1670 .flat_map(|pane| pane.read(cx).items_of_type())
1671 }
1672
1673 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1674 self.active_pane().read(cx).active_item()
1675 }
1676
1677 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1678 let item = self.active_item(cx)?;
1679 item.to_any().downcast::<I>().ok()
1680 }
1681
1682 fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
1683 self.active_item(cx).and_then(|item| item.project_path(cx))
1684 }
1685
1686 pub fn save_active_item(
1687 &mut self,
1688 save_intent: SaveIntent,
1689 cx: &mut WindowContext,
1690 ) -> Task<Result<()>> {
1691 let project = self.project.clone();
1692 let pane = self.active_pane();
1693 let item_ix = pane.read(cx).active_item_index();
1694 let item = pane.read(cx).active_item();
1695 let pane = pane.downgrade();
1696
1697 cx.spawn(|mut cx| async move {
1698 if let Some(item) = item {
1699 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1700 .await
1701 .map(|_| ())
1702 } else {
1703 Ok(())
1704 }
1705 })
1706 }
1707
1708 pub fn close_inactive_items_and_panes(
1709 &mut self,
1710 action: &CloseInactiveTabsAndPanes,
1711 cx: &mut ViewContext<Self>,
1712 ) {
1713 if let Some(task) =
1714 self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1715 {
1716 task.detach_and_log_err(cx)
1717 }
1718 }
1719
1720 pub fn close_all_items_and_panes(
1721 &mut self,
1722 action: &CloseAllItemsAndPanes,
1723 cx: &mut ViewContext<Self>,
1724 ) {
1725 if let Some(task) =
1726 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1727 {
1728 task.detach_and_log_err(cx)
1729 }
1730 }
1731
1732 fn close_all_internal(
1733 &mut self,
1734 retain_active_pane: bool,
1735 save_intent: SaveIntent,
1736 cx: &mut ViewContext<Self>,
1737 ) -> Option<Task<Result<()>>> {
1738 let current_pane = self.active_pane();
1739
1740 let mut tasks = Vec::new();
1741
1742 if retain_active_pane {
1743 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1744 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
1745 }) {
1746 tasks.push(current_pane_close);
1747 };
1748 }
1749
1750 for pane in self.panes() {
1751 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
1752 continue;
1753 }
1754
1755 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1756 pane.close_all_items(
1757 &CloseAllItems {
1758 save_intent: Some(save_intent),
1759 },
1760 cx,
1761 )
1762 }) {
1763 tasks.push(close_pane_items)
1764 }
1765 }
1766
1767 if tasks.is_empty() {
1768 None
1769 } else {
1770 Some(cx.spawn(|_, _| async move {
1771 for task in tasks {
1772 task.await?
1773 }
1774 Ok(())
1775 }))
1776 }
1777 }
1778
1779 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1780 let dock = match dock_side {
1781 DockPosition::Left => &self.left_dock,
1782 DockPosition::Bottom => &self.bottom_dock,
1783 DockPosition::Right => &self.right_dock,
1784 };
1785 let mut focus_center = false;
1786 let mut reveal_dock = false;
1787 dock.update(cx, |dock, cx| {
1788 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1789 let was_visible = dock.is_open() && !other_is_zoomed;
1790 dock.set_open(!was_visible, cx);
1791
1792 if let Some(active_panel) = dock.active_panel() {
1793 if was_visible {
1794 if active_panel.focus_handle(cx).contains_focused(cx) {
1795 focus_center = true;
1796 }
1797 } else {
1798 let focus_handle = &active_panel.focus_handle(cx);
1799 cx.focus(focus_handle);
1800 reveal_dock = true;
1801 }
1802 }
1803 });
1804
1805 if reveal_dock {
1806 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1807 }
1808
1809 if focus_center {
1810 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1811 }
1812
1813 cx.notify();
1814 self.serialize_workspace(cx).detach();
1815 }
1816
1817 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1818 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1819
1820 for dock in docks {
1821 dock.update(cx, |dock, cx| {
1822 dock.set_open(false, cx);
1823 });
1824 }
1825
1826 cx.focus_self();
1827 cx.notify();
1828 self.serialize_workspace(cx).detach();
1829 }
1830
1831 /// Transfer focus to the panel of the given type.
1832 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
1833 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
1834 panel.to_any().downcast().ok()
1835 }
1836
1837 /// Focus the panel of the given type if it isn't already focused. If it is
1838 /// already focused, then transfer focus back to the workspace center.
1839 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1840 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
1841 !panel.focus_handle(cx).contains_focused(cx)
1842 });
1843 }
1844
1845 /// Focus or unfocus the given panel type, depending on the given callback.
1846 fn focus_or_unfocus_panel<T: Panel>(
1847 &mut self,
1848 cx: &mut ViewContext<Self>,
1849 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1850 ) -> Option<Arc<dyn PanelHandle>> {
1851 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1852 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1853 let mut focus_center = false;
1854 let panel = dock.update(cx, |dock, cx| {
1855 dock.activate_panel(panel_index, cx);
1856
1857 let panel = dock.active_panel().cloned();
1858 if let Some(panel) = panel.as_ref() {
1859 if should_focus(&**panel, cx) {
1860 dock.set_open(true, cx);
1861 panel.focus_handle(cx).focus(cx);
1862 } else {
1863 focus_center = true;
1864 }
1865 }
1866 panel
1867 });
1868
1869 if focus_center {
1870 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1871 }
1872
1873 self.serialize_workspace(cx).detach();
1874 cx.notify();
1875 return panel;
1876 }
1877 }
1878 None
1879 }
1880
1881 /// Open the panel of the given type
1882 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1883 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1884 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1885 dock.update(cx, |dock, cx| {
1886 dock.activate_panel(panel_index, cx);
1887 dock.set_open(true, cx);
1888 });
1889 }
1890 }
1891 }
1892
1893 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1894 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1895 let dock = dock.read(cx);
1896 if let Some(panel) = dock.panel::<T>() {
1897 return Some(panel);
1898 }
1899 }
1900 None
1901 }
1902
1903 fn dismiss_zoomed_items_to_reveal(
1904 &mut self,
1905 dock_to_reveal: Option<DockPosition>,
1906 cx: &mut ViewContext<Self>,
1907 ) {
1908 // If a center pane is zoomed, unzoom it.
1909 for pane in &self.panes {
1910 if pane != &self.active_pane || dock_to_reveal.is_some() {
1911 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1912 }
1913 }
1914
1915 // If another dock is zoomed, hide it.
1916 let mut focus_center = false;
1917 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1918 dock.update(cx, |dock, cx| {
1919 if Some(dock.position()) != dock_to_reveal {
1920 if let Some(panel) = dock.active_panel() {
1921 if panel.is_zoomed(cx) {
1922 focus_center |= panel.focus_handle(cx).contains_focused(cx);
1923 dock.set_open(false, cx);
1924 }
1925 }
1926 }
1927 });
1928 }
1929
1930 if focus_center {
1931 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1932 }
1933
1934 if self.zoomed_position != dock_to_reveal {
1935 self.zoomed = None;
1936 self.zoomed_position = None;
1937 }
1938
1939 cx.notify();
1940 }
1941
1942 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
1943 let pane = cx.new_view(|cx| {
1944 Pane::new(
1945 self.weak_handle(),
1946 self.project.clone(),
1947 self.pane_history_timestamp.clone(),
1948 None,
1949 NewFile.boxed_clone(),
1950 cx,
1951 )
1952 });
1953 cx.subscribe(&pane, Self::handle_pane_event).detach();
1954 self.panes.push(pane.clone());
1955 cx.focus_view(&pane);
1956 cx.emit(Event::PaneAdded(pane.clone()));
1957 pane
1958 }
1959
1960 pub fn add_item_to_center(
1961 &mut self,
1962 item: Box<dyn ItemHandle>,
1963 cx: &mut ViewContext<Self>,
1964 ) -> bool {
1965 if let Some(center_pane) = self.last_active_center_pane.clone() {
1966 if let Some(center_pane) = center_pane.upgrade() {
1967 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1968 true
1969 } else {
1970 false
1971 }
1972 } else {
1973 false
1974 }
1975 }
1976
1977 pub fn add_item_to_active_pane(&mut self, item: Box<dyn ItemHandle>, cx: &mut WindowContext) {
1978 self.add_item(self.active_pane.clone(), item, cx)
1979 }
1980
1981 pub fn add_item(
1982 &mut self,
1983 pane: View<Pane>,
1984 item: Box<dyn ItemHandle>,
1985 cx: &mut WindowContext,
1986 ) {
1987 if let Some(text) = item.telemetry_event_text(cx) {
1988 self.client()
1989 .telemetry()
1990 .report_app_event(format!("{}: open", text));
1991 }
1992
1993 pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1994 }
1995
1996 pub fn split_item(
1997 &mut self,
1998 split_direction: SplitDirection,
1999 item: Box<dyn ItemHandle>,
2000 cx: &mut ViewContext<Self>,
2001 ) {
2002 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2003 self.add_item(new_pane, item, cx);
2004 }
2005
2006 pub fn open_abs_path(
2007 &mut self,
2008 abs_path: PathBuf,
2009 visible: bool,
2010 cx: &mut ViewContext<Self>,
2011 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2012 cx.spawn(|workspace, mut cx| async move {
2013 let open_paths_task_result = workspace
2014 .update(&mut cx, |workspace, cx| {
2015 workspace.open_paths(
2016 vec![abs_path.clone()],
2017 if visible {
2018 OpenVisible::All
2019 } else {
2020 OpenVisible::None
2021 },
2022 None,
2023 cx,
2024 )
2025 })
2026 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2027 .await;
2028 anyhow::ensure!(
2029 open_paths_task_result.len() == 1,
2030 "open abs path {abs_path:?} task returned incorrect number of results"
2031 );
2032 match open_paths_task_result
2033 .into_iter()
2034 .next()
2035 .expect("ensured single task result")
2036 {
2037 Some(open_result) => {
2038 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2039 }
2040 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2041 }
2042 })
2043 }
2044
2045 pub fn split_abs_path(
2046 &mut self,
2047 abs_path: PathBuf,
2048 visible: bool,
2049 cx: &mut ViewContext<Self>,
2050 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2051 let project_path_task =
2052 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2053 cx.spawn(|this, mut cx| async move {
2054 let (_, path) = project_path_task.await?;
2055 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2056 .await
2057 })
2058 }
2059
2060 pub fn open_path(
2061 &mut self,
2062 path: impl Into<ProjectPath>,
2063 pane: Option<WeakView<Pane>>,
2064 focus_item: bool,
2065 cx: &mut WindowContext,
2066 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2067 let pane = pane.unwrap_or_else(|| {
2068 self.last_active_center_pane.clone().unwrap_or_else(|| {
2069 self.panes
2070 .first()
2071 .expect("There must be an active pane")
2072 .downgrade()
2073 })
2074 });
2075
2076 let task = self.load_path(path.into(), cx);
2077 cx.spawn(move |mut cx| async move {
2078 let (project_entry_id, build_item) = task.await?;
2079 pane.update(&mut cx, |pane, cx| {
2080 pane.open_item(project_entry_id, focus_item, cx, build_item)
2081 })
2082 })
2083 }
2084
2085 pub fn split_path(
2086 &mut self,
2087 path: impl Into<ProjectPath>,
2088 cx: &mut ViewContext<Self>,
2089 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2090 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2091 self.panes
2092 .first()
2093 .expect("There must be an active pane")
2094 .downgrade()
2095 });
2096
2097 if let Member::Pane(center_pane) = &self.center.root {
2098 if center_pane.read(cx).items_len() == 0 {
2099 return self.open_path(path, Some(pane), true, cx);
2100 }
2101 }
2102
2103 let task = self.load_path(path.into(), cx);
2104 cx.spawn(|this, mut cx| async move {
2105 let (project_entry_id, build_item) = task.await?;
2106 this.update(&mut cx, move |this, cx| -> Option<_> {
2107 let pane = pane.upgrade()?;
2108 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2109 new_pane.update(cx, |new_pane, cx| {
2110 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2111 })
2112 })
2113 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2114 })
2115 }
2116
2117 fn load_path(
2118 &mut self,
2119 path: ProjectPath,
2120 cx: &mut WindowContext,
2121 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2122 let project = self.project().clone();
2123 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2124 let Some(open_project_item) = project_item_builders
2125 .iter()
2126 .rev()
2127 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2128 else {
2129 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2130 };
2131 open_project_item
2132 }
2133
2134 pub fn open_project_item<T>(
2135 &mut self,
2136 pane: View<Pane>,
2137 project_item: Model<T::Item>,
2138 cx: &mut ViewContext<Self>,
2139 ) -> View<T>
2140 where
2141 T: ProjectItem,
2142 {
2143 use project::Item as _;
2144
2145 let entry_id = project_item.read(cx).entry_id(cx);
2146 if let Some(item) = entry_id
2147 .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
2148 .and_then(|item| item.downcast())
2149 {
2150 self.activate_item(&item, cx);
2151 return item;
2152 }
2153
2154 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2155 self.add_item(pane, Box::new(item.clone()), cx);
2156 item
2157 }
2158
2159 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2160 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2161 self.active_pane.update(cx, |pane, cx| {
2162 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2163 });
2164 }
2165 }
2166
2167 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2168 let result = self.panes.iter().find_map(|pane| {
2169 pane.read(cx)
2170 .index_for_item(item)
2171 .map(|ix| (pane.clone(), ix))
2172 });
2173 if let Some((pane, ix)) = result {
2174 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2175 true
2176 } else {
2177 false
2178 }
2179 }
2180
2181 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2182 let panes = self.center.panes();
2183 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2184 cx.focus_view(&pane);
2185 } else {
2186 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2187 }
2188 }
2189
2190 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2191 let panes = self.center.panes();
2192 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2193 let next_ix = (ix + 1) % panes.len();
2194 let next_pane = panes[next_ix].clone();
2195 cx.focus_view(&next_pane);
2196 }
2197 }
2198
2199 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2200 let panes = self.center.panes();
2201 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2202 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2203 let prev_pane = panes[prev_ix].clone();
2204 cx.focus_view(&prev_pane);
2205 }
2206 }
2207
2208 pub fn activate_pane_in_direction(
2209 &mut self,
2210 direction: SplitDirection,
2211 cx: &mut WindowContext,
2212 ) {
2213 use ActivateInDirectionTarget as Target;
2214 enum Origin {
2215 LeftDock,
2216 RightDock,
2217 BottomDock,
2218 Center,
2219 }
2220
2221 let origin: Origin = [
2222 (&self.left_dock, Origin::LeftDock),
2223 (&self.right_dock, Origin::RightDock),
2224 (&self.bottom_dock, Origin::BottomDock),
2225 ]
2226 .into_iter()
2227 .find_map(|(dock, origin)| {
2228 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2229 Some(origin)
2230 } else {
2231 None
2232 }
2233 })
2234 .unwrap_or(Origin::Center);
2235
2236 let get_last_active_pane = || {
2237 self.last_active_center_pane.as_ref().and_then(|p| {
2238 let p = p.upgrade()?;
2239 (p.read(cx).items_len() != 0).then_some(p)
2240 })
2241 };
2242
2243 let try_dock =
2244 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2245
2246 let target = match (origin, direction) {
2247 // We're in the center, so we first try to go to a different pane,
2248 // otherwise try to go to a dock.
2249 (Origin::Center, direction) => {
2250 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2251 Some(Target::Pane(pane))
2252 } else {
2253 match direction {
2254 SplitDirection::Up => None,
2255 SplitDirection::Down => try_dock(&self.bottom_dock),
2256 SplitDirection::Left => try_dock(&self.left_dock),
2257 SplitDirection::Right => try_dock(&self.right_dock),
2258 }
2259 }
2260 }
2261
2262 (Origin::LeftDock, SplitDirection::Right) => {
2263 if let Some(last_active_pane) = get_last_active_pane() {
2264 Some(Target::Pane(last_active_pane))
2265 } else {
2266 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2267 }
2268 }
2269
2270 (Origin::LeftDock, SplitDirection::Down)
2271 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2272
2273 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2274 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2275 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2276
2277 (Origin::RightDock, SplitDirection::Left) => {
2278 if let Some(last_active_pane) = get_last_active_pane() {
2279 Some(Target::Pane(last_active_pane))
2280 } else {
2281 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2282 }
2283 }
2284
2285 _ => None,
2286 };
2287
2288 match target {
2289 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2290 Some(ActivateInDirectionTarget::Dock(dock)) => {
2291 if let Some(panel) = dock.read(cx).active_panel() {
2292 panel.focus_handle(cx).focus(cx);
2293 } else {
2294 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2295 }
2296 }
2297 None => {}
2298 }
2299 }
2300
2301 fn find_pane_in_direction(
2302 &mut self,
2303 direction: SplitDirection,
2304 cx: &WindowContext,
2305 ) -> Option<View<Pane>> {
2306 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2307 return None;
2308 };
2309 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2310 let center = match cursor {
2311 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2312 _ => bounding_box.center(),
2313 };
2314
2315 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2316
2317 let target = match direction {
2318 SplitDirection::Left => {
2319 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2320 }
2321 SplitDirection::Right => {
2322 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2323 }
2324 SplitDirection::Up => {
2325 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2326 }
2327 SplitDirection::Down => {
2328 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2329 }
2330 };
2331 self.center.pane_at_pixel_position(target).cloned()
2332 }
2333
2334 pub fn swap_pane_in_direction(
2335 &mut self,
2336 direction: SplitDirection,
2337 cx: &mut ViewContext<Self>,
2338 ) {
2339 if let Some(to) = self
2340 .find_pane_in_direction(direction, cx)
2341 .map(|pane| pane.clone())
2342 {
2343 self.center.swap(&self.active_pane.clone(), &to);
2344 cx.notify();
2345 }
2346 }
2347
2348 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2349 if self.active_pane != pane {
2350 self.active_pane = pane.clone();
2351 self.status_bar.update(cx, |status_bar, cx| {
2352 status_bar.set_active_pane(&self.active_pane, cx);
2353 });
2354 self.active_item_path_changed(cx);
2355 self.last_active_center_pane = Some(pane.downgrade());
2356 }
2357
2358 self.dismiss_zoomed_items_to_reveal(None, cx);
2359 if pane.read(cx).is_zoomed() {
2360 self.zoomed = Some(pane.downgrade().into());
2361 } else {
2362 self.zoomed = None;
2363 }
2364 self.zoomed_position = None;
2365 self.update_active_view_for_followers(cx);
2366
2367 cx.notify();
2368 }
2369
2370 fn handle_pane_event(
2371 &mut self,
2372 pane: View<Pane>,
2373 event: &pane::Event,
2374 cx: &mut ViewContext<Self>,
2375 ) {
2376 match event {
2377 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2378 pane::Event::Split(direction) => {
2379 self.split_and_clone(pane, *direction, cx);
2380 }
2381 pane::Event::Remove => self.remove_pane(pane, cx),
2382 pane::Event::ActivateItem { local } => {
2383 if *local {
2384 self.unfollow(&pane, cx);
2385 }
2386 if &pane == self.active_pane() {
2387 self.active_item_path_changed(cx);
2388 self.update_active_view_for_followers(cx);
2389 }
2390 }
2391 pane::Event::ChangeItemTitle => {
2392 if pane == self.active_pane {
2393 self.active_item_path_changed(cx);
2394 }
2395 self.update_window_edited(cx);
2396 }
2397 pane::Event::RemoveItem { item_id } => {
2398 self.update_window_edited(cx);
2399 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2400 if entry.get().entity_id() == pane.entity_id() {
2401 entry.remove();
2402 }
2403 }
2404 }
2405 pane::Event::Focus => {
2406 self.handle_pane_focused(pane.clone(), cx);
2407 }
2408 pane::Event::ZoomIn => {
2409 if pane == self.active_pane {
2410 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2411 if pane.read(cx).has_focus(cx) {
2412 self.zoomed = Some(pane.downgrade().into());
2413 self.zoomed_position = None;
2414 }
2415 cx.notify();
2416 }
2417 }
2418 pane::Event::ZoomOut => {
2419 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2420 if self.zoomed_position.is_none() {
2421 self.zoomed = None;
2422 }
2423 cx.notify();
2424 }
2425 }
2426
2427 self.serialize_workspace(cx).detach();
2428 }
2429
2430 pub fn split_pane(
2431 &mut self,
2432 pane_to_split: View<Pane>,
2433 split_direction: SplitDirection,
2434 cx: &mut ViewContext<Self>,
2435 ) -> View<Pane> {
2436 let new_pane = self.add_pane(cx);
2437 self.center
2438 .split(&pane_to_split, &new_pane, split_direction)
2439 .unwrap();
2440 cx.notify();
2441 new_pane
2442 }
2443
2444 pub fn split_and_clone(
2445 &mut self,
2446 pane: View<Pane>,
2447 direction: SplitDirection,
2448 cx: &mut ViewContext<Self>,
2449 ) -> Option<View<Pane>> {
2450 let item = pane.read(cx).active_item()?;
2451 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2452 let new_pane = self.add_pane(cx);
2453 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2454 self.center.split(&pane, &new_pane, direction).unwrap();
2455 Some(new_pane)
2456 } else {
2457 None
2458 };
2459 cx.notify();
2460 maybe_pane_handle
2461 }
2462
2463 pub fn split_pane_with_item(
2464 &mut self,
2465 pane_to_split: WeakView<Pane>,
2466 split_direction: SplitDirection,
2467 from: WeakView<Pane>,
2468 item_id_to_move: EntityId,
2469 cx: &mut ViewContext<Self>,
2470 ) {
2471 let Some(pane_to_split) = pane_to_split.upgrade() else {
2472 return;
2473 };
2474 let Some(from) = from.upgrade() else {
2475 return;
2476 };
2477
2478 let new_pane = self.add_pane(cx);
2479 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2480 self.center
2481 .split(&pane_to_split, &new_pane, split_direction)
2482 .unwrap();
2483 cx.notify();
2484 }
2485
2486 pub fn split_pane_with_project_entry(
2487 &mut self,
2488 pane_to_split: WeakView<Pane>,
2489 split_direction: SplitDirection,
2490 project_entry: ProjectEntryId,
2491 cx: &mut ViewContext<Self>,
2492 ) -> Option<Task<Result<()>>> {
2493 let pane_to_split = pane_to_split.upgrade()?;
2494 let new_pane = self.add_pane(cx);
2495 self.center
2496 .split(&pane_to_split, &new_pane, split_direction)
2497 .unwrap();
2498
2499 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2500 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2501 Some(cx.foreground_executor().spawn(async move {
2502 task.await?;
2503 Ok(())
2504 }))
2505 }
2506
2507 pub fn move_item(
2508 &mut self,
2509 source: View<Pane>,
2510 destination: View<Pane>,
2511 item_id_to_move: EntityId,
2512 destination_index: usize,
2513 cx: &mut ViewContext<Self>,
2514 ) {
2515 let Some((item_ix, item_handle)) = source
2516 .read(cx)
2517 .items()
2518 .enumerate()
2519 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2520 else {
2521 // Tab was closed during drag
2522 return;
2523 };
2524
2525 let item_handle = item_handle.clone();
2526
2527 if source != destination {
2528 // Close item from previous pane
2529 source.update(cx, |source, cx| {
2530 source.remove_item(item_ix, false, cx);
2531 });
2532 }
2533
2534 // This automatically removes duplicate items in the pane
2535 destination.update(cx, |destination, cx| {
2536 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2537 destination.focus(cx)
2538 });
2539 }
2540
2541 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2542 if self.center.remove(&pane).unwrap() {
2543 self.force_remove_pane(&pane, cx);
2544 self.unfollow(&pane, cx);
2545 self.last_leaders_by_pane.remove(&pane.downgrade());
2546 for removed_item in pane.read(cx).items() {
2547 self.panes_by_item.remove(&removed_item.item_id());
2548 }
2549
2550 cx.notify();
2551 } else {
2552 self.active_item_path_changed(cx);
2553 }
2554 }
2555
2556 pub fn panes(&self) -> &[View<Pane>] {
2557 &self.panes
2558 }
2559
2560 pub fn active_pane(&self) -> &View<Pane> {
2561 &self.active_pane
2562 }
2563
2564 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2565 self.find_pane_in_direction(SplitDirection::Right, cx)
2566 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
2567 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
2568 .clone()
2569 }
2570
2571 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2572 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2573 weak_pane.upgrade()
2574 }
2575
2576 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2577 self.follower_states.retain(|_, state| {
2578 if state.leader_id == peer_id {
2579 for item in state.items_by_leader_view_id.values() {
2580 item.set_leader_peer_id(None, cx);
2581 }
2582 false
2583 } else {
2584 true
2585 }
2586 });
2587 cx.notify();
2588 }
2589
2590 pub fn start_following(
2591 &mut self,
2592 leader_id: PeerId,
2593 cx: &mut ViewContext<Self>,
2594 ) -> Option<Task<Result<()>>> {
2595 let pane = self.active_pane().clone();
2596
2597 self.last_leaders_by_pane
2598 .insert(pane.downgrade(), leader_id);
2599 self.unfollow(&pane, cx);
2600 self.follower_states.insert(
2601 pane.clone(),
2602 FollowerState {
2603 leader_id,
2604 active_view_id: None,
2605 items_by_leader_view_id: Default::default(),
2606 },
2607 );
2608 cx.notify();
2609
2610 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2611 let project_id = self.project.read(cx).remote_id();
2612 let request = self.app_state.client.request(proto::Follow {
2613 room_id,
2614 project_id,
2615 leader_id: Some(leader_id),
2616 });
2617
2618 Some(cx.spawn(|this, mut cx| async move {
2619 let response = request.await?;
2620 this.update(&mut cx, |this, _| {
2621 let state = this
2622 .follower_states
2623 .get_mut(&pane)
2624 .ok_or_else(|| anyhow!("following interrupted"))?;
2625 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2626 Some(ViewId::from_proto(active_view_id)?)
2627 } else {
2628 None
2629 };
2630 Ok::<_, anyhow::Error>(())
2631 })??;
2632 if let Some(view) = response.active_view {
2633 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
2634 .await?;
2635 }
2636 Self::add_views_from_leader(
2637 this.clone(),
2638 leader_id,
2639 vec![pane],
2640 response.views,
2641 &mut cx,
2642 )
2643 .await?;
2644 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2645 Ok(())
2646 }))
2647 }
2648
2649 pub fn follow_next_collaborator(
2650 &mut self,
2651 _: &FollowNextCollaborator,
2652 cx: &mut ViewContext<Self>,
2653 ) {
2654 let collaborators = self.project.read(cx).collaborators();
2655 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2656 let mut collaborators = collaborators.keys().copied();
2657 for peer_id in collaborators.by_ref() {
2658 if peer_id == leader_id {
2659 break;
2660 }
2661 }
2662 collaborators.next()
2663 } else if let Some(last_leader_id) =
2664 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2665 {
2666 if collaborators.contains_key(last_leader_id) {
2667 Some(*last_leader_id)
2668 } else {
2669 None
2670 }
2671 } else {
2672 None
2673 };
2674
2675 let pane = self.active_pane.clone();
2676 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2677 else {
2678 return;
2679 };
2680 if Some(leader_id) == self.unfollow(&pane, cx) {
2681 return;
2682 }
2683 if let Some(task) = self.start_following(leader_id, cx) {
2684 task.detach_and_log_err(cx)
2685 }
2686 }
2687
2688 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2689 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2690 return;
2691 };
2692 let room = room.read(cx);
2693 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2694 return;
2695 };
2696
2697 let project = self.project.read(cx);
2698
2699 let other_project_id = match remote_participant.location {
2700 call::ParticipantLocation::External => None,
2701 call::ParticipantLocation::UnsharedProject => None,
2702 call::ParticipantLocation::SharedProject { project_id } => {
2703 if Some(project_id) == project.remote_id() {
2704 None
2705 } else {
2706 Some(project_id)
2707 }
2708 }
2709 };
2710
2711 // if they are active in another project, follow there.
2712 if let Some(project_id) = other_project_id {
2713 let app_state = self.app_state.clone();
2714 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
2715 .detach_and_log_err(cx);
2716 }
2717
2718 // if you're already following, find the right pane and focus it.
2719 for (pane, state) in &self.follower_states {
2720 if leader_id == state.leader_id {
2721 cx.focus_view(pane);
2722 return;
2723 }
2724 }
2725
2726 // Otherwise, follow.
2727 if let Some(task) = self.start_following(leader_id, cx) {
2728 task.detach_and_log_err(cx)
2729 }
2730 }
2731
2732 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2733 let state = self.follower_states.remove(pane)?;
2734 let leader_id = state.leader_id;
2735 for (_, item) in state.items_by_leader_view_id {
2736 item.set_leader_peer_id(None, cx);
2737 }
2738
2739 if self
2740 .follower_states
2741 .values()
2742 .all(|state| state.leader_id != leader_id)
2743 {
2744 let project_id = self.project.read(cx).remote_id();
2745 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2746 self.app_state
2747 .client
2748 .send(proto::Unfollow {
2749 room_id,
2750 project_id,
2751 leader_id: Some(leader_id),
2752 })
2753 .log_err();
2754 }
2755
2756 cx.notify();
2757 Some(leader_id)
2758 }
2759
2760 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2761 self.follower_states
2762 .values()
2763 .any(|state| state.leader_id == peer_id)
2764 }
2765
2766 fn active_item_path_changed(&mut self, cx: &mut WindowContext) {
2767 let active_entry = self.active_project_path(cx);
2768 self.project
2769 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2770 self.update_window_title(cx);
2771 }
2772
2773 fn update_window_title(&mut self, cx: &mut WindowContext) {
2774 let project = self.project().read(cx);
2775 let mut title = String::new();
2776
2777 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2778 let filename = path
2779 .path
2780 .file_name()
2781 .map(|s| s.to_string_lossy())
2782 .or_else(|| {
2783 Some(Cow::Borrowed(
2784 project
2785 .worktree_for_id(path.worktree_id, cx)?
2786 .read(cx)
2787 .root_name(),
2788 ))
2789 });
2790
2791 if let Some(filename) = filename {
2792 title.push_str(filename.as_ref());
2793 title.push_str(" — ");
2794 }
2795 }
2796
2797 for (i, name) in project.worktree_root_names(cx).enumerate() {
2798 if i > 0 {
2799 title.push_str(", ");
2800 }
2801 title.push_str(name);
2802 }
2803
2804 if title.is_empty() {
2805 title = "empty project".to_string();
2806 }
2807
2808 if project.is_remote() {
2809 title.push_str(" ↙");
2810 } else if project.is_shared() {
2811 title.push_str(" ↗");
2812 }
2813
2814 cx.set_window_title(&title);
2815 }
2816
2817 fn update_window_edited(&mut self, cx: &mut WindowContext) {
2818 let is_edited = !self.project.read(cx).is_disconnected()
2819 && self
2820 .items(cx)
2821 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2822 if is_edited != self.window_edited {
2823 self.window_edited = is_edited;
2824 cx.set_window_edited(self.window_edited)
2825 }
2826 }
2827
2828 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2829 if self.notifications.is_empty() {
2830 None
2831 } else {
2832 Some(
2833 div()
2834 .absolute()
2835 .right_3()
2836 .bottom_3()
2837 .w_112()
2838 .h_full()
2839 .flex()
2840 .flex_col()
2841 .justify_end()
2842 .gap_2()
2843 .children(
2844 self.notifications
2845 .iter()
2846 .map(|(_, _, notification)| notification.to_any()),
2847 ),
2848 )
2849 }
2850 }
2851
2852 // RPC handlers
2853
2854 fn active_view_for_follower(
2855 &self,
2856 follower_project_id: Option<u64>,
2857 cx: &mut ViewContext<Self>,
2858 ) -> Option<proto::View> {
2859 let item = self.active_item(cx)?;
2860 let leader_id = self
2861 .pane_for(&*item)
2862 .and_then(|pane| self.leader_for_pane(&pane));
2863
2864 let item_handle = item.to_followable_item_handle(cx)?;
2865 let id = item_handle.remote_id(&self.app_state.client, cx)?;
2866 let variant = item_handle.to_state_proto(cx)?;
2867
2868 if item_handle.is_project_item(cx)
2869 && (follower_project_id.is_none()
2870 || follower_project_id != self.project.read(cx).remote_id())
2871 {
2872 return None;
2873 }
2874
2875 Some(proto::View {
2876 id: Some(id.to_proto()),
2877 leader_id,
2878 variant: Some(variant),
2879 })
2880 }
2881
2882 fn handle_follow(
2883 &mut self,
2884 follower_project_id: Option<u64>,
2885 cx: &mut ViewContext<Self>,
2886 ) -> proto::FollowResponse {
2887 let client = &self.app_state.client;
2888 let project_id = self.project.read(cx).remote_id();
2889
2890 let active_view = self.active_view_for_follower(follower_project_id, cx);
2891 let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
2892
2893 cx.notify();
2894
2895 proto::FollowResponse {
2896 active_view,
2897 // TODO: once v0.124.0 is retired we can stop sending these
2898 active_view_id,
2899 views: self
2900 .panes()
2901 .iter()
2902 .flat_map(|pane| {
2903 let leader_id = self.leader_for_pane(pane);
2904 pane.read(cx).items().filter_map({
2905 let cx = &cx;
2906 move |item| {
2907 let item = item.to_followable_item_handle(cx)?;
2908
2909 // If the item belongs to a particular project, then it should
2910 // only be included if this project is shared, and the follower
2911 // is in the project.
2912 //
2913 // Some items, like channel notes, do not belong to a particular
2914 // project, so they should be included regardless of whether the
2915 // current project is shared, or what project the follower is in.
2916 if item.is_project_item(cx)
2917 && (project_id.is_none() || project_id != follower_project_id)
2918 {
2919 return None;
2920 }
2921
2922 let id = item.remote_id(client, cx)?.to_proto();
2923 let variant = item.to_state_proto(cx)?;
2924 Some(proto::View {
2925 id: Some(id),
2926 leader_id,
2927 variant: Some(variant),
2928 })
2929 }
2930 })
2931 })
2932 .collect(),
2933 }
2934 }
2935
2936 fn handle_update_followers(
2937 &mut self,
2938 leader_id: PeerId,
2939 message: proto::UpdateFollowers,
2940 _cx: &mut ViewContext<Self>,
2941 ) {
2942 self.leader_updates_tx
2943 .unbounded_send((leader_id, message))
2944 .ok();
2945 }
2946
2947 async fn process_leader_update(
2948 this: &WeakView<Self>,
2949 leader_id: PeerId,
2950 update: proto::UpdateFollowers,
2951 cx: &mut AsyncWindowContext,
2952 ) -> Result<()> {
2953 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2954 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2955 let panes_missing_view = this.update(cx, |this, _| {
2956 let mut panes = vec![];
2957 for (pane, state) in &mut this.follower_states {
2958 if state.leader_id != leader_id {
2959 continue;
2960 }
2961
2962 state.active_view_id =
2963 if let Some(active_view_id) = update_active_view.id.clone() {
2964 Some(ViewId::from_proto(active_view_id)?)
2965 } else {
2966 None
2967 };
2968
2969 if state.active_view_id.is_some_and(|view_id| {
2970 !state.items_by_leader_view_id.contains_key(&view_id)
2971 }) {
2972 panes.push(pane.clone())
2973 }
2974 }
2975 anyhow::Ok(panes)
2976 })??;
2977
2978 if let Some(view) = update_active_view.view {
2979 for pane in panes_missing_view {
2980 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
2981 .await?
2982 }
2983 }
2984 }
2985 proto::update_followers::Variant::UpdateView(update_view) => {
2986 let variant = update_view
2987 .variant
2988 .ok_or_else(|| anyhow!("missing update view variant"))?;
2989 let id = update_view
2990 .id
2991 .ok_or_else(|| anyhow!("missing update view id"))?;
2992 let mut tasks = Vec::new();
2993 this.update(cx, |this, cx| {
2994 let project = this.project.clone();
2995 for (_, state) in &mut this.follower_states {
2996 if state.leader_id == leader_id {
2997 let view_id = ViewId::from_proto(id.clone())?;
2998 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2999 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
3000 }
3001 }
3002 }
3003 anyhow::Ok(())
3004 })??;
3005 try_join_all(tasks).await.log_err();
3006 }
3007 proto::update_followers::Variant::CreateView(view) => {
3008 let panes = this.update(cx, |this, _| {
3009 this.follower_states
3010 .iter()
3011 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
3012 .cloned()
3013 .collect()
3014 })?;
3015 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
3016 }
3017 }
3018 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3019 Ok(())
3020 }
3021
3022 async fn add_view_from_leader(
3023 this: WeakView<Self>,
3024 leader_id: PeerId,
3025 pane: View<Pane>,
3026 view: &proto::View,
3027 cx: &mut AsyncWindowContext,
3028 ) -> Result<()> {
3029 let this = this.upgrade().context("workspace dropped")?;
3030
3031 let item_builders = cx.update(|cx| {
3032 cx.default_global::<FollowableItemBuilders>()
3033 .values()
3034 .map(|b| b.0)
3035 .collect::<Vec<_>>()
3036 })?;
3037
3038 let Some(id) = view.id.clone() else {
3039 return Err(anyhow!("no id for view"));
3040 };
3041 let id = ViewId::from_proto(id)?;
3042
3043 let mut variant = view.variant.clone();
3044 if variant.is_none() {
3045 Err(anyhow!("missing view variant"))?;
3046 }
3047
3048 let task = item_builders.iter().find_map(|build_item| {
3049 cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
3050 .log_err()
3051 .flatten()
3052 });
3053 let Some(task) = task else {
3054 return Err(anyhow!(
3055 "failed to construct view from leader (maybe from a different version of zed?)"
3056 ));
3057 };
3058
3059 let item = task.await?;
3060
3061 this.update(cx, |this, cx| {
3062 let state = this.follower_states.get_mut(&pane)?;
3063 item.set_leader_peer_id(Some(leader_id), cx);
3064 state.items_by_leader_view_id.insert(id, item);
3065
3066 Some(())
3067 })?;
3068
3069 Ok(())
3070 }
3071
3072 async fn add_views_from_leader(
3073 this: WeakView<Self>,
3074 leader_id: PeerId,
3075 panes: Vec<View<Pane>>,
3076 views: Vec<proto::View>,
3077 cx: &mut AsyncWindowContext,
3078 ) -> Result<()> {
3079 let this = this.upgrade().context("workspace dropped")?;
3080
3081 let item_builders = cx.update(|cx| {
3082 cx.default_global::<FollowableItemBuilders>()
3083 .values()
3084 .map(|b| b.0)
3085 .collect::<Vec<_>>()
3086 })?;
3087
3088 let mut item_tasks_by_pane = HashMap::default();
3089 for pane in panes {
3090 let mut item_tasks = Vec::new();
3091 let mut leader_view_ids = Vec::new();
3092 for view in &views {
3093 let Some(id) = &view.id else {
3094 continue;
3095 };
3096 let id = ViewId::from_proto(id.clone())?;
3097 let mut variant = view.variant.clone();
3098 if variant.is_none() {
3099 Err(anyhow!("missing view variant"))?;
3100 }
3101 for build_item in &item_builders {
3102 let task = cx.update(|cx| {
3103 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3104 })?;
3105 if let Some(task) = task {
3106 item_tasks.push(task);
3107 leader_view_ids.push(id);
3108 break;
3109 } else if variant.is_none() {
3110 Err(anyhow!(
3111 "failed to construct view from leader (maybe from a different version of zed?)"
3112 ))?;
3113 }
3114 }
3115 }
3116
3117 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3118 }
3119
3120 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3121 let items = futures::future::try_join_all(item_tasks).await?;
3122 this.update(cx, |this, cx| {
3123 let state = this.follower_states.get_mut(&pane)?;
3124 for (id, item) in leader_view_ids.into_iter().zip(items) {
3125 item.set_leader_peer_id(Some(leader_id), cx);
3126 state.items_by_leader_view_id.insert(id, item);
3127 }
3128
3129 Some(())
3130 })?;
3131 }
3132 Ok(())
3133 }
3134
3135 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3136 let mut is_project_item = true;
3137 let mut update = proto::UpdateActiveView::default();
3138 if cx.is_window_active() {
3139 if let Some(item) = self.active_item(cx) {
3140 if item.focus_handle(cx).contains_focused(cx) {
3141 let leader_id = self
3142 .pane_for(&*item)
3143 .and_then(|pane| self.leader_for_pane(&pane));
3144
3145 if let Some(item) = item.to_followable_item_handle(cx) {
3146 let id = item
3147 .remote_id(&self.app_state.client, cx)
3148 .map(|id| id.to_proto());
3149
3150 if let Some(id) = id.clone() {
3151 if let Some(variant) = item.to_state_proto(cx) {
3152 let view = Some(proto::View {
3153 id: Some(id.clone()),
3154 leader_id,
3155 variant: Some(variant),
3156 });
3157
3158 is_project_item = item.is_project_item(cx);
3159 update = proto::UpdateActiveView {
3160 view,
3161 // TODO: once v0.124.0 is retired we can stop sending these
3162 id: Some(id),
3163 leader_id,
3164 };
3165 }
3166 };
3167 }
3168 }
3169 }
3170 }
3171
3172 if &update.id != &self.last_active_view_id {
3173 self.last_active_view_id = update.id.clone();
3174 self.update_followers(
3175 is_project_item,
3176 proto::update_followers::Variant::UpdateActiveView(update),
3177 cx,
3178 );
3179 }
3180 }
3181
3182 fn update_followers(
3183 &self,
3184 project_only: bool,
3185 update: proto::update_followers::Variant,
3186 cx: &mut WindowContext,
3187 ) -> Option<()> {
3188 // If this update only applies to for followers in the current project,
3189 // then skip it unless this project is shared. If it applies to all
3190 // followers, regardless of project, then set `project_id` to none,
3191 // indicating that it goes to all followers.
3192 let project_id = if project_only {
3193 Some(self.project.read(cx).remote_id()?)
3194 } else {
3195 None
3196 };
3197 self.app_state().workspace_store.update(cx, |store, cx| {
3198 store.update_followers(project_id, update, cx)
3199 })
3200 }
3201
3202 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3203 self.follower_states.get(pane).map(|state| state.leader_id)
3204 }
3205
3206 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3207 cx.notify();
3208
3209 let call = self.active_call()?;
3210 let room = call.read(cx).room()?.read(cx);
3211 let participant = room.remote_participant_for_peer_id(leader_id)?;
3212 let mut items_to_activate = Vec::new();
3213
3214 let leader_in_this_app;
3215 let leader_in_this_project;
3216 match participant.location {
3217 call::ParticipantLocation::SharedProject { project_id } => {
3218 leader_in_this_app = true;
3219 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3220 }
3221 call::ParticipantLocation::UnsharedProject => {
3222 leader_in_this_app = true;
3223 leader_in_this_project = false;
3224 }
3225 call::ParticipantLocation::External => {
3226 leader_in_this_app = false;
3227 leader_in_this_project = false;
3228 }
3229 };
3230
3231 for (pane, state) in &self.follower_states {
3232 if state.leader_id != leader_id {
3233 continue;
3234 }
3235 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3236 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3237 if leader_in_this_project || !item.is_project_item(cx) {
3238 items_to_activate.push((pane.clone(), item.boxed_clone()));
3239 }
3240 }
3241 continue;
3242 }
3243
3244 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3245 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3246 }
3247 }
3248
3249 for (pane, item) in items_to_activate {
3250 let pane_was_focused = pane.read(cx).has_focus(cx);
3251 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3252 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3253 } else {
3254 pane.update(cx, |pane, cx| {
3255 pane.add_item(item.boxed_clone(), false, false, None, cx)
3256 });
3257 }
3258
3259 if pane_was_focused {
3260 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3261 }
3262 }
3263
3264 None
3265 }
3266
3267 fn shared_screen_for_peer(
3268 &self,
3269 peer_id: PeerId,
3270 pane: &View<Pane>,
3271 cx: &mut WindowContext,
3272 ) -> Option<View<SharedScreen>> {
3273 let call = self.active_call()?;
3274 let room = call.read(cx).room()?.read(cx);
3275 let participant = room.remote_participant_for_peer_id(peer_id)?;
3276 let track = participant.video_tracks.values().next()?.clone();
3277 let user = participant.user.clone();
3278
3279 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3280 if item.read(cx).peer_id == peer_id {
3281 return Some(item);
3282 }
3283 }
3284
3285 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3286 }
3287
3288 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3289 if cx.is_window_active() {
3290 self.update_active_view_for_followers(cx);
3291 cx.background_executor()
3292 .spawn(persistence::DB.update_timestamp(self.database_id()))
3293 .detach();
3294 } else {
3295 for pane in &self.panes {
3296 pane.update(cx, |pane, cx| {
3297 if let Some(item) = pane.active_item() {
3298 item.workspace_deactivated(cx);
3299 }
3300 if matches!(
3301 WorkspaceSettings::get_global(cx).autosave,
3302 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3303 ) {
3304 for item in pane.items() {
3305 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3306 .detach_and_log_err(cx);
3307 }
3308 }
3309 });
3310 }
3311 }
3312 }
3313
3314 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3315 self.active_call.as_ref().map(|(call, _)| call)
3316 }
3317
3318 fn on_active_call_event(
3319 &mut self,
3320 _: Model<ActiveCall>,
3321 event: &call::room::Event,
3322 cx: &mut ViewContext<Self>,
3323 ) {
3324 match event {
3325 call::room::Event::ParticipantLocationChanged { participant_id }
3326 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3327 self.leader_updated(*participant_id, cx);
3328 }
3329 _ => {}
3330 }
3331 }
3332
3333 pub fn database_id(&self) -> WorkspaceId {
3334 self.database_id
3335 }
3336
3337 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3338 let project = self.project().read(cx);
3339
3340 if project.is_local() {
3341 Some(
3342 project
3343 .visible_worktrees(cx)
3344 .map(|worktree| worktree.read(cx).abs_path())
3345 .collect::<Vec<_>>()
3346 .into(),
3347 )
3348 } else {
3349 None
3350 }
3351 }
3352
3353 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3354 match member {
3355 Member::Axis(PaneAxis { members, .. }) => {
3356 for child in members.iter() {
3357 self.remove_panes(child.clone(), cx)
3358 }
3359 }
3360 Member::Pane(pane) => {
3361 self.force_remove_pane(&pane, cx);
3362 }
3363 }
3364 }
3365
3366 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3367 self.panes.retain(|p| p != pane);
3368 self.panes
3369 .last()
3370 .unwrap()
3371 .update(cx, |pane, cx| pane.focus(cx));
3372 if self.last_active_center_pane == Some(pane.downgrade()) {
3373 self.last_active_center_pane = None;
3374 }
3375 cx.notify();
3376 }
3377
3378 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3379 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3380 cx.background_executor()
3381 .timer(Duration::from_millis(100))
3382 .await;
3383 this.update(&mut cx, |this, cx| this.serialize_workspace(cx).detach())
3384 .log_err();
3385 }));
3386 }
3387
3388 fn serialize_workspace(&self, cx: &mut WindowContext) -> Task<()> {
3389 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3390 let (items, active) = {
3391 let pane = pane_handle.read(cx);
3392 let active_item_id = pane.active_item().map(|item| item.item_id());
3393 (
3394 pane.items()
3395 .filter_map(|item_handle| {
3396 Some(SerializedItem {
3397 kind: Arc::from(item_handle.serialized_item_kind()?),
3398 item_id: item_handle.item_id().as_u64(),
3399 active: Some(item_handle.item_id()) == active_item_id,
3400 })
3401 })
3402 .collect::<Vec<_>>(),
3403 pane.has_focus(cx),
3404 )
3405 };
3406
3407 SerializedPane::new(items, active)
3408 }
3409
3410 fn build_serialized_pane_group(
3411 pane_group: &Member,
3412 cx: &WindowContext,
3413 ) -> SerializedPaneGroup {
3414 match pane_group {
3415 Member::Axis(PaneAxis {
3416 axis,
3417 members,
3418 flexes,
3419 bounding_boxes: _,
3420 }) => SerializedPaneGroup::Group {
3421 axis: SerializedAxis(*axis),
3422 children: members
3423 .iter()
3424 .map(|member| build_serialized_pane_group(member, cx))
3425 .collect::<Vec<_>>(),
3426 flexes: Some(flexes.lock().clone()),
3427 },
3428 Member::Pane(pane_handle) => {
3429 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3430 }
3431 }
3432 }
3433
3434 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3435 let left_dock = this.left_dock.read(cx);
3436 let left_visible = left_dock.is_open();
3437 let left_active_panel = left_dock
3438 .visible_panel()
3439 .map(|panel| panel.persistent_name().to_string());
3440 let left_dock_zoom = left_dock
3441 .visible_panel()
3442 .map(|panel| panel.is_zoomed(cx))
3443 .unwrap_or(false);
3444
3445 let right_dock = this.right_dock.read(cx);
3446 let right_visible = right_dock.is_open();
3447 let right_active_panel = right_dock
3448 .visible_panel()
3449 .map(|panel| panel.persistent_name().to_string());
3450 let right_dock_zoom = right_dock
3451 .visible_panel()
3452 .map(|panel| panel.is_zoomed(cx))
3453 .unwrap_or(false);
3454
3455 let bottom_dock = this.bottom_dock.read(cx);
3456 let bottom_visible = bottom_dock.is_open();
3457 let bottom_active_panel = bottom_dock
3458 .visible_panel()
3459 .map(|panel| panel.persistent_name().to_string());
3460 let bottom_dock_zoom = bottom_dock
3461 .visible_panel()
3462 .map(|panel| panel.is_zoomed(cx))
3463 .unwrap_or(false);
3464
3465 DockStructure {
3466 left: DockData {
3467 visible: left_visible,
3468 active_panel: left_active_panel,
3469 zoom: left_dock_zoom,
3470 },
3471 right: DockData {
3472 visible: right_visible,
3473 active_panel: right_active_panel,
3474 zoom: right_dock_zoom,
3475 },
3476 bottom: DockData {
3477 visible: bottom_visible,
3478 active_panel: bottom_active_panel,
3479 zoom: bottom_dock_zoom,
3480 },
3481 }
3482 }
3483
3484 if let Some(location) = self.location(cx) {
3485 // Load bearing special case:
3486 // - with_local_workspace() relies on this to not have other stuff open
3487 // when you open your log
3488 if !location.paths().is_empty() {
3489 let center_group = build_serialized_pane_group(&self.center.root, cx);
3490 let docks = build_serialized_docks(self, cx);
3491 let serialized_workspace = SerializedWorkspace {
3492 id: self.database_id,
3493 location,
3494 center_group,
3495 bounds: Default::default(),
3496 display: Default::default(),
3497 docks,
3498 fullscreen: cx.is_fullscreen(),
3499 };
3500 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3501 }
3502 }
3503 Task::ready(())
3504 }
3505
3506 fn refresh_recent_documents(&self, cx: &mut AppContext) -> Task<Result<()>> {
3507 if !self.project.read(cx).is_local() {
3508 return Task::ready(Ok(()));
3509 }
3510 cx.spawn(|cx| async move {
3511 let recents = WORKSPACE_DB
3512 .recent_workspaces_on_disk()
3513 .await
3514 .unwrap_or_default();
3515 let mut unique_paths = HashMap::default();
3516 for (id, workspace) in &recents {
3517 for path in workspace.paths().iter() {
3518 unique_paths.insert(path.clone(), id);
3519 }
3520 }
3521 let current_paths = unique_paths
3522 .into_iter()
3523 .sorted_by_key(|(_, id)| *id)
3524 .map(|(path, _)| path)
3525 .collect::<Vec<_>>();
3526 cx.update(|cx| {
3527 cx.clear_recent_documents();
3528 cx.add_recent_documents(¤t_paths);
3529 })
3530 })
3531 }
3532
3533 pub(crate) fn load_workspace(
3534 serialized_workspace: SerializedWorkspace,
3535 paths_to_open: Vec<Option<ProjectPath>>,
3536 cx: &mut ViewContext<Workspace>,
3537 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3538 cx.spawn(|workspace, mut cx| async move {
3539 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3540
3541 let mut center_group = None;
3542 let mut center_items = None;
3543
3544 // Traverse the splits tree and add to things
3545 if let Some((group, active_pane, items)) = serialized_workspace
3546 .center_group
3547 .deserialize(
3548 &project,
3549 serialized_workspace.id,
3550 workspace.clone(),
3551 &mut cx,
3552 )
3553 .await
3554 {
3555 center_items = Some(items);
3556 center_group = Some((group, active_pane))
3557 }
3558
3559 let mut items_by_project_path = cx.update(|cx| {
3560 center_items
3561 .unwrap_or_default()
3562 .into_iter()
3563 .filter_map(|item| {
3564 let item = item?;
3565 let project_path = item.project_path(cx)?;
3566 Some((project_path, item))
3567 })
3568 .collect::<HashMap<_, _>>()
3569 })?;
3570
3571 let opened_items = paths_to_open
3572 .into_iter()
3573 .map(|path_to_open| {
3574 path_to_open
3575 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3576 })
3577 .collect::<Vec<_>>();
3578
3579 // Remove old panes from workspace panes list
3580 workspace.update(&mut cx, |workspace, cx| {
3581 if let Some((center_group, active_pane)) = center_group {
3582 workspace.remove_panes(workspace.center.root.clone(), cx);
3583
3584 // Swap workspace center group
3585 workspace.center = PaneGroup::with_root(center_group);
3586 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3587 if let Some(active_pane) = active_pane {
3588 workspace.active_pane = active_pane;
3589 cx.focus_self();
3590 } else {
3591 workspace.active_pane = workspace.center.first_pane().clone();
3592 }
3593 }
3594
3595 let docks = serialized_workspace.docks;
3596
3597 let right = docks.right.clone();
3598 workspace
3599 .right_dock
3600 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3601 let left = docks.left.clone();
3602 workspace
3603 .left_dock
3604 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3605 let bottom = docks.bottom.clone();
3606 workspace
3607 .bottom_dock
3608 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3609
3610 cx.notify();
3611 })?;
3612
3613 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3614 workspace.update(&mut cx, |workspace, cx| {
3615 workspace.serialize_workspace(cx).detach()
3616 })?;
3617
3618 Ok(opened_items)
3619 })
3620 }
3621
3622 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3623 self.add_workspace_actions_listeners(div, cx)
3624 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3625 .on_action(cx.listener(Self::close_all_items_and_panes))
3626 .on_action(cx.listener(Self::save_all))
3627 .on_action(cx.listener(Self::send_keystrokes))
3628 .on_action(cx.listener(Self::add_folder_to_project))
3629 .on_action(cx.listener(Self::follow_next_collaborator))
3630 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3631 let pane = workspace.active_pane().clone();
3632 workspace.unfollow(&pane, cx);
3633 }))
3634 .on_action(cx.listener(|workspace, action: &Save, cx| {
3635 workspace
3636 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3637 .detach_and_log_err(cx);
3638 }))
3639 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
3640 workspace
3641 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
3642 .detach_and_log_err(cx);
3643 }))
3644 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3645 workspace
3646 .save_active_item(SaveIntent::SaveAs, cx)
3647 .detach_and_log_err(cx);
3648 }))
3649 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3650 workspace.activate_previous_pane(cx)
3651 }))
3652 .on_action(
3653 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3654 )
3655 .on_action(
3656 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3657 workspace.activate_pane_in_direction(action.0, cx)
3658 }),
3659 )
3660 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3661 workspace.swap_pane_in_direction(action.0, cx)
3662 }))
3663 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3664 this.toggle_dock(DockPosition::Left, cx);
3665 }))
3666 .on_action(
3667 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3668 workspace.toggle_dock(DockPosition::Right, cx);
3669 }),
3670 )
3671 .on_action(
3672 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3673 workspace.toggle_dock(DockPosition::Bottom, cx);
3674 }),
3675 )
3676 .on_action(
3677 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3678 workspace.close_all_docks(cx);
3679 }),
3680 )
3681 .on_action(cx.listener(Workspace::open))
3682 .on_action(cx.listener(Workspace::close_window))
3683 .on_action(cx.listener(Workspace::activate_pane_at_index))
3684 .on_action(
3685 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3686 workspace.reopen_closed_item(cx).detach();
3687 }),
3688 )
3689 }
3690
3691 #[cfg(any(test, feature = "test-support"))]
3692 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3693 use node_runtime::FakeNodeRuntime;
3694
3695 let client = project.read(cx).client();
3696 let user_store = project.read(cx).user_store();
3697
3698 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3699 cx.activate_window();
3700 let app_state = Arc::new(AppState {
3701 languages: project.read(cx).languages().clone(),
3702 workspace_store,
3703 client,
3704 user_store,
3705 fs: project.read(cx).fs().clone(),
3706 build_window_options: |_, _| Default::default(),
3707 node_runtime: FakeNodeRuntime::new(),
3708 });
3709 let workspace = Self::new(Default::default(), project, app_state, cx);
3710 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3711 workspace
3712 }
3713
3714 pub fn register_action<A: Action>(
3715 &mut self,
3716 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3717 ) -> &mut Self {
3718 let callback = Arc::new(callback);
3719
3720 self.workspace_actions.push(Box::new(move |div, cx| {
3721 let callback = callback.clone();
3722 div.on_action(
3723 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3724 )
3725 }));
3726 self
3727 }
3728
3729 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3730 let mut div = div
3731 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3732 .on_action(cx.listener(Self::close_all_items_and_panes))
3733 .on_action(cx.listener(Self::add_folder_to_project))
3734 .on_action(cx.listener(Self::save_all))
3735 .on_action(cx.listener(Self::open));
3736 for action in self.workspace_actions.iter() {
3737 div = (action)(div, cx)
3738 }
3739 div
3740 }
3741
3742 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3743 self.modal_layer.read(cx).has_active_modal()
3744 }
3745
3746 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3747 self.modal_layer.read(cx).active_modal()
3748 }
3749
3750 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3751 where
3752 B: FnOnce(&mut ViewContext<V>) -> V,
3753 {
3754 self.modal_layer
3755 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3756 }
3757}
3758
3759fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<Bounds<GlobalPixels>> {
3760 let display_origin = cx
3761 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3762 .ok()??;
3763 ZED_WINDOW_POSITION
3764 .zip(*ZED_WINDOW_SIZE)
3765 .map(|(position, size)| Bounds {
3766 origin: display_origin + position,
3767 size,
3768 })
3769}
3770
3771fn open_items(
3772 serialized_workspace: Option<SerializedWorkspace>,
3773 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3774 app_state: Arc<AppState>,
3775 cx: &mut ViewContext<Workspace>,
3776) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3777 let restored_items = serialized_workspace.map(|serialized_workspace| {
3778 Workspace::load_workspace(
3779 serialized_workspace,
3780 project_paths_to_open
3781 .iter()
3782 .map(|(_, project_path)| project_path)
3783 .cloned()
3784 .collect(),
3785 cx,
3786 )
3787 });
3788
3789 cx.spawn(|workspace, mut cx| async move {
3790 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3791
3792 if let Some(restored_items) = restored_items {
3793 let restored_items = restored_items.await?;
3794
3795 let restored_project_paths = restored_items
3796 .iter()
3797 .filter_map(|item| {
3798 cx.update(|cx| item.as_ref()?.project_path(cx))
3799 .ok()
3800 .flatten()
3801 })
3802 .collect::<HashSet<_>>();
3803
3804 for restored_item in restored_items {
3805 opened_items.push(restored_item.map(Ok));
3806 }
3807
3808 project_paths_to_open
3809 .iter_mut()
3810 .for_each(|(_, project_path)| {
3811 if let Some(project_path_to_open) = project_path {
3812 if restored_project_paths.contains(project_path_to_open) {
3813 *project_path = None;
3814 }
3815 }
3816 });
3817 } else {
3818 for _ in 0..project_paths_to_open.len() {
3819 opened_items.push(None);
3820 }
3821 }
3822 assert!(opened_items.len() == project_paths_to_open.len());
3823
3824 let tasks =
3825 project_paths_to_open
3826 .into_iter()
3827 .enumerate()
3828 .map(|(ix, (abs_path, project_path))| {
3829 let workspace = workspace.clone();
3830 cx.spawn(|mut cx| {
3831 let fs = app_state.fs.clone();
3832 async move {
3833 let file_project_path = project_path?;
3834 if fs.is_dir(&abs_path).await {
3835 None
3836 } else {
3837 Some((
3838 ix,
3839 workspace
3840 .update(&mut cx, |workspace, cx| {
3841 workspace.open_path(file_project_path, None, true, cx)
3842 })
3843 .log_err()?
3844 .await,
3845 ))
3846 }
3847 }
3848 })
3849 });
3850
3851 let tasks = tasks.collect::<Vec<_>>();
3852
3853 let tasks = futures::future::join_all(tasks);
3854 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
3855 opened_items[ix] = Some(path_open_result);
3856 }
3857
3858 Ok(opened_items)
3859 })
3860}
3861
3862enum ActivateInDirectionTarget {
3863 Pane(View<Pane>),
3864 Dock(View<Dock>),
3865}
3866
3867fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3868 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3869
3870 workspace
3871 .update(cx, |workspace, cx| {
3872 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3873 workspace.show_notification_once(0, cx, |cx| {
3874 cx.new_view(|_| {
3875 MessageNotification::new("Failed to load the database file.")
3876 .with_click_message("Click to let us know about this error")
3877 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3878 })
3879 });
3880 }
3881 })
3882 .log_err();
3883}
3884
3885impl FocusableView for Workspace {
3886 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3887 self.active_pane.focus_handle(cx)
3888 }
3889}
3890
3891#[derive(Clone, Render)]
3892struct DraggedDock(DockPosition);
3893
3894impl Render for Workspace {
3895 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3896 let mut context = KeyContext::default();
3897 context.add("Workspace");
3898
3899 let (ui_font, ui_font_size) = {
3900 let theme_settings = ThemeSettings::get_global(cx);
3901 (
3902 theme_settings.ui_font.family.clone(),
3903 theme_settings.ui_font_size,
3904 )
3905 };
3906
3907 let theme = cx.theme().clone();
3908 let colors = theme.colors();
3909 cx.set_rem_size(ui_font_size);
3910
3911 self.actions(div(), cx)
3912 .key_context(context)
3913 .relative()
3914 .size_full()
3915 .flex()
3916 .flex_col()
3917 .font(ui_font)
3918 .gap_0()
3919 .justify_start()
3920 .items_start()
3921 .text_color(colors.text)
3922 .bg(colors.background)
3923 .children(self.titlebar_item.clone())
3924 .child(
3925 div()
3926 .id("workspace")
3927 .relative()
3928 .flex_1()
3929 .w_full()
3930 .flex()
3931 .flex_col()
3932 .overflow_hidden()
3933 .border_t()
3934 .border_b()
3935 .border_color(colors.border)
3936 .child({
3937 let this = cx.view().clone();
3938 canvas(
3939 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
3940 |_, _, _| {},
3941 )
3942 .absolute()
3943 .size_full()
3944 })
3945 .on_drag_move(
3946 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3947 match e.drag(cx).0 {
3948 DockPosition::Left => {
3949 let size = workspace.bounds.left() + e.event.position.x;
3950 workspace.left_dock.update(cx, |left_dock, cx| {
3951 left_dock.resize_active_panel(Some(size), cx);
3952 });
3953 }
3954 DockPosition::Right => {
3955 let size = workspace.bounds.right() - e.event.position.x;
3956 workspace.right_dock.update(cx, |right_dock, cx| {
3957 right_dock.resize_active_panel(Some(size), cx);
3958 });
3959 }
3960 DockPosition::Bottom => {
3961 let size = workspace.bounds.bottom() - e.event.position.y;
3962 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3963 bottom_dock.resize_active_panel(Some(size), cx);
3964 });
3965 }
3966 }
3967 }),
3968 )
3969 .child(
3970 div()
3971 .flex()
3972 .flex_row()
3973 .h_full()
3974 // Left Dock
3975 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3976 || {
3977 div()
3978 .flex()
3979 .flex_none()
3980 .overflow_hidden()
3981 .child(self.left_dock.clone())
3982 },
3983 ))
3984 // Panes
3985 .child(
3986 div()
3987 .flex()
3988 .flex_col()
3989 .flex_1()
3990 .overflow_hidden()
3991 .child(self.center.render(
3992 &self.project,
3993 &self.follower_states,
3994 self.active_call(),
3995 &self.active_pane,
3996 self.zoomed.as_ref(),
3997 &self.app_state,
3998 cx,
3999 ))
4000 .children(
4001 self.zoomed_position
4002 .ne(&Some(DockPosition::Bottom))
4003 .then(|| self.bottom_dock.clone()),
4004 ),
4005 )
4006 // Right Dock
4007 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
4008 || {
4009 div()
4010 .flex()
4011 .flex_none()
4012 .overflow_hidden()
4013 .child(self.right_dock.clone())
4014 },
4015 )),
4016 )
4017 .children(self.zoomed.as_ref().and_then(|view| {
4018 let zoomed_view = view.upgrade()?;
4019 let div = div()
4020 .occlude()
4021 .absolute()
4022 .overflow_hidden()
4023 .border_color(colors.border)
4024 .bg(colors.background)
4025 .child(zoomed_view)
4026 .inset_0()
4027 .shadow_lg();
4028
4029 Some(match self.zoomed_position {
4030 Some(DockPosition::Left) => div.right_2().border_r(),
4031 Some(DockPosition::Right) => div.left_2().border_l(),
4032 Some(DockPosition::Bottom) => div.top_2().border_t(),
4033 None => div.top_2().bottom_2().left_2().right_2().border(),
4034 })
4035 }))
4036 .child(self.modal_layer.clone())
4037 .children(self.render_notifications(cx)),
4038 )
4039 .child(self.status_bar.clone())
4040 .children(if self.project.read(cx).is_disconnected() {
4041 Some(DisconnectedOverlay)
4042 } else {
4043 None
4044 })
4045 }
4046}
4047
4048impl WorkspaceStore {
4049 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4050 Self {
4051 workspaces: Default::default(),
4052 _subscriptions: vec![
4053 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4054 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4055 ],
4056 client,
4057 }
4058 }
4059
4060 pub fn update_followers(
4061 &self,
4062 project_id: Option<u64>,
4063 update: proto::update_followers::Variant,
4064 cx: &AppContext,
4065 ) -> Option<()> {
4066 let active_call = ActiveCall::try_global(cx)?;
4067 let room_id = active_call.read(cx).room()?.read(cx).id();
4068 self.client
4069 .send(proto::UpdateFollowers {
4070 room_id,
4071 project_id,
4072 variant: Some(update),
4073 })
4074 .log_err()
4075 }
4076
4077 pub async fn handle_follow(
4078 this: Model<Self>,
4079 envelope: TypedEnvelope<proto::Follow>,
4080 _: Arc<Client>,
4081 mut cx: AsyncAppContext,
4082 ) -> Result<proto::FollowResponse> {
4083 this.update(&mut cx, |this, cx| {
4084 let follower = Follower {
4085 project_id: envelope.payload.project_id,
4086 peer_id: envelope.original_sender_id()?,
4087 };
4088
4089 let mut response = proto::FollowResponse::default();
4090 this.workspaces.retain(|workspace| {
4091 workspace
4092 .update(cx, |workspace, cx| {
4093 let handler_response = workspace.handle_follow(follower.project_id, cx);
4094 if response.views.is_empty() {
4095 response.views = handler_response.views;
4096 } else {
4097 response.views.extend_from_slice(&handler_response.views);
4098 }
4099
4100 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4101 if response.active_view_id.is_none()
4102 || workspace.project.read(cx).remote_id() == follower.project_id
4103 {
4104 response.active_view_id = Some(active_view_id);
4105 }
4106 }
4107
4108 if let Some(active_view) = handler_response.active_view.clone() {
4109 if response.active_view_id.is_none()
4110 || workspace.project.read(cx).remote_id() == follower.project_id
4111 {
4112 response.active_view = Some(active_view)
4113 }
4114 }
4115 })
4116 .is_ok()
4117 });
4118
4119 Ok(response)
4120 })?
4121 }
4122
4123 async fn handle_update_followers(
4124 this: Model<Self>,
4125 envelope: TypedEnvelope<proto::UpdateFollowers>,
4126 _: Arc<Client>,
4127 mut cx: AsyncAppContext,
4128 ) -> Result<()> {
4129 let leader_id = envelope.original_sender_id()?;
4130 let update = envelope.payload;
4131
4132 this.update(&mut cx, |this, cx| {
4133 this.workspaces.retain(|workspace| {
4134 workspace
4135 .update(cx, |workspace, cx| {
4136 let project_id = workspace.project.read(cx).remote_id();
4137 if update.project_id != project_id && update.project_id.is_some() {
4138 return;
4139 }
4140 workspace.handle_update_followers(leader_id, update.clone(), cx);
4141 })
4142 .is_ok()
4143 });
4144 Ok(())
4145 })?
4146 }
4147}
4148
4149impl ViewId {
4150 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4151 Ok(Self {
4152 creator: message
4153 .creator
4154 .ok_or_else(|| anyhow!("creator is missing"))?,
4155 id: message.id,
4156 })
4157 }
4158
4159 pub(crate) fn to_proto(&self) -> proto::ViewId {
4160 proto::ViewId {
4161 creator: Some(self.creator),
4162 id: self.id,
4163 }
4164 }
4165}
4166
4167pub trait WorkspaceHandle {
4168 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4169}
4170
4171impl WorkspaceHandle for View<Workspace> {
4172 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4173 self.read(cx)
4174 .worktrees(cx)
4175 .flat_map(|worktree| {
4176 let worktree_id = worktree.read(cx).id();
4177 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4178 worktree_id,
4179 path: f.path.clone(),
4180 })
4181 })
4182 .collect::<Vec<_>>()
4183 }
4184}
4185
4186impl std::fmt::Debug for OpenPaths {
4187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4188 f.debug_struct("OpenPaths")
4189 .field("paths", &self.paths)
4190 .finish()
4191 }
4192}
4193
4194pub fn activate_workspace_for_project(
4195 cx: &mut AppContext,
4196 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4197) -> Option<WindowHandle<Workspace>> {
4198 for window in cx.windows() {
4199 let Some(workspace) = window.downcast::<Workspace>() else {
4200 continue;
4201 };
4202
4203 let predicate = workspace
4204 .update(cx, |workspace, cx| {
4205 let project = workspace.project.read(cx);
4206 if predicate(project, cx) {
4207 cx.activate_window();
4208 true
4209 } else {
4210 false
4211 }
4212 })
4213 .log_err()
4214 .unwrap_or(false);
4215
4216 if predicate {
4217 return Some(workspace);
4218 }
4219 }
4220
4221 None
4222}
4223
4224pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4225 DB.last_workspace().await.log_err().flatten()
4226}
4227
4228actions!(collab, [OpenChannelNotes]);
4229actions!(zed, [OpenLog]);
4230
4231async fn join_channel_internal(
4232 channel_id: ChannelId,
4233 app_state: &Arc<AppState>,
4234 requesting_window: Option<WindowHandle<Workspace>>,
4235 active_call: &Model<ActiveCall>,
4236 cx: &mut AsyncAppContext,
4237) -> Result<bool> {
4238 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4239 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4240 return (false, None);
4241 };
4242
4243 let already_in_channel = room.channel_id() == Some(channel_id);
4244 let should_prompt = room.is_sharing_project()
4245 && room.remote_participants().len() > 0
4246 && !already_in_channel;
4247 let open_room = if already_in_channel {
4248 active_call.room().cloned()
4249 } else {
4250 None
4251 };
4252 (should_prompt, open_room)
4253 })?;
4254
4255 if let Some(room) = open_room {
4256 let task = room.update(cx, |room, cx| {
4257 if let Some((project, host)) = room.most_active_project(cx) {
4258 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4259 }
4260
4261 None
4262 })?;
4263 if let Some(task) = task {
4264 task.await?;
4265 }
4266 return anyhow::Ok(true);
4267 }
4268
4269 if should_prompt {
4270 if let Some(workspace) = requesting_window {
4271 let answer = workspace
4272 .update(cx, |_, cx| {
4273 cx.prompt(
4274 PromptLevel::Warning,
4275 "Do you want to switch channels?",
4276 Some("Leaving this call will unshare your current project."),
4277 &["Yes, Join Channel", "Cancel"],
4278 )
4279 })?
4280 .await;
4281
4282 if answer == Ok(1) {
4283 return Ok(false);
4284 }
4285 } else {
4286 return Ok(false); // unreachable!() hopefully
4287 }
4288 }
4289
4290 let client = cx.update(|cx| active_call.read(cx).client())?;
4291
4292 let mut client_status = client.status();
4293
4294 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4295 'outer: loop {
4296 let Some(status) = client_status.recv().await else {
4297 return Err(anyhow!("error connecting"));
4298 };
4299
4300 match status {
4301 Status::Connecting
4302 | Status::Authenticating
4303 | Status::Reconnecting
4304 | Status::Reauthenticating => continue,
4305 Status::Connected { .. } => break 'outer,
4306 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4307 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4308 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4309 return Err(ErrorCode::Disconnected.into());
4310 }
4311 }
4312 }
4313
4314 let room = active_call
4315 .update(cx, |active_call, cx| {
4316 active_call.join_channel(channel_id, cx)
4317 })?
4318 .await?;
4319
4320 let Some(room) = room else {
4321 return anyhow::Ok(true);
4322 };
4323
4324 room.update(cx, |room, _| room.room_update_completed())?
4325 .await;
4326
4327 let task = room.update(cx, |room, cx| {
4328 if let Some((project, host)) = room.most_active_project(cx) {
4329 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4330 }
4331
4332 // if you are the first to join a channel, share your project
4333 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4334 if let Some(workspace) = requesting_window {
4335 let project = workspace.update(cx, |workspace, cx| {
4336 if !CallSettings::get_global(cx).share_on_join {
4337 return None;
4338 }
4339 let project = workspace.project.read(cx);
4340 if project.is_local()
4341 && project.visible_worktrees(cx).any(|tree| {
4342 tree.read(cx)
4343 .root_entry()
4344 .map_or(false, |entry| entry.is_dir())
4345 })
4346 {
4347 Some(workspace.project.clone())
4348 } else {
4349 None
4350 }
4351 });
4352 if let Ok(Some(project)) = project {
4353 return Some(cx.spawn(|room, mut cx| async move {
4354 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4355 .await?;
4356 Ok(())
4357 }));
4358 }
4359 }
4360 }
4361
4362 None
4363 })?;
4364 if let Some(task) = task {
4365 task.await?;
4366 return anyhow::Ok(true);
4367 }
4368 anyhow::Ok(false)
4369}
4370
4371pub fn join_channel(
4372 channel_id: ChannelId,
4373 app_state: Arc<AppState>,
4374 requesting_window: Option<WindowHandle<Workspace>>,
4375 cx: &mut AppContext,
4376) -> Task<Result<()>> {
4377 let active_call = ActiveCall::global(cx);
4378 cx.spawn(|mut cx| async move {
4379 let result = join_channel_internal(
4380 channel_id,
4381 &app_state,
4382 requesting_window,
4383 &active_call,
4384 &mut cx,
4385 )
4386 .await;
4387
4388 // join channel succeeded, and opened a window
4389 if matches!(result, Ok(true)) {
4390 return anyhow::Ok(());
4391 }
4392
4393 // find an existing workspace to focus and show call controls
4394 let mut active_window =
4395 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4396 if active_window.is_none() {
4397 // no open workspaces, make one to show the error in (blergh)
4398 let (window_handle, _) = cx
4399 .update(|cx| {
4400 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4401 })?
4402 .await?;
4403
4404 if result.is_ok() {
4405 cx.update(|cx| {
4406 cx.dispatch_action(&OpenChannelNotes);
4407 }).log_err();
4408 }
4409
4410 active_window = Some(window_handle);
4411 }
4412
4413 if let Err(err) = result {
4414 log::error!("failed to join channel: {}", err);
4415 if let Some(active_window) = active_window {
4416 active_window
4417 .update(&mut cx, |_, cx| {
4418 let detail: SharedString = match err.error_code() {
4419 ErrorCode::SignedOut => {
4420 "Please sign in to continue.".into()
4421 }
4422 ErrorCode::UpgradeRequired => {
4423 "Your are running an unsupported version of Zed. Please update to continue.".into()
4424 }
4425 ErrorCode::NoSuchChannel => {
4426 "No matching channel was found. Please check the link and try again.".into()
4427 }
4428 ErrorCode::Forbidden => {
4429 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4430 }
4431 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4432 _ => format!("{}\n\nPlease try again.", err).into(),
4433 };
4434 cx.prompt(
4435 PromptLevel::Critical,
4436 "Failed to join channel",
4437 Some(&detail),
4438 &["Ok"],
4439 )
4440 })?
4441 .await
4442 .ok();
4443 }
4444 }
4445
4446 // return ok, we showed the error to the user.
4447 return anyhow::Ok(());
4448 })
4449}
4450
4451pub async fn get_any_active_workspace(
4452 app_state: Arc<AppState>,
4453 mut cx: AsyncAppContext,
4454) -> anyhow::Result<WindowHandle<Workspace>> {
4455 // find an existing workspace to focus and show call controls
4456 let active_window = activate_any_workspace_window(&mut cx);
4457 if active_window.is_none() {
4458 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4459 .await?;
4460 }
4461 activate_any_workspace_window(&mut cx).context("could not open zed")
4462}
4463
4464fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4465 cx.update(|cx| {
4466 if let Some(workspace_window) = cx
4467 .active_window()
4468 .and_then(|window| window.downcast::<Workspace>())
4469 {
4470 return Some(workspace_window);
4471 }
4472
4473 for window in cx.windows() {
4474 if let Some(workspace_window) = window.downcast::<Workspace>() {
4475 workspace_window
4476 .update(cx, |_, cx| cx.activate_window())
4477 .ok();
4478 return Some(workspace_window);
4479 }
4480 }
4481 None
4482 })
4483 .ok()
4484 .flatten()
4485}
4486
4487fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
4488 cx.windows()
4489 .into_iter()
4490 .filter_map(|window| window.downcast::<Workspace>())
4491 .filter(|workspace| {
4492 workspace
4493 .read(cx)
4494 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
4495 })
4496 .collect()
4497}
4498
4499#[derive(Default)]
4500pub struct OpenOptions {
4501 pub open_new_workspace: Option<bool>,
4502 pub replace_window: Option<WindowHandle<Workspace>>,
4503}
4504
4505#[allow(clippy::type_complexity)]
4506pub fn open_paths(
4507 abs_paths: &[PathBuf],
4508 app_state: Arc<AppState>,
4509 open_options: OpenOptions,
4510 cx: &mut AppContext,
4511) -> Task<
4512 anyhow::Result<(
4513 WindowHandle<Workspace>,
4514 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4515 )>,
4516> {
4517 let abs_paths = abs_paths.to_vec();
4518 let mut existing = None;
4519 let mut best_match = None;
4520 let mut open_visible = OpenVisible::All;
4521
4522 if open_options.open_new_workspace != Some(true) {
4523 for window in local_workspace_windows(cx) {
4524 if let Ok(workspace) = window.read(cx) {
4525 let m = workspace
4526 .project
4527 .read(cx)
4528 .visibility_for_paths(&abs_paths, cx);
4529 if m > best_match {
4530 existing = Some(window);
4531 best_match = m;
4532 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
4533 existing = Some(window)
4534 }
4535 }
4536 }
4537 }
4538
4539 cx.spawn(move |mut cx| async move {
4540 if open_options.open_new_workspace.is_none() && existing.is_none() {
4541 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
4542 if futures::future::join_all(all_files)
4543 .await
4544 .into_iter()
4545 .filter_map(|result| result.ok().flatten())
4546 .all(|file| !file.is_dir)
4547 {
4548 cx.update(|cx| {
4549 for window in local_workspace_windows(cx) {
4550 if let Ok(workspace) = window.read(cx) {
4551 let project = workspace.project().read(cx);
4552 if project.is_remote() {
4553 continue;
4554 }
4555 existing = Some(window);
4556 open_visible = OpenVisible::None;
4557 break;
4558 }
4559 }
4560 })?;
4561 }
4562 }
4563
4564 if let Some(existing) = existing {
4565 Ok((
4566 existing,
4567 existing
4568 .update(&mut cx, |workspace, cx| {
4569 cx.activate_window();
4570 workspace.open_paths(abs_paths, open_visible, None, cx)
4571 })?
4572 .await,
4573 ))
4574 } else {
4575 cx.update(move |cx| {
4576 Workspace::new_local(
4577 abs_paths,
4578 app_state.clone(),
4579 open_options.replace_window,
4580 cx,
4581 )
4582 })?
4583 .await
4584 }
4585 })
4586}
4587
4588pub fn open_new(
4589 app_state: Arc<AppState>,
4590 cx: &mut AppContext,
4591 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4592) -> Task<()> {
4593 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
4594 cx.spawn(|mut cx| async move {
4595 if let Some((workspace, opened_paths)) = task.await.log_err() {
4596 workspace
4597 .update(&mut cx, |workspace, cx| {
4598 if opened_paths.is_empty() {
4599 init(workspace, cx)
4600 }
4601 })
4602 .log_err();
4603 }
4604 })
4605}
4606
4607pub fn create_and_open_local_file(
4608 path: &'static Path,
4609 cx: &mut ViewContext<Workspace>,
4610 default_content: impl 'static + Send + FnOnce() -> Rope,
4611) -> Task<Result<Box<dyn ItemHandle>>> {
4612 cx.spawn(|workspace, mut cx| async move {
4613 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4614 if !fs.is_file(path).await {
4615 fs.create_file(path, Default::default()).await?;
4616 fs.save(path, &default_content(), Default::default())
4617 .await?;
4618 }
4619
4620 let mut items = workspace
4621 .update(&mut cx, |workspace, cx| {
4622 workspace.with_local_workspace(cx, |workspace, cx| {
4623 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4624 })
4625 })?
4626 .await?
4627 .await;
4628
4629 let item = items.pop().flatten();
4630 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4631 })
4632}
4633
4634pub fn join_hosted_project(
4635 hosted_project_id: ProjectId,
4636 app_state: Arc<AppState>,
4637 cx: &mut AppContext,
4638) -> Task<Result<()>> {
4639 cx.spawn(|mut cx| async move {
4640 let existing_window = cx.update(|cx| {
4641 cx.windows().into_iter().find_map(|window| {
4642 let workspace = window.downcast::<Workspace>()?;
4643 workspace
4644 .read(cx)
4645 .is_ok_and(|workspace| {
4646 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
4647 })
4648 .then(|| workspace)
4649 })
4650 })?;
4651
4652 let workspace = if let Some(existing_window) = existing_window {
4653 existing_window
4654 } else {
4655 let project = Project::hosted(
4656 hosted_project_id,
4657 app_state.user_store.clone(),
4658 app_state.client.clone(),
4659 app_state.languages.clone(),
4660 app_state.fs.clone(),
4661 cx.clone(),
4662 )
4663 .await?;
4664
4665 let window_bounds_override = window_bounds_env_override(&cx);
4666 cx.update(|cx| {
4667 let mut options = (app_state.build_window_options)(None, cx);
4668 options.bounds = window_bounds_override;
4669 cx.open_window(options, |cx| {
4670 cx.new_view(|cx| {
4671 Workspace::new(Default::default(), project, app_state.clone(), cx)
4672 })
4673 })
4674 })?
4675 };
4676
4677 workspace.update(&mut cx, |_, cx| {
4678 cx.activate(true);
4679 cx.activate_window();
4680 })?;
4681
4682 Ok(())
4683 })
4684}
4685
4686pub fn join_in_room_project(
4687 project_id: u64,
4688 follow_user_id: u64,
4689 app_state: Arc<AppState>,
4690 cx: &mut AppContext,
4691) -> Task<Result<()>> {
4692 let windows = cx.windows();
4693 cx.spawn(|mut cx| async move {
4694 let existing_workspace = windows.into_iter().find_map(|window| {
4695 window.downcast::<Workspace>().and_then(|window| {
4696 window
4697 .update(&mut cx, |workspace, cx| {
4698 if workspace.project().read(cx).remote_id() == Some(project_id) {
4699 Some(window)
4700 } else {
4701 None
4702 }
4703 })
4704 .unwrap_or(None)
4705 })
4706 });
4707
4708 let workspace = if let Some(existing_workspace) = existing_workspace {
4709 existing_workspace
4710 } else {
4711 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4712 let room = active_call
4713 .read_with(&cx, |call, _| call.room().cloned())?
4714 .ok_or_else(|| anyhow!("not in a call"))?;
4715 let project = room
4716 .update(&mut cx, |room, cx| {
4717 room.join_project(
4718 project_id,
4719 app_state.languages.clone(),
4720 app_state.fs.clone(),
4721 cx,
4722 )
4723 })?
4724 .await?;
4725
4726 let window_bounds_override = window_bounds_env_override(&cx);
4727 cx.update(|cx| {
4728 let mut options = (app_state.build_window_options)(None, cx);
4729 options.bounds = window_bounds_override;
4730 cx.open_window(options, |cx| {
4731 cx.new_view(|cx| {
4732 Workspace::new(Default::default(), project, app_state.clone(), cx)
4733 })
4734 })
4735 })?
4736 };
4737
4738 workspace.update(&mut cx, |workspace, cx| {
4739 cx.activate(true);
4740 cx.activate_window();
4741
4742 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4743 let follow_peer_id = room
4744 .read(cx)
4745 .remote_participants()
4746 .iter()
4747 .find(|(_, participant)| participant.user.id == follow_user_id)
4748 .map(|(_, p)| p.peer_id)
4749 .or_else(|| {
4750 // If we couldn't follow the given user, follow the host instead.
4751 let collaborator = workspace
4752 .project()
4753 .read(cx)
4754 .collaborators()
4755 .values()
4756 .find(|collaborator| collaborator.replica_id == 0)?;
4757 Some(collaborator.peer_id)
4758 });
4759
4760 if let Some(follow_peer_id) = follow_peer_id {
4761 workspace.follow(follow_peer_id, cx);
4762 }
4763 }
4764 })?;
4765
4766 anyhow::Ok(())
4767 })
4768}
4769
4770pub fn restart(_: &Restart, cx: &mut AppContext) {
4771 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4772 let mut workspace_windows = cx
4773 .windows()
4774 .into_iter()
4775 .filter_map(|window| window.downcast::<Workspace>())
4776 .collect::<Vec<_>>();
4777
4778 // If multiple windows have unsaved changes, and need a save prompt,
4779 // prompt in the active window before switching to a different window.
4780 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4781
4782 let mut prompt = None;
4783 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4784 prompt = window
4785 .update(cx, |_, cx| {
4786 cx.prompt(
4787 PromptLevel::Info,
4788 "Are you sure you want to restart?",
4789 None,
4790 &["Restart", "Cancel"],
4791 )
4792 })
4793 .ok();
4794 }
4795
4796 cx.spawn(|mut cx| async move {
4797 if let Some(prompt) = prompt {
4798 let answer = prompt.await?;
4799 if answer != 0 {
4800 return Ok(());
4801 }
4802 }
4803
4804 // If the user cancels any save prompt, then keep the app open.
4805 for window in workspace_windows {
4806 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4807 workspace.prepare_to_close(true, cx)
4808 }) {
4809 if !should_close.await? {
4810 return Ok(());
4811 }
4812 }
4813 }
4814
4815 cx.update(|cx| cx.restart())
4816 })
4817 .detach_and_log_err(cx);
4818}
4819
4820fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4821 let mut parts = value.split(',');
4822 let x: usize = parts.next()?.parse().ok()?;
4823 let y: usize = parts.next()?.parse().ok()?;
4824 Some(point((x as f64).into(), (y as f64).into()))
4825}
4826
4827fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4828 let mut parts = value.split(',');
4829 let width: usize = parts.next()?.parse().ok()?;
4830 let height: usize = parts.next()?.parse().ok()?;
4831 Some(size((width as f64).into(), (height as f64).into()))
4832}
4833
4834struct DisconnectedOverlay;
4835
4836impl Element for DisconnectedOverlay {
4837 type BeforeLayout = AnyElement;
4838 type AfterLayout = ();
4839
4840 fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
4841 let mut background = cx.theme().colors().elevated_surface_background;
4842 background.fade_out(0.2);
4843 let mut overlay = div()
4844 .bg(background)
4845 .absolute()
4846 .left_0()
4847 .top(ui::TitleBar::height(cx))
4848 .size_full()
4849 .flex()
4850 .items_center()
4851 .justify_center()
4852 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4853 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4854 .child(Label::new(
4855 "Your connection to the remote project has been lost.",
4856 ))
4857 .into_any();
4858 (overlay.before_layout(cx), overlay)
4859 }
4860
4861 fn after_layout(
4862 &mut self,
4863 bounds: Bounds<Pixels>,
4864 overlay: &mut Self::BeforeLayout,
4865 cx: &mut ElementContext,
4866 ) {
4867 cx.insert_hitbox(bounds, true);
4868 overlay.after_layout(cx);
4869 }
4870
4871 fn paint(
4872 &mut self,
4873 _: Bounds<Pixels>,
4874 overlay: &mut Self::BeforeLayout,
4875 _: &mut Self::AfterLayout,
4876 cx: &mut ElementContext,
4877 ) {
4878 overlay.paint(cx)
4879 }
4880}
4881
4882impl IntoElement for DisconnectedOverlay {
4883 type Element = Self;
4884
4885 fn into_element(self) -> Self::Element {
4886 self
4887 }
4888}
4889
4890#[cfg(test)]
4891mod tests {
4892 use std::{cell::RefCell, rc::Rc};
4893
4894 use super::*;
4895 use crate::{
4896 dock::{test::TestPanel, PanelEvent},
4897 item::{
4898 test::{TestItem, TestProjectItem},
4899 ItemEvent,
4900 },
4901 };
4902 use fs::FakeFs;
4903 use gpui::{
4904 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
4905 VisualTestContext,
4906 };
4907 use project::{Project, ProjectEntryId};
4908 use serde_json::json;
4909 use settings::SettingsStore;
4910
4911 #[gpui::test]
4912 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4913 init_test(cx);
4914
4915 let fs = FakeFs::new(cx.executor());
4916 let project = Project::test(fs, [], cx).await;
4917 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4918
4919 // Adding an item with no ambiguity renders the tab without detail.
4920 let item1 = cx.new_view(|cx| {
4921 let mut item = TestItem::new(cx);
4922 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4923 item
4924 });
4925 workspace.update(cx, |workspace, cx| {
4926 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
4927 });
4928 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4929
4930 // Adding an item that creates ambiguity increases the level of detail on
4931 // both tabs.
4932 let item2 = cx.new_view(|cx| {
4933 let mut item = TestItem::new(cx);
4934 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4935 item
4936 });
4937 workspace.update(cx, |workspace, cx| {
4938 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
4939 });
4940 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4941 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4942
4943 // Adding an item that creates ambiguity increases the level of detail only
4944 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4945 // we stop at the highest detail available.
4946 let item3 = cx.new_view(|cx| {
4947 let mut item = TestItem::new(cx);
4948 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4949 item
4950 });
4951 workspace.update(cx, |workspace, cx| {
4952 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
4953 });
4954 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4955 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4956 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4957 }
4958
4959 #[gpui::test]
4960 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4961 init_test(cx);
4962
4963 let fs = FakeFs::new(cx.executor());
4964 fs.insert_tree(
4965 "/root1",
4966 json!({
4967 "one.txt": "",
4968 "two.txt": "",
4969 }),
4970 )
4971 .await;
4972 fs.insert_tree(
4973 "/root2",
4974 json!({
4975 "three.txt": "",
4976 }),
4977 )
4978 .await;
4979
4980 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4981 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4982 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4983 let worktree_id = project.update(cx, |project, cx| {
4984 project.worktrees().next().unwrap().read(cx).id()
4985 });
4986
4987 let item1 = cx.new_view(|cx| {
4988 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4989 });
4990 let item2 = cx.new_view(|cx| {
4991 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4992 });
4993
4994 // Add an item to an empty pane
4995 workspace.update(cx, |workspace, cx| {
4996 workspace.add_item_to_active_pane(Box::new(item1), cx)
4997 });
4998 project.update(cx, |project, cx| {
4999 assert_eq!(
5000 project.active_entry(),
5001 project
5002 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5003 .map(|e| e.id)
5004 );
5005 });
5006 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5007
5008 // Add a second item to a non-empty pane
5009 workspace.update(cx, |workspace, cx| {
5010 workspace.add_item_to_active_pane(Box::new(item2), cx)
5011 });
5012 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5013 project.update(cx, |project, cx| {
5014 assert_eq!(
5015 project.active_entry(),
5016 project
5017 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5018 .map(|e| e.id)
5019 );
5020 });
5021
5022 // Close the active item
5023 pane.update(cx, |pane, cx| {
5024 pane.close_active_item(&Default::default(), cx).unwrap()
5025 })
5026 .await
5027 .unwrap();
5028 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5029 project.update(cx, |project, cx| {
5030 assert_eq!(
5031 project.active_entry(),
5032 project
5033 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5034 .map(|e| e.id)
5035 );
5036 });
5037
5038 // Add a project folder
5039 project
5040 .update(cx, |project, cx| {
5041 project.find_or_create_local_worktree("/root2", true, cx)
5042 })
5043 .await
5044 .unwrap();
5045 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5046
5047 // Remove a project folder
5048 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5049 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5050 }
5051
5052 #[gpui::test]
5053 async fn test_close_window(cx: &mut TestAppContext) {
5054 init_test(cx);
5055
5056 let fs = FakeFs::new(cx.executor());
5057 fs.insert_tree("/root", json!({ "one": "" })).await;
5058
5059 let project = Project::test(fs, ["root".as_ref()], cx).await;
5060 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5061
5062 // When there are no dirty items, there's nothing to do.
5063 let item1 = cx.new_view(|cx| TestItem::new(cx));
5064 workspace.update(cx, |w, cx| {
5065 w.add_item_to_active_pane(Box::new(item1.clone()), cx)
5066 });
5067 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5068 assert!(task.await.unwrap());
5069
5070 // When there are dirty untitled items, prompt to save each one. If the user
5071 // cancels any prompt, then abort.
5072 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5073 let item3 = cx.new_view(|cx| {
5074 TestItem::new(cx)
5075 .with_dirty(true)
5076 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5077 });
5078 workspace.update(cx, |w, cx| {
5079 w.add_item_to_active_pane(Box::new(item2.clone()), cx);
5080 w.add_item_to_active_pane(Box::new(item3.clone()), cx);
5081 });
5082 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5083 cx.executor().run_until_parked();
5084 cx.simulate_prompt_answer(2); // cancel save all
5085 cx.executor().run_until_parked();
5086 cx.simulate_prompt_answer(2); // cancel save all
5087 cx.executor().run_until_parked();
5088 assert!(!cx.has_pending_prompt());
5089 assert!(!task.await.unwrap());
5090 }
5091
5092 #[gpui::test]
5093 async fn test_close_pane_items(cx: &mut TestAppContext) {
5094 init_test(cx);
5095
5096 let fs = FakeFs::new(cx.executor());
5097
5098 let project = Project::test(fs, None, cx).await;
5099 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5100
5101 let item1 = cx.new_view(|cx| {
5102 TestItem::new(cx)
5103 .with_dirty(true)
5104 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5105 });
5106 let item2 = cx.new_view(|cx| {
5107 TestItem::new(cx)
5108 .with_dirty(true)
5109 .with_conflict(true)
5110 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5111 });
5112 let item3 = cx.new_view(|cx| {
5113 TestItem::new(cx)
5114 .with_dirty(true)
5115 .with_conflict(true)
5116 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5117 });
5118 let item4 = cx.new_view(|cx| {
5119 TestItem::new(cx)
5120 .with_dirty(true)
5121 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5122 });
5123 let pane = workspace.update(cx, |workspace, cx| {
5124 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
5125 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
5126 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
5127 workspace.add_item_to_active_pane(Box::new(item4.clone()), cx);
5128 workspace.active_pane().clone()
5129 });
5130
5131 let close_items = pane.update(cx, |pane, cx| {
5132 pane.activate_item(1, true, true, cx);
5133 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5134 let item1_id = item1.item_id();
5135 let item3_id = item3.item_id();
5136 let item4_id = item4.item_id();
5137 pane.close_items(cx, SaveIntent::Close, move |id| {
5138 [item1_id, item3_id, item4_id].contains(&id)
5139 })
5140 });
5141 cx.executor().run_until_parked();
5142
5143 assert!(cx.has_pending_prompt());
5144 // Ignore "Save all" prompt
5145 cx.simulate_prompt_answer(2);
5146 cx.executor().run_until_parked();
5147 // There's a prompt to save item 1.
5148 pane.update(cx, |pane, _| {
5149 assert_eq!(pane.items_len(), 4);
5150 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
5151 });
5152 // Confirm saving item 1.
5153 cx.simulate_prompt_answer(0);
5154 cx.executor().run_until_parked();
5155
5156 // Item 1 is saved. There's a prompt to save item 3.
5157 pane.update(cx, |pane, cx| {
5158 assert_eq!(item1.read(cx).save_count, 1);
5159 assert_eq!(item1.read(cx).save_as_count, 0);
5160 assert_eq!(item1.read(cx).reload_count, 0);
5161 assert_eq!(pane.items_len(), 3);
5162 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
5163 });
5164 assert!(cx.has_pending_prompt());
5165
5166 // Cancel saving item 3.
5167 cx.simulate_prompt_answer(1);
5168 cx.executor().run_until_parked();
5169
5170 // Item 3 is reloaded. There's a prompt to save item 4.
5171 pane.update(cx, |pane, cx| {
5172 assert_eq!(item3.read(cx).save_count, 0);
5173 assert_eq!(item3.read(cx).save_as_count, 0);
5174 assert_eq!(item3.read(cx).reload_count, 1);
5175 assert_eq!(pane.items_len(), 2);
5176 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
5177 });
5178 assert!(cx.has_pending_prompt());
5179
5180 // Confirm saving item 4.
5181 cx.simulate_prompt_answer(0);
5182 cx.executor().run_until_parked();
5183
5184 // There's a prompt for a path for item 4.
5185 cx.simulate_new_path_selection(|_| Some(Default::default()));
5186 close_items.await.unwrap();
5187
5188 // The requested items are closed.
5189 pane.update(cx, |pane, cx| {
5190 assert_eq!(item4.read(cx).save_count, 0);
5191 assert_eq!(item4.read(cx).save_as_count, 1);
5192 assert_eq!(item4.read(cx).reload_count, 0);
5193 assert_eq!(pane.items_len(), 1);
5194 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5195 });
5196 }
5197
5198 #[gpui::test]
5199 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
5200 init_test(cx);
5201
5202 let fs = FakeFs::new(cx.executor());
5203 let project = Project::test(fs, [], cx).await;
5204 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5205
5206 // Create several workspace items with single project entries, and two
5207 // workspace items with multiple project entries.
5208 let single_entry_items = (0..=4)
5209 .map(|project_entry_id| {
5210 cx.new_view(|cx| {
5211 TestItem::new(cx)
5212 .with_dirty(true)
5213 .with_project_items(&[TestProjectItem::new(
5214 project_entry_id,
5215 &format!("{project_entry_id}.txt"),
5216 cx,
5217 )])
5218 })
5219 })
5220 .collect::<Vec<_>>();
5221 let item_2_3 = cx.new_view(|cx| {
5222 TestItem::new(cx)
5223 .with_dirty(true)
5224 .with_singleton(false)
5225 .with_project_items(&[
5226 single_entry_items[2].read(cx).project_items[0].clone(),
5227 single_entry_items[3].read(cx).project_items[0].clone(),
5228 ])
5229 });
5230 let item_3_4 = cx.new_view(|cx| {
5231 TestItem::new(cx)
5232 .with_dirty(true)
5233 .with_singleton(false)
5234 .with_project_items(&[
5235 single_entry_items[3].read(cx).project_items[0].clone(),
5236 single_entry_items[4].read(cx).project_items[0].clone(),
5237 ])
5238 });
5239
5240 // Create two panes that contain the following project entries:
5241 // left pane:
5242 // multi-entry items: (2, 3)
5243 // single-entry items: 0, 1, 2, 3, 4
5244 // right pane:
5245 // single-entry items: 1
5246 // multi-entry items: (3, 4)
5247 let left_pane = workspace.update(cx, |workspace, cx| {
5248 let left_pane = workspace.active_pane().clone();
5249 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), cx);
5250 for item in single_entry_items {
5251 workspace.add_item_to_active_pane(Box::new(item), cx);
5252 }
5253 left_pane.update(cx, |pane, cx| {
5254 pane.activate_item(2, true, true, cx);
5255 });
5256
5257 let right_pane = workspace
5258 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5259 .unwrap();
5260
5261 right_pane.update(cx, |pane, cx| {
5262 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5263 });
5264
5265 left_pane
5266 });
5267
5268 cx.focus_view(&left_pane);
5269
5270 // When closing all of the items in the left pane, we should be prompted twice:
5271 // once for project entry 0, and once for project entry 2. Project entries 1,
5272 // 3, and 4 are all still open in the other paten. After those two
5273 // prompts, the task should complete.
5274
5275 let close = left_pane.update(cx, |pane, cx| {
5276 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5277 });
5278 cx.executor().run_until_parked();
5279
5280 // Discard "Save all" prompt
5281 cx.simulate_prompt_answer(2);
5282
5283 cx.executor().run_until_parked();
5284 left_pane.update(cx, |pane, cx| {
5285 assert_eq!(
5286 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5287 &[ProjectEntryId::from_proto(0)]
5288 );
5289 });
5290 cx.simulate_prompt_answer(0);
5291
5292 cx.executor().run_until_parked();
5293 left_pane.update(cx, |pane, cx| {
5294 assert_eq!(
5295 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5296 &[ProjectEntryId::from_proto(2)]
5297 );
5298 });
5299 cx.simulate_prompt_answer(0);
5300
5301 cx.executor().run_until_parked();
5302 close.await.unwrap();
5303 left_pane.update(cx, |pane, _| {
5304 assert_eq!(pane.items_len(), 0);
5305 });
5306 }
5307
5308 #[gpui::test]
5309 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5310 init_test(cx);
5311
5312 let fs = FakeFs::new(cx.executor());
5313 let project = Project::test(fs, [], cx).await;
5314 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5315 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5316
5317 let item = cx.new_view(|cx| {
5318 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5319 });
5320 let item_id = item.entity_id();
5321 workspace.update(cx, |workspace, cx| {
5322 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5323 });
5324
5325 // Autosave on window change.
5326 item.update(cx, |item, cx| {
5327 cx.update_global(|settings: &mut SettingsStore, cx| {
5328 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5329 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5330 })
5331 });
5332 item.is_dirty = true;
5333 });
5334
5335 // Deactivating the window saves the file.
5336 cx.deactivate_window();
5337 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5338
5339 // Re-activating the window doesn't save the file.
5340 cx.update(|cx| cx.activate_window());
5341 cx.executor().run_until_parked();
5342 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5343
5344 // Autosave on focus change.
5345 item.update(cx, |item, cx| {
5346 cx.focus_self();
5347 cx.update_global(|settings: &mut SettingsStore, cx| {
5348 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5349 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5350 })
5351 });
5352 item.is_dirty = true;
5353 });
5354
5355 // Blurring the item saves the file.
5356 item.update(cx, |_, cx| cx.blur());
5357 cx.executor().run_until_parked();
5358 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5359
5360 // Deactivating the window still saves the file.
5361 item.update(cx, |item, cx| {
5362 cx.focus_self();
5363 item.is_dirty = true;
5364 });
5365 cx.deactivate_window();
5366 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5367
5368 // Autosave after delay.
5369 item.update(cx, |item, cx| {
5370 cx.update_global(|settings: &mut SettingsStore, cx| {
5371 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5372 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5373 })
5374 });
5375 item.is_dirty = true;
5376 cx.emit(ItemEvent::Edit);
5377 });
5378
5379 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5380 cx.executor().advance_clock(Duration::from_millis(250));
5381 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5382
5383 // After delay expires, the file is saved.
5384 cx.executor().advance_clock(Duration::from_millis(250));
5385 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5386
5387 // Autosave on focus change, ensuring closing the tab counts as such.
5388 item.update(cx, |item, cx| {
5389 cx.update_global(|settings: &mut SettingsStore, cx| {
5390 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5391 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5392 })
5393 });
5394 item.is_dirty = true;
5395 });
5396
5397 pane.update(cx, |pane, cx| {
5398 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5399 })
5400 .await
5401 .unwrap();
5402 assert!(!cx.has_pending_prompt());
5403 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5404
5405 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5406 workspace.update(cx, |workspace, cx| {
5407 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5408 });
5409 item.update(cx, |item, cx| {
5410 item.project_items[0].update(cx, |item, _| {
5411 item.entry_id = None;
5412 });
5413 item.is_dirty = true;
5414 cx.blur();
5415 });
5416 cx.run_until_parked();
5417 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5418
5419 // Ensure autosave is prevented for deleted files also when closing the buffer.
5420 let _close_items = pane.update(cx, |pane, cx| {
5421 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5422 });
5423 cx.run_until_parked();
5424 assert!(cx.has_pending_prompt());
5425 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5426 }
5427
5428 #[gpui::test]
5429 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5430 init_test(cx);
5431
5432 let fs = FakeFs::new(cx.executor());
5433
5434 let project = Project::test(fs, [], cx).await;
5435 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5436
5437 let item = cx.new_view(|cx| {
5438 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5439 });
5440 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5441 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5442 let toolbar_notify_count = Rc::new(RefCell::new(0));
5443
5444 workspace.update(cx, |workspace, cx| {
5445 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5446 let toolbar_notification_count = toolbar_notify_count.clone();
5447 cx.observe(&toolbar, move |_, _, _| {
5448 *toolbar_notification_count.borrow_mut() += 1
5449 })
5450 .detach();
5451 });
5452
5453 pane.update(cx, |pane, _| {
5454 assert!(!pane.can_navigate_backward());
5455 assert!(!pane.can_navigate_forward());
5456 });
5457
5458 item.update(cx, |item, cx| {
5459 item.set_state("one".to_string(), cx);
5460 });
5461
5462 // Toolbar must be notified to re-render the navigation buttons
5463 assert_eq!(*toolbar_notify_count.borrow(), 1);
5464
5465 pane.update(cx, |pane, _| {
5466 assert!(pane.can_navigate_backward());
5467 assert!(!pane.can_navigate_forward());
5468 });
5469
5470 workspace
5471 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5472 .await
5473 .unwrap();
5474
5475 assert_eq!(*toolbar_notify_count.borrow(), 2);
5476 pane.update(cx, |pane, _| {
5477 assert!(!pane.can_navigate_backward());
5478 assert!(pane.can_navigate_forward());
5479 });
5480 }
5481
5482 #[gpui::test]
5483 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5484 init_test(cx);
5485 let fs = FakeFs::new(cx.executor());
5486
5487 let project = Project::test(fs, [], cx).await;
5488 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5489
5490 let panel = workspace.update(cx, |workspace, cx| {
5491 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5492 workspace.add_panel(panel.clone(), cx);
5493
5494 workspace
5495 .right_dock()
5496 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5497
5498 panel
5499 });
5500
5501 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5502 pane.update(cx, |pane, cx| {
5503 let item = cx.new_view(|cx| TestItem::new(cx));
5504 pane.add_item(Box::new(item), true, true, None, cx);
5505 });
5506
5507 // Transfer focus from center to panel
5508 workspace.update(cx, |workspace, cx| {
5509 workspace.toggle_panel_focus::<TestPanel>(cx);
5510 });
5511
5512 workspace.update(cx, |workspace, cx| {
5513 assert!(workspace.right_dock().read(cx).is_open());
5514 assert!(!panel.is_zoomed(cx));
5515 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5516 });
5517
5518 // Transfer focus from panel to center
5519 workspace.update(cx, |workspace, cx| {
5520 workspace.toggle_panel_focus::<TestPanel>(cx);
5521 });
5522
5523 workspace.update(cx, |workspace, cx| {
5524 assert!(workspace.right_dock().read(cx).is_open());
5525 assert!(!panel.is_zoomed(cx));
5526 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5527 });
5528
5529 // Close the dock
5530 workspace.update(cx, |workspace, cx| {
5531 workspace.toggle_dock(DockPosition::Right, cx);
5532 });
5533
5534 workspace.update(cx, |workspace, cx| {
5535 assert!(!workspace.right_dock().read(cx).is_open());
5536 assert!(!panel.is_zoomed(cx));
5537 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5538 });
5539
5540 // Open the dock
5541 workspace.update(cx, |workspace, cx| {
5542 workspace.toggle_dock(DockPosition::Right, cx);
5543 });
5544
5545 workspace.update(cx, |workspace, cx| {
5546 assert!(workspace.right_dock().read(cx).is_open());
5547 assert!(!panel.is_zoomed(cx));
5548 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5549 });
5550
5551 // Focus and zoom panel
5552 panel.update(cx, |panel, cx| {
5553 cx.focus_self();
5554 panel.set_zoomed(true, cx)
5555 });
5556
5557 workspace.update(cx, |workspace, cx| {
5558 assert!(workspace.right_dock().read(cx).is_open());
5559 assert!(panel.is_zoomed(cx));
5560 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5561 });
5562
5563 // Transfer focus to the center closes the dock
5564 workspace.update(cx, |workspace, cx| {
5565 workspace.toggle_panel_focus::<TestPanel>(cx);
5566 });
5567
5568 workspace.update(cx, |workspace, cx| {
5569 assert!(!workspace.right_dock().read(cx).is_open());
5570 assert!(panel.is_zoomed(cx));
5571 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5572 });
5573
5574 // Transferring focus back to the panel keeps it zoomed
5575 workspace.update(cx, |workspace, cx| {
5576 workspace.toggle_panel_focus::<TestPanel>(cx);
5577 });
5578
5579 workspace.update(cx, |workspace, cx| {
5580 assert!(workspace.right_dock().read(cx).is_open());
5581 assert!(panel.is_zoomed(cx));
5582 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5583 });
5584
5585 // Close the dock while it is zoomed
5586 workspace.update(cx, |workspace, cx| {
5587 workspace.toggle_dock(DockPosition::Right, cx)
5588 });
5589
5590 workspace.update(cx, |workspace, cx| {
5591 assert!(!workspace.right_dock().read(cx).is_open());
5592 assert!(panel.is_zoomed(cx));
5593 assert!(workspace.zoomed.is_none());
5594 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5595 });
5596
5597 // Opening the dock, when it's zoomed, retains focus
5598 workspace.update(cx, |workspace, cx| {
5599 workspace.toggle_dock(DockPosition::Right, cx)
5600 });
5601
5602 workspace.update(cx, |workspace, cx| {
5603 assert!(workspace.right_dock().read(cx).is_open());
5604 assert!(panel.is_zoomed(cx));
5605 assert!(workspace.zoomed.is_some());
5606 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5607 });
5608
5609 // Unzoom and close the panel, zoom the active pane.
5610 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5611 workspace.update(cx, |workspace, cx| {
5612 workspace.toggle_dock(DockPosition::Right, cx)
5613 });
5614 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5615
5616 // Opening a dock unzooms the pane.
5617 workspace.update(cx, |workspace, cx| {
5618 workspace.toggle_dock(DockPosition::Right, cx)
5619 });
5620 workspace.update(cx, |workspace, cx| {
5621 let pane = pane.read(cx);
5622 assert!(!pane.is_zoomed());
5623 assert!(!pane.focus_handle(cx).is_focused(cx));
5624 assert!(workspace.right_dock().read(cx).is_open());
5625 assert!(workspace.zoomed.is_none());
5626 });
5627 }
5628
5629 struct TestModal(FocusHandle);
5630
5631 impl TestModal {
5632 fn new(cx: &mut ViewContext<Self>) -> Self {
5633 Self(cx.focus_handle())
5634 }
5635 }
5636
5637 impl EventEmitter<DismissEvent> for TestModal {}
5638
5639 impl FocusableView for TestModal {
5640 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5641 self.0.clone()
5642 }
5643 }
5644
5645 impl ModalView for TestModal {}
5646
5647 impl Render for TestModal {
5648 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5649 div().track_focus(&self.0)
5650 }
5651 }
5652
5653 #[gpui::test]
5654 async fn test_panels(cx: &mut gpui::TestAppContext) {
5655 init_test(cx);
5656 let fs = FakeFs::new(cx.executor());
5657
5658 let project = Project::test(fs, [], cx).await;
5659 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5660
5661 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5662 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5663 workspace.add_panel(panel_1.clone(), cx);
5664 workspace
5665 .left_dock()
5666 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5667 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5668 workspace.add_panel(panel_2.clone(), cx);
5669 workspace
5670 .right_dock()
5671 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5672
5673 let left_dock = workspace.left_dock();
5674 assert_eq!(
5675 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5676 panel_1.panel_id()
5677 );
5678 assert_eq!(
5679 left_dock.read(cx).active_panel_size(cx).unwrap(),
5680 panel_1.size(cx)
5681 );
5682
5683 left_dock.update(cx, |left_dock, cx| {
5684 left_dock.resize_active_panel(Some(px(1337.)), cx)
5685 });
5686 assert_eq!(
5687 workspace
5688 .right_dock()
5689 .read(cx)
5690 .visible_panel()
5691 .unwrap()
5692 .panel_id(),
5693 panel_2.panel_id(),
5694 );
5695
5696 (panel_1, panel_2)
5697 });
5698
5699 // Move panel_1 to the right
5700 panel_1.update(cx, |panel_1, cx| {
5701 panel_1.set_position(DockPosition::Right, cx)
5702 });
5703
5704 workspace.update(cx, |workspace, cx| {
5705 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5706 // Since it was the only panel on the left, the left dock should now be closed.
5707 assert!(!workspace.left_dock().read(cx).is_open());
5708 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5709 let right_dock = workspace.right_dock();
5710 assert_eq!(
5711 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5712 panel_1.panel_id()
5713 );
5714 assert_eq!(
5715 right_dock.read(cx).active_panel_size(cx).unwrap(),
5716 px(1337.)
5717 );
5718
5719 // Now we move panel_2 to the left
5720 panel_2.set_position(DockPosition::Left, cx);
5721 });
5722
5723 workspace.update(cx, |workspace, cx| {
5724 // Since panel_2 was not visible on the right, we don't open the left dock.
5725 assert!(!workspace.left_dock().read(cx).is_open());
5726 // And the right dock is unaffected in its displaying of panel_1
5727 assert!(workspace.right_dock().read(cx).is_open());
5728 assert_eq!(
5729 workspace
5730 .right_dock()
5731 .read(cx)
5732 .visible_panel()
5733 .unwrap()
5734 .panel_id(),
5735 panel_1.panel_id(),
5736 );
5737 });
5738
5739 // Move panel_1 back to the left
5740 panel_1.update(cx, |panel_1, cx| {
5741 panel_1.set_position(DockPosition::Left, cx)
5742 });
5743
5744 workspace.update(cx, |workspace, cx| {
5745 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5746 let left_dock = workspace.left_dock();
5747 assert!(left_dock.read(cx).is_open());
5748 assert_eq!(
5749 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5750 panel_1.panel_id()
5751 );
5752 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5753 // And the right dock should be closed as it no longer has any panels.
5754 assert!(!workspace.right_dock().read(cx).is_open());
5755
5756 // Now we move panel_1 to the bottom
5757 panel_1.set_position(DockPosition::Bottom, cx);
5758 });
5759
5760 workspace.update(cx, |workspace, cx| {
5761 // Since panel_1 was visible on the left, we close the left dock.
5762 assert!(!workspace.left_dock().read(cx).is_open());
5763 // The bottom dock is sized based on the panel's default size,
5764 // since the panel orientation changed from vertical to horizontal.
5765 let bottom_dock = workspace.bottom_dock();
5766 assert_eq!(
5767 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5768 panel_1.size(cx),
5769 );
5770 // Close bottom dock and move panel_1 back to the left.
5771 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5772 panel_1.set_position(DockPosition::Left, cx);
5773 });
5774
5775 // Emit activated event on panel 1
5776 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5777
5778 // Now the left dock is open and panel_1 is active and focused.
5779 workspace.update(cx, |workspace, cx| {
5780 let left_dock = workspace.left_dock();
5781 assert!(left_dock.read(cx).is_open());
5782 assert_eq!(
5783 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5784 panel_1.panel_id(),
5785 );
5786 assert!(panel_1.focus_handle(cx).is_focused(cx));
5787 });
5788
5789 // Emit closed event on panel 2, which is not active
5790 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5791
5792 // Wo don't close the left dock, because panel_2 wasn't the active panel
5793 workspace.update(cx, |workspace, cx| {
5794 let left_dock = workspace.left_dock();
5795 assert!(left_dock.read(cx).is_open());
5796 assert_eq!(
5797 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5798 panel_1.panel_id(),
5799 );
5800 });
5801
5802 // Emitting a ZoomIn event shows the panel as zoomed.
5803 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5804 workspace.update(cx, |workspace, _| {
5805 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5806 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5807 });
5808
5809 // Move panel to another dock while it is zoomed
5810 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5811 workspace.update(cx, |workspace, _| {
5812 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5813
5814 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5815 });
5816
5817 // This is a helper for getting a:
5818 // - valid focus on an element,
5819 // - that isn't a part of the panes and panels system of the Workspace,
5820 // - and doesn't trigger the 'on_focus_lost' API.
5821 let focus_other_view = {
5822 let workspace = workspace.clone();
5823 move |cx: &mut VisualTestContext| {
5824 workspace.update(cx, |workspace, cx| {
5825 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5826 workspace.toggle_modal(cx, TestModal::new);
5827 workspace.toggle_modal(cx, TestModal::new);
5828 } else {
5829 workspace.toggle_modal(cx, TestModal::new);
5830 }
5831 })
5832 }
5833 };
5834
5835 // If focus is transferred to another view that's not a panel or another pane, we still show
5836 // the panel as zoomed.
5837 focus_other_view(cx);
5838 workspace.update(cx, |workspace, _| {
5839 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5840 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5841 });
5842
5843 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5844 workspace.update(cx, |_, cx| cx.focus_self());
5845 workspace.update(cx, |workspace, _| {
5846 assert_eq!(workspace.zoomed, None);
5847 assert_eq!(workspace.zoomed_position, None);
5848 });
5849
5850 // If focus is transferred again to another view that's not a panel or a pane, we won't
5851 // show the panel as zoomed because it wasn't zoomed before.
5852 focus_other_view(cx);
5853 workspace.update(cx, |workspace, _| {
5854 assert_eq!(workspace.zoomed, None);
5855 assert_eq!(workspace.zoomed_position, None);
5856 });
5857
5858 // When the panel is activated, it is zoomed again.
5859 cx.dispatch_action(ToggleRightDock);
5860 workspace.update(cx, |workspace, _| {
5861 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5862 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5863 });
5864
5865 // Emitting a ZoomOut event unzooms the panel.
5866 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5867 workspace.update(cx, |workspace, _| {
5868 assert_eq!(workspace.zoomed, None);
5869 assert_eq!(workspace.zoomed_position, None);
5870 });
5871
5872 // Emit closed event on panel 1, which is active
5873 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5874
5875 // Now the left dock is closed, because panel_1 was the active panel
5876 workspace.update(cx, |workspace, cx| {
5877 let right_dock = workspace.right_dock();
5878 assert!(!right_dock.read(cx).is_open());
5879 });
5880 }
5881
5882 mod register_project_item_tests {
5883 use ui::Context as _;
5884
5885 use super::*;
5886
5887 const TEST_PNG_KIND: &str = "TestPngItemView";
5888 // View
5889 struct TestPngItemView {
5890 focus_handle: FocusHandle,
5891 }
5892 // Model
5893 struct TestPngItem {}
5894
5895 impl project::Item for TestPngItem {
5896 fn try_open(
5897 _project: &Model<Project>,
5898 path: &ProjectPath,
5899 cx: &mut AppContext,
5900 ) -> Option<Task<gpui::Result<Model<Self>>>> {
5901 if path.path.extension().unwrap() == "png" {
5902 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
5903 } else {
5904 None
5905 }
5906 }
5907
5908 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
5909 None
5910 }
5911
5912 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
5913 None
5914 }
5915 }
5916
5917 impl Item for TestPngItemView {
5918 type Event = ();
5919
5920 fn serialized_item_kind() -> Option<&'static str> {
5921 Some(TEST_PNG_KIND)
5922 }
5923 }
5924 impl EventEmitter<()> for TestPngItemView {}
5925 impl FocusableView for TestPngItemView {
5926 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5927 self.focus_handle.clone()
5928 }
5929 }
5930
5931 impl Render for TestPngItemView {
5932 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
5933 Empty
5934 }
5935 }
5936
5937 impl ProjectItem for TestPngItemView {
5938 type Item = TestPngItem;
5939
5940 fn for_project_item(
5941 _project: Model<Project>,
5942 _item: Model<Self::Item>,
5943 cx: &mut ViewContext<Self>,
5944 ) -> Self
5945 where
5946 Self: Sized,
5947 {
5948 Self {
5949 focus_handle: cx.focus_handle(),
5950 }
5951 }
5952 }
5953
5954 const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
5955 // View
5956 struct TestIpynbItemView {
5957 focus_handle: FocusHandle,
5958 }
5959 // Model
5960 struct TestIpynbItem {}
5961
5962 impl project::Item for TestIpynbItem {
5963 fn try_open(
5964 _project: &Model<Project>,
5965 path: &ProjectPath,
5966 cx: &mut AppContext,
5967 ) -> Option<Task<gpui::Result<Model<Self>>>> {
5968 if path.path.extension().unwrap() == "ipynb" {
5969 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
5970 } else {
5971 None
5972 }
5973 }
5974
5975 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
5976 None
5977 }
5978
5979 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
5980 None
5981 }
5982 }
5983
5984 impl Item for TestIpynbItemView {
5985 type Event = ();
5986
5987 fn serialized_item_kind() -> Option<&'static str> {
5988 Some(TEST_IPYNB_KIND)
5989 }
5990 }
5991 impl EventEmitter<()> for TestIpynbItemView {}
5992 impl FocusableView for TestIpynbItemView {
5993 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5994 self.focus_handle.clone()
5995 }
5996 }
5997
5998 impl Render for TestIpynbItemView {
5999 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6000 Empty
6001 }
6002 }
6003
6004 impl ProjectItem for TestIpynbItemView {
6005 type Item = TestIpynbItem;
6006
6007 fn for_project_item(
6008 _project: Model<Project>,
6009 _item: Model<Self::Item>,
6010 cx: &mut ViewContext<Self>,
6011 ) -> Self
6012 where
6013 Self: Sized,
6014 {
6015 Self {
6016 focus_handle: cx.focus_handle(),
6017 }
6018 }
6019 }
6020
6021 struct TestAlternatePngItemView {
6022 focus_handle: FocusHandle,
6023 }
6024
6025 const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
6026 impl Item for TestAlternatePngItemView {
6027 type Event = ();
6028
6029 fn serialized_item_kind() -> Option<&'static str> {
6030 Some(TEST_ALTERNATE_PNG_KIND)
6031 }
6032 }
6033 impl EventEmitter<()> for TestAlternatePngItemView {}
6034 impl FocusableView for TestAlternatePngItemView {
6035 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6036 self.focus_handle.clone()
6037 }
6038 }
6039
6040 impl Render for TestAlternatePngItemView {
6041 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6042 Empty
6043 }
6044 }
6045
6046 impl ProjectItem for TestAlternatePngItemView {
6047 type Item = TestPngItem;
6048
6049 fn for_project_item(
6050 _project: Model<Project>,
6051 _item: Model<Self::Item>,
6052 cx: &mut ViewContext<Self>,
6053 ) -> Self
6054 where
6055 Self: Sized,
6056 {
6057 Self {
6058 focus_handle: cx.focus_handle(),
6059 }
6060 }
6061 }
6062
6063 #[gpui::test]
6064 async fn test_register_project_item(cx: &mut TestAppContext) {
6065 init_test(cx);
6066
6067 cx.update(|cx| {
6068 register_project_item::<TestPngItemView>(cx);
6069 register_project_item::<TestIpynbItemView>(cx);
6070 });
6071
6072 let fs = FakeFs::new(cx.executor());
6073 fs.insert_tree(
6074 "/root1",
6075 json!({
6076 "one.png": "BINARYDATAHERE",
6077 "two.ipynb": "{ totally a notebook }",
6078 "three.txt": "editing text, sure why not?"
6079 }),
6080 )
6081 .await;
6082
6083 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6084 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6085
6086 let worktree_id = project.update(cx, |project, cx| {
6087 project.worktrees().next().unwrap().read(cx).id()
6088 });
6089
6090 let handle = workspace
6091 .update(cx, |workspace, cx| {
6092 let project_path = (worktree_id, "one.png");
6093 workspace.open_path(project_path, None, true, cx)
6094 })
6095 .await
6096 .unwrap();
6097
6098 // Now we can check if the handle we got back errored or not
6099 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
6100
6101 let handle = workspace
6102 .update(cx, |workspace, cx| {
6103 let project_path = (worktree_id, "two.ipynb");
6104 workspace.open_path(project_path, None, true, cx)
6105 })
6106 .await
6107 .unwrap();
6108
6109 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
6110
6111 let handle = workspace
6112 .update(cx, |workspace, cx| {
6113 let project_path = (worktree_id, "three.txt");
6114 workspace.open_path(project_path, None, true, cx)
6115 })
6116 .await;
6117 assert!(handle.is_err());
6118 }
6119
6120 #[gpui::test]
6121 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
6122 init_test(cx);
6123
6124 cx.update(|cx| {
6125 register_project_item::<TestPngItemView>(cx);
6126 register_project_item::<TestAlternatePngItemView>(cx);
6127 });
6128
6129 let fs = FakeFs::new(cx.executor());
6130 fs.insert_tree(
6131 "/root1",
6132 json!({
6133 "one.png": "BINARYDATAHERE",
6134 "two.ipynb": "{ totally a notebook }",
6135 "three.txt": "editing text, sure why not?"
6136 }),
6137 )
6138 .await;
6139
6140 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6141 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6142
6143 let worktree_id = project.update(cx, |project, cx| {
6144 project.worktrees().next().unwrap().read(cx).id()
6145 });
6146
6147 let handle = workspace
6148 .update(cx, |workspace, cx| {
6149 let project_path = (worktree_id, "one.png");
6150 workspace.open_path(project_path, None, true, cx)
6151 })
6152 .await
6153 .unwrap();
6154
6155 // This _must_ be the second item registered
6156 assert_eq!(
6157 handle.serialized_item_kind().unwrap(),
6158 TEST_ALTERNATE_PNG_KIND
6159 );
6160
6161 let handle = workspace
6162 .update(cx, |workspace, cx| {
6163 let project_path = (worktree_id, "three.txt");
6164 workspace.open_path(project_path, None, true, cx)
6165 })
6166 .await;
6167 assert!(handle.is_err());
6168 }
6169 }
6170
6171 pub fn init_test(cx: &mut TestAppContext) {
6172 cx.update(|cx| {
6173 let settings_store = SettingsStore::test(cx);
6174 cx.set_global(settings_store);
6175 theme::init(theme::LoadThemes::JustBase, cx);
6176 language::init(cx);
6177 crate::init_settings(cx);
6178 Project::init_settings(cx);
6179 });
6180 }
6181}