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