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