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