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