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