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