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