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