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