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