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