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