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