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).detach();
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).detach();
827 cx.notify();
828 }),
829 cx.observe(&bottom_dock, |this, _, cx| {
830 this.serialize_workspace(cx).detach();
831 cx.notify();
832 }),
833 cx.observe(&right_dock, |this, _, cx| {
834 this.serialize_workspace(cx).detach();
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).detach();
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).detach();
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 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1938 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1939 let mut focus_center = false;
1940 let panel = dock.update(cx, |dock, cx| {
1941 dock.activate_panel(panel_index, cx);
1942
1943 let panel = dock.active_panel().cloned();
1944 if let Some(panel) = panel.as_ref() {
1945 if should_focus(&**panel, cx) {
1946 dock.set_open(true, cx);
1947 panel.focus_handle(cx).focus(cx);
1948 } else {
1949 focus_center = true;
1950 }
1951 }
1952 panel
1953 });
1954
1955 if focus_center {
1956 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1957 }
1958
1959 self.serialize_workspace(cx).detach();
1960 cx.notify();
1961 return panel;
1962 }
1963 }
1964 None
1965 }
1966
1967 /// Open the panel of the given type
1968 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1969 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1970 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1971 dock.update(cx, |dock, cx| {
1972 dock.activate_panel(panel_index, cx);
1973 dock.set_open(true, cx);
1974 });
1975 }
1976 }
1977 }
1978
1979 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1980 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1981 let dock = dock.read(cx);
1982 if let Some(panel) = dock.panel::<T>() {
1983 return Some(panel);
1984 }
1985 }
1986 None
1987 }
1988
1989 fn dismiss_zoomed_items_to_reveal(
1990 &mut self,
1991 dock_to_reveal: Option<DockPosition>,
1992 cx: &mut ViewContext<Self>,
1993 ) {
1994 // If a center pane is zoomed, unzoom it.
1995 for pane in &self.panes {
1996 if pane != &self.active_pane || dock_to_reveal.is_some() {
1997 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1998 }
1999 }
2000
2001 // If another dock is zoomed, hide it.
2002 let mut focus_center = false;
2003 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2004 dock.update(cx, |dock, cx| {
2005 if Some(dock.position()) != dock_to_reveal {
2006 if let Some(panel) = dock.active_panel() {
2007 if panel.is_zoomed(cx) {
2008 focus_center |= panel.focus_handle(cx).contains_focused(cx);
2009 dock.set_open(false, cx);
2010 }
2011 }
2012 }
2013 });
2014 }
2015
2016 if focus_center {
2017 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2018 }
2019
2020 if self.zoomed_position != dock_to_reveal {
2021 self.zoomed = None;
2022 self.zoomed_position = None;
2023 cx.emit(Event::ZoomChanged);
2024 }
2025
2026 cx.notify();
2027 }
2028
2029 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2030 let pane = cx.new_view(|cx| {
2031 Pane::new(
2032 self.weak_handle(),
2033 self.project.clone(),
2034 self.pane_history_timestamp.clone(),
2035 None,
2036 NewFile.boxed_clone(),
2037 cx,
2038 )
2039 });
2040 cx.subscribe(&pane, Self::handle_pane_event).detach();
2041 self.panes.push(pane.clone());
2042 cx.focus_view(&pane);
2043 cx.emit(Event::PaneAdded(pane.clone()));
2044 pane
2045 }
2046
2047 pub fn add_item_to_center(
2048 &mut self,
2049 item: Box<dyn ItemHandle>,
2050 cx: &mut ViewContext<Self>,
2051 ) -> bool {
2052 if let Some(center_pane) = self.last_active_center_pane.clone() {
2053 if let Some(center_pane) = center_pane.upgrade() {
2054 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2055 true
2056 } else {
2057 false
2058 }
2059 } else {
2060 false
2061 }
2062 }
2063
2064 pub fn add_item_to_active_pane(
2065 &mut self,
2066 item: Box<dyn ItemHandle>,
2067 destination_index: Option<usize>,
2068 cx: &mut WindowContext,
2069 ) {
2070 self.add_item(self.active_pane.clone(), item, destination_index, cx)
2071 }
2072
2073 pub fn add_item(
2074 &mut self,
2075 pane: View<Pane>,
2076 item: Box<dyn ItemHandle>,
2077 destination_index: Option<usize>,
2078 cx: &mut WindowContext,
2079 ) {
2080 if let Some(text) = item.telemetry_event_text(cx) {
2081 self.client()
2082 .telemetry()
2083 .report_app_event(format!("{}: open", text));
2084 }
2085
2086 pane.update(cx, |pane, cx| {
2087 pane.add_item(item, true, true, destination_index, cx)
2088 });
2089 }
2090
2091 pub fn split_item(
2092 &mut self,
2093 split_direction: SplitDirection,
2094 item: Box<dyn ItemHandle>,
2095 cx: &mut ViewContext<Self>,
2096 ) {
2097 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2098 self.add_item(new_pane, item, None, cx);
2099 }
2100
2101 pub fn open_abs_path(
2102 &mut self,
2103 abs_path: PathBuf,
2104 visible: bool,
2105 cx: &mut ViewContext<Self>,
2106 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2107 cx.spawn(|workspace, mut cx| async move {
2108 let open_paths_task_result = workspace
2109 .update(&mut cx, |workspace, cx| {
2110 workspace.open_paths(
2111 vec![abs_path.clone()],
2112 if visible {
2113 OpenVisible::All
2114 } else {
2115 OpenVisible::None
2116 },
2117 None,
2118 cx,
2119 )
2120 })
2121 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2122 .await;
2123 anyhow::ensure!(
2124 open_paths_task_result.len() == 1,
2125 "open abs path {abs_path:?} task returned incorrect number of results"
2126 );
2127 match open_paths_task_result
2128 .into_iter()
2129 .next()
2130 .expect("ensured single task result")
2131 {
2132 Some(open_result) => {
2133 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2134 }
2135 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2136 }
2137 })
2138 }
2139
2140 pub fn split_abs_path(
2141 &mut self,
2142 abs_path: PathBuf,
2143 visible: bool,
2144 cx: &mut ViewContext<Self>,
2145 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2146 let project_path_task =
2147 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2148 cx.spawn(|this, mut cx| async move {
2149 let (_, path) = project_path_task.await?;
2150 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2151 .await
2152 })
2153 }
2154
2155 pub fn open_path(
2156 &mut self,
2157 path: impl Into<ProjectPath>,
2158 pane: Option<WeakView<Pane>>,
2159 focus_item: bool,
2160 cx: &mut WindowContext,
2161 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2162 self.open_path_preview(path, pane, focus_item, false, cx)
2163 }
2164
2165 pub fn open_path_preview(
2166 &mut self,
2167 path: impl Into<ProjectPath>,
2168 pane: Option<WeakView<Pane>>,
2169 focus_item: bool,
2170 allow_preview: bool,
2171 cx: &mut WindowContext,
2172 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2173 let pane = pane.unwrap_or_else(|| {
2174 self.last_active_center_pane.clone().unwrap_or_else(|| {
2175 self.panes
2176 .first()
2177 .expect("There must be an active pane")
2178 .downgrade()
2179 })
2180 });
2181
2182 let task = self.load_path(path.into(), cx);
2183 cx.spawn(move |mut cx| async move {
2184 let (project_entry_id, build_item) = task.await?;
2185 pane.update(&mut cx, |pane, cx| {
2186 pane.open_item(project_entry_id, focus_item, allow_preview, cx, build_item)
2187 })
2188 })
2189 }
2190
2191 pub fn split_path(
2192 &mut self,
2193 path: impl Into<ProjectPath>,
2194 cx: &mut ViewContext<Self>,
2195 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2196 self.split_path_preview(path, false, cx)
2197 }
2198
2199 pub fn split_path_preview(
2200 &mut self,
2201 path: impl Into<ProjectPath>,
2202 allow_preview: bool,
2203 cx: &mut ViewContext<Self>,
2204 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2205 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2206 self.panes
2207 .first()
2208 .expect("There must be an active pane")
2209 .downgrade()
2210 });
2211
2212 if let Member::Pane(center_pane) = &self.center.root {
2213 if center_pane.read(cx).items_len() == 0 {
2214 return self.open_path(path, Some(pane), true, cx);
2215 }
2216 }
2217
2218 let task = self.load_path(path.into(), cx);
2219 cx.spawn(|this, mut cx| async move {
2220 let (project_entry_id, build_item) = task.await?;
2221 this.update(&mut cx, move |this, cx| -> Option<_> {
2222 let pane = pane.upgrade()?;
2223 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2224 new_pane.update(cx, |new_pane, cx| {
2225 Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item))
2226 })
2227 })
2228 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2229 })
2230 }
2231
2232 fn load_path(
2233 &mut self,
2234 path: ProjectPath,
2235 cx: &mut WindowContext,
2236 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2237 let project = self.project().clone();
2238 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2239 let Some(open_project_item) = project_item_builders
2240 .iter()
2241 .rev()
2242 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2243 else {
2244 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2245 };
2246 open_project_item
2247 }
2248
2249 pub fn open_project_item<T>(
2250 &mut self,
2251 pane: View<Pane>,
2252 project_item: Model<T::Item>,
2253 cx: &mut ViewContext<Self>,
2254 ) -> View<T>
2255 where
2256 T: ProjectItem,
2257 {
2258 use project::Item as _;
2259
2260 let entry_id = project_item.read(cx).entry_id(cx);
2261 if let Some(item) = entry_id
2262 .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
2263 .and_then(|item| item.downcast())
2264 {
2265 self.activate_item(&item, cx);
2266 return item;
2267 }
2268
2269 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2270
2271 let item_id = item.item_id();
2272 let mut destination_index = None;
2273 pane.update(cx, |pane, cx| {
2274 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2275 if let Some(preview_item_id) = pane.preview_item_id() {
2276 if preview_item_id != item_id {
2277 destination_index = pane.close_current_preview_item(cx);
2278 }
2279 }
2280 }
2281 pane.set_preview_item_id(Some(item.item_id()), cx)
2282 });
2283
2284 self.add_item(pane, Box::new(item.clone()), destination_index, cx);
2285 item
2286 }
2287
2288 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2289 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2290 self.active_pane.update(cx, |pane, cx| {
2291 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2292 });
2293 }
2294 }
2295
2296 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2297 let result = self.panes.iter().find_map(|pane| {
2298 pane.read(cx)
2299 .index_for_item(item)
2300 .map(|ix| (pane.clone(), ix))
2301 });
2302 if let Some((pane, ix)) = result {
2303 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2304 true
2305 } else {
2306 false
2307 }
2308 }
2309
2310 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2311 let panes = self.center.panes();
2312 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2313 cx.focus_view(&pane);
2314 } else {
2315 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2316 }
2317 }
2318
2319 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2320 let panes = self.center.panes();
2321 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2322 let next_ix = (ix + 1) % panes.len();
2323 let next_pane = panes[next_ix].clone();
2324 cx.focus_view(&next_pane);
2325 }
2326 }
2327
2328 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2329 let panes = self.center.panes();
2330 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2331 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2332 let prev_pane = panes[prev_ix].clone();
2333 cx.focus_view(&prev_pane);
2334 }
2335 }
2336
2337 pub fn activate_pane_in_direction(
2338 &mut self,
2339 direction: SplitDirection,
2340 cx: &mut WindowContext,
2341 ) {
2342 use ActivateInDirectionTarget as Target;
2343 enum Origin {
2344 LeftDock,
2345 RightDock,
2346 BottomDock,
2347 Center,
2348 }
2349
2350 let origin: Origin = [
2351 (&self.left_dock, Origin::LeftDock),
2352 (&self.right_dock, Origin::RightDock),
2353 (&self.bottom_dock, Origin::BottomDock),
2354 ]
2355 .into_iter()
2356 .find_map(|(dock, origin)| {
2357 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2358 Some(origin)
2359 } else {
2360 None
2361 }
2362 })
2363 .unwrap_or(Origin::Center);
2364
2365 let get_last_active_pane = || {
2366 self.last_active_center_pane.as_ref().and_then(|p| {
2367 let p = p.upgrade()?;
2368 (p.read(cx).items_len() != 0).then_some(p)
2369 })
2370 };
2371
2372 let try_dock =
2373 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2374
2375 let target = match (origin, direction) {
2376 // We're in the center, so we first try to go to a different pane,
2377 // otherwise try to go to a dock.
2378 (Origin::Center, direction) => {
2379 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2380 Some(Target::Pane(pane))
2381 } else {
2382 match direction {
2383 SplitDirection::Up => None,
2384 SplitDirection::Down => try_dock(&self.bottom_dock),
2385 SplitDirection::Left => try_dock(&self.left_dock),
2386 SplitDirection::Right => try_dock(&self.right_dock),
2387 }
2388 }
2389 }
2390
2391 (Origin::LeftDock, SplitDirection::Right) => {
2392 if let Some(last_active_pane) = get_last_active_pane() {
2393 Some(Target::Pane(last_active_pane))
2394 } else {
2395 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2396 }
2397 }
2398
2399 (Origin::LeftDock, SplitDirection::Down)
2400 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2401
2402 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2403 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2404 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2405
2406 (Origin::RightDock, SplitDirection::Left) => {
2407 if let Some(last_active_pane) = get_last_active_pane() {
2408 Some(Target::Pane(last_active_pane))
2409 } else {
2410 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2411 }
2412 }
2413
2414 _ => None,
2415 };
2416
2417 match target {
2418 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2419 Some(ActivateInDirectionTarget::Dock(dock)) => {
2420 if let Some(panel) = dock.read(cx).active_panel() {
2421 panel.focus_handle(cx).focus(cx);
2422 } else {
2423 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2424 }
2425 }
2426 None => {}
2427 }
2428 }
2429
2430 pub fn find_pane_in_direction(
2431 &mut self,
2432 direction: SplitDirection,
2433 cx: &WindowContext,
2434 ) -> Option<View<Pane>> {
2435 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2436 return None;
2437 };
2438 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2439 let center = match cursor {
2440 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2441 _ => bounding_box.center(),
2442 };
2443
2444 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2445
2446 let target = match direction {
2447 SplitDirection::Left => {
2448 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2449 }
2450 SplitDirection::Right => {
2451 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2452 }
2453 SplitDirection::Up => {
2454 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2455 }
2456 SplitDirection::Down => {
2457 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2458 }
2459 };
2460 self.center.pane_at_pixel_position(target).cloned()
2461 }
2462
2463 pub fn swap_pane_in_direction(
2464 &mut self,
2465 direction: SplitDirection,
2466 cx: &mut ViewContext<Self>,
2467 ) {
2468 if let Some(to) = self
2469 .find_pane_in_direction(direction, cx)
2470 .map(|pane| pane.clone())
2471 {
2472 self.center.swap(&self.active_pane.clone(), &to);
2473 cx.notify();
2474 }
2475 }
2476
2477 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2478 // This is explicitly hoisted out of the following check for pane identity as
2479 // terminal panel panes are not registered as a center panes.
2480 self.status_bar.update(cx, |status_bar, cx| {
2481 status_bar.set_active_pane(&pane, cx);
2482 });
2483 if self.active_pane != pane {
2484 self.active_pane = pane.clone();
2485 self.active_item_path_changed(cx);
2486 self.last_active_center_pane = Some(pane.downgrade());
2487 }
2488
2489 self.dismiss_zoomed_items_to_reveal(None, cx);
2490 if pane.read(cx).is_zoomed() {
2491 self.zoomed = Some(pane.downgrade().into());
2492 } else {
2493 self.zoomed = None;
2494 }
2495 self.zoomed_position = None;
2496 cx.emit(Event::ZoomChanged);
2497 self.update_active_view_for_followers(cx);
2498
2499 cx.notify();
2500 }
2501
2502 fn handle_pane_event(
2503 &mut self,
2504 pane: View<Pane>,
2505 event: &pane::Event,
2506 cx: &mut ViewContext<Self>,
2507 ) {
2508 match event {
2509 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2510 pane::Event::Split(direction) => {
2511 self.split_and_clone(pane, *direction, cx);
2512 }
2513 pane::Event::Remove => self.remove_pane(pane, cx),
2514 pane::Event::ActivateItem { local } => {
2515 if *local {
2516 self.unfollow(&pane, cx);
2517 }
2518 if &pane == self.active_pane() {
2519 self.active_item_path_changed(cx);
2520 self.update_active_view_for_followers(cx);
2521 }
2522 }
2523 pane::Event::ChangeItemTitle => {
2524 if pane == self.active_pane {
2525 self.active_item_path_changed(cx);
2526 }
2527 self.update_window_edited(cx);
2528 }
2529 pane::Event::RemoveItem { item_id } => {
2530 cx.emit(Event::ActiveItemChanged);
2531 self.update_window_edited(cx);
2532 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2533 if entry.get().entity_id() == pane.entity_id() {
2534 entry.remove();
2535 }
2536 }
2537 }
2538 pane::Event::Focus => {
2539 self.handle_pane_focused(pane.clone(), cx);
2540 }
2541 pane::Event::ZoomIn => {
2542 if pane == self.active_pane {
2543 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2544 if pane.read(cx).has_focus(cx) {
2545 self.zoomed = Some(pane.downgrade().into());
2546 self.zoomed_position = None;
2547 cx.emit(Event::ZoomChanged);
2548 }
2549 cx.notify();
2550 }
2551 }
2552 pane::Event::ZoomOut => {
2553 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2554 if self.zoomed_position.is_none() {
2555 self.zoomed = None;
2556 cx.emit(Event::ZoomChanged);
2557 }
2558 cx.notify();
2559 }
2560 }
2561
2562 self.serialize_workspace(cx).detach();
2563 }
2564
2565 pub fn split_pane(
2566 &mut self,
2567 pane_to_split: View<Pane>,
2568 split_direction: SplitDirection,
2569 cx: &mut ViewContext<Self>,
2570 ) -> View<Pane> {
2571 let new_pane = self.add_pane(cx);
2572 self.center
2573 .split(&pane_to_split, &new_pane, split_direction)
2574 .unwrap();
2575 cx.notify();
2576 new_pane
2577 }
2578
2579 pub fn split_and_clone(
2580 &mut self,
2581 pane: View<Pane>,
2582 direction: SplitDirection,
2583 cx: &mut ViewContext<Self>,
2584 ) -> Option<View<Pane>> {
2585 let item = pane.read(cx).active_item()?;
2586 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2587 let new_pane = self.add_pane(cx);
2588 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2589 self.center.split(&pane, &new_pane, direction).unwrap();
2590 Some(new_pane)
2591 } else {
2592 None
2593 };
2594 cx.notify();
2595 maybe_pane_handle
2596 }
2597
2598 pub fn split_pane_with_item(
2599 &mut self,
2600 pane_to_split: WeakView<Pane>,
2601 split_direction: SplitDirection,
2602 from: WeakView<Pane>,
2603 item_id_to_move: EntityId,
2604 cx: &mut ViewContext<Self>,
2605 ) {
2606 let Some(pane_to_split) = pane_to_split.upgrade() else {
2607 return;
2608 };
2609 let Some(from) = from.upgrade() else {
2610 return;
2611 };
2612
2613 let new_pane = self.add_pane(cx);
2614 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2615 self.center
2616 .split(&pane_to_split, &new_pane, split_direction)
2617 .unwrap();
2618 cx.notify();
2619 }
2620
2621 pub fn split_pane_with_project_entry(
2622 &mut self,
2623 pane_to_split: WeakView<Pane>,
2624 split_direction: SplitDirection,
2625 project_entry: ProjectEntryId,
2626 cx: &mut ViewContext<Self>,
2627 ) -> Option<Task<Result<()>>> {
2628 let pane_to_split = pane_to_split.upgrade()?;
2629 let new_pane = self.add_pane(cx);
2630 self.center
2631 .split(&pane_to_split, &new_pane, split_direction)
2632 .unwrap();
2633
2634 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2635 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2636 Some(cx.foreground_executor().spawn(async move {
2637 task.await?;
2638 Ok(())
2639 }))
2640 }
2641
2642 pub fn move_item(
2643 &mut self,
2644 source: View<Pane>,
2645 destination: View<Pane>,
2646 item_id_to_move: EntityId,
2647 destination_index: usize,
2648 cx: &mut ViewContext<Self>,
2649 ) {
2650 let Some((item_ix, item_handle)) = source
2651 .read(cx)
2652 .items()
2653 .enumerate()
2654 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2655 else {
2656 // Tab was closed during drag
2657 return;
2658 };
2659
2660 let item_handle = item_handle.clone();
2661
2662 if source != destination {
2663 // Close item from previous pane
2664 source.update(cx, |source, cx| {
2665 source.remove_item(item_ix, false, true, cx);
2666 });
2667 }
2668
2669 // This automatically removes duplicate items in the pane
2670 destination.update(cx, |destination, cx| {
2671 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2672 destination.focus(cx)
2673 });
2674 }
2675
2676 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2677 if self.center.remove(&pane).unwrap() {
2678 self.force_remove_pane(&pane, cx);
2679 self.unfollow(&pane, cx);
2680 self.last_leaders_by_pane.remove(&pane.downgrade());
2681 for removed_item in pane.read(cx).items() {
2682 self.panes_by_item.remove(&removed_item.item_id());
2683 }
2684
2685 cx.notify();
2686 } else {
2687 self.active_item_path_changed(cx);
2688 }
2689 }
2690
2691 pub fn panes(&self) -> &[View<Pane>] {
2692 &self.panes
2693 }
2694
2695 pub fn active_pane(&self) -> &View<Pane> {
2696 &self.active_pane
2697 }
2698
2699 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2700 self.find_pane_in_direction(SplitDirection::Right, cx)
2701 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
2702 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
2703 .clone()
2704 }
2705
2706 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2707 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2708 weak_pane.upgrade()
2709 }
2710
2711 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2712 self.follower_states.retain(|_, state| {
2713 if state.leader_id == peer_id {
2714 for item in state.items_by_leader_view_id.values() {
2715 item.set_leader_peer_id(None, cx);
2716 }
2717 false
2718 } else {
2719 true
2720 }
2721 });
2722 cx.notify();
2723 }
2724
2725 pub fn start_following(
2726 &mut self,
2727 leader_id: PeerId,
2728 cx: &mut ViewContext<Self>,
2729 ) -> Option<Task<Result<()>>> {
2730 let pane = self.active_pane().clone();
2731
2732 self.last_leaders_by_pane
2733 .insert(pane.downgrade(), leader_id);
2734 self.unfollow(&pane, cx);
2735 self.follower_states.insert(
2736 pane.clone(),
2737 FollowerState {
2738 leader_id,
2739 active_view_id: None,
2740 items_by_leader_view_id: Default::default(),
2741 },
2742 );
2743 cx.notify();
2744
2745 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2746 let project_id = self.project.read(cx).remote_id();
2747 let request = self.app_state.client.request(proto::Follow {
2748 room_id,
2749 project_id,
2750 leader_id: Some(leader_id),
2751 });
2752
2753 Some(cx.spawn(|this, mut cx| async move {
2754 let response = request.await?;
2755 this.update(&mut cx, |this, _| {
2756 let state = this
2757 .follower_states
2758 .get_mut(&pane)
2759 .ok_or_else(|| anyhow!("following interrupted"))?;
2760 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2761 Some(ViewId::from_proto(active_view_id)?)
2762 } else {
2763 None
2764 };
2765 Ok::<_, anyhow::Error>(())
2766 })??;
2767 if let Some(view) = response.active_view {
2768 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
2769 .await?;
2770 }
2771 Self::add_views_from_leader(
2772 this.clone(),
2773 leader_id,
2774 vec![pane],
2775 response.views,
2776 &mut cx,
2777 )
2778 .await?;
2779 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2780 Ok(())
2781 }))
2782 }
2783
2784 pub fn follow_next_collaborator(
2785 &mut self,
2786 _: &FollowNextCollaborator,
2787 cx: &mut ViewContext<Self>,
2788 ) {
2789 let collaborators = self.project.read(cx).collaborators();
2790 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2791 let mut collaborators = collaborators.keys().copied();
2792 for peer_id in collaborators.by_ref() {
2793 if peer_id == leader_id {
2794 break;
2795 }
2796 }
2797 collaborators.next()
2798 } else if let Some(last_leader_id) =
2799 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2800 {
2801 if collaborators.contains_key(last_leader_id) {
2802 Some(*last_leader_id)
2803 } else {
2804 None
2805 }
2806 } else {
2807 None
2808 };
2809
2810 let pane = self.active_pane.clone();
2811 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2812 else {
2813 return;
2814 };
2815 if Some(leader_id) == self.unfollow(&pane, cx) {
2816 return;
2817 }
2818 if let Some(task) = self.start_following(leader_id, cx) {
2819 task.detach_and_log_err(cx)
2820 }
2821 }
2822
2823 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2824 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2825 return;
2826 };
2827 let room = room.read(cx);
2828 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2829 return;
2830 };
2831
2832 let project = self.project.read(cx);
2833
2834 let other_project_id = match remote_participant.location {
2835 call::ParticipantLocation::External => None,
2836 call::ParticipantLocation::UnsharedProject => None,
2837 call::ParticipantLocation::SharedProject { project_id } => {
2838 if Some(project_id) == project.remote_id() {
2839 None
2840 } else {
2841 Some(project_id)
2842 }
2843 }
2844 };
2845
2846 // if they are active in another project, follow there.
2847 if let Some(project_id) = other_project_id {
2848 let app_state = self.app_state.clone();
2849 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
2850 .detach_and_log_err(cx);
2851 }
2852
2853 // if you're already following, find the right pane and focus it.
2854 for (pane, state) in &self.follower_states {
2855 if leader_id == state.leader_id {
2856 cx.focus_view(pane);
2857 return;
2858 }
2859 }
2860
2861 // Otherwise, follow.
2862 if let Some(task) = self.start_following(leader_id, cx) {
2863 task.detach_and_log_err(cx)
2864 }
2865 }
2866
2867 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2868 let state = self.follower_states.remove(pane)?;
2869 let leader_id = state.leader_id;
2870 for (_, item) in state.items_by_leader_view_id {
2871 item.set_leader_peer_id(None, cx);
2872 }
2873
2874 if self
2875 .follower_states
2876 .values()
2877 .all(|state| state.leader_id != leader_id)
2878 {
2879 let project_id = self.project.read(cx).remote_id();
2880 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2881 self.app_state
2882 .client
2883 .send(proto::Unfollow {
2884 room_id,
2885 project_id,
2886 leader_id: Some(leader_id),
2887 })
2888 .log_err();
2889 }
2890
2891 cx.notify();
2892 Some(leader_id)
2893 }
2894
2895 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2896 self.follower_states
2897 .values()
2898 .any(|state| state.leader_id == peer_id)
2899 }
2900
2901 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2902 cx.emit(Event::ActiveItemChanged);
2903 let active_entry = self.active_project_path(cx);
2904 self.project
2905 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2906
2907 self.update_window_title(cx);
2908 }
2909
2910 fn update_window_title(&mut self, cx: &mut WindowContext) {
2911 let project = self.project().read(cx);
2912 let mut title = String::new();
2913
2914 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2915 let filename = path
2916 .path
2917 .file_name()
2918 .map(|s| s.to_string_lossy())
2919 .or_else(|| {
2920 Some(Cow::Borrowed(
2921 project
2922 .worktree_for_id(path.worktree_id, cx)?
2923 .read(cx)
2924 .root_name(),
2925 ))
2926 });
2927
2928 if let Some(filename) = filename {
2929 title.push_str(filename.as_ref());
2930 title.push_str(" — ");
2931 }
2932 }
2933
2934 for (i, name) in project.worktree_root_names(cx).enumerate() {
2935 if i > 0 {
2936 title.push_str(", ");
2937 }
2938 title.push_str(name);
2939 }
2940
2941 if title.is_empty() {
2942 title = "empty project".to_string();
2943 }
2944
2945 if project.is_remote() {
2946 title.push_str(" ↙");
2947 } else if project.is_shared() {
2948 title.push_str(" ↗");
2949 }
2950
2951 cx.set_window_title(&title);
2952 }
2953
2954 fn update_window_edited(&mut self, cx: &mut WindowContext) {
2955 let is_edited = !self.project.read(cx).is_disconnected()
2956 && self
2957 .items(cx)
2958 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2959 if is_edited != self.window_edited {
2960 self.window_edited = is_edited;
2961 cx.set_window_edited(self.window_edited)
2962 }
2963 }
2964
2965 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2966 if self.notifications.is_empty() {
2967 None
2968 } else {
2969 Some(
2970 div()
2971 .absolute()
2972 .right_3()
2973 .bottom_3()
2974 .w_112()
2975 .h_full()
2976 .flex()
2977 .flex_col()
2978 .justify_end()
2979 .gap_2()
2980 .children(
2981 self.notifications
2982 .iter()
2983 .map(|(_, notification)| notification.to_any()),
2984 ),
2985 )
2986 }
2987 }
2988
2989 // RPC handlers
2990
2991 fn active_view_for_follower(
2992 &self,
2993 follower_project_id: Option<u64>,
2994 cx: &mut ViewContext<Self>,
2995 ) -> Option<proto::View> {
2996 let item = self.active_item(cx)?;
2997 let leader_id = self
2998 .pane_for(&*item)
2999 .and_then(|pane| self.leader_for_pane(&pane));
3000
3001 let item_handle = item.to_followable_item_handle(cx)?;
3002 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3003 let variant = item_handle.to_state_proto(cx)?;
3004
3005 if item_handle.is_project_item(cx)
3006 && (follower_project_id.is_none()
3007 || follower_project_id != self.project.read(cx).remote_id())
3008 {
3009 return None;
3010 }
3011
3012 Some(proto::View {
3013 id: Some(id.to_proto()),
3014 leader_id,
3015 variant: Some(variant),
3016 })
3017 }
3018
3019 fn handle_follow(
3020 &mut self,
3021 follower_project_id: Option<u64>,
3022 cx: &mut ViewContext<Self>,
3023 ) -> proto::FollowResponse {
3024 let client = &self.app_state.client;
3025 let project_id = self.project.read(cx).remote_id();
3026
3027 let active_view = self.active_view_for_follower(follower_project_id, cx);
3028 let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
3029
3030 cx.notify();
3031
3032 proto::FollowResponse {
3033 active_view,
3034 // TODO: once v0.124.0 is retired we can stop sending these
3035 active_view_id,
3036 views: self
3037 .panes()
3038 .iter()
3039 .flat_map(|pane| {
3040 let leader_id = self.leader_for_pane(pane);
3041 pane.read(cx).items().filter_map({
3042 let cx = &cx;
3043 move |item| {
3044 let item = item.to_followable_item_handle(cx)?;
3045
3046 // If the item belongs to a particular project, then it should
3047 // only be included if this project is shared, and the follower
3048 // is in the project.
3049 //
3050 // Some items, like channel notes, do not belong to a particular
3051 // project, so they should be included regardless of whether the
3052 // current project is shared, or what project the follower is in.
3053 if item.is_project_item(cx)
3054 && (project_id.is_none() || project_id != follower_project_id)
3055 {
3056 return None;
3057 }
3058
3059 let id = item.remote_id(client, cx)?.to_proto();
3060 let variant = item.to_state_proto(cx)?;
3061 Some(proto::View {
3062 id: Some(id),
3063 leader_id,
3064 variant: Some(variant),
3065 })
3066 }
3067 })
3068 })
3069 .collect(),
3070 }
3071 }
3072
3073 fn handle_update_followers(
3074 &mut self,
3075 leader_id: PeerId,
3076 message: proto::UpdateFollowers,
3077 _cx: &mut ViewContext<Self>,
3078 ) {
3079 self.leader_updates_tx
3080 .unbounded_send((leader_id, message))
3081 .ok();
3082 }
3083
3084 async fn process_leader_update(
3085 this: &WeakView<Self>,
3086 leader_id: PeerId,
3087 update: proto::UpdateFollowers,
3088 cx: &mut AsyncWindowContext,
3089 ) -> Result<()> {
3090 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3091 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3092 let panes_missing_view = this.update(cx, |this, _| {
3093 let mut panes = vec![];
3094 for (pane, state) in &mut this.follower_states {
3095 if state.leader_id != leader_id {
3096 continue;
3097 }
3098
3099 state.active_view_id =
3100 if let Some(active_view_id) = update_active_view.id.clone() {
3101 Some(ViewId::from_proto(active_view_id)?)
3102 } else {
3103 None
3104 };
3105
3106 if state.active_view_id.is_some_and(|view_id| {
3107 !state.items_by_leader_view_id.contains_key(&view_id)
3108 }) {
3109 panes.push(pane.clone())
3110 }
3111 }
3112 anyhow::Ok(panes)
3113 })??;
3114
3115 if let Some(view) = update_active_view.view {
3116 for pane in panes_missing_view {
3117 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
3118 .await?
3119 }
3120 }
3121 }
3122 proto::update_followers::Variant::UpdateView(update_view) => {
3123 let variant = update_view
3124 .variant
3125 .ok_or_else(|| anyhow!("missing update view variant"))?;
3126 let id = update_view
3127 .id
3128 .ok_or_else(|| anyhow!("missing update view id"))?;
3129 let mut tasks = Vec::new();
3130 this.update(cx, |this, cx| {
3131 let project = this.project.clone();
3132 for (_, state) in &mut this.follower_states {
3133 if state.leader_id == leader_id {
3134 let view_id = ViewId::from_proto(id.clone())?;
3135 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3136 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
3137 }
3138 }
3139 }
3140 anyhow::Ok(())
3141 })??;
3142 try_join_all(tasks).await.log_err();
3143 }
3144 proto::update_followers::Variant::CreateView(view) => {
3145 let panes = this.update(cx, |this, _| {
3146 this.follower_states
3147 .iter()
3148 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
3149 .cloned()
3150 .collect()
3151 })?;
3152 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
3153 }
3154 }
3155 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3156 Ok(())
3157 }
3158
3159 async fn add_view_from_leader(
3160 this: WeakView<Self>,
3161 leader_id: PeerId,
3162 pane: View<Pane>,
3163 view: &proto::View,
3164 cx: &mut AsyncWindowContext,
3165 ) -> Result<()> {
3166 let this = this.upgrade().context("workspace dropped")?;
3167
3168 let item_builders = cx.update(|cx| {
3169 cx.default_global::<FollowableItemBuilders>()
3170 .values()
3171 .map(|b| b.0)
3172 .collect::<Vec<_>>()
3173 })?;
3174
3175 let Some(id) = view.id.clone() else {
3176 return Err(anyhow!("no id for view"));
3177 };
3178 let id = ViewId::from_proto(id)?;
3179
3180 let mut variant = view.variant.clone();
3181 if variant.is_none() {
3182 Err(anyhow!("missing view variant"))?;
3183 }
3184
3185 let task = item_builders.iter().find_map(|build_item| {
3186 cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
3187 .log_err()
3188 .flatten()
3189 });
3190 let Some(task) = task else {
3191 return Err(anyhow!(
3192 "failed to construct view from leader (maybe from a different version of zed?)"
3193 ));
3194 };
3195
3196 let item = task.await?;
3197
3198 this.update(cx, |this, cx| {
3199 let state = this.follower_states.get_mut(&pane)?;
3200 item.set_leader_peer_id(Some(leader_id), cx);
3201 state.items_by_leader_view_id.insert(id, item);
3202
3203 Some(())
3204 })?;
3205
3206 Ok(())
3207 }
3208
3209 async fn add_views_from_leader(
3210 this: WeakView<Self>,
3211 leader_id: PeerId,
3212 panes: Vec<View<Pane>>,
3213 views: Vec<proto::View>,
3214 cx: &mut AsyncWindowContext,
3215 ) -> Result<()> {
3216 let this = this.upgrade().context("workspace dropped")?;
3217
3218 let item_builders = cx.update(|cx| {
3219 cx.default_global::<FollowableItemBuilders>()
3220 .values()
3221 .map(|b| b.0)
3222 .collect::<Vec<_>>()
3223 })?;
3224
3225 let mut item_tasks_by_pane = HashMap::default();
3226 for pane in panes {
3227 let mut item_tasks = Vec::new();
3228 let mut leader_view_ids = Vec::new();
3229 for view in &views {
3230 let Some(id) = &view.id else {
3231 continue;
3232 };
3233 let id = ViewId::from_proto(id.clone())?;
3234 let mut variant = view.variant.clone();
3235 if variant.is_none() {
3236 Err(anyhow!("missing view variant"))?;
3237 }
3238 for build_item in &item_builders {
3239 let task = cx.update(|cx| {
3240 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3241 })?;
3242 if let Some(task) = task {
3243 item_tasks.push(task);
3244 leader_view_ids.push(id);
3245 break;
3246 } else if variant.is_none() {
3247 Err(anyhow!(
3248 "failed to construct view from leader (maybe from a different version of zed?)"
3249 ))?;
3250 }
3251 }
3252 }
3253
3254 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3255 }
3256
3257 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3258 let items = futures::future::try_join_all(item_tasks).await?;
3259 this.update(cx, |this, cx| {
3260 let state = this.follower_states.get_mut(&pane)?;
3261 for (id, item) in leader_view_ids.into_iter().zip(items) {
3262 item.set_leader_peer_id(Some(leader_id), cx);
3263 state.items_by_leader_view_id.insert(id, item);
3264 }
3265
3266 Some(())
3267 })?;
3268 }
3269 Ok(())
3270 }
3271
3272 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3273 let mut is_project_item = true;
3274 let mut update = proto::UpdateActiveView::default();
3275 if cx.is_window_active() {
3276 if let Some(item) = self.active_item(cx) {
3277 if item.focus_handle(cx).contains_focused(cx) {
3278 let leader_id = self
3279 .pane_for(&*item)
3280 .and_then(|pane| self.leader_for_pane(&pane));
3281
3282 if let Some(item) = item.to_followable_item_handle(cx) {
3283 let id = item
3284 .remote_id(&self.app_state.client, cx)
3285 .map(|id| id.to_proto());
3286
3287 if let Some(id) = id.clone() {
3288 if let Some(variant) = item.to_state_proto(cx) {
3289 let view = Some(proto::View {
3290 id: Some(id.clone()),
3291 leader_id,
3292 variant: Some(variant),
3293 });
3294
3295 is_project_item = item.is_project_item(cx);
3296 update = proto::UpdateActiveView {
3297 view,
3298 // TODO: once v0.124.0 is retired we can stop sending these
3299 id: Some(id),
3300 leader_id,
3301 };
3302 }
3303 };
3304 }
3305 }
3306 }
3307 }
3308
3309 if &update.id != &self.last_active_view_id {
3310 self.last_active_view_id = update.id.clone();
3311 self.update_followers(
3312 is_project_item,
3313 proto::update_followers::Variant::UpdateActiveView(update),
3314 cx,
3315 );
3316 }
3317 }
3318
3319 fn update_followers(
3320 &self,
3321 project_only: bool,
3322 update: proto::update_followers::Variant,
3323 cx: &mut WindowContext,
3324 ) -> Option<()> {
3325 // If this update only applies to for followers in the current project,
3326 // then skip it unless this project is shared. If it applies to all
3327 // followers, regardless of project, then set `project_id` to none,
3328 // indicating that it goes to all followers.
3329 let project_id = if project_only {
3330 Some(self.project.read(cx).remote_id()?)
3331 } else {
3332 None
3333 };
3334 self.app_state().workspace_store.update(cx, |store, cx| {
3335 store.update_followers(project_id, update, cx)
3336 })
3337 }
3338
3339 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3340 self.follower_states.get(pane).map(|state| state.leader_id)
3341 }
3342
3343 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3344 cx.notify();
3345
3346 let call = self.active_call()?;
3347 let room = call.read(cx).room()?.read(cx);
3348 let participant = room.remote_participant_for_peer_id(leader_id)?;
3349 let mut items_to_activate = Vec::new();
3350
3351 let leader_in_this_app;
3352 let leader_in_this_project;
3353 match participant.location {
3354 call::ParticipantLocation::SharedProject { project_id } => {
3355 leader_in_this_app = true;
3356 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3357 }
3358 call::ParticipantLocation::UnsharedProject => {
3359 leader_in_this_app = true;
3360 leader_in_this_project = false;
3361 }
3362 call::ParticipantLocation::External => {
3363 leader_in_this_app = false;
3364 leader_in_this_project = false;
3365 }
3366 };
3367
3368 for (pane, state) in &self.follower_states {
3369 if state.leader_id != leader_id {
3370 continue;
3371 }
3372 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3373 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3374 if leader_in_this_project || !item.is_project_item(cx) {
3375 items_to_activate.push((pane.clone(), item.boxed_clone()));
3376 }
3377 }
3378 continue;
3379 }
3380
3381 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3382 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3383 }
3384 }
3385
3386 for (pane, item) in items_to_activate {
3387 let pane_was_focused = pane.read(cx).has_focus(cx);
3388 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3389 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3390 } else {
3391 pane.update(cx, |pane, cx| {
3392 pane.add_item(item.boxed_clone(), false, false, None, cx)
3393 });
3394 }
3395
3396 if pane_was_focused {
3397 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3398 }
3399 }
3400
3401 None
3402 }
3403
3404 fn shared_screen_for_peer(
3405 &self,
3406 peer_id: PeerId,
3407 pane: &View<Pane>,
3408 cx: &mut WindowContext,
3409 ) -> Option<View<SharedScreen>> {
3410 let call = self.active_call()?;
3411 let room = call.read(cx).room()?.read(cx);
3412 let participant = room.remote_participant_for_peer_id(peer_id)?;
3413 let track = participant.video_tracks.values().next()?.clone();
3414 let user = participant.user.clone();
3415
3416 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3417 if item.read(cx).peer_id == peer_id {
3418 return Some(item);
3419 }
3420 }
3421
3422 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3423 }
3424
3425 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3426 if cx.is_window_active() {
3427 self.update_active_view_for_followers(cx);
3428 cx.background_executor()
3429 .spawn(persistence::DB.update_timestamp(self.database_id()))
3430 .detach();
3431 } else {
3432 for pane in &self.panes {
3433 pane.update(cx, |pane, cx| {
3434 if let Some(item) = pane.active_item() {
3435 item.workspace_deactivated(cx);
3436 }
3437 if matches!(
3438 WorkspaceSettings::get_global(cx).autosave,
3439 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3440 ) {
3441 for item in pane.items() {
3442 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3443 .detach_and_log_err(cx);
3444 }
3445 }
3446 });
3447 }
3448 }
3449 }
3450
3451 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3452 self.active_call.as_ref().map(|(call, _)| call)
3453 }
3454
3455 fn on_active_call_event(
3456 &mut self,
3457 _: Model<ActiveCall>,
3458 event: &call::room::Event,
3459 cx: &mut ViewContext<Self>,
3460 ) {
3461 match event {
3462 call::room::Event::ParticipantLocationChanged { participant_id }
3463 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3464 self.leader_updated(*participant_id, cx);
3465 }
3466 _ => {}
3467 }
3468 }
3469
3470 pub fn database_id(&self) -> WorkspaceId {
3471 self.database_id
3472 }
3473
3474 fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
3475 let project = self.project().read(cx);
3476
3477 if project.is_local() {
3478 Some(LocalPaths::new(
3479 project
3480 .visible_worktrees(cx)
3481 .map(|worktree| worktree.read(cx).abs_path())
3482 .collect::<Vec<_>>(),
3483 ))
3484 } else {
3485 None
3486 }
3487 }
3488
3489 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3490 match member {
3491 Member::Axis(PaneAxis { members, .. }) => {
3492 for child in members.iter() {
3493 self.remove_panes(child.clone(), cx)
3494 }
3495 }
3496 Member::Pane(pane) => {
3497 self.force_remove_pane(&pane, cx);
3498 }
3499 }
3500 }
3501
3502 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3503 self.panes.retain(|p| p != pane);
3504 self.panes
3505 .last()
3506 .unwrap()
3507 .update(cx, |pane, cx| pane.focus(cx));
3508 if self.last_active_center_pane == Some(pane.downgrade()) {
3509 self.last_active_center_pane = None;
3510 }
3511 cx.notify();
3512 }
3513
3514 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3515 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3516 cx.background_executor()
3517 .timer(Duration::from_millis(100))
3518 .await;
3519 this.update(&mut cx, |this, cx| this.serialize_workspace(cx).detach())
3520 .log_err();
3521 }));
3522 }
3523
3524 fn serialize_workspace(&self, cx: &mut WindowContext) -> Task<()> {
3525 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3526 let (items, active) = {
3527 let pane = pane_handle.read(cx);
3528 let active_item_id = pane.active_item().map(|item| item.item_id());
3529 (
3530 pane.items()
3531 .filter_map(|item_handle| {
3532 Some(SerializedItem {
3533 kind: Arc::from(item_handle.serialized_item_kind()?),
3534 item_id: item_handle.item_id().as_u64(),
3535 active: Some(item_handle.item_id()) == active_item_id,
3536 preview: pane.is_active_preview_item(item_handle.item_id()),
3537 })
3538 })
3539 .collect::<Vec<_>>(),
3540 pane.has_focus(cx),
3541 )
3542 };
3543
3544 SerializedPane::new(items, active)
3545 }
3546
3547 fn build_serialized_pane_group(
3548 pane_group: &Member,
3549 cx: &WindowContext,
3550 ) -> SerializedPaneGroup {
3551 match pane_group {
3552 Member::Axis(PaneAxis {
3553 axis,
3554 members,
3555 flexes,
3556 bounding_boxes: _,
3557 }) => SerializedPaneGroup::Group {
3558 axis: SerializedAxis(*axis),
3559 children: members
3560 .iter()
3561 .map(|member| build_serialized_pane_group(member, cx))
3562 .collect::<Vec<_>>(),
3563 flexes: Some(flexes.lock().clone()),
3564 },
3565 Member::Pane(pane_handle) => {
3566 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3567 }
3568 }
3569 }
3570
3571 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3572 let left_dock = this.left_dock.read(cx);
3573 let left_visible = left_dock.is_open();
3574 let left_active_panel = left_dock
3575 .visible_panel()
3576 .map(|panel| panel.persistent_name().to_string());
3577 let left_dock_zoom = left_dock
3578 .visible_panel()
3579 .map(|panel| panel.is_zoomed(cx))
3580 .unwrap_or(false);
3581
3582 let right_dock = this.right_dock.read(cx);
3583 let right_visible = right_dock.is_open();
3584 let right_active_panel = right_dock
3585 .visible_panel()
3586 .map(|panel| panel.persistent_name().to_string());
3587 let right_dock_zoom = right_dock
3588 .visible_panel()
3589 .map(|panel| panel.is_zoomed(cx))
3590 .unwrap_or(false);
3591
3592 let bottom_dock = this.bottom_dock.read(cx);
3593 let bottom_visible = bottom_dock.is_open();
3594 let bottom_active_panel = bottom_dock
3595 .visible_panel()
3596 .map(|panel| panel.persistent_name().to_string());
3597 let bottom_dock_zoom = bottom_dock
3598 .visible_panel()
3599 .map(|panel| panel.is_zoomed(cx))
3600 .unwrap_or(false);
3601
3602 DockStructure {
3603 left: DockData {
3604 visible: left_visible,
3605 active_panel: left_active_panel,
3606 zoom: left_dock_zoom,
3607 },
3608 right: DockData {
3609 visible: right_visible,
3610 active_panel: right_active_panel,
3611 zoom: right_dock_zoom,
3612 },
3613 bottom: DockData {
3614 visible: bottom_visible,
3615 active_panel: bottom_active_panel,
3616 zoom: bottom_dock_zoom,
3617 },
3618 }
3619 }
3620
3621 let location = if let Some(local_paths) = self.local_paths(cx) {
3622 if !local_paths.paths().is_empty() {
3623 Some(SerializedWorkspaceLocation::Local(local_paths))
3624 } else {
3625 None
3626 }
3627 } else if let Some(remote_project_id) = self.project().read(cx).remote_project_id() {
3628 let store = remote_projects::Store::global(cx).read(cx);
3629 maybe!({
3630 let project = store.remote_project(remote_project_id)?;
3631 let dev_server = store.dev_server(project.dev_server_id)?;
3632
3633 let remote_project = SerializedRemoteProject {
3634 id: remote_project_id,
3635 dev_server_name: dev_server.name.to_string(),
3636 path: project.path.to_string(),
3637 };
3638 Some(SerializedWorkspaceLocation::Remote(remote_project))
3639 })
3640 } else {
3641 None
3642 };
3643
3644 // don't save workspace state for the empty workspace.
3645 if let Some(location) = location {
3646 let center_group = build_serialized_pane_group(&self.center.root, cx);
3647 let docks = build_serialized_docks(self, cx);
3648 let serialized_workspace = SerializedWorkspace {
3649 id: self.database_id,
3650 location,
3651 center_group,
3652 bounds: Default::default(),
3653 display: Default::default(),
3654 docks,
3655 fullscreen: cx.is_fullscreen(),
3656 centered_layout: self.centered_layout,
3657 };
3658 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3659 }
3660 Task::ready(())
3661 }
3662
3663 pub(crate) fn load_workspace(
3664 serialized_workspace: SerializedWorkspace,
3665 paths_to_open: Vec<Option<ProjectPath>>,
3666 cx: &mut ViewContext<Workspace>,
3667 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3668 cx.spawn(|workspace, mut cx| async move {
3669 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3670
3671 let mut center_group = None;
3672 let mut center_items = None;
3673
3674 // Traverse the splits tree and add to things
3675 if let Some((group, active_pane, items)) = serialized_workspace
3676 .center_group
3677 .deserialize(
3678 &project,
3679 serialized_workspace.id,
3680 workspace.clone(),
3681 &mut cx,
3682 )
3683 .await
3684 {
3685 center_items = Some(items);
3686 center_group = Some((group, active_pane))
3687 }
3688
3689 let mut items_by_project_path = cx.update(|cx| {
3690 center_items
3691 .unwrap_or_default()
3692 .into_iter()
3693 .filter_map(|item| {
3694 let item = item?;
3695 let project_path = item.project_path(cx)?;
3696 Some((project_path, item))
3697 })
3698 .collect::<HashMap<_, _>>()
3699 })?;
3700
3701 let opened_items = paths_to_open
3702 .into_iter()
3703 .map(|path_to_open| {
3704 path_to_open
3705 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3706 })
3707 .collect::<Vec<_>>();
3708
3709 // Remove old panes from workspace panes list
3710 workspace.update(&mut cx, |workspace, cx| {
3711 if let Some((center_group, active_pane)) = center_group {
3712 workspace.remove_panes(workspace.center.root.clone(), cx);
3713
3714 // Swap workspace center group
3715 workspace.center = PaneGroup::with_root(center_group);
3716 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3717 if let Some(active_pane) = active_pane {
3718 workspace.active_pane = active_pane;
3719 cx.focus_self();
3720 } else {
3721 workspace.active_pane = workspace.center.first_pane().clone();
3722 }
3723 }
3724
3725 let docks = serialized_workspace.docks;
3726
3727 let right = docks.right.clone();
3728 workspace
3729 .right_dock
3730 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3731 let left = docks.left.clone();
3732 workspace
3733 .left_dock
3734 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3735 let bottom = docks.bottom.clone();
3736 workspace
3737 .bottom_dock
3738 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3739
3740 cx.notify();
3741 })?;
3742
3743 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3744 workspace.update(&mut cx, |workspace, cx| {
3745 workspace.serialize_workspace(cx).detach()
3746 })?;
3747
3748 Ok(opened_items)
3749 })
3750 }
3751
3752 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3753 self.add_workspace_actions_listeners(div, cx)
3754 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3755 .on_action(cx.listener(Self::close_all_items_and_panes))
3756 .on_action(cx.listener(Self::save_all))
3757 .on_action(cx.listener(Self::send_keystrokes))
3758 .on_action(cx.listener(Self::add_folder_to_project))
3759 .on_action(cx.listener(Self::follow_next_collaborator))
3760 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3761 let pane = workspace.active_pane().clone();
3762 workspace.unfollow(&pane, cx);
3763 }))
3764 .on_action(cx.listener(|workspace, action: &Save, cx| {
3765 workspace
3766 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3767 .detach_and_log_err(cx);
3768 }))
3769 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
3770 workspace
3771 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
3772 .detach_and_log_err(cx);
3773 }))
3774 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3775 workspace
3776 .save_active_item(SaveIntent::SaveAs, cx)
3777 .detach_and_log_err(cx);
3778 }))
3779 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3780 workspace.activate_previous_pane(cx)
3781 }))
3782 .on_action(
3783 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3784 )
3785 .on_action(
3786 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3787 workspace.activate_pane_in_direction(action.0, cx)
3788 }),
3789 )
3790 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3791 workspace.swap_pane_in_direction(action.0, cx)
3792 }))
3793 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3794 this.toggle_dock(DockPosition::Left, cx);
3795 }))
3796 .on_action(
3797 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3798 workspace.toggle_dock(DockPosition::Right, cx);
3799 }),
3800 )
3801 .on_action(
3802 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3803 workspace.toggle_dock(DockPosition::Bottom, cx);
3804 }),
3805 )
3806 .on_action(
3807 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3808 workspace.close_all_docks(cx);
3809 }),
3810 )
3811 .on_action(cx.listener(Workspace::open))
3812 .on_action(cx.listener(Workspace::close_window))
3813 .on_action(cx.listener(Workspace::activate_pane_at_index))
3814 .on_action(
3815 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3816 workspace.reopen_closed_item(cx).detach();
3817 }),
3818 )
3819 .on_action(cx.listener(Workspace::toggle_centered_layout))
3820 }
3821
3822 #[cfg(any(test, feature = "test-support"))]
3823 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3824 use node_runtime::FakeNodeRuntime;
3825
3826 let client = project.read(cx).client();
3827 let user_store = project.read(cx).user_store();
3828
3829 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3830 cx.activate_window();
3831 let app_state = Arc::new(AppState {
3832 languages: project.read(cx).languages().clone(),
3833 workspace_store,
3834 client,
3835 user_store,
3836 fs: project.read(cx).fs().clone(),
3837 build_window_options: |_, _| Default::default(),
3838 node_runtime: FakeNodeRuntime::new(),
3839 });
3840 let workspace = Self::new(Default::default(), project, app_state, cx);
3841 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3842 workspace
3843 }
3844
3845 pub fn register_action<A: Action>(
3846 &mut self,
3847 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3848 ) -> &mut Self {
3849 let callback = Arc::new(callback);
3850
3851 self.workspace_actions.push(Box::new(move |div, cx| {
3852 let callback = callback.clone();
3853 div.on_action(
3854 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3855 )
3856 }));
3857 self
3858 }
3859
3860 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3861 let mut div = div
3862 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3863 .on_action(cx.listener(Self::close_all_items_and_panes))
3864 .on_action(cx.listener(Self::add_folder_to_project))
3865 .on_action(cx.listener(Self::save_all))
3866 .on_action(cx.listener(Self::open));
3867 for action in self.workspace_actions.iter() {
3868 div = (action)(div, cx)
3869 }
3870 div
3871 }
3872
3873 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3874 self.modal_layer.read(cx).has_active_modal()
3875 }
3876
3877 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3878 self.modal_layer.read(cx).active_modal()
3879 }
3880
3881 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3882 where
3883 B: FnOnce(&mut ViewContext<V>) -> V,
3884 {
3885 self.modal_layer
3886 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3887 }
3888
3889 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
3890 self.centered_layout = !self.centered_layout;
3891 cx.background_executor()
3892 .spawn(DB.set_centered_layout(self.database_id, self.centered_layout))
3893 .detach_and_log_err(cx);
3894 cx.notify();
3895 }
3896
3897 fn adjust_padding(padding: Option<f32>) -> f32 {
3898 padding
3899 .unwrap_or(Self::DEFAULT_PADDING)
3900 .min(Self::MAX_PADDING)
3901 .max(0.0)
3902 }
3903}
3904
3905fn window_bounds_env_override() -> Option<Bounds<DevicePixels>> {
3906 ZED_WINDOW_POSITION
3907 .zip(*ZED_WINDOW_SIZE)
3908 .map(|(position, size)| Bounds {
3909 origin: position,
3910 size,
3911 })
3912}
3913
3914fn open_items(
3915 serialized_workspace: Option<SerializedWorkspace>,
3916 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3917 app_state: Arc<AppState>,
3918 cx: &mut ViewContext<Workspace>,
3919) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3920 let restored_items = serialized_workspace.map(|serialized_workspace| {
3921 Workspace::load_workspace(
3922 serialized_workspace,
3923 project_paths_to_open
3924 .iter()
3925 .map(|(_, project_path)| project_path)
3926 .cloned()
3927 .collect(),
3928 cx,
3929 )
3930 });
3931
3932 cx.spawn(|workspace, mut cx| async move {
3933 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3934
3935 if let Some(restored_items) = restored_items {
3936 let restored_items = restored_items.await?;
3937
3938 let restored_project_paths = restored_items
3939 .iter()
3940 .filter_map(|item| {
3941 cx.update(|cx| item.as_ref()?.project_path(cx))
3942 .ok()
3943 .flatten()
3944 })
3945 .collect::<HashSet<_>>();
3946
3947 for restored_item in restored_items {
3948 opened_items.push(restored_item.map(Ok));
3949 }
3950
3951 project_paths_to_open
3952 .iter_mut()
3953 .for_each(|(_, project_path)| {
3954 if let Some(project_path_to_open) = project_path {
3955 if restored_project_paths.contains(project_path_to_open) {
3956 *project_path = None;
3957 }
3958 }
3959 });
3960 } else {
3961 for _ in 0..project_paths_to_open.len() {
3962 opened_items.push(None);
3963 }
3964 }
3965 assert!(opened_items.len() == project_paths_to_open.len());
3966
3967 let tasks =
3968 project_paths_to_open
3969 .into_iter()
3970 .enumerate()
3971 .map(|(ix, (abs_path, project_path))| {
3972 let workspace = workspace.clone();
3973 cx.spawn(|mut cx| {
3974 let fs = app_state.fs.clone();
3975 async move {
3976 let file_project_path = project_path?;
3977 if fs.is_dir(&abs_path).await {
3978 None
3979 } else {
3980 Some((
3981 ix,
3982 workspace
3983 .update(&mut cx, |workspace, cx| {
3984 workspace.open_path(file_project_path, None, true, cx)
3985 })
3986 .log_err()?
3987 .await,
3988 ))
3989 }
3990 }
3991 })
3992 });
3993
3994 let tasks = tasks.collect::<Vec<_>>();
3995
3996 let tasks = futures::future::join_all(tasks);
3997 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
3998 opened_items[ix] = Some(path_open_result);
3999 }
4000
4001 Ok(opened_items)
4002 })
4003}
4004
4005enum ActivateInDirectionTarget {
4006 Pane(View<Pane>),
4007 Dock(View<Dock>),
4008}
4009
4010fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4011 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4012
4013 workspace
4014 .update(cx, |workspace, cx| {
4015 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4016 struct DatabaseFailedNotification;
4017
4018 workspace.show_notification_once(
4019 NotificationId::unique::<DatabaseFailedNotification>(),
4020 cx,
4021 |cx| {
4022 cx.new_view(|_| {
4023 MessageNotification::new("Failed to load the database file.")
4024 .with_click_message("Click to let us know about this error")
4025 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4026 })
4027 },
4028 );
4029 }
4030 })
4031 .log_err();
4032}
4033
4034impl FocusableView for Workspace {
4035 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4036 self.active_pane.focus_handle(cx)
4037 }
4038}
4039
4040#[derive(Clone, Render)]
4041struct DraggedDock(DockPosition);
4042
4043impl Render for Workspace {
4044 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4045 let mut context = KeyContext::new_with_defaults();
4046 context.add("Workspace");
4047 let centered_layout = self.centered_layout
4048 && self.center.panes().len() == 1
4049 && self.active_item(cx).is_some();
4050 let render_padding = |size| {
4051 (size > 0.0).then(|| {
4052 div()
4053 .h_full()
4054 .w(relative(size))
4055 .bg(cx.theme().colors().editor_background)
4056 .border_color(cx.theme().colors().pane_group_border)
4057 })
4058 };
4059 let paddings = if centered_layout {
4060 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4061 (
4062 render_padding(Self::adjust_padding(settings.left_padding)),
4063 render_padding(Self::adjust_padding(settings.right_padding)),
4064 )
4065 } else {
4066 (None, None)
4067 };
4068 let (ui_font, ui_font_size) = {
4069 let theme_settings = ThemeSettings::get_global(cx);
4070 (
4071 theme_settings.ui_font.family.clone(),
4072 theme_settings.ui_font_size,
4073 )
4074 };
4075
4076 let theme = cx.theme().clone();
4077 let colors = theme.colors();
4078 cx.set_rem_size(ui_font_size);
4079
4080 self.actions(div(), cx)
4081 .key_context(context)
4082 .relative()
4083 .size_full()
4084 .flex()
4085 .flex_col()
4086 .font_family(ui_font)
4087 .gap_0()
4088 .justify_start()
4089 .items_start()
4090 .text_color(colors.text)
4091 .bg(colors.background)
4092 .children(self.titlebar_item.clone())
4093 .child(
4094 div()
4095 .id("workspace")
4096 .relative()
4097 .flex_1()
4098 .w_full()
4099 .flex()
4100 .flex_col()
4101 .overflow_hidden()
4102 .border_t()
4103 .border_b()
4104 .border_color(colors.border)
4105 .child({
4106 let this = cx.view().clone();
4107 canvas(
4108 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4109 |_, _, _| {},
4110 )
4111 .absolute()
4112 .size_full()
4113 })
4114 .when(self.zoomed.is_none(), |this| {
4115 this.on_drag_move(cx.listener(
4116 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4117 DockPosition::Left => {
4118 let size = workspace.bounds.left() + e.event.position.x;
4119 workspace.left_dock.update(cx, |left_dock, cx| {
4120 left_dock.resize_active_panel(Some(size), cx);
4121 });
4122 }
4123 DockPosition::Right => {
4124 let size = workspace.bounds.right() - e.event.position.x;
4125 workspace.right_dock.update(cx, |right_dock, cx| {
4126 right_dock.resize_active_panel(Some(size), cx);
4127 });
4128 }
4129 DockPosition::Bottom => {
4130 let size = workspace.bounds.bottom() - e.event.position.y;
4131 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4132 bottom_dock.resize_active_panel(Some(size), cx);
4133 });
4134 }
4135 },
4136 ))
4137 })
4138 .child(
4139 div()
4140 .flex()
4141 .flex_row()
4142 .h_full()
4143 // Left Dock
4144 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
4145 || {
4146 div()
4147 .flex()
4148 .flex_none()
4149 .overflow_hidden()
4150 .child(self.left_dock.clone())
4151 },
4152 ))
4153 // Panes
4154 .child(
4155 div()
4156 .flex()
4157 .flex_col()
4158 .flex_1()
4159 .overflow_hidden()
4160 .child(
4161 h_flex()
4162 .flex_1()
4163 .when_some(paddings.0, |this, p| {
4164 this.child(p.border_r_1())
4165 })
4166 .child(self.center.render(
4167 &self.project,
4168 &self.follower_states,
4169 self.active_call(),
4170 &self.active_pane,
4171 self.zoomed.as_ref(),
4172 &self.app_state,
4173 cx,
4174 ))
4175 .when_some(paddings.1, |this, p| {
4176 this.child(p.border_l_1())
4177 }),
4178 )
4179 .children(
4180 self.zoomed_position
4181 .ne(&Some(DockPosition::Bottom))
4182 .then(|| self.bottom_dock.clone()),
4183 ),
4184 )
4185 // Right Dock
4186 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
4187 || {
4188 div()
4189 .flex()
4190 .flex_none()
4191 .overflow_hidden()
4192 .child(self.right_dock.clone())
4193 },
4194 )),
4195 )
4196 .children(self.zoomed.as_ref().and_then(|view| {
4197 let zoomed_view = view.upgrade()?;
4198 let div = div()
4199 .occlude()
4200 .absolute()
4201 .overflow_hidden()
4202 .border_color(colors.border)
4203 .bg(colors.background)
4204 .child(zoomed_view)
4205 .inset_0()
4206 .shadow_lg();
4207
4208 Some(match self.zoomed_position {
4209 Some(DockPosition::Left) => div.right_2().border_r(),
4210 Some(DockPosition::Right) => div.left_2().border_l(),
4211 Some(DockPosition::Bottom) => div.top_2().border_t(),
4212 None => div.top_2().bottom_2().left_2().right_2().border(),
4213 })
4214 }))
4215 .child(self.modal_layer.clone())
4216 .children(self.render_notifications(cx)),
4217 )
4218 .child(self.status_bar.clone())
4219 .children(if self.project.read(cx).is_disconnected() {
4220 Some(DisconnectedOverlay)
4221 } else {
4222 None
4223 })
4224 }
4225}
4226
4227impl WorkspaceStore {
4228 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4229 Self {
4230 workspaces: Default::default(),
4231 _subscriptions: vec![
4232 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4233 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4234 ],
4235 client,
4236 }
4237 }
4238
4239 pub fn update_followers(
4240 &self,
4241 project_id: Option<u64>,
4242 update: proto::update_followers::Variant,
4243 cx: &AppContext,
4244 ) -> Option<()> {
4245 let active_call = ActiveCall::try_global(cx)?;
4246 let room_id = active_call.read(cx).room()?.read(cx).id();
4247 self.client
4248 .send(proto::UpdateFollowers {
4249 room_id,
4250 project_id,
4251 variant: Some(update),
4252 })
4253 .log_err()
4254 }
4255
4256 pub async fn handle_follow(
4257 this: Model<Self>,
4258 envelope: TypedEnvelope<proto::Follow>,
4259 _: Arc<Client>,
4260 mut cx: AsyncAppContext,
4261 ) -> Result<proto::FollowResponse> {
4262 this.update(&mut cx, |this, cx| {
4263 let follower = Follower {
4264 project_id: envelope.payload.project_id,
4265 peer_id: envelope.original_sender_id()?,
4266 };
4267
4268 let mut response = proto::FollowResponse::default();
4269 this.workspaces.retain(|workspace| {
4270 workspace
4271 .update(cx, |workspace, cx| {
4272 let handler_response = workspace.handle_follow(follower.project_id, cx);
4273 if response.views.is_empty() {
4274 response.views = handler_response.views;
4275 } else {
4276 response.views.extend_from_slice(&handler_response.views);
4277 }
4278
4279 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4280 if response.active_view_id.is_none()
4281 || workspace.project.read(cx).remote_id() == follower.project_id
4282 {
4283 response.active_view_id = Some(active_view_id);
4284 }
4285 }
4286
4287 if let Some(active_view) = handler_response.active_view.clone() {
4288 if response.active_view_id.is_none()
4289 || workspace.project.read(cx).remote_id() == follower.project_id
4290 {
4291 response.active_view = Some(active_view)
4292 }
4293 }
4294 })
4295 .is_ok()
4296 });
4297
4298 Ok(response)
4299 })?
4300 }
4301
4302 async fn handle_update_followers(
4303 this: Model<Self>,
4304 envelope: TypedEnvelope<proto::UpdateFollowers>,
4305 _: Arc<Client>,
4306 mut cx: AsyncAppContext,
4307 ) -> Result<()> {
4308 let leader_id = envelope.original_sender_id()?;
4309 let update = envelope.payload;
4310
4311 this.update(&mut cx, |this, cx| {
4312 this.workspaces.retain(|workspace| {
4313 workspace
4314 .update(cx, |workspace, cx| {
4315 let project_id = workspace.project.read(cx).remote_id();
4316 if update.project_id != project_id && update.project_id.is_some() {
4317 return;
4318 }
4319 workspace.handle_update_followers(leader_id, update.clone(), cx);
4320 })
4321 .is_ok()
4322 });
4323 Ok(())
4324 })?
4325 }
4326}
4327
4328impl ViewId {
4329 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4330 Ok(Self {
4331 creator: message
4332 .creator
4333 .ok_or_else(|| anyhow!("creator is missing"))?,
4334 id: message.id,
4335 })
4336 }
4337
4338 pub(crate) fn to_proto(&self) -> proto::ViewId {
4339 proto::ViewId {
4340 creator: Some(self.creator),
4341 id: self.id,
4342 }
4343 }
4344}
4345
4346pub trait WorkspaceHandle {
4347 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4348}
4349
4350impl WorkspaceHandle for View<Workspace> {
4351 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4352 self.read(cx)
4353 .worktrees(cx)
4354 .flat_map(|worktree| {
4355 let worktree_id = worktree.read(cx).id();
4356 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4357 worktree_id,
4358 path: f.path.clone(),
4359 })
4360 })
4361 .collect::<Vec<_>>()
4362 }
4363}
4364
4365impl std::fmt::Debug for OpenPaths {
4366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4367 f.debug_struct("OpenPaths")
4368 .field("paths", &self.paths)
4369 .finish()
4370 }
4371}
4372
4373pub fn activate_workspace_for_project(
4374 cx: &mut AppContext,
4375 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4376) -> Option<WindowHandle<Workspace>> {
4377 for window in cx.windows() {
4378 let Some(workspace) = window.downcast::<Workspace>() else {
4379 continue;
4380 };
4381
4382 let predicate = workspace
4383 .update(cx, |workspace, cx| {
4384 let project = workspace.project.read(cx);
4385 if predicate(project, cx) {
4386 cx.activate_window();
4387 true
4388 } else {
4389 false
4390 }
4391 })
4392 .log_err()
4393 .unwrap_or(false);
4394
4395 if predicate {
4396 return Some(workspace);
4397 }
4398 }
4399
4400 None
4401}
4402
4403pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
4404 DB.last_workspace().await.log_err().flatten()
4405}
4406
4407actions!(collab, [OpenChannelNotes]);
4408actions!(zed, [OpenLog]);
4409
4410async fn join_channel_internal(
4411 channel_id: ChannelId,
4412 app_state: &Arc<AppState>,
4413 requesting_window: Option<WindowHandle<Workspace>>,
4414 active_call: &Model<ActiveCall>,
4415 cx: &mut AsyncAppContext,
4416) -> Result<bool> {
4417 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4418 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4419 return (false, None);
4420 };
4421
4422 let already_in_channel = room.channel_id() == Some(channel_id);
4423 let should_prompt = room.is_sharing_project()
4424 && room.remote_participants().len() > 0
4425 && !already_in_channel;
4426 let open_room = if already_in_channel {
4427 active_call.room().cloned()
4428 } else {
4429 None
4430 };
4431 (should_prompt, open_room)
4432 })?;
4433
4434 if let Some(room) = open_room {
4435 let task = room.update(cx, |room, cx| {
4436 if let Some((project, host)) = room.most_active_project(cx) {
4437 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4438 }
4439
4440 None
4441 })?;
4442 if let Some(task) = task {
4443 task.await?;
4444 }
4445 return anyhow::Ok(true);
4446 }
4447
4448 if should_prompt {
4449 if let Some(workspace) = requesting_window {
4450 let answer = workspace
4451 .update(cx, |_, cx| {
4452 cx.prompt(
4453 PromptLevel::Warning,
4454 "Do you want to switch channels?",
4455 Some("Leaving this call will unshare your current project."),
4456 &["Yes, Join Channel", "Cancel"],
4457 )
4458 })?
4459 .await;
4460
4461 if answer == Ok(1) {
4462 return Ok(false);
4463 }
4464 } else {
4465 return Ok(false); // unreachable!() hopefully
4466 }
4467 }
4468
4469 let client = cx.update(|cx| active_call.read(cx).client())?;
4470
4471 let mut client_status = client.status();
4472
4473 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4474 'outer: loop {
4475 let Some(status) = client_status.recv().await else {
4476 return Err(anyhow!("error connecting"));
4477 };
4478
4479 match status {
4480 Status::Connecting
4481 | Status::Authenticating
4482 | Status::Reconnecting
4483 | Status::Reauthenticating => continue,
4484 Status::Connected { .. } => break 'outer,
4485 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4486 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4487 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4488 return Err(ErrorCode::Disconnected.into());
4489 }
4490 }
4491 }
4492
4493 let room = active_call
4494 .update(cx, |active_call, cx| {
4495 active_call.join_channel(channel_id, cx)
4496 })?
4497 .await?;
4498
4499 let Some(room) = room else {
4500 return anyhow::Ok(true);
4501 };
4502
4503 room.update(cx, |room, _| room.room_update_completed())?
4504 .await;
4505
4506 let task = room.update(cx, |room, cx| {
4507 if let Some((project, host)) = room.most_active_project(cx) {
4508 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4509 }
4510 // if you are the first to join a channel, share your project
4511 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4512 if let Some(workspace) = requesting_window {
4513 let project = workspace.update(cx, |workspace, cx| {
4514 if !CallSettings::get_global(cx).share_on_join {
4515 return None;
4516 }
4517 let project = workspace.project.read(cx);
4518 if (project.is_local() || project.remote_project_id().is_some())
4519 && project.visible_worktrees(cx).any(|tree| {
4520 tree.read(cx)
4521 .root_entry()
4522 .map_or(false, |entry| entry.is_dir())
4523 })
4524 {
4525 Some(workspace.project.clone())
4526 } else {
4527 None
4528 }
4529 });
4530 if let Ok(Some(project)) = project {
4531 return Some(cx.spawn(|room, mut cx| async move {
4532 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4533 .await?;
4534 Ok(())
4535 }));
4536 }
4537 }
4538 }
4539
4540 None
4541 })?;
4542 if let Some(task) = task {
4543 task.await?;
4544 return anyhow::Ok(true);
4545 }
4546 anyhow::Ok(false)
4547}
4548
4549pub fn join_channel(
4550 channel_id: ChannelId,
4551 app_state: Arc<AppState>,
4552 requesting_window: Option<WindowHandle<Workspace>>,
4553 cx: &mut AppContext,
4554) -> Task<Result<()>> {
4555 let active_call = ActiveCall::global(cx);
4556 cx.spawn(|mut cx| async move {
4557 let result = join_channel_internal(
4558 channel_id,
4559 &app_state,
4560 requesting_window,
4561 &active_call,
4562 &mut cx,
4563 )
4564 .await;
4565
4566 // join channel succeeded, and opened a window
4567 if matches!(result, Ok(true)) {
4568 return anyhow::Ok(());
4569 }
4570
4571 // find an existing workspace to focus and show call controls
4572 let mut active_window =
4573 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4574 if active_window.is_none() {
4575 // no open workspaces, make one to show the error in (blergh)
4576 let (window_handle, _) = cx
4577 .update(|cx| {
4578 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4579 })?
4580 .await?;
4581
4582 if result.is_ok() {
4583 cx.update(|cx| {
4584 cx.dispatch_action(&OpenChannelNotes);
4585 }).log_err();
4586 }
4587
4588 active_window = Some(window_handle);
4589 }
4590
4591 if let Err(err) = result {
4592 log::error!("failed to join channel: {}", err);
4593 if let Some(active_window) = active_window {
4594 active_window
4595 .update(&mut cx, |_, cx| {
4596 let detail: SharedString = match err.error_code() {
4597 ErrorCode::SignedOut => {
4598 "Please sign in to continue.".into()
4599 }
4600 ErrorCode::UpgradeRequired => {
4601 "Your are running an unsupported version of Zed. Please update to continue.".into()
4602 }
4603 ErrorCode::NoSuchChannel => {
4604 "No matching channel was found. Please check the link and try again.".into()
4605 }
4606 ErrorCode::Forbidden => {
4607 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4608 }
4609 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4610 _ => format!("{}\n\nPlease try again.", err).into(),
4611 };
4612 cx.prompt(
4613 PromptLevel::Critical,
4614 "Failed to join channel",
4615 Some(&detail),
4616 &["Ok"],
4617 )
4618 })?
4619 .await
4620 .ok();
4621 }
4622 }
4623
4624 // return ok, we showed the error to the user.
4625 return anyhow::Ok(());
4626 })
4627}
4628
4629pub async fn get_any_active_workspace(
4630 app_state: Arc<AppState>,
4631 mut cx: AsyncAppContext,
4632) -> anyhow::Result<WindowHandle<Workspace>> {
4633 // find an existing workspace to focus and show call controls
4634 let active_window = activate_any_workspace_window(&mut cx);
4635 if active_window.is_none() {
4636 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4637 .await?;
4638 }
4639 activate_any_workspace_window(&mut cx).context("could not open zed")
4640}
4641
4642fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4643 cx.update(|cx| {
4644 if let Some(workspace_window) = cx
4645 .active_window()
4646 .and_then(|window| window.downcast::<Workspace>())
4647 {
4648 return Some(workspace_window);
4649 }
4650
4651 for window in cx.windows() {
4652 if let Some(workspace_window) = window.downcast::<Workspace>() {
4653 workspace_window
4654 .update(cx, |_, cx| cx.activate_window())
4655 .ok();
4656 return Some(workspace_window);
4657 }
4658 }
4659 None
4660 })
4661 .ok()
4662 .flatten()
4663}
4664
4665fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
4666 cx.windows()
4667 .into_iter()
4668 .filter_map(|window| window.downcast::<Workspace>())
4669 .filter(|workspace| {
4670 workspace
4671 .read(cx)
4672 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
4673 })
4674 .collect()
4675}
4676
4677#[derive(Default)]
4678pub struct OpenOptions {
4679 pub open_new_workspace: Option<bool>,
4680 pub replace_window: Option<WindowHandle<Workspace>>,
4681}
4682
4683#[allow(clippy::type_complexity)]
4684pub fn open_paths(
4685 abs_paths: &[PathBuf],
4686 app_state: Arc<AppState>,
4687 open_options: OpenOptions,
4688 cx: &mut AppContext,
4689) -> Task<
4690 anyhow::Result<(
4691 WindowHandle<Workspace>,
4692 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4693 )>,
4694> {
4695 let abs_paths = abs_paths.to_vec();
4696 let mut existing = None;
4697 let mut best_match = None;
4698 let mut open_visible = OpenVisible::All;
4699
4700 if open_options.open_new_workspace != Some(true) {
4701 for window in local_workspace_windows(cx) {
4702 if let Ok(workspace) = window.read(cx) {
4703 let m = workspace
4704 .project
4705 .read(cx)
4706 .visibility_for_paths(&abs_paths, cx);
4707 if m > best_match {
4708 existing = Some(window);
4709 best_match = m;
4710 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
4711 existing = Some(window)
4712 }
4713 }
4714 }
4715 }
4716
4717 cx.spawn(move |mut cx| async move {
4718 if open_options.open_new_workspace.is_none() && existing.is_none() {
4719 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
4720 if futures::future::join_all(all_files)
4721 .await
4722 .into_iter()
4723 .filter_map(|result| result.ok().flatten())
4724 .all(|file| !file.is_dir)
4725 {
4726 cx.update(|cx| {
4727 for window in local_workspace_windows(cx) {
4728 if let Ok(workspace) = window.read(cx) {
4729 let project = workspace.project().read(cx);
4730 if project.is_remote() {
4731 continue;
4732 }
4733 existing = Some(window);
4734 open_visible = OpenVisible::None;
4735 break;
4736 }
4737 }
4738 })?;
4739 }
4740 }
4741
4742 if let Some(existing) = existing {
4743 Ok((
4744 existing,
4745 existing
4746 .update(&mut cx, |workspace, cx| {
4747 cx.activate_window();
4748 workspace.open_paths(abs_paths, open_visible, None, cx)
4749 })?
4750 .await,
4751 ))
4752 } else {
4753 cx.update(move |cx| {
4754 Workspace::new_local(
4755 abs_paths,
4756 app_state.clone(),
4757 open_options.replace_window,
4758 cx,
4759 )
4760 })?
4761 .await
4762 }
4763 })
4764}
4765
4766pub fn open_new(
4767 app_state: Arc<AppState>,
4768 cx: &mut AppContext,
4769 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4770) -> Task<()> {
4771 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
4772 cx.spawn(|mut cx| async move {
4773 if let Some((workspace, opened_paths)) = task.await.log_err() {
4774 workspace
4775 .update(&mut cx, |workspace, cx| {
4776 if opened_paths.is_empty() {
4777 init(workspace, cx)
4778 }
4779 })
4780 .log_err();
4781 }
4782 })
4783}
4784
4785pub fn create_and_open_local_file(
4786 path: &'static Path,
4787 cx: &mut ViewContext<Workspace>,
4788 default_content: impl 'static + Send + FnOnce() -> Rope,
4789) -> Task<Result<Box<dyn ItemHandle>>> {
4790 cx.spawn(|workspace, mut cx| async move {
4791 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4792 if !fs.is_file(path).await {
4793 fs.create_file(path, Default::default()).await?;
4794 fs.save(path, &default_content(), Default::default())
4795 .await?;
4796 }
4797
4798 let mut items = workspace
4799 .update(&mut cx, |workspace, cx| {
4800 workspace.with_local_workspace(cx, |workspace, cx| {
4801 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4802 })
4803 })?
4804 .await?
4805 .await;
4806
4807 let item = items.pop().flatten();
4808 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4809 })
4810}
4811
4812pub fn join_hosted_project(
4813 hosted_project_id: ProjectId,
4814 app_state: Arc<AppState>,
4815 cx: &mut AppContext,
4816) -> Task<Result<()>> {
4817 cx.spawn(|mut cx| async move {
4818 let existing_window = cx.update(|cx| {
4819 cx.windows().into_iter().find_map(|window| {
4820 let workspace = window.downcast::<Workspace>()?;
4821 workspace
4822 .read(cx)
4823 .is_ok_and(|workspace| {
4824 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
4825 })
4826 .then(|| workspace)
4827 })
4828 })?;
4829
4830 let workspace = if let Some(existing_window) = existing_window {
4831 existing_window
4832 } else {
4833 let project = Project::hosted(
4834 hosted_project_id,
4835 app_state.user_store.clone(),
4836 app_state.client.clone(),
4837 app_state.languages.clone(),
4838 app_state.fs.clone(),
4839 cx.clone(),
4840 )
4841 .await?;
4842
4843 let window_bounds_override = window_bounds_env_override();
4844 cx.update(|cx| {
4845 let mut options = (app_state.build_window_options)(None, cx);
4846 options.bounds = window_bounds_override;
4847 cx.open_window(options, |cx| {
4848 cx.new_view(|cx| {
4849 Workspace::new(Default::default(), project, app_state.clone(), cx)
4850 })
4851 })
4852 })?
4853 };
4854
4855 workspace.update(&mut cx, |_, cx| {
4856 cx.activate(true);
4857 cx.activate_window();
4858 })?;
4859
4860 Ok(())
4861 })
4862}
4863
4864pub fn join_remote_project(
4865 project_id: ProjectId,
4866 app_state: Arc<AppState>,
4867 window_to_replace: Option<WindowHandle<Workspace>>,
4868 cx: &mut AppContext,
4869) -> Task<Result<WindowHandle<Workspace>>> {
4870 let windows = cx.windows();
4871 cx.spawn(|mut cx| async move {
4872 let existing_workspace = windows.into_iter().find_map(|window| {
4873 window.downcast::<Workspace>().and_then(|window| {
4874 window
4875 .update(&mut cx, |workspace, cx| {
4876 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
4877 Some(window)
4878 } else {
4879 None
4880 }
4881 })
4882 .unwrap_or(None)
4883 })
4884 });
4885
4886 let workspace = if let Some(existing_workspace) = existing_workspace {
4887 existing_workspace
4888 } else {
4889 let project = Project::remote(
4890 project_id.0,
4891 app_state.client.clone(),
4892 app_state.user_store.clone(),
4893 app_state.languages.clone(),
4894 app_state.fs.clone(),
4895 cx.clone(),
4896 )
4897 .await?;
4898
4899 if let Some(window_to_replace) = window_to_replace {
4900 cx.update_window(window_to_replace.into(), |_, cx| {
4901 cx.replace_root_view(|cx| {
4902 Workspace::new(Default::default(), project, app_state.clone(), cx)
4903 });
4904 })?;
4905 window_to_replace
4906 } else {
4907 let window_bounds_override = window_bounds_env_override();
4908 cx.update(|cx| {
4909 let mut options = (app_state.build_window_options)(None, cx);
4910 options.bounds = window_bounds_override;
4911 cx.open_window(options, |cx| {
4912 cx.new_view(|cx| {
4913 Workspace::new(Default::default(), project, app_state.clone(), cx)
4914 })
4915 })
4916 })?
4917 }
4918 };
4919
4920 workspace.update(&mut cx, |_, cx| {
4921 cx.activate(true);
4922 cx.activate_window();
4923 })?;
4924
4925 anyhow::Ok(workspace)
4926 })
4927}
4928
4929pub fn join_in_room_project(
4930 project_id: u64,
4931 follow_user_id: u64,
4932 app_state: Arc<AppState>,
4933 cx: &mut AppContext,
4934) -> Task<Result<()>> {
4935 let windows = cx.windows();
4936 cx.spawn(|mut cx| async move {
4937 let existing_workspace = windows.into_iter().find_map(|window| {
4938 window.downcast::<Workspace>().and_then(|window| {
4939 window
4940 .update(&mut cx, |workspace, cx| {
4941 if workspace.project().read(cx).remote_id() == Some(project_id) {
4942 Some(window)
4943 } else {
4944 None
4945 }
4946 })
4947 .unwrap_or(None)
4948 })
4949 });
4950
4951 let workspace = if let Some(existing_workspace) = existing_workspace {
4952 existing_workspace
4953 } else {
4954 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4955 let room = active_call
4956 .read_with(&cx, |call, _| call.room().cloned())?
4957 .ok_or_else(|| anyhow!("not in a call"))?;
4958 let project = room
4959 .update(&mut cx, |room, cx| {
4960 room.join_project(
4961 project_id,
4962 app_state.languages.clone(),
4963 app_state.fs.clone(),
4964 cx,
4965 )
4966 })?
4967 .await?;
4968
4969 let window_bounds_override = window_bounds_env_override();
4970 cx.update(|cx| {
4971 let mut options = (app_state.build_window_options)(None, cx);
4972 options.bounds = window_bounds_override;
4973 cx.open_window(options, |cx| {
4974 cx.new_view(|cx| {
4975 Workspace::new(Default::default(), project, app_state.clone(), cx)
4976 })
4977 })
4978 })?
4979 };
4980
4981 workspace.update(&mut cx, |workspace, cx| {
4982 cx.activate(true);
4983 cx.activate_window();
4984
4985 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4986 let follow_peer_id = room
4987 .read(cx)
4988 .remote_participants()
4989 .iter()
4990 .find(|(_, participant)| participant.user.id == follow_user_id)
4991 .map(|(_, p)| p.peer_id)
4992 .or_else(|| {
4993 // If we couldn't follow the given user, follow the host instead.
4994 let collaborator = workspace
4995 .project()
4996 .read(cx)
4997 .collaborators()
4998 .values()
4999 .find(|collaborator| collaborator.replica_id == 0)?;
5000 Some(collaborator.peer_id)
5001 });
5002
5003 if let Some(follow_peer_id) = follow_peer_id {
5004 workspace.follow(follow_peer_id, cx);
5005 }
5006 }
5007 })?;
5008
5009 anyhow::Ok(())
5010 })
5011}
5012
5013pub fn restart(_: &Restart, cx: &mut AppContext) {
5014 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5015 let mut workspace_windows = cx
5016 .windows()
5017 .into_iter()
5018 .filter_map(|window| window.downcast::<Workspace>())
5019 .collect::<Vec<_>>();
5020
5021 // If multiple windows have unsaved changes, and need a save prompt,
5022 // prompt in the active window before switching to a different window.
5023 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5024
5025 let mut prompt = None;
5026 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5027 prompt = window
5028 .update(cx, |_, cx| {
5029 cx.prompt(
5030 PromptLevel::Info,
5031 "Are you sure you want to restart?",
5032 None,
5033 &["Restart", "Cancel"],
5034 )
5035 })
5036 .ok();
5037 }
5038
5039 cx.spawn(|mut cx| async move {
5040 if let Some(prompt) = prompt {
5041 let answer = prompt.await?;
5042 if answer != 0 {
5043 return Ok(());
5044 }
5045 }
5046
5047 // If the user cancels any save prompt, then keep the app open.
5048 for window in workspace_windows {
5049 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5050 workspace.prepare_to_close(true, cx)
5051 }) {
5052 if !should_close.await? {
5053 return Ok(());
5054 }
5055 }
5056 }
5057
5058 cx.update(|cx| cx.restart())
5059 })
5060 .detach_and_log_err(cx);
5061}
5062
5063fn parse_pixel_position_env_var(value: &str) -> Option<Point<DevicePixels>> {
5064 let mut parts = value.split(',');
5065 let x: usize = parts.next()?.parse().ok()?;
5066 let y: usize = parts.next()?.parse().ok()?;
5067 Some(point((x as i32).into(), (y as i32).into()))
5068}
5069
5070fn parse_pixel_size_env_var(value: &str) -> Option<Size<DevicePixels>> {
5071 let mut parts = value.split(',');
5072 let width: usize = parts.next()?.parse().ok()?;
5073 let height: usize = parts.next()?.parse().ok()?;
5074 Some(size((width as i32).into(), (height as i32).into()))
5075}
5076
5077struct DisconnectedOverlay;
5078
5079impl Element for DisconnectedOverlay {
5080 type RequestLayoutState = AnyElement;
5081 type PrepaintState = ();
5082
5083 fn id(&self) -> Option<ElementId> {
5084 None
5085 }
5086
5087 fn request_layout(
5088 &mut self,
5089 _id: Option<&GlobalElementId>,
5090 cx: &mut WindowContext,
5091 ) -> (LayoutId, Self::RequestLayoutState) {
5092 let mut background = cx.theme().colors().elevated_surface_background;
5093 background.fade_out(0.2);
5094 let mut overlay = div()
5095 .bg(background)
5096 .absolute()
5097 .left_0()
5098 .top(ui::TitleBar::height(cx))
5099 .size_full()
5100 .flex()
5101 .items_center()
5102 .justify_center()
5103 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
5104 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
5105 .child(Label::new(
5106 "Your connection to the remote project has been lost.",
5107 ))
5108 .into_any();
5109 (overlay.request_layout(cx), overlay)
5110 }
5111
5112 fn prepaint(
5113 &mut self,
5114 _id: Option<&GlobalElementId>,
5115 bounds: Bounds<Pixels>,
5116 overlay: &mut Self::RequestLayoutState,
5117 cx: &mut WindowContext,
5118 ) {
5119 cx.insert_hitbox(bounds, true);
5120 overlay.prepaint(cx);
5121 }
5122
5123 fn paint(
5124 &mut self,
5125 _id: Option<&GlobalElementId>,
5126 _: Bounds<Pixels>,
5127 overlay: &mut Self::RequestLayoutState,
5128 _: &mut Self::PrepaintState,
5129 cx: &mut WindowContext,
5130 ) {
5131 overlay.paint(cx)
5132 }
5133}
5134
5135impl IntoElement for DisconnectedOverlay {
5136 type Element = Self;
5137
5138 fn into_element(self) -> Self::Element {
5139 self
5140 }
5141}
5142
5143#[cfg(test)]
5144mod tests {
5145 use std::{cell::RefCell, rc::Rc};
5146
5147 use super::*;
5148 use crate::{
5149 dock::{test::TestPanel, PanelEvent},
5150 item::{
5151 test::{TestItem, TestProjectItem},
5152 ItemEvent,
5153 },
5154 };
5155 use fs::FakeFs;
5156 use gpui::{
5157 px, BorrowAppContext, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView,
5158 Render, TestAppContext, VisualTestContext,
5159 };
5160 use project::{Project, ProjectEntryId};
5161 use serde_json::json;
5162 use settings::SettingsStore;
5163
5164 #[gpui::test]
5165 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
5166 init_test(cx);
5167
5168 let fs = FakeFs::new(cx.executor());
5169 let project = Project::test(fs, [], cx).await;
5170 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5171
5172 // Adding an item with no ambiguity renders the tab without detail.
5173 let item1 = cx.new_view(|cx| {
5174 let mut item = TestItem::new(cx);
5175 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
5176 item
5177 });
5178 workspace.update(cx, |workspace, cx| {
5179 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5180 });
5181 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
5182
5183 // Adding an item that creates ambiguity increases the level of detail on
5184 // both tabs.
5185 let item2 = cx.new_view(|cx| {
5186 let mut item = TestItem::new(cx);
5187 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5188 item
5189 });
5190 workspace.update(cx, |workspace, cx| {
5191 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5192 });
5193 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5194 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5195
5196 // Adding an item that creates ambiguity increases the level of detail only
5197 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
5198 // we stop at the highest detail available.
5199 let item3 = cx.new_view(|cx| {
5200 let mut item = TestItem::new(cx);
5201 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5202 item
5203 });
5204 workspace.update(cx, |workspace, cx| {
5205 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5206 });
5207 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5208 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5209 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5210 }
5211
5212 #[gpui::test]
5213 async fn test_tracking_active_path(cx: &mut TestAppContext) {
5214 init_test(cx);
5215
5216 let fs = FakeFs::new(cx.executor());
5217 fs.insert_tree(
5218 "/root1",
5219 json!({
5220 "one.txt": "",
5221 "two.txt": "",
5222 }),
5223 )
5224 .await;
5225 fs.insert_tree(
5226 "/root2",
5227 json!({
5228 "three.txt": "",
5229 }),
5230 )
5231 .await;
5232
5233 let project = Project::test(fs, ["root1".as_ref()], cx).await;
5234 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5235 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5236 let worktree_id = project.update(cx, |project, cx| {
5237 project.worktrees().next().unwrap().read(cx).id()
5238 });
5239
5240 let item1 = cx.new_view(|cx| {
5241 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5242 });
5243 let item2 = cx.new_view(|cx| {
5244 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
5245 });
5246
5247 // Add an item to an empty pane
5248 workspace.update(cx, |workspace, cx| {
5249 workspace.add_item_to_active_pane(Box::new(item1), None, cx)
5250 });
5251 project.update(cx, |project, cx| {
5252 assert_eq!(
5253 project.active_entry(),
5254 project
5255 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5256 .map(|e| e.id)
5257 );
5258 });
5259 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5260
5261 // Add a second item to a non-empty pane
5262 workspace.update(cx, |workspace, cx| {
5263 workspace.add_item_to_active_pane(Box::new(item2), None, cx)
5264 });
5265 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5266 project.update(cx, |project, cx| {
5267 assert_eq!(
5268 project.active_entry(),
5269 project
5270 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5271 .map(|e| e.id)
5272 );
5273 });
5274
5275 // Close the active item
5276 pane.update(cx, |pane, cx| {
5277 pane.close_active_item(&Default::default(), cx).unwrap()
5278 })
5279 .await
5280 .unwrap();
5281 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5282 project.update(cx, |project, cx| {
5283 assert_eq!(
5284 project.active_entry(),
5285 project
5286 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5287 .map(|e| e.id)
5288 );
5289 });
5290
5291 // Add a project folder
5292 project
5293 .update(cx, |project, cx| {
5294 project.find_or_create_local_worktree("/root2", true, cx)
5295 })
5296 .await
5297 .unwrap();
5298 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5299
5300 // Remove a project folder
5301 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5302 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5303 }
5304
5305 #[gpui::test]
5306 async fn test_close_window(cx: &mut TestAppContext) {
5307 init_test(cx);
5308
5309 let fs = FakeFs::new(cx.executor());
5310 fs.insert_tree("/root", json!({ "one": "" })).await;
5311
5312 let project = Project::test(fs, ["root".as_ref()], cx).await;
5313 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5314
5315 // When there are no dirty items, there's nothing to do.
5316 let item1 = cx.new_view(|cx| TestItem::new(cx));
5317 workspace.update(cx, |w, cx| {
5318 w.add_item_to_active_pane(Box::new(item1.clone()), None, cx)
5319 });
5320 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5321 assert!(task.await.unwrap());
5322
5323 // When there are dirty untitled items, prompt to save each one. If the user
5324 // cancels any prompt, then abort.
5325 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5326 let item3 = cx.new_view(|cx| {
5327 TestItem::new(cx)
5328 .with_dirty(true)
5329 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5330 });
5331 workspace.update(cx, |w, cx| {
5332 w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5333 w.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5334 });
5335 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5336 cx.executor().run_until_parked();
5337 cx.simulate_prompt_answer(2); // cancel save all
5338 cx.executor().run_until_parked();
5339 cx.simulate_prompt_answer(2); // cancel save all
5340 cx.executor().run_until_parked();
5341 assert!(!cx.has_pending_prompt());
5342 assert!(!task.await.unwrap());
5343 }
5344
5345 #[gpui::test]
5346 async fn test_close_pane_items(cx: &mut TestAppContext) {
5347 init_test(cx);
5348
5349 let fs = FakeFs::new(cx.executor());
5350
5351 let project = Project::test(fs, None, cx).await;
5352 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5353
5354 let item1 = cx.new_view(|cx| {
5355 TestItem::new(cx)
5356 .with_dirty(true)
5357 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5358 });
5359 let item2 = cx.new_view(|cx| {
5360 TestItem::new(cx)
5361 .with_dirty(true)
5362 .with_conflict(true)
5363 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5364 });
5365 let item3 = cx.new_view(|cx| {
5366 TestItem::new(cx)
5367 .with_dirty(true)
5368 .with_conflict(true)
5369 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5370 });
5371 let item4 = cx.new_view(|cx| {
5372 TestItem::new(cx)
5373 .with_dirty(true)
5374 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5375 });
5376 let pane = workspace.update(cx, |workspace, cx| {
5377 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5378 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5379 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5380 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, cx);
5381 workspace.active_pane().clone()
5382 });
5383
5384 let close_items = pane.update(cx, |pane, cx| {
5385 pane.activate_item(1, true, true, cx);
5386 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5387 let item1_id = item1.item_id();
5388 let item3_id = item3.item_id();
5389 let item4_id = item4.item_id();
5390 pane.close_items(cx, SaveIntent::Close, move |id| {
5391 [item1_id, item3_id, item4_id].contains(&id)
5392 })
5393 });
5394 cx.executor().run_until_parked();
5395
5396 assert!(cx.has_pending_prompt());
5397 // Ignore "Save all" prompt
5398 cx.simulate_prompt_answer(2);
5399 cx.executor().run_until_parked();
5400 // There's a prompt to save item 1.
5401 pane.update(cx, |pane, _| {
5402 assert_eq!(pane.items_len(), 4);
5403 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
5404 });
5405 // Confirm saving item 1.
5406 cx.simulate_prompt_answer(0);
5407 cx.executor().run_until_parked();
5408
5409 // Item 1 is saved. There's a prompt to save item 3.
5410 pane.update(cx, |pane, cx| {
5411 assert_eq!(item1.read(cx).save_count, 1);
5412 assert_eq!(item1.read(cx).save_as_count, 0);
5413 assert_eq!(item1.read(cx).reload_count, 0);
5414 assert_eq!(pane.items_len(), 3);
5415 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
5416 });
5417 assert!(cx.has_pending_prompt());
5418
5419 // Cancel saving item 3.
5420 cx.simulate_prompt_answer(1);
5421 cx.executor().run_until_parked();
5422
5423 // Item 3 is reloaded. There's a prompt to save item 4.
5424 pane.update(cx, |pane, cx| {
5425 assert_eq!(item3.read(cx).save_count, 0);
5426 assert_eq!(item3.read(cx).save_as_count, 0);
5427 assert_eq!(item3.read(cx).reload_count, 1);
5428 assert_eq!(pane.items_len(), 2);
5429 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
5430 });
5431 assert!(cx.has_pending_prompt());
5432
5433 // Confirm saving item 4.
5434 cx.simulate_prompt_answer(0);
5435 cx.executor().run_until_parked();
5436
5437 // There's a prompt for a path for item 4.
5438 cx.simulate_new_path_selection(|_| Some(Default::default()));
5439 close_items.await.unwrap();
5440
5441 // The requested items are closed.
5442 pane.update(cx, |pane, cx| {
5443 assert_eq!(item4.read(cx).save_count, 0);
5444 assert_eq!(item4.read(cx).save_as_count, 1);
5445 assert_eq!(item4.read(cx).reload_count, 0);
5446 assert_eq!(pane.items_len(), 1);
5447 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5448 });
5449 }
5450
5451 #[gpui::test]
5452 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
5453 init_test(cx);
5454
5455 let fs = FakeFs::new(cx.executor());
5456 let project = Project::test(fs, [], cx).await;
5457 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5458
5459 // Create several workspace items with single project entries, and two
5460 // workspace items with multiple project entries.
5461 let single_entry_items = (0..=4)
5462 .map(|project_entry_id| {
5463 cx.new_view(|cx| {
5464 TestItem::new(cx)
5465 .with_dirty(true)
5466 .with_project_items(&[TestProjectItem::new(
5467 project_entry_id,
5468 &format!("{project_entry_id}.txt"),
5469 cx,
5470 )])
5471 })
5472 })
5473 .collect::<Vec<_>>();
5474 let item_2_3 = cx.new_view(|cx| {
5475 TestItem::new(cx)
5476 .with_dirty(true)
5477 .with_singleton(false)
5478 .with_project_items(&[
5479 single_entry_items[2].read(cx).project_items[0].clone(),
5480 single_entry_items[3].read(cx).project_items[0].clone(),
5481 ])
5482 });
5483 let item_3_4 = cx.new_view(|cx| {
5484 TestItem::new(cx)
5485 .with_dirty(true)
5486 .with_singleton(false)
5487 .with_project_items(&[
5488 single_entry_items[3].read(cx).project_items[0].clone(),
5489 single_entry_items[4].read(cx).project_items[0].clone(),
5490 ])
5491 });
5492
5493 // Create two panes that contain the following project entries:
5494 // left pane:
5495 // multi-entry items: (2, 3)
5496 // single-entry items: 0, 1, 2, 3, 4
5497 // right pane:
5498 // single-entry items: 1
5499 // multi-entry items: (3, 4)
5500 let left_pane = workspace.update(cx, |workspace, cx| {
5501 let left_pane = workspace.active_pane().clone();
5502 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, cx);
5503 for item in single_entry_items {
5504 workspace.add_item_to_active_pane(Box::new(item), None, cx);
5505 }
5506 left_pane.update(cx, |pane, cx| {
5507 pane.activate_item(2, true, true, cx);
5508 });
5509
5510 let right_pane = workspace
5511 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5512 .unwrap();
5513
5514 right_pane.update(cx, |pane, cx| {
5515 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5516 });
5517
5518 left_pane
5519 });
5520
5521 cx.focus_view(&left_pane);
5522
5523 // When closing all of the items in the left pane, we should be prompted twice:
5524 // once for project entry 0, and once for project entry 2. Project entries 1,
5525 // 3, and 4 are all still open in the other paten. After those two
5526 // prompts, the task should complete.
5527
5528 let close = left_pane.update(cx, |pane, cx| {
5529 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5530 });
5531 cx.executor().run_until_parked();
5532
5533 // Discard "Save all" prompt
5534 cx.simulate_prompt_answer(2);
5535
5536 cx.executor().run_until_parked();
5537 left_pane.update(cx, |pane, cx| {
5538 assert_eq!(
5539 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5540 &[ProjectEntryId::from_proto(0)]
5541 );
5542 });
5543 cx.simulate_prompt_answer(0);
5544
5545 cx.executor().run_until_parked();
5546 left_pane.update(cx, |pane, cx| {
5547 assert_eq!(
5548 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5549 &[ProjectEntryId::from_proto(2)]
5550 );
5551 });
5552 cx.simulate_prompt_answer(0);
5553
5554 cx.executor().run_until_parked();
5555 close.await.unwrap();
5556 left_pane.update(cx, |pane, _| {
5557 assert_eq!(pane.items_len(), 0);
5558 });
5559 }
5560
5561 #[gpui::test]
5562 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5563 init_test(cx);
5564
5565 let fs = FakeFs::new(cx.executor());
5566 let project = Project::test(fs, [], cx).await;
5567 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5568 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5569
5570 let item = cx.new_view(|cx| {
5571 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5572 });
5573 let item_id = item.entity_id();
5574 workspace.update(cx, |workspace, cx| {
5575 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5576 });
5577
5578 // Autosave on window change.
5579 item.update(cx, |item, cx| {
5580 cx.update_global(|settings: &mut SettingsStore, cx| {
5581 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5582 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5583 })
5584 });
5585 item.is_dirty = true;
5586 });
5587
5588 // Deactivating the window saves the file.
5589 cx.deactivate_window();
5590 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5591
5592 // Re-activating the window doesn't save the file.
5593 cx.update(|cx| cx.activate_window());
5594 cx.executor().run_until_parked();
5595 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5596
5597 // Autosave on focus change.
5598 item.update(cx, |item, cx| {
5599 cx.focus_self();
5600 cx.update_global(|settings: &mut SettingsStore, cx| {
5601 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5602 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5603 })
5604 });
5605 item.is_dirty = true;
5606 });
5607
5608 // Blurring the item saves the file.
5609 item.update(cx, |_, cx| cx.blur());
5610 cx.executor().run_until_parked();
5611 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5612
5613 // Deactivating the window still saves the file.
5614 item.update(cx, |item, cx| {
5615 cx.focus_self();
5616 item.is_dirty = true;
5617 });
5618 cx.deactivate_window();
5619 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5620
5621 // Autosave after delay.
5622 item.update(cx, |item, cx| {
5623 cx.update_global(|settings: &mut SettingsStore, cx| {
5624 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5625 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5626 })
5627 });
5628 item.is_dirty = true;
5629 cx.emit(ItemEvent::Edit);
5630 });
5631
5632 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5633 cx.executor().advance_clock(Duration::from_millis(250));
5634 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5635
5636 // After delay expires, the file is saved.
5637 cx.executor().advance_clock(Duration::from_millis(250));
5638 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5639
5640 // Autosave on focus change, ensuring closing the tab counts as such.
5641 item.update(cx, |item, cx| {
5642 cx.update_global(|settings: &mut SettingsStore, cx| {
5643 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5644 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5645 })
5646 });
5647 item.is_dirty = true;
5648 });
5649
5650 pane.update(cx, |pane, cx| {
5651 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5652 })
5653 .await
5654 .unwrap();
5655 assert!(!cx.has_pending_prompt());
5656 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5657
5658 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5659 workspace.update(cx, |workspace, cx| {
5660 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5661 });
5662 item.update(cx, |item, cx| {
5663 item.project_items[0].update(cx, |item, _| {
5664 item.entry_id = None;
5665 });
5666 item.is_dirty = true;
5667 cx.blur();
5668 });
5669 cx.run_until_parked();
5670 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5671
5672 // Ensure autosave is prevented for deleted files also when closing the buffer.
5673 let _close_items = pane.update(cx, |pane, cx| {
5674 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5675 });
5676 cx.run_until_parked();
5677 assert!(cx.has_pending_prompt());
5678 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5679 }
5680
5681 #[gpui::test]
5682 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5683 init_test(cx);
5684
5685 let fs = FakeFs::new(cx.executor());
5686
5687 let project = Project::test(fs, [], cx).await;
5688 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5689
5690 let item = cx.new_view(|cx| {
5691 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5692 });
5693 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5694 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5695 let toolbar_notify_count = Rc::new(RefCell::new(0));
5696
5697 workspace.update(cx, |workspace, cx| {
5698 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5699 let toolbar_notification_count = toolbar_notify_count.clone();
5700 cx.observe(&toolbar, move |_, _, _| {
5701 *toolbar_notification_count.borrow_mut() += 1
5702 })
5703 .detach();
5704 });
5705
5706 pane.update(cx, |pane, _| {
5707 assert!(!pane.can_navigate_backward());
5708 assert!(!pane.can_navigate_forward());
5709 });
5710
5711 item.update(cx, |item, cx| {
5712 item.set_state("one".to_string(), cx);
5713 });
5714
5715 // Toolbar must be notified to re-render the navigation buttons
5716 assert_eq!(*toolbar_notify_count.borrow(), 1);
5717
5718 pane.update(cx, |pane, _| {
5719 assert!(pane.can_navigate_backward());
5720 assert!(!pane.can_navigate_forward());
5721 });
5722
5723 workspace
5724 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5725 .await
5726 .unwrap();
5727
5728 assert_eq!(*toolbar_notify_count.borrow(), 2);
5729 pane.update(cx, |pane, _| {
5730 assert!(!pane.can_navigate_backward());
5731 assert!(pane.can_navigate_forward());
5732 });
5733 }
5734
5735 #[gpui::test]
5736 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5737 init_test(cx);
5738 let fs = FakeFs::new(cx.executor());
5739
5740 let project = Project::test(fs, [], cx).await;
5741 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5742
5743 let panel = workspace.update(cx, |workspace, cx| {
5744 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5745 workspace.add_panel(panel.clone(), cx);
5746
5747 workspace
5748 .right_dock()
5749 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5750
5751 panel
5752 });
5753
5754 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5755 pane.update(cx, |pane, cx| {
5756 let item = cx.new_view(|cx| TestItem::new(cx));
5757 pane.add_item(Box::new(item), true, true, None, cx);
5758 });
5759
5760 // Transfer focus from center to panel
5761 workspace.update(cx, |workspace, cx| {
5762 workspace.toggle_panel_focus::<TestPanel>(cx);
5763 });
5764
5765 workspace.update(cx, |workspace, cx| {
5766 assert!(workspace.right_dock().read(cx).is_open());
5767 assert!(!panel.is_zoomed(cx));
5768 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5769 });
5770
5771 // Transfer focus from panel to center
5772 workspace.update(cx, |workspace, cx| {
5773 workspace.toggle_panel_focus::<TestPanel>(cx);
5774 });
5775
5776 workspace.update(cx, |workspace, cx| {
5777 assert!(workspace.right_dock().read(cx).is_open());
5778 assert!(!panel.is_zoomed(cx));
5779 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5780 });
5781
5782 // Close the dock
5783 workspace.update(cx, |workspace, cx| {
5784 workspace.toggle_dock(DockPosition::Right, cx);
5785 });
5786
5787 workspace.update(cx, |workspace, cx| {
5788 assert!(!workspace.right_dock().read(cx).is_open());
5789 assert!(!panel.is_zoomed(cx));
5790 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5791 });
5792
5793 // Open the dock
5794 workspace.update(cx, |workspace, cx| {
5795 workspace.toggle_dock(DockPosition::Right, cx);
5796 });
5797
5798 workspace.update(cx, |workspace, cx| {
5799 assert!(workspace.right_dock().read(cx).is_open());
5800 assert!(!panel.is_zoomed(cx));
5801 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5802 });
5803
5804 // Focus and zoom panel
5805 panel.update(cx, |panel, cx| {
5806 cx.focus_self();
5807 panel.set_zoomed(true, cx)
5808 });
5809
5810 workspace.update(cx, |workspace, cx| {
5811 assert!(workspace.right_dock().read(cx).is_open());
5812 assert!(panel.is_zoomed(cx));
5813 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5814 });
5815
5816 // Transfer focus to the center closes the dock
5817 workspace.update(cx, |workspace, cx| {
5818 workspace.toggle_panel_focus::<TestPanel>(cx);
5819 });
5820
5821 workspace.update(cx, |workspace, cx| {
5822 assert!(!workspace.right_dock().read(cx).is_open());
5823 assert!(panel.is_zoomed(cx));
5824 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5825 });
5826
5827 // Transferring focus back to the panel keeps it zoomed
5828 workspace.update(cx, |workspace, cx| {
5829 workspace.toggle_panel_focus::<TestPanel>(cx);
5830 });
5831
5832 workspace.update(cx, |workspace, cx| {
5833 assert!(workspace.right_dock().read(cx).is_open());
5834 assert!(panel.is_zoomed(cx));
5835 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5836 });
5837
5838 // Close the dock while it is zoomed
5839 workspace.update(cx, |workspace, cx| {
5840 workspace.toggle_dock(DockPosition::Right, cx)
5841 });
5842
5843 workspace.update(cx, |workspace, cx| {
5844 assert!(!workspace.right_dock().read(cx).is_open());
5845 assert!(panel.is_zoomed(cx));
5846 assert!(workspace.zoomed.is_none());
5847 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5848 });
5849
5850 // Opening the dock, when it's zoomed, retains focus
5851 workspace.update(cx, |workspace, cx| {
5852 workspace.toggle_dock(DockPosition::Right, cx)
5853 });
5854
5855 workspace.update(cx, |workspace, cx| {
5856 assert!(workspace.right_dock().read(cx).is_open());
5857 assert!(panel.is_zoomed(cx));
5858 assert!(workspace.zoomed.is_some());
5859 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5860 });
5861
5862 // Unzoom and close the panel, zoom the active pane.
5863 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5864 workspace.update(cx, |workspace, cx| {
5865 workspace.toggle_dock(DockPosition::Right, cx)
5866 });
5867 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5868
5869 // Opening a dock unzooms the pane.
5870 workspace.update(cx, |workspace, cx| {
5871 workspace.toggle_dock(DockPosition::Right, cx)
5872 });
5873 workspace.update(cx, |workspace, cx| {
5874 let pane = pane.read(cx);
5875 assert!(!pane.is_zoomed());
5876 assert!(!pane.focus_handle(cx).is_focused(cx));
5877 assert!(workspace.right_dock().read(cx).is_open());
5878 assert!(workspace.zoomed.is_none());
5879 });
5880 }
5881
5882 struct TestModal(FocusHandle);
5883
5884 impl TestModal {
5885 fn new(cx: &mut ViewContext<Self>) -> Self {
5886 Self(cx.focus_handle())
5887 }
5888 }
5889
5890 impl EventEmitter<DismissEvent> for TestModal {}
5891
5892 impl FocusableView for TestModal {
5893 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5894 self.0.clone()
5895 }
5896 }
5897
5898 impl ModalView for TestModal {}
5899
5900 impl Render for TestModal {
5901 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5902 div().track_focus(&self.0)
5903 }
5904 }
5905
5906 #[gpui::test]
5907 async fn test_panels(cx: &mut gpui::TestAppContext) {
5908 init_test(cx);
5909 let fs = FakeFs::new(cx.executor());
5910
5911 let project = Project::test(fs, [], cx).await;
5912 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5913
5914 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5915 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5916 workspace.add_panel(panel_1.clone(), cx);
5917 workspace
5918 .left_dock()
5919 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5920 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5921 workspace.add_panel(panel_2.clone(), cx);
5922 workspace
5923 .right_dock()
5924 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5925
5926 let left_dock = workspace.left_dock();
5927 assert_eq!(
5928 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5929 panel_1.panel_id()
5930 );
5931 assert_eq!(
5932 left_dock.read(cx).active_panel_size(cx).unwrap(),
5933 panel_1.size(cx)
5934 );
5935
5936 left_dock.update(cx, |left_dock, cx| {
5937 left_dock.resize_active_panel(Some(px(1337.)), cx)
5938 });
5939 assert_eq!(
5940 workspace
5941 .right_dock()
5942 .read(cx)
5943 .visible_panel()
5944 .unwrap()
5945 .panel_id(),
5946 panel_2.panel_id(),
5947 );
5948
5949 (panel_1, panel_2)
5950 });
5951
5952 // Move panel_1 to the right
5953 panel_1.update(cx, |panel_1, cx| {
5954 panel_1.set_position(DockPosition::Right, cx)
5955 });
5956
5957 workspace.update(cx, |workspace, cx| {
5958 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5959 // Since it was the only panel on the left, the left dock should now be closed.
5960 assert!(!workspace.left_dock().read(cx).is_open());
5961 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5962 let right_dock = workspace.right_dock();
5963 assert_eq!(
5964 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5965 panel_1.panel_id()
5966 );
5967 assert_eq!(
5968 right_dock.read(cx).active_panel_size(cx).unwrap(),
5969 px(1337.)
5970 );
5971
5972 // Now we move panel_2 to the left
5973 panel_2.set_position(DockPosition::Left, cx);
5974 });
5975
5976 workspace.update(cx, |workspace, cx| {
5977 // Since panel_2 was not visible on the right, we don't open the left dock.
5978 assert!(!workspace.left_dock().read(cx).is_open());
5979 // And the right dock is unaffected in its displaying of panel_1
5980 assert!(workspace.right_dock().read(cx).is_open());
5981 assert_eq!(
5982 workspace
5983 .right_dock()
5984 .read(cx)
5985 .visible_panel()
5986 .unwrap()
5987 .panel_id(),
5988 panel_1.panel_id(),
5989 );
5990 });
5991
5992 // Move panel_1 back to the left
5993 panel_1.update(cx, |panel_1, cx| {
5994 panel_1.set_position(DockPosition::Left, cx)
5995 });
5996
5997 workspace.update(cx, |workspace, cx| {
5998 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5999 let left_dock = workspace.left_dock();
6000 assert!(left_dock.read(cx).is_open());
6001 assert_eq!(
6002 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6003 panel_1.panel_id()
6004 );
6005 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
6006 // And the right dock should be closed as it no longer has any panels.
6007 assert!(!workspace.right_dock().read(cx).is_open());
6008
6009 // Now we move panel_1 to the bottom
6010 panel_1.set_position(DockPosition::Bottom, cx);
6011 });
6012
6013 workspace.update(cx, |workspace, cx| {
6014 // Since panel_1 was visible on the left, we close the left dock.
6015 assert!(!workspace.left_dock().read(cx).is_open());
6016 // The bottom dock is sized based on the panel's default size,
6017 // since the panel orientation changed from vertical to horizontal.
6018 let bottom_dock = workspace.bottom_dock();
6019 assert_eq!(
6020 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
6021 panel_1.size(cx),
6022 );
6023 // Close bottom dock and move panel_1 back to the left.
6024 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
6025 panel_1.set_position(DockPosition::Left, cx);
6026 });
6027
6028 // Emit activated event on panel 1
6029 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
6030
6031 // Now the left dock is open and panel_1 is active and focused.
6032 workspace.update(cx, |workspace, cx| {
6033 let left_dock = workspace.left_dock();
6034 assert!(left_dock.read(cx).is_open());
6035 assert_eq!(
6036 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6037 panel_1.panel_id(),
6038 );
6039 assert!(panel_1.focus_handle(cx).is_focused(cx));
6040 });
6041
6042 // Emit closed event on panel 2, which is not active
6043 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6044
6045 // Wo don't close the left dock, because panel_2 wasn't the active panel
6046 workspace.update(cx, |workspace, cx| {
6047 let left_dock = workspace.left_dock();
6048 assert!(left_dock.read(cx).is_open());
6049 assert_eq!(
6050 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6051 panel_1.panel_id(),
6052 );
6053 });
6054
6055 // Emitting a ZoomIn event shows the panel as zoomed.
6056 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
6057 workspace.update(cx, |workspace, _| {
6058 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6059 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
6060 });
6061
6062 // Move panel to another dock while it is zoomed
6063 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
6064 workspace.update(cx, |workspace, _| {
6065 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6066
6067 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6068 });
6069
6070 // This is a helper for getting a:
6071 // - valid focus on an element,
6072 // - that isn't a part of the panes and panels system of the Workspace,
6073 // - and doesn't trigger the 'on_focus_lost' API.
6074 let focus_other_view = {
6075 let workspace = workspace.clone();
6076 move |cx: &mut VisualTestContext| {
6077 workspace.update(cx, |workspace, cx| {
6078 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
6079 workspace.toggle_modal(cx, TestModal::new);
6080 workspace.toggle_modal(cx, TestModal::new);
6081 } else {
6082 workspace.toggle_modal(cx, TestModal::new);
6083 }
6084 })
6085 }
6086 };
6087
6088 // If focus is transferred to another view that's not a panel or another pane, we still show
6089 // the panel as zoomed.
6090 focus_other_view(cx);
6091 workspace.update(cx, |workspace, _| {
6092 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6093 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6094 });
6095
6096 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
6097 workspace.update(cx, |_, cx| cx.focus_self());
6098 workspace.update(cx, |workspace, _| {
6099 assert_eq!(workspace.zoomed, None);
6100 assert_eq!(workspace.zoomed_position, None);
6101 });
6102
6103 // If focus is transferred again to another view that's not a panel or a pane, we won't
6104 // show the panel as zoomed because it wasn't zoomed before.
6105 focus_other_view(cx);
6106 workspace.update(cx, |workspace, _| {
6107 assert_eq!(workspace.zoomed, None);
6108 assert_eq!(workspace.zoomed_position, None);
6109 });
6110
6111 // When the panel is activated, it is zoomed again.
6112 cx.dispatch_action(ToggleRightDock);
6113 workspace.update(cx, |workspace, _| {
6114 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6115 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6116 });
6117
6118 // Emitting a ZoomOut event unzooms the panel.
6119 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
6120 workspace.update(cx, |workspace, _| {
6121 assert_eq!(workspace.zoomed, None);
6122 assert_eq!(workspace.zoomed_position, None);
6123 });
6124
6125 // Emit closed event on panel 1, which is active
6126 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6127
6128 // Now the left dock is closed, because panel_1 was the active panel
6129 workspace.update(cx, |workspace, cx| {
6130 let right_dock = workspace.right_dock();
6131 assert!(!right_dock.read(cx).is_open());
6132 });
6133 }
6134
6135 mod register_project_item_tests {
6136 use ui::Context as _;
6137
6138 use super::*;
6139
6140 const TEST_PNG_KIND: &str = "TestPngItemView";
6141 // View
6142 struct TestPngItemView {
6143 focus_handle: FocusHandle,
6144 }
6145 // Model
6146 struct TestPngItem {}
6147
6148 impl project::Item for TestPngItem {
6149 fn try_open(
6150 _project: &Model<Project>,
6151 path: &ProjectPath,
6152 cx: &mut AppContext,
6153 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6154 if path.path.extension().unwrap() == "png" {
6155 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
6156 } else {
6157 None
6158 }
6159 }
6160
6161 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6162 None
6163 }
6164
6165 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6166 None
6167 }
6168 }
6169
6170 impl Item for TestPngItemView {
6171 type Event = ();
6172
6173 fn serialized_item_kind() -> Option<&'static str> {
6174 Some(TEST_PNG_KIND)
6175 }
6176 }
6177 impl EventEmitter<()> for TestPngItemView {}
6178 impl FocusableView for TestPngItemView {
6179 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6180 self.focus_handle.clone()
6181 }
6182 }
6183
6184 impl Render for TestPngItemView {
6185 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6186 Empty
6187 }
6188 }
6189
6190 impl ProjectItem for TestPngItemView {
6191 type Item = TestPngItem;
6192
6193 fn for_project_item(
6194 _project: Model<Project>,
6195 _item: Model<Self::Item>,
6196 cx: &mut ViewContext<Self>,
6197 ) -> Self
6198 where
6199 Self: Sized,
6200 {
6201 Self {
6202 focus_handle: cx.focus_handle(),
6203 }
6204 }
6205 }
6206
6207 const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
6208 // View
6209 struct TestIpynbItemView {
6210 focus_handle: FocusHandle,
6211 }
6212 // Model
6213 struct TestIpynbItem {}
6214
6215 impl project::Item for TestIpynbItem {
6216 fn try_open(
6217 _project: &Model<Project>,
6218 path: &ProjectPath,
6219 cx: &mut AppContext,
6220 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6221 if path.path.extension().unwrap() == "ipynb" {
6222 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
6223 } else {
6224 None
6225 }
6226 }
6227
6228 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6229 None
6230 }
6231
6232 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6233 None
6234 }
6235 }
6236
6237 impl Item for TestIpynbItemView {
6238 type Event = ();
6239
6240 fn serialized_item_kind() -> Option<&'static str> {
6241 Some(TEST_IPYNB_KIND)
6242 }
6243 }
6244 impl EventEmitter<()> for TestIpynbItemView {}
6245 impl FocusableView for TestIpynbItemView {
6246 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6247 self.focus_handle.clone()
6248 }
6249 }
6250
6251 impl Render for TestIpynbItemView {
6252 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6253 Empty
6254 }
6255 }
6256
6257 impl ProjectItem for TestIpynbItemView {
6258 type Item = TestIpynbItem;
6259
6260 fn for_project_item(
6261 _project: Model<Project>,
6262 _item: Model<Self::Item>,
6263 cx: &mut ViewContext<Self>,
6264 ) -> Self
6265 where
6266 Self: Sized,
6267 {
6268 Self {
6269 focus_handle: cx.focus_handle(),
6270 }
6271 }
6272 }
6273
6274 struct TestAlternatePngItemView {
6275 focus_handle: FocusHandle,
6276 }
6277
6278 const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
6279 impl Item for TestAlternatePngItemView {
6280 type Event = ();
6281
6282 fn serialized_item_kind() -> Option<&'static str> {
6283 Some(TEST_ALTERNATE_PNG_KIND)
6284 }
6285 }
6286 impl EventEmitter<()> for TestAlternatePngItemView {}
6287 impl FocusableView for TestAlternatePngItemView {
6288 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6289 self.focus_handle.clone()
6290 }
6291 }
6292
6293 impl Render for TestAlternatePngItemView {
6294 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6295 Empty
6296 }
6297 }
6298
6299 impl ProjectItem for TestAlternatePngItemView {
6300 type Item = TestPngItem;
6301
6302 fn for_project_item(
6303 _project: Model<Project>,
6304 _item: Model<Self::Item>,
6305 cx: &mut ViewContext<Self>,
6306 ) -> Self
6307 where
6308 Self: Sized,
6309 {
6310 Self {
6311 focus_handle: cx.focus_handle(),
6312 }
6313 }
6314 }
6315
6316 #[gpui::test]
6317 async fn test_register_project_item(cx: &mut TestAppContext) {
6318 init_test(cx);
6319
6320 cx.update(|cx| {
6321 register_project_item::<TestPngItemView>(cx);
6322 register_project_item::<TestIpynbItemView>(cx);
6323 });
6324
6325 let fs = FakeFs::new(cx.executor());
6326 fs.insert_tree(
6327 "/root1",
6328 json!({
6329 "one.png": "BINARYDATAHERE",
6330 "two.ipynb": "{ totally a notebook }",
6331 "three.txt": "editing text, sure why not?"
6332 }),
6333 )
6334 .await;
6335
6336 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6337 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6338
6339 let worktree_id = project.update(cx, |project, cx| {
6340 project.worktrees().next().unwrap().read(cx).id()
6341 });
6342
6343 let handle = workspace
6344 .update(cx, |workspace, cx| {
6345 let project_path = (worktree_id, "one.png");
6346 workspace.open_path(project_path, None, true, cx)
6347 })
6348 .await
6349 .unwrap();
6350
6351 // Now we can check if the handle we got back errored or not
6352 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
6353
6354 let handle = workspace
6355 .update(cx, |workspace, cx| {
6356 let project_path = (worktree_id, "two.ipynb");
6357 workspace.open_path(project_path, None, true, cx)
6358 })
6359 .await
6360 .unwrap();
6361
6362 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
6363
6364 let handle = workspace
6365 .update(cx, |workspace, cx| {
6366 let project_path = (worktree_id, "three.txt");
6367 workspace.open_path(project_path, None, true, cx)
6368 })
6369 .await;
6370 assert!(handle.is_err());
6371 }
6372
6373 #[gpui::test]
6374 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
6375 init_test(cx);
6376
6377 cx.update(|cx| {
6378 register_project_item::<TestPngItemView>(cx);
6379 register_project_item::<TestAlternatePngItemView>(cx);
6380 });
6381
6382 let fs = FakeFs::new(cx.executor());
6383 fs.insert_tree(
6384 "/root1",
6385 json!({
6386 "one.png": "BINARYDATAHERE",
6387 "two.ipynb": "{ totally a notebook }",
6388 "three.txt": "editing text, sure why not?"
6389 }),
6390 )
6391 .await;
6392
6393 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6394 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6395
6396 let worktree_id = project.update(cx, |project, cx| {
6397 project.worktrees().next().unwrap().read(cx).id()
6398 });
6399
6400 let handle = workspace
6401 .update(cx, |workspace, cx| {
6402 let project_path = (worktree_id, "one.png");
6403 workspace.open_path(project_path, None, true, cx)
6404 })
6405 .await
6406 .unwrap();
6407
6408 // This _must_ be the second item registered
6409 assert_eq!(
6410 handle.serialized_item_kind().unwrap(),
6411 TEST_ALTERNATE_PNG_KIND
6412 );
6413
6414 let handle = workspace
6415 .update(cx, |workspace, cx| {
6416 let project_path = (worktree_id, "three.txt");
6417 workspace.open_path(project_path, None, true, cx)
6418 })
6419 .await;
6420 assert!(handle.is_err());
6421 }
6422 }
6423
6424 pub fn init_test(cx: &mut TestAppContext) {
6425 cx.update(|cx| {
6426 let settings_store = SettingsStore::test(cx);
6427 cx.set_global(settings_store);
6428 theme::init(theme::LoadThemes::JustBase, cx);
6429 language::init(cx);
6430 crate::init_settings(cx);
6431 Project::init_settings(cx);
6432 });
6433 }
6434}