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