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