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