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 if matches!(
3506 WorkspaceSettings::get_global(cx).autosave,
3507 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3508 ) {
3509 for item in pane.items() {
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 _: Arc<Client>,
4354 mut cx: AsyncAppContext,
4355 ) -> Result<proto::FollowResponse> {
4356 this.update(&mut cx, |this, cx| {
4357 let follower = Follower {
4358 project_id: envelope.payload.project_id,
4359 peer_id: envelope.original_sender_id()?,
4360 };
4361
4362 let mut response = proto::FollowResponse::default();
4363 this.workspaces.retain(|workspace| {
4364 workspace
4365 .update(cx, |workspace, cx| {
4366 let handler_response = workspace.handle_follow(follower.project_id, cx);
4367 if response.views.is_empty() {
4368 response.views = handler_response.views;
4369 } else {
4370 response.views.extend_from_slice(&handler_response.views);
4371 }
4372
4373 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4374 if response.active_view_id.is_none()
4375 || workspace.project.read(cx).remote_id() == follower.project_id
4376 {
4377 response.active_view_id = Some(active_view_id);
4378 }
4379 }
4380
4381 if let Some(active_view) = handler_response.active_view.clone() {
4382 if response.active_view_id.is_none()
4383 || workspace.project.read(cx).remote_id() == follower.project_id
4384 {
4385 response.active_view = Some(active_view)
4386 }
4387 }
4388 })
4389 .is_ok()
4390 });
4391
4392 Ok(response)
4393 })?
4394 }
4395
4396 async fn handle_update_followers(
4397 this: Model<Self>,
4398 envelope: TypedEnvelope<proto::UpdateFollowers>,
4399 _: Arc<Client>,
4400 mut cx: AsyncAppContext,
4401 ) -> Result<()> {
4402 let leader_id = envelope.original_sender_id()?;
4403 let update = envelope.payload;
4404
4405 this.update(&mut cx, |this, cx| {
4406 this.workspaces.retain(|workspace| {
4407 workspace
4408 .update(cx, |workspace, cx| {
4409 let project_id = workspace.project.read(cx).remote_id();
4410 if update.project_id != project_id && update.project_id.is_some() {
4411 return;
4412 }
4413 workspace.handle_update_followers(leader_id, update.clone(), cx);
4414 })
4415 .is_ok()
4416 });
4417 Ok(())
4418 })?
4419 }
4420}
4421
4422impl ViewId {
4423 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4424 Ok(Self {
4425 creator: message
4426 .creator
4427 .ok_or_else(|| anyhow!("creator is missing"))?,
4428 id: message.id,
4429 })
4430 }
4431
4432 pub(crate) fn to_proto(&self) -> proto::ViewId {
4433 proto::ViewId {
4434 creator: Some(self.creator),
4435 id: self.id,
4436 }
4437 }
4438}
4439
4440pub trait WorkspaceHandle {
4441 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4442}
4443
4444impl WorkspaceHandle for View<Workspace> {
4445 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4446 self.read(cx)
4447 .worktrees(cx)
4448 .flat_map(|worktree| {
4449 let worktree_id = worktree.read(cx).id();
4450 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4451 worktree_id,
4452 path: f.path.clone(),
4453 })
4454 })
4455 .collect::<Vec<_>>()
4456 }
4457}
4458
4459impl std::fmt::Debug for OpenPaths {
4460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4461 f.debug_struct("OpenPaths")
4462 .field("paths", &self.paths)
4463 .finish()
4464 }
4465}
4466
4467pub fn activate_workspace_for_project(
4468 cx: &mut AppContext,
4469 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4470) -> Option<WindowHandle<Workspace>> {
4471 for window in cx.windows() {
4472 let Some(workspace) = window.downcast::<Workspace>() else {
4473 continue;
4474 };
4475
4476 let predicate = workspace
4477 .update(cx, |workspace, cx| {
4478 let project = workspace.project.read(cx);
4479 if predicate(project, cx) {
4480 cx.activate_window();
4481 true
4482 } else {
4483 false
4484 }
4485 })
4486 .log_err()
4487 .unwrap_or(false);
4488
4489 if predicate {
4490 return Some(workspace);
4491 }
4492 }
4493
4494 None
4495}
4496
4497pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
4498 DB.last_workspace().await.log_err().flatten()
4499}
4500
4501actions!(collab, [OpenChannelNotes]);
4502actions!(zed, [OpenLog]);
4503
4504async fn join_channel_internal(
4505 channel_id: ChannelId,
4506 app_state: &Arc<AppState>,
4507 requesting_window: Option<WindowHandle<Workspace>>,
4508 active_call: &Model<ActiveCall>,
4509 cx: &mut AsyncAppContext,
4510) -> Result<bool> {
4511 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4512 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4513 return (false, None);
4514 };
4515
4516 let already_in_channel = room.channel_id() == Some(channel_id);
4517 let should_prompt = room.is_sharing_project()
4518 && room.remote_participants().len() > 0
4519 && !already_in_channel;
4520 let open_room = if already_in_channel {
4521 active_call.room().cloned()
4522 } else {
4523 None
4524 };
4525 (should_prompt, open_room)
4526 })?;
4527
4528 if let Some(room) = open_room {
4529 let task = room.update(cx, |room, cx| {
4530 if let Some((project, host)) = room.most_active_project(cx) {
4531 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4532 }
4533
4534 None
4535 })?;
4536 if let Some(task) = task {
4537 task.await?;
4538 }
4539 return anyhow::Ok(true);
4540 }
4541
4542 if should_prompt {
4543 if let Some(workspace) = requesting_window {
4544 let answer = workspace
4545 .update(cx, |_, cx| {
4546 cx.prompt(
4547 PromptLevel::Warning,
4548 "Do you want to switch channels?",
4549 Some("Leaving this call will unshare your current project."),
4550 &["Yes, Join Channel", "Cancel"],
4551 )
4552 })?
4553 .await;
4554
4555 if answer == Ok(1) {
4556 return Ok(false);
4557 }
4558 } else {
4559 return Ok(false); // unreachable!() hopefully
4560 }
4561 }
4562
4563 let client = cx.update(|cx| active_call.read(cx).client())?;
4564
4565 let mut client_status = client.status();
4566
4567 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4568 'outer: loop {
4569 let Some(status) = client_status.recv().await else {
4570 return Err(anyhow!("error connecting"));
4571 };
4572
4573 match status {
4574 Status::Connecting
4575 | Status::Authenticating
4576 | Status::Reconnecting
4577 | Status::Reauthenticating => continue,
4578 Status::Connected { .. } => break 'outer,
4579 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4580 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4581 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4582 return Err(ErrorCode::Disconnected.into());
4583 }
4584 }
4585 }
4586
4587 let room = active_call
4588 .update(cx, |active_call, cx| {
4589 active_call.join_channel(channel_id, cx)
4590 })?
4591 .await?;
4592
4593 let Some(room) = room else {
4594 return anyhow::Ok(true);
4595 };
4596
4597 room.update(cx, |room, _| room.room_update_completed())?
4598 .await;
4599
4600 let task = room.update(cx, |room, cx| {
4601 if let Some((project, host)) = room.most_active_project(cx) {
4602 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4603 }
4604
4605 // If you are the first to join a channel, see if you should share your project.
4606 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
4607 if let Some(workspace) = requesting_window {
4608 let project = workspace.update(cx, |workspace, cx| {
4609 let project = workspace.project.read(cx);
4610 let is_dev_server = project.dev_server_project_id().is_some();
4611
4612 if !is_dev_server && !CallSettings::get_global(cx).share_on_join {
4613 return None;
4614 }
4615
4616 if (project.is_local() || is_dev_server)
4617 && project.visible_worktrees(cx).any(|tree| {
4618 tree.read(cx)
4619 .root_entry()
4620 .map_or(false, |entry| entry.is_dir())
4621 })
4622 {
4623 Some(workspace.project.clone())
4624 } else {
4625 None
4626 }
4627 });
4628 if let Ok(Some(project)) = project {
4629 return Some(cx.spawn(|room, mut cx| async move {
4630 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4631 .await?;
4632 Ok(())
4633 }));
4634 }
4635 }
4636 }
4637
4638 None
4639 })?;
4640 if let Some(task) = task {
4641 task.await?;
4642 return anyhow::Ok(true);
4643 }
4644 anyhow::Ok(false)
4645}
4646
4647pub fn join_channel(
4648 channel_id: ChannelId,
4649 app_state: Arc<AppState>,
4650 requesting_window: Option<WindowHandle<Workspace>>,
4651 cx: &mut AppContext,
4652) -> Task<Result<()>> {
4653 let active_call = ActiveCall::global(cx);
4654 cx.spawn(|mut cx| async move {
4655 let result = join_channel_internal(
4656 channel_id,
4657 &app_state,
4658 requesting_window,
4659 &active_call,
4660 &mut cx,
4661 )
4662 .await;
4663
4664 // join channel succeeded, and opened a window
4665 if matches!(result, Ok(true)) {
4666 return anyhow::Ok(());
4667 }
4668
4669 // find an existing workspace to focus and show call controls
4670 let mut active_window =
4671 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4672 if active_window.is_none() {
4673 // no open workspaces, make one to show the error in (blergh)
4674 let (window_handle, _) = cx
4675 .update(|cx| {
4676 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4677 })?
4678 .await?;
4679
4680 if result.is_ok() {
4681 cx.update(|cx| {
4682 cx.dispatch_action(&OpenChannelNotes);
4683 }).log_err();
4684 }
4685
4686 active_window = Some(window_handle);
4687 }
4688
4689 if let Err(err) = result {
4690 log::error!("failed to join channel: {}", err);
4691 if let Some(active_window) = active_window {
4692 active_window
4693 .update(&mut cx, |_, cx| {
4694 let detail: SharedString = match err.error_code() {
4695 ErrorCode::SignedOut => {
4696 "Please sign in to continue.".into()
4697 }
4698 ErrorCode::UpgradeRequired => {
4699 "Your are running an unsupported version of Zed. Please update to continue.".into()
4700 }
4701 ErrorCode::NoSuchChannel => {
4702 "No matching channel was found. Please check the link and try again.".into()
4703 }
4704 ErrorCode::Forbidden => {
4705 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4706 }
4707 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4708 _ => format!("{}\n\nPlease try again.", err).into(),
4709 };
4710 cx.prompt(
4711 PromptLevel::Critical,
4712 "Failed to join channel",
4713 Some(&detail),
4714 &["Ok"],
4715 )
4716 })?
4717 .await
4718 .ok();
4719 }
4720 }
4721
4722 // return ok, we showed the error to the user.
4723 return anyhow::Ok(());
4724 })
4725}
4726
4727pub async fn get_any_active_workspace(
4728 app_state: Arc<AppState>,
4729 mut cx: AsyncAppContext,
4730) -> anyhow::Result<WindowHandle<Workspace>> {
4731 // find an existing workspace to focus and show call controls
4732 let active_window = activate_any_workspace_window(&mut cx);
4733 if active_window.is_none() {
4734 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4735 .await?;
4736 }
4737 activate_any_workspace_window(&mut cx).context("could not open zed")
4738}
4739
4740fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4741 cx.update(|cx| {
4742 if let Some(workspace_window) = cx
4743 .active_window()
4744 .and_then(|window| window.downcast::<Workspace>())
4745 {
4746 return Some(workspace_window);
4747 }
4748
4749 for window in cx.windows() {
4750 if let Some(workspace_window) = window.downcast::<Workspace>() {
4751 workspace_window
4752 .update(cx, |_, cx| cx.activate_window())
4753 .ok();
4754 return Some(workspace_window);
4755 }
4756 }
4757 None
4758 })
4759 .ok()
4760 .flatten()
4761}
4762
4763fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
4764 cx.windows()
4765 .into_iter()
4766 .filter_map(|window| window.downcast::<Workspace>())
4767 .filter(|workspace| {
4768 workspace
4769 .read(cx)
4770 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
4771 })
4772 .collect()
4773}
4774
4775#[derive(Default)]
4776pub struct OpenOptions {
4777 pub open_new_workspace: Option<bool>,
4778 pub replace_window: Option<WindowHandle<Workspace>>,
4779}
4780
4781#[allow(clippy::type_complexity)]
4782pub fn open_paths(
4783 abs_paths: &[PathBuf],
4784 app_state: Arc<AppState>,
4785 open_options: OpenOptions,
4786 cx: &mut AppContext,
4787) -> Task<
4788 anyhow::Result<(
4789 WindowHandle<Workspace>,
4790 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4791 )>,
4792> {
4793 let abs_paths = abs_paths.to_vec();
4794 let mut existing = None;
4795 let mut best_match = None;
4796 let mut open_visible = OpenVisible::All;
4797
4798 if open_options.open_new_workspace != Some(true) {
4799 for window in local_workspace_windows(cx) {
4800 if let Ok(workspace) = window.read(cx) {
4801 let m = workspace
4802 .project
4803 .read(cx)
4804 .visibility_for_paths(&abs_paths, cx);
4805 if m > best_match {
4806 existing = Some(window);
4807 best_match = m;
4808 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
4809 existing = Some(window)
4810 }
4811 }
4812 }
4813 }
4814
4815 cx.spawn(move |mut cx| async move {
4816 if open_options.open_new_workspace.is_none() && existing.is_none() {
4817 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
4818 if futures::future::join_all(all_files)
4819 .await
4820 .into_iter()
4821 .filter_map(|result| result.ok().flatten())
4822 .all(|file| !file.is_dir)
4823 {
4824 cx.update(|cx| {
4825 for window in local_workspace_windows(cx) {
4826 if let Ok(workspace) = window.read(cx) {
4827 let project = workspace.project().read(cx);
4828 if project.is_remote() {
4829 continue;
4830 }
4831 existing = Some(window);
4832 open_visible = OpenVisible::None;
4833 break;
4834 }
4835 }
4836 })?;
4837 }
4838 }
4839
4840 if let Some(existing) = existing {
4841 Ok((
4842 existing,
4843 existing
4844 .update(&mut cx, |workspace, cx| {
4845 cx.activate_window();
4846 workspace.open_paths(abs_paths, open_visible, None, cx)
4847 })?
4848 .await,
4849 ))
4850 } else {
4851 cx.update(move |cx| {
4852 Workspace::new_local(
4853 abs_paths,
4854 app_state.clone(),
4855 open_options.replace_window,
4856 cx,
4857 )
4858 })?
4859 .await
4860 }
4861 })
4862}
4863
4864pub fn open_new(
4865 app_state: Arc<AppState>,
4866 cx: &mut AppContext,
4867 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4868) -> Task<anyhow::Result<()>> {
4869 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
4870 cx.spawn(|mut cx| async move {
4871 let (workspace, opened_paths) = task.await?;
4872 workspace.update(&mut cx, |workspace, cx| {
4873 if opened_paths.is_empty() {
4874 init(workspace, cx)
4875 }
4876 })?;
4877 Ok(())
4878 })
4879}
4880
4881pub fn create_and_open_local_file(
4882 path: &'static Path,
4883 cx: &mut ViewContext<Workspace>,
4884 default_content: impl 'static + Send + FnOnce() -> Rope,
4885) -> Task<Result<Box<dyn ItemHandle>>> {
4886 cx.spawn(|workspace, mut cx| async move {
4887 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4888 if !fs.is_file(path).await {
4889 fs.create_file(path, Default::default()).await?;
4890 fs.save(path, &default_content(), Default::default())
4891 .await?;
4892 }
4893
4894 let mut items = workspace
4895 .update(&mut cx, |workspace, cx| {
4896 workspace.with_local_workspace(cx, |workspace, cx| {
4897 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4898 })
4899 })?
4900 .await?
4901 .await;
4902
4903 let item = items.pop().flatten();
4904 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4905 })
4906}
4907
4908pub fn join_hosted_project(
4909 hosted_project_id: ProjectId,
4910 app_state: Arc<AppState>,
4911 cx: &mut AppContext,
4912) -> Task<Result<()>> {
4913 cx.spawn(|mut cx| async move {
4914 let existing_window = cx.update(|cx| {
4915 cx.windows().into_iter().find_map(|window| {
4916 let workspace = window.downcast::<Workspace>()?;
4917 workspace
4918 .read(cx)
4919 .is_ok_and(|workspace| {
4920 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
4921 })
4922 .then(|| workspace)
4923 })
4924 })?;
4925
4926 let workspace = if let Some(existing_window) = existing_window {
4927 existing_window
4928 } else {
4929 let project = Project::hosted(
4930 hosted_project_id,
4931 app_state.user_store.clone(),
4932 app_state.client.clone(),
4933 app_state.languages.clone(),
4934 app_state.fs.clone(),
4935 cx.clone(),
4936 )
4937 .await?;
4938
4939 let window_bounds_override = window_bounds_env_override();
4940 cx.update(|cx| {
4941 let mut options = (app_state.build_window_options)(None, cx);
4942 options.window_bounds =
4943 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
4944 cx.open_window(options, |cx| {
4945 cx.new_view(|cx| {
4946 Workspace::new(Default::default(), project, app_state.clone(), cx)
4947 })
4948 })
4949 })??
4950 };
4951
4952 workspace.update(&mut cx, |_, cx| {
4953 cx.activate(true);
4954 cx.activate_window();
4955 })?;
4956
4957 Ok(())
4958 })
4959}
4960
4961pub fn join_dev_server_project(
4962 dev_server_project_id: DevServerProjectId,
4963 project_id: ProjectId,
4964 app_state: Arc<AppState>,
4965 window_to_replace: Option<WindowHandle<Workspace>>,
4966 cx: &mut AppContext,
4967) -> Task<Result<WindowHandle<Workspace>>> {
4968 let windows = cx.windows();
4969 cx.spawn(|mut cx| async move {
4970 let existing_workspace = windows.into_iter().find_map(|window| {
4971 window.downcast::<Workspace>().and_then(|window| {
4972 window
4973 .update(&mut cx, |workspace, cx| {
4974 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
4975 Some(window)
4976 } else {
4977 None
4978 }
4979 })
4980 .unwrap_or(None)
4981 })
4982 });
4983
4984 let workspace = if let Some(existing_workspace) = existing_workspace {
4985 existing_workspace
4986 } else {
4987 let project = Project::remote(
4988 project_id.0,
4989 app_state.client.clone(),
4990 app_state.user_store.clone(),
4991 app_state.languages.clone(),
4992 app_state.fs.clone(),
4993 cx.clone(),
4994 )
4995 .await?;
4996
4997 let serialized_workspace: Option<SerializedWorkspace> =
4998 persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
4999
5000 let workspace_id = if let Some(serialized_workspace) = serialized_workspace {
5001 serialized_workspace.id
5002 } else {
5003 persistence::DB.next_id().await?
5004 };
5005
5006 if let Some(window_to_replace) = window_to_replace {
5007 cx.update_window(window_to_replace.into(), |_, cx| {
5008 cx.replace_root_view(|cx| {
5009 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5010 });
5011 })?;
5012 window_to_replace
5013 } else {
5014 let window_bounds_override = window_bounds_env_override();
5015 cx.update(|cx| {
5016 let mut options = (app_state.build_window_options)(None, cx);
5017 options.window_bounds =
5018 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5019 cx.open_window(options, |cx| {
5020 cx.new_view(|cx| {
5021 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5022 })
5023 })
5024 })??
5025 }
5026 };
5027
5028 workspace.update(&mut cx, |_, cx| {
5029 cx.activate(true);
5030 cx.activate_window();
5031 })?;
5032
5033 anyhow::Ok(workspace)
5034 })
5035}
5036
5037pub fn join_in_room_project(
5038 project_id: u64,
5039 follow_user_id: u64,
5040 app_state: Arc<AppState>,
5041 cx: &mut AppContext,
5042) -> Task<Result<()>> {
5043 let windows = cx.windows();
5044 cx.spawn(|mut cx| async move {
5045 let existing_workspace = windows.into_iter().find_map(|window| {
5046 window.downcast::<Workspace>().and_then(|window| {
5047 window
5048 .update(&mut cx, |workspace, cx| {
5049 if workspace.project().read(cx).remote_id() == Some(project_id) {
5050 Some(window)
5051 } else {
5052 None
5053 }
5054 })
5055 .unwrap_or(None)
5056 })
5057 });
5058
5059 let workspace = if let Some(existing_workspace) = existing_workspace {
5060 existing_workspace
5061 } else {
5062 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5063 let room = active_call
5064 .read_with(&cx, |call, _| call.room().cloned())?
5065 .ok_or_else(|| anyhow!("not in a call"))?;
5066 let project = room
5067 .update(&mut cx, |room, cx| {
5068 room.join_project(
5069 project_id,
5070 app_state.languages.clone(),
5071 app_state.fs.clone(),
5072 cx,
5073 )
5074 })?
5075 .await?;
5076
5077 let window_bounds_override = window_bounds_env_override();
5078 cx.update(|cx| {
5079 let mut options = (app_state.build_window_options)(None, cx);
5080 options.window_bounds =
5081 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5082 cx.open_window(options, |cx| {
5083 cx.new_view(|cx| {
5084 Workspace::new(Default::default(), project, app_state.clone(), cx)
5085 })
5086 })
5087 })??
5088 };
5089
5090 workspace.update(&mut cx, |workspace, cx| {
5091 cx.activate(true);
5092 cx.activate_window();
5093
5094 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5095 let follow_peer_id = room
5096 .read(cx)
5097 .remote_participants()
5098 .iter()
5099 .find(|(_, participant)| participant.user.id == follow_user_id)
5100 .map(|(_, p)| p.peer_id)
5101 .or_else(|| {
5102 // If we couldn't follow the given user, follow the host instead.
5103 let collaborator = workspace
5104 .project()
5105 .read(cx)
5106 .collaborators()
5107 .values()
5108 .find(|collaborator| collaborator.replica_id == 0)?;
5109 Some(collaborator.peer_id)
5110 });
5111
5112 if let Some(follow_peer_id) = follow_peer_id {
5113 workspace.follow(follow_peer_id, cx);
5114 }
5115 }
5116 })?;
5117
5118 anyhow::Ok(())
5119 })
5120}
5121
5122pub fn reload(reload: &Reload, cx: &mut AppContext) {
5123 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5124 let mut workspace_windows = cx
5125 .windows()
5126 .into_iter()
5127 .filter_map(|window| window.downcast::<Workspace>())
5128 .collect::<Vec<_>>();
5129
5130 // If multiple windows have unsaved changes, and need a save prompt,
5131 // prompt in the active window before switching to a different window.
5132 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5133
5134 let mut prompt = None;
5135 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5136 prompt = window
5137 .update(cx, |_, cx| {
5138 cx.prompt(
5139 PromptLevel::Info,
5140 "Are you sure you want to restart?",
5141 None,
5142 &["Restart", "Cancel"],
5143 )
5144 })
5145 .ok();
5146 }
5147
5148 let binary_path = reload.binary_path.clone();
5149 cx.spawn(|mut cx| async move {
5150 if let Some(prompt) = prompt {
5151 let answer = prompt.await?;
5152 if answer != 0 {
5153 return Ok(());
5154 }
5155 }
5156
5157 // If the user cancels any save prompt, then keep the app open.
5158 for window in workspace_windows {
5159 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5160 workspace.prepare_to_close(true, cx)
5161 }) {
5162 if !should_close.await? {
5163 return Ok(());
5164 }
5165 }
5166 }
5167
5168 cx.update(|cx| cx.restart(binary_path))
5169 })
5170 .detach_and_log_err(cx);
5171}
5172
5173fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5174 let mut parts = value.split(',');
5175 let x: usize = parts.next()?.parse().ok()?;
5176 let y: usize = parts.next()?.parse().ok()?;
5177 Some(point(px(x as f32), px(y as f32)))
5178}
5179
5180fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5181 let mut parts = value.split(',');
5182 let width: usize = parts.next()?.parse().ok()?;
5183 let height: usize = parts.next()?.parse().ok()?;
5184 Some(size(px(width as f32), px(height as f32)))
5185}
5186
5187#[cfg(test)]
5188mod tests {
5189 use std::{cell::RefCell, rc::Rc};
5190
5191 use super::*;
5192 use crate::{
5193 dock::{test::TestPanel, PanelEvent},
5194 item::{
5195 test::{TestItem, TestProjectItem},
5196 ItemEvent,
5197 },
5198 };
5199 use fs::FakeFs;
5200 use gpui::{
5201 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
5202 UpdateGlobal, VisualTestContext,
5203 };
5204 use project::{Project, ProjectEntryId};
5205 use serde_json::json;
5206 use settings::SettingsStore;
5207
5208 #[gpui::test]
5209 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
5210 init_test(cx);
5211
5212 let fs = FakeFs::new(cx.executor());
5213 let project = Project::test(fs, [], cx).await;
5214 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5215
5216 // Adding an item with no ambiguity renders the tab without detail.
5217 let item1 = cx.new_view(|cx| {
5218 let mut item = TestItem::new(cx);
5219 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
5220 item
5221 });
5222 workspace.update(cx, |workspace, cx| {
5223 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5224 });
5225 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
5226
5227 // Adding an item that creates ambiguity increases the level of detail on
5228 // both tabs.
5229 let item2 = cx.new_view(|cx| {
5230 let mut item = TestItem::new(cx);
5231 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5232 item
5233 });
5234 workspace.update(cx, |workspace, cx| {
5235 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5236 });
5237 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5238 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5239
5240 // Adding an item that creates ambiguity increases the level of detail only
5241 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
5242 // we stop at the highest detail available.
5243 let item3 = cx.new_view(|cx| {
5244 let mut item = TestItem::new(cx);
5245 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5246 item
5247 });
5248 workspace.update(cx, |workspace, cx| {
5249 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5250 });
5251 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5252 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5253 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5254 }
5255
5256 #[gpui::test]
5257 async fn test_tracking_active_path(cx: &mut TestAppContext) {
5258 init_test(cx);
5259
5260 let fs = FakeFs::new(cx.executor());
5261 fs.insert_tree(
5262 "/root1",
5263 json!({
5264 "one.txt": "",
5265 "two.txt": "",
5266 }),
5267 )
5268 .await;
5269 fs.insert_tree(
5270 "/root2",
5271 json!({
5272 "three.txt": "",
5273 }),
5274 )
5275 .await;
5276
5277 let project = Project::test(fs, ["root1".as_ref()], cx).await;
5278 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5279 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5280 let worktree_id = project.update(cx, |project, cx| {
5281 project.worktrees().next().unwrap().read(cx).id()
5282 });
5283
5284 let item1 = cx.new_view(|cx| {
5285 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5286 });
5287 let item2 = cx.new_view(|cx| {
5288 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
5289 });
5290
5291 // Add an item to an empty pane
5292 workspace.update(cx, |workspace, cx| {
5293 workspace.add_item_to_active_pane(Box::new(item1), None, cx)
5294 });
5295 project.update(cx, |project, cx| {
5296 assert_eq!(
5297 project.active_entry(),
5298 project
5299 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5300 .map(|e| e.id)
5301 );
5302 });
5303 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5304
5305 // Add a second item to a non-empty pane
5306 workspace.update(cx, |workspace, cx| {
5307 workspace.add_item_to_active_pane(Box::new(item2), None, cx)
5308 });
5309 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5310 project.update(cx, |project, cx| {
5311 assert_eq!(
5312 project.active_entry(),
5313 project
5314 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5315 .map(|e| e.id)
5316 );
5317 });
5318
5319 // Close the active item
5320 pane.update(cx, |pane, cx| {
5321 pane.close_active_item(&Default::default(), cx).unwrap()
5322 })
5323 .await
5324 .unwrap();
5325 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5326 project.update(cx, |project, cx| {
5327 assert_eq!(
5328 project.active_entry(),
5329 project
5330 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5331 .map(|e| e.id)
5332 );
5333 });
5334
5335 // Add a project folder
5336 project
5337 .update(cx, |project, cx| {
5338 project.find_or_create_local_worktree("root2", true, cx)
5339 })
5340 .await
5341 .unwrap();
5342 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5343
5344 // Remove a project folder
5345 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5346 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5347 }
5348
5349 #[gpui::test]
5350 async fn test_close_window(cx: &mut TestAppContext) {
5351 init_test(cx);
5352
5353 let fs = FakeFs::new(cx.executor());
5354 fs.insert_tree("/root", json!({ "one": "" })).await;
5355
5356 let project = Project::test(fs, ["root".as_ref()], cx).await;
5357 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5358
5359 // When there are no dirty items, there's nothing to do.
5360 let item1 = cx.new_view(|cx| TestItem::new(cx));
5361 workspace.update(cx, |w, cx| {
5362 w.add_item_to_active_pane(Box::new(item1.clone()), None, cx)
5363 });
5364 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5365 assert!(task.await.unwrap());
5366
5367 // When there are dirty untitled items, prompt to save each one. If the user
5368 // cancels any prompt, then abort.
5369 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5370 let item3 = cx.new_view(|cx| {
5371 TestItem::new(cx)
5372 .with_dirty(true)
5373 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5374 });
5375 workspace.update(cx, |w, cx| {
5376 w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5377 w.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5378 });
5379 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5380 cx.executor().run_until_parked();
5381 cx.simulate_prompt_answer(2); // cancel save all
5382 cx.executor().run_until_parked();
5383 cx.simulate_prompt_answer(2); // cancel save all
5384 cx.executor().run_until_parked();
5385 assert!(!cx.has_pending_prompt());
5386 assert!(!task.await.unwrap());
5387 }
5388
5389 #[gpui::test]
5390 async fn test_close_pane_items(cx: &mut TestAppContext) {
5391 init_test(cx);
5392
5393 let fs = FakeFs::new(cx.executor());
5394
5395 let project = Project::test(fs, None, cx).await;
5396 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5397
5398 let item1 = cx.new_view(|cx| {
5399 TestItem::new(cx)
5400 .with_dirty(true)
5401 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5402 });
5403 let item2 = cx.new_view(|cx| {
5404 TestItem::new(cx)
5405 .with_dirty(true)
5406 .with_conflict(true)
5407 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5408 });
5409 let item3 = cx.new_view(|cx| {
5410 TestItem::new(cx)
5411 .with_dirty(true)
5412 .with_conflict(true)
5413 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5414 });
5415 let item4 = cx.new_view(|cx| {
5416 TestItem::new(cx)
5417 .with_dirty(true)
5418 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5419 });
5420 let pane = workspace.update(cx, |workspace, cx| {
5421 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5422 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5423 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5424 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, cx);
5425 workspace.active_pane().clone()
5426 });
5427
5428 let close_items = pane.update(cx, |pane, cx| {
5429 pane.activate_item(1, true, true, cx);
5430 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5431 let item1_id = item1.item_id();
5432 let item3_id = item3.item_id();
5433 let item4_id = item4.item_id();
5434 pane.close_items(cx, SaveIntent::Close, move |id| {
5435 [item1_id, item3_id, item4_id].contains(&id)
5436 })
5437 });
5438 cx.executor().run_until_parked();
5439
5440 assert!(cx.has_pending_prompt());
5441 // Ignore "Save all" prompt
5442 cx.simulate_prompt_answer(2);
5443 cx.executor().run_until_parked();
5444 // There's a prompt to save item 1.
5445 pane.update(cx, |pane, _| {
5446 assert_eq!(pane.items_len(), 4);
5447 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
5448 });
5449 // Confirm saving item 1.
5450 cx.simulate_prompt_answer(0);
5451 cx.executor().run_until_parked();
5452
5453 // Item 1 is saved. There's a prompt to save item 3.
5454 pane.update(cx, |pane, cx| {
5455 assert_eq!(item1.read(cx).save_count, 1);
5456 assert_eq!(item1.read(cx).save_as_count, 0);
5457 assert_eq!(item1.read(cx).reload_count, 0);
5458 assert_eq!(pane.items_len(), 3);
5459 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
5460 });
5461 assert!(cx.has_pending_prompt());
5462
5463 // Cancel saving item 3.
5464 cx.simulate_prompt_answer(1);
5465 cx.executor().run_until_parked();
5466
5467 // Item 3 is reloaded. There's a prompt to save item 4.
5468 pane.update(cx, |pane, cx| {
5469 assert_eq!(item3.read(cx).save_count, 0);
5470 assert_eq!(item3.read(cx).save_as_count, 0);
5471 assert_eq!(item3.read(cx).reload_count, 1);
5472 assert_eq!(pane.items_len(), 2);
5473 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
5474 });
5475 assert!(cx.has_pending_prompt());
5476
5477 // Confirm saving item 4.
5478 cx.simulate_prompt_answer(0);
5479 cx.executor().run_until_parked();
5480
5481 // There's a prompt for a path for item 4.
5482 cx.simulate_new_path_selection(|_| Some(Default::default()));
5483 close_items.await.unwrap();
5484
5485 // The requested items are closed.
5486 pane.update(cx, |pane, cx| {
5487 assert_eq!(item4.read(cx).save_count, 0);
5488 assert_eq!(item4.read(cx).save_as_count, 1);
5489 assert_eq!(item4.read(cx).reload_count, 0);
5490 assert_eq!(pane.items_len(), 1);
5491 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5492 });
5493 }
5494
5495 #[gpui::test]
5496 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
5497 init_test(cx);
5498
5499 let fs = FakeFs::new(cx.executor());
5500 let project = Project::test(fs, [], cx).await;
5501 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5502
5503 // Create several workspace items with single project entries, and two
5504 // workspace items with multiple project entries.
5505 let single_entry_items = (0..=4)
5506 .map(|project_entry_id| {
5507 cx.new_view(|cx| {
5508 TestItem::new(cx)
5509 .with_dirty(true)
5510 .with_project_items(&[TestProjectItem::new(
5511 project_entry_id,
5512 &format!("{project_entry_id}.txt"),
5513 cx,
5514 )])
5515 })
5516 })
5517 .collect::<Vec<_>>();
5518 let item_2_3 = cx.new_view(|cx| {
5519 TestItem::new(cx)
5520 .with_dirty(true)
5521 .with_singleton(false)
5522 .with_project_items(&[
5523 single_entry_items[2].read(cx).project_items[0].clone(),
5524 single_entry_items[3].read(cx).project_items[0].clone(),
5525 ])
5526 });
5527 let item_3_4 = cx.new_view(|cx| {
5528 TestItem::new(cx)
5529 .with_dirty(true)
5530 .with_singleton(false)
5531 .with_project_items(&[
5532 single_entry_items[3].read(cx).project_items[0].clone(),
5533 single_entry_items[4].read(cx).project_items[0].clone(),
5534 ])
5535 });
5536
5537 // Create two panes that contain the following project entries:
5538 // left pane:
5539 // multi-entry items: (2, 3)
5540 // single-entry items: 0, 1, 2, 3, 4
5541 // right pane:
5542 // single-entry items: 1
5543 // multi-entry items: (3, 4)
5544 let left_pane = workspace.update(cx, |workspace, cx| {
5545 let left_pane = workspace.active_pane().clone();
5546 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, cx);
5547 for item in single_entry_items {
5548 workspace.add_item_to_active_pane(Box::new(item), None, cx);
5549 }
5550 left_pane.update(cx, |pane, cx| {
5551 pane.activate_item(2, true, true, cx);
5552 });
5553
5554 let right_pane = workspace
5555 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5556 .unwrap();
5557
5558 right_pane.update(cx, |pane, cx| {
5559 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5560 });
5561
5562 left_pane
5563 });
5564
5565 cx.focus_view(&left_pane);
5566
5567 // When closing all of the items in the left pane, we should be prompted twice:
5568 // once for project entry 0, and once for project entry 2. Project entries 1,
5569 // 3, and 4 are all still open in the other paten. After those two
5570 // prompts, the task should complete.
5571
5572 let close = left_pane.update(cx, |pane, cx| {
5573 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5574 });
5575 cx.executor().run_until_parked();
5576
5577 // Discard "Save all" prompt
5578 cx.simulate_prompt_answer(2);
5579
5580 cx.executor().run_until_parked();
5581 left_pane.update(cx, |pane, cx| {
5582 assert_eq!(
5583 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5584 &[ProjectEntryId::from_proto(0)]
5585 );
5586 });
5587 cx.simulate_prompt_answer(0);
5588
5589 cx.executor().run_until_parked();
5590 left_pane.update(cx, |pane, cx| {
5591 assert_eq!(
5592 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5593 &[ProjectEntryId::from_proto(2)]
5594 );
5595 });
5596 cx.simulate_prompt_answer(0);
5597
5598 cx.executor().run_until_parked();
5599 close.await.unwrap();
5600 left_pane.update(cx, |pane, _| {
5601 assert_eq!(pane.items_len(), 0);
5602 });
5603 }
5604
5605 #[gpui::test]
5606 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5607 init_test(cx);
5608
5609 let fs = FakeFs::new(cx.executor());
5610 let project = Project::test(fs, [], cx).await;
5611 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5612 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5613
5614 let item = cx.new_view(|cx| {
5615 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5616 });
5617 let item_id = item.entity_id();
5618 workspace.update(cx, |workspace, cx| {
5619 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5620 });
5621
5622 // Autosave on window change.
5623 item.update(cx, |item, cx| {
5624 SettingsStore::update_global(cx, |settings, cx| {
5625 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5626 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5627 })
5628 });
5629 item.is_dirty = true;
5630 });
5631
5632 // Deactivating the window saves the file.
5633 cx.deactivate_window();
5634 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5635
5636 // Re-activating the window doesn't save the file.
5637 cx.update(|cx| cx.activate_window());
5638 cx.executor().run_until_parked();
5639 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5640
5641 // Autosave on focus change.
5642 item.update(cx, |item, cx| {
5643 cx.focus_self();
5644 SettingsStore::update_global(cx, |settings, cx| {
5645 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5646 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5647 })
5648 });
5649 item.is_dirty = true;
5650 });
5651
5652 // Blurring the item saves the file.
5653 item.update(cx, |_, cx| cx.blur());
5654 cx.executor().run_until_parked();
5655 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5656
5657 // Deactivating the window still saves the file.
5658 item.update(cx, |item, cx| {
5659 cx.focus_self();
5660 item.is_dirty = true;
5661 });
5662 cx.deactivate_window();
5663 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5664
5665 // Autosave after delay.
5666 item.update(cx, |item, cx| {
5667 SettingsStore::update_global(cx, |settings, cx| {
5668 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5669 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5670 })
5671 });
5672 item.is_dirty = true;
5673 cx.emit(ItemEvent::Edit);
5674 });
5675
5676 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5677 cx.executor().advance_clock(Duration::from_millis(250));
5678 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5679
5680 // After delay expires, the file is saved.
5681 cx.executor().advance_clock(Duration::from_millis(250));
5682 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5683
5684 // Autosave on focus change, ensuring closing the tab counts as such.
5685 item.update(cx, |item, cx| {
5686 SettingsStore::update_global(cx, |settings, cx| {
5687 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5688 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5689 })
5690 });
5691 item.is_dirty = true;
5692 });
5693
5694 pane.update(cx, |pane, cx| {
5695 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5696 })
5697 .await
5698 .unwrap();
5699 assert!(!cx.has_pending_prompt());
5700 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5701
5702 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5703 workspace.update(cx, |workspace, cx| {
5704 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5705 });
5706 item.update(cx, |item, cx| {
5707 item.project_items[0].update(cx, |item, _| {
5708 item.entry_id = None;
5709 });
5710 item.is_dirty = true;
5711 cx.blur();
5712 });
5713 cx.run_until_parked();
5714 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5715
5716 // Ensure autosave is prevented for deleted files also when closing the buffer.
5717 let _close_items = pane.update(cx, |pane, cx| {
5718 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5719 });
5720 cx.run_until_parked();
5721 assert!(cx.has_pending_prompt());
5722 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5723 }
5724
5725 #[gpui::test]
5726 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5727 init_test(cx);
5728
5729 let fs = FakeFs::new(cx.executor());
5730
5731 let project = Project::test(fs, [], cx).await;
5732 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5733
5734 let item = cx.new_view(|cx| {
5735 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5736 });
5737 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5738 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5739 let toolbar_notify_count = Rc::new(RefCell::new(0));
5740
5741 workspace.update(cx, |workspace, cx| {
5742 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
5743 let toolbar_notification_count = toolbar_notify_count.clone();
5744 cx.observe(&toolbar, move |_, _, _| {
5745 *toolbar_notification_count.borrow_mut() += 1
5746 })
5747 .detach();
5748 });
5749
5750 pane.update(cx, |pane, _| {
5751 assert!(!pane.can_navigate_backward());
5752 assert!(!pane.can_navigate_forward());
5753 });
5754
5755 item.update(cx, |item, cx| {
5756 item.set_state("one".to_string(), cx);
5757 });
5758
5759 // Toolbar must be notified to re-render the navigation buttons
5760 assert_eq!(*toolbar_notify_count.borrow(), 1);
5761
5762 pane.update(cx, |pane, _| {
5763 assert!(pane.can_navigate_backward());
5764 assert!(!pane.can_navigate_forward());
5765 });
5766
5767 workspace
5768 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5769 .await
5770 .unwrap();
5771
5772 assert_eq!(*toolbar_notify_count.borrow(), 2);
5773 pane.update(cx, |pane, _| {
5774 assert!(!pane.can_navigate_backward());
5775 assert!(pane.can_navigate_forward());
5776 });
5777 }
5778
5779 #[gpui::test]
5780 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5781 init_test(cx);
5782 let fs = FakeFs::new(cx.executor());
5783
5784 let project = Project::test(fs, [], cx).await;
5785 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5786
5787 let panel = workspace.update(cx, |workspace, cx| {
5788 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5789 workspace.add_panel(panel.clone(), cx);
5790
5791 workspace
5792 .right_dock()
5793 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5794
5795 panel
5796 });
5797
5798 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5799 pane.update(cx, |pane, cx| {
5800 let item = cx.new_view(|cx| TestItem::new(cx));
5801 pane.add_item(Box::new(item), true, true, None, cx);
5802 });
5803
5804 // Transfer focus from center to panel
5805 workspace.update(cx, |workspace, cx| {
5806 workspace.toggle_panel_focus::<TestPanel>(cx);
5807 });
5808
5809 workspace.update(cx, |workspace, cx| {
5810 assert!(workspace.right_dock().read(cx).is_open());
5811 assert!(!panel.is_zoomed(cx));
5812 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5813 });
5814
5815 // Transfer focus from panel to center
5816 workspace.update(cx, |workspace, cx| {
5817 workspace.toggle_panel_focus::<TestPanel>(cx);
5818 });
5819
5820 workspace.update(cx, |workspace, cx| {
5821 assert!(workspace.right_dock().read(cx).is_open());
5822 assert!(!panel.is_zoomed(cx));
5823 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5824 });
5825
5826 // Close the dock
5827 workspace.update(cx, |workspace, cx| {
5828 workspace.toggle_dock(DockPosition::Right, cx);
5829 });
5830
5831 workspace.update(cx, |workspace, cx| {
5832 assert!(!workspace.right_dock().read(cx).is_open());
5833 assert!(!panel.is_zoomed(cx));
5834 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5835 });
5836
5837 // Open the dock
5838 workspace.update(cx, |workspace, cx| {
5839 workspace.toggle_dock(DockPosition::Right, cx);
5840 });
5841
5842 workspace.update(cx, |workspace, cx| {
5843 assert!(workspace.right_dock().read(cx).is_open());
5844 assert!(!panel.is_zoomed(cx));
5845 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5846 });
5847
5848 // Focus and zoom panel
5849 panel.update(cx, |panel, cx| {
5850 cx.focus_self();
5851 panel.set_zoomed(true, cx)
5852 });
5853
5854 workspace.update(cx, |workspace, cx| {
5855 assert!(workspace.right_dock().read(cx).is_open());
5856 assert!(panel.is_zoomed(cx));
5857 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5858 });
5859
5860 // Transfer focus to the center closes the dock
5861 workspace.update(cx, |workspace, cx| {
5862 workspace.toggle_panel_focus::<TestPanel>(cx);
5863 });
5864
5865 workspace.update(cx, |workspace, cx| {
5866 assert!(!workspace.right_dock().read(cx).is_open());
5867 assert!(panel.is_zoomed(cx));
5868 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5869 });
5870
5871 // Transferring focus back to the panel keeps it zoomed
5872 workspace.update(cx, |workspace, cx| {
5873 workspace.toggle_panel_focus::<TestPanel>(cx);
5874 });
5875
5876 workspace.update(cx, |workspace, cx| {
5877 assert!(workspace.right_dock().read(cx).is_open());
5878 assert!(panel.is_zoomed(cx));
5879 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5880 });
5881
5882 // Close the dock while it is zoomed
5883 workspace.update(cx, |workspace, cx| {
5884 workspace.toggle_dock(DockPosition::Right, cx)
5885 });
5886
5887 workspace.update(cx, |workspace, cx| {
5888 assert!(!workspace.right_dock().read(cx).is_open());
5889 assert!(panel.is_zoomed(cx));
5890 assert!(workspace.zoomed.is_none());
5891 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5892 });
5893
5894 // Opening the dock, when it's zoomed, retains focus
5895 workspace.update(cx, |workspace, cx| {
5896 workspace.toggle_dock(DockPosition::Right, cx)
5897 });
5898
5899 workspace.update(cx, |workspace, cx| {
5900 assert!(workspace.right_dock().read(cx).is_open());
5901 assert!(panel.is_zoomed(cx));
5902 assert!(workspace.zoomed.is_some());
5903 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5904 });
5905
5906 // Unzoom and close the panel, zoom the active pane.
5907 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5908 workspace.update(cx, |workspace, cx| {
5909 workspace.toggle_dock(DockPosition::Right, cx)
5910 });
5911 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5912
5913 // Opening a dock unzooms the pane.
5914 workspace.update(cx, |workspace, cx| {
5915 workspace.toggle_dock(DockPosition::Right, cx)
5916 });
5917 workspace.update(cx, |workspace, cx| {
5918 let pane = pane.read(cx);
5919 assert!(!pane.is_zoomed());
5920 assert!(!pane.focus_handle(cx).is_focused(cx));
5921 assert!(workspace.right_dock().read(cx).is_open());
5922 assert!(workspace.zoomed.is_none());
5923 });
5924 }
5925
5926 struct TestModal(FocusHandle);
5927
5928 impl TestModal {
5929 fn new(cx: &mut ViewContext<Self>) -> Self {
5930 Self(cx.focus_handle())
5931 }
5932 }
5933
5934 impl EventEmitter<DismissEvent> for TestModal {}
5935
5936 impl FocusableView for TestModal {
5937 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5938 self.0.clone()
5939 }
5940 }
5941
5942 impl ModalView for TestModal {}
5943
5944 impl Render for TestModal {
5945 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5946 div().track_focus(&self.0)
5947 }
5948 }
5949
5950 #[gpui::test]
5951 async fn test_panels(cx: &mut gpui::TestAppContext) {
5952 init_test(cx);
5953 let fs = FakeFs::new(cx.executor());
5954
5955 let project = Project::test(fs, [], cx).await;
5956 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5957
5958 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5959 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5960 workspace.add_panel(panel_1.clone(), cx);
5961 workspace
5962 .left_dock()
5963 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5964 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5965 workspace.add_panel(panel_2.clone(), cx);
5966 workspace
5967 .right_dock()
5968 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5969
5970 let left_dock = workspace.left_dock();
5971 assert_eq!(
5972 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5973 panel_1.panel_id()
5974 );
5975 assert_eq!(
5976 left_dock.read(cx).active_panel_size(cx).unwrap(),
5977 panel_1.size(cx)
5978 );
5979
5980 left_dock.update(cx, |left_dock, cx| {
5981 left_dock.resize_active_panel(Some(px(1337.)), cx)
5982 });
5983 assert_eq!(
5984 workspace
5985 .right_dock()
5986 .read(cx)
5987 .visible_panel()
5988 .unwrap()
5989 .panel_id(),
5990 panel_2.panel_id(),
5991 );
5992
5993 (panel_1, panel_2)
5994 });
5995
5996 // Move panel_1 to the right
5997 panel_1.update(cx, |panel_1, cx| {
5998 panel_1.set_position(DockPosition::Right, cx)
5999 });
6000
6001 workspace.update(cx, |workspace, cx| {
6002 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
6003 // Since it was the only panel on the left, the left dock should now be closed.
6004 assert!(!workspace.left_dock().read(cx).is_open());
6005 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
6006 let right_dock = workspace.right_dock();
6007 assert_eq!(
6008 right_dock.read(cx).visible_panel().unwrap().panel_id(),
6009 panel_1.panel_id()
6010 );
6011 assert_eq!(
6012 right_dock.read(cx).active_panel_size(cx).unwrap(),
6013 px(1337.)
6014 );
6015
6016 // Now we move panel_2 to the left
6017 panel_2.set_position(DockPosition::Left, cx);
6018 });
6019
6020 workspace.update(cx, |workspace, cx| {
6021 // Since panel_2 was not visible on the right, we don't open the left dock.
6022 assert!(!workspace.left_dock().read(cx).is_open());
6023 // And the right dock is unaffected in its displaying of panel_1
6024 assert!(workspace.right_dock().read(cx).is_open());
6025 assert_eq!(
6026 workspace
6027 .right_dock()
6028 .read(cx)
6029 .visible_panel()
6030 .unwrap()
6031 .panel_id(),
6032 panel_1.panel_id(),
6033 );
6034 });
6035
6036 // Move panel_1 back to the left
6037 panel_1.update(cx, |panel_1, cx| {
6038 panel_1.set_position(DockPosition::Left, cx)
6039 });
6040
6041 workspace.update(cx, |workspace, cx| {
6042 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
6043 let left_dock = workspace.left_dock();
6044 assert!(left_dock.read(cx).is_open());
6045 assert_eq!(
6046 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6047 panel_1.panel_id()
6048 );
6049 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
6050 // And the right dock should be closed as it no longer has any panels.
6051 assert!(!workspace.right_dock().read(cx).is_open());
6052
6053 // Now we move panel_1 to the bottom
6054 panel_1.set_position(DockPosition::Bottom, cx);
6055 });
6056
6057 workspace.update(cx, |workspace, cx| {
6058 // Since panel_1 was visible on the left, we close the left dock.
6059 assert!(!workspace.left_dock().read(cx).is_open());
6060 // The bottom dock is sized based on the panel's default size,
6061 // since the panel orientation changed from vertical to horizontal.
6062 let bottom_dock = workspace.bottom_dock();
6063 assert_eq!(
6064 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
6065 panel_1.size(cx),
6066 );
6067 // Close bottom dock and move panel_1 back to the left.
6068 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
6069 panel_1.set_position(DockPosition::Left, cx);
6070 });
6071
6072 // Emit activated event on panel 1
6073 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
6074
6075 // Now the left dock is open and panel_1 is active and focused.
6076 workspace.update(cx, |workspace, cx| {
6077 let left_dock = workspace.left_dock();
6078 assert!(left_dock.read(cx).is_open());
6079 assert_eq!(
6080 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6081 panel_1.panel_id(),
6082 );
6083 assert!(panel_1.focus_handle(cx).is_focused(cx));
6084 });
6085
6086 // Emit closed event on panel 2, which is not active
6087 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6088
6089 // Wo don't close the left dock, because panel_2 wasn't the active panel
6090 workspace.update(cx, |workspace, cx| {
6091 let left_dock = workspace.left_dock();
6092 assert!(left_dock.read(cx).is_open());
6093 assert_eq!(
6094 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6095 panel_1.panel_id(),
6096 );
6097 });
6098
6099 // Emitting a ZoomIn event shows the panel as zoomed.
6100 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
6101 workspace.update(cx, |workspace, _| {
6102 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6103 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
6104 });
6105
6106 // Move panel to another dock while it is zoomed
6107 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
6108 workspace.update(cx, |workspace, _| {
6109 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6110
6111 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6112 });
6113
6114 // This is a helper for getting a:
6115 // - valid focus on an element,
6116 // - that isn't a part of the panes and panels system of the Workspace,
6117 // - and doesn't trigger the 'on_focus_lost' API.
6118 let focus_other_view = {
6119 let workspace = workspace.clone();
6120 move |cx: &mut VisualTestContext| {
6121 workspace.update(cx, |workspace, cx| {
6122 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
6123 workspace.toggle_modal(cx, TestModal::new);
6124 workspace.toggle_modal(cx, TestModal::new);
6125 } else {
6126 workspace.toggle_modal(cx, TestModal::new);
6127 }
6128 })
6129 }
6130 };
6131
6132 // If focus is transferred to another view that's not a panel or another pane, we still show
6133 // the panel as zoomed.
6134 focus_other_view(cx);
6135 workspace.update(cx, |workspace, _| {
6136 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6137 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6138 });
6139
6140 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
6141 workspace.update(cx, |_, cx| cx.focus_self());
6142 workspace.update(cx, |workspace, _| {
6143 assert_eq!(workspace.zoomed, None);
6144 assert_eq!(workspace.zoomed_position, None);
6145 });
6146
6147 // If focus is transferred again to another view that's not a panel or a pane, we won't
6148 // show the panel as zoomed because it wasn't zoomed before.
6149 focus_other_view(cx);
6150 workspace.update(cx, |workspace, _| {
6151 assert_eq!(workspace.zoomed, None);
6152 assert_eq!(workspace.zoomed_position, None);
6153 });
6154
6155 // When the panel is activated, it is zoomed again.
6156 cx.dispatch_action(ToggleRightDock);
6157 workspace.update(cx, |workspace, _| {
6158 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6159 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6160 });
6161
6162 // Emitting a ZoomOut event unzooms the panel.
6163 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
6164 workspace.update(cx, |workspace, _| {
6165 assert_eq!(workspace.zoomed, None);
6166 assert_eq!(workspace.zoomed_position, None);
6167 });
6168
6169 // Emit closed event on panel 1, which is active
6170 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6171
6172 // Now the left dock is closed, because panel_1 was the active panel
6173 workspace.update(cx, |workspace, cx| {
6174 let right_dock = workspace.right_dock();
6175 assert!(!right_dock.read(cx).is_open());
6176 });
6177 }
6178
6179 mod register_project_item_tests {
6180 use ui::Context as _;
6181
6182 use super::*;
6183
6184 const TEST_PNG_KIND: &str = "TestPngItemView";
6185 // View
6186 struct TestPngItemView {
6187 focus_handle: FocusHandle,
6188 }
6189 // Model
6190 struct TestPngItem {}
6191
6192 impl project::Item for TestPngItem {
6193 fn try_open(
6194 _project: &Model<Project>,
6195 path: &ProjectPath,
6196 cx: &mut AppContext,
6197 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6198 if path.path.extension().unwrap() == "png" {
6199 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
6200 } else {
6201 None
6202 }
6203 }
6204
6205 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6206 None
6207 }
6208
6209 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6210 None
6211 }
6212 }
6213
6214 impl Item for TestPngItemView {
6215 type Event = ();
6216
6217 fn serialized_item_kind() -> Option<&'static str> {
6218 Some(TEST_PNG_KIND)
6219 }
6220 }
6221 impl EventEmitter<()> for TestPngItemView {}
6222 impl FocusableView for TestPngItemView {
6223 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6224 self.focus_handle.clone()
6225 }
6226 }
6227
6228 impl Render for TestPngItemView {
6229 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6230 Empty
6231 }
6232 }
6233
6234 impl ProjectItem for TestPngItemView {
6235 type Item = TestPngItem;
6236
6237 fn for_project_item(
6238 _project: Model<Project>,
6239 _item: Model<Self::Item>,
6240 cx: &mut ViewContext<Self>,
6241 ) -> Self
6242 where
6243 Self: Sized,
6244 {
6245 Self {
6246 focus_handle: cx.focus_handle(),
6247 }
6248 }
6249 }
6250
6251 const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
6252 // View
6253 struct TestIpynbItemView {
6254 focus_handle: FocusHandle,
6255 }
6256 // Model
6257 struct TestIpynbItem {}
6258
6259 impl project::Item for TestIpynbItem {
6260 fn try_open(
6261 _project: &Model<Project>,
6262 path: &ProjectPath,
6263 cx: &mut AppContext,
6264 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6265 if path.path.extension().unwrap() == "ipynb" {
6266 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
6267 } else {
6268 None
6269 }
6270 }
6271
6272 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6273 None
6274 }
6275
6276 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6277 None
6278 }
6279 }
6280
6281 impl Item for TestIpynbItemView {
6282 type Event = ();
6283
6284 fn serialized_item_kind() -> Option<&'static str> {
6285 Some(TEST_IPYNB_KIND)
6286 }
6287 }
6288 impl EventEmitter<()> for TestIpynbItemView {}
6289 impl FocusableView for TestIpynbItemView {
6290 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6291 self.focus_handle.clone()
6292 }
6293 }
6294
6295 impl Render for TestIpynbItemView {
6296 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6297 Empty
6298 }
6299 }
6300
6301 impl ProjectItem for TestIpynbItemView {
6302 type Item = TestIpynbItem;
6303
6304 fn for_project_item(
6305 _project: Model<Project>,
6306 _item: Model<Self::Item>,
6307 cx: &mut ViewContext<Self>,
6308 ) -> Self
6309 where
6310 Self: Sized,
6311 {
6312 Self {
6313 focus_handle: cx.focus_handle(),
6314 }
6315 }
6316 }
6317
6318 struct TestAlternatePngItemView {
6319 focus_handle: FocusHandle,
6320 }
6321
6322 const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
6323 impl Item for TestAlternatePngItemView {
6324 type Event = ();
6325
6326 fn serialized_item_kind() -> Option<&'static str> {
6327 Some(TEST_ALTERNATE_PNG_KIND)
6328 }
6329 }
6330 impl EventEmitter<()> for TestAlternatePngItemView {}
6331 impl FocusableView for TestAlternatePngItemView {
6332 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6333 self.focus_handle.clone()
6334 }
6335 }
6336
6337 impl Render for TestAlternatePngItemView {
6338 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6339 Empty
6340 }
6341 }
6342
6343 impl ProjectItem for TestAlternatePngItemView {
6344 type Item = TestPngItem;
6345
6346 fn for_project_item(
6347 _project: Model<Project>,
6348 _item: Model<Self::Item>,
6349 cx: &mut ViewContext<Self>,
6350 ) -> Self
6351 where
6352 Self: Sized,
6353 {
6354 Self {
6355 focus_handle: cx.focus_handle(),
6356 }
6357 }
6358 }
6359
6360 #[gpui::test]
6361 async fn test_register_project_item(cx: &mut TestAppContext) {
6362 init_test(cx);
6363
6364 cx.update(|cx| {
6365 register_project_item::<TestPngItemView>(cx);
6366 register_project_item::<TestIpynbItemView>(cx);
6367 });
6368
6369 let fs = FakeFs::new(cx.executor());
6370 fs.insert_tree(
6371 "/root1",
6372 json!({
6373 "one.png": "BINARYDATAHERE",
6374 "two.ipynb": "{ totally a notebook }",
6375 "three.txt": "editing text, sure why not?"
6376 }),
6377 )
6378 .await;
6379
6380 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6381 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6382
6383 let worktree_id = project.update(cx, |project, cx| {
6384 project.worktrees().next().unwrap().read(cx).id()
6385 });
6386
6387 let handle = workspace
6388 .update(cx, |workspace, cx| {
6389 let project_path = (worktree_id, "one.png");
6390 workspace.open_path(project_path, None, true, cx)
6391 })
6392 .await
6393 .unwrap();
6394
6395 // Now we can check if the handle we got back errored or not
6396 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
6397
6398 let handle = workspace
6399 .update(cx, |workspace, cx| {
6400 let project_path = (worktree_id, "two.ipynb");
6401 workspace.open_path(project_path, None, true, cx)
6402 })
6403 .await
6404 .unwrap();
6405
6406 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
6407
6408 let handle = workspace
6409 .update(cx, |workspace, cx| {
6410 let project_path = (worktree_id, "three.txt");
6411 workspace.open_path(project_path, None, true, cx)
6412 })
6413 .await;
6414 assert!(handle.is_err());
6415 }
6416
6417 #[gpui::test]
6418 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
6419 init_test(cx);
6420
6421 cx.update(|cx| {
6422 register_project_item::<TestPngItemView>(cx);
6423 register_project_item::<TestAlternatePngItemView>(cx);
6424 });
6425
6426 let fs = FakeFs::new(cx.executor());
6427 fs.insert_tree(
6428 "/root1",
6429 json!({
6430 "one.png": "BINARYDATAHERE",
6431 "two.ipynb": "{ totally a notebook }",
6432 "three.txt": "editing text, sure why not?"
6433 }),
6434 )
6435 .await;
6436
6437 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6438 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6439
6440 let worktree_id = project.update(cx, |project, cx| {
6441 project.worktrees().next().unwrap().read(cx).id()
6442 });
6443
6444 let handle = workspace
6445 .update(cx, |workspace, cx| {
6446 let project_path = (worktree_id, "one.png");
6447 workspace.open_path(project_path, None, true, cx)
6448 })
6449 .await
6450 .unwrap();
6451
6452 // This _must_ be the second item registered
6453 assert_eq!(
6454 handle.serialized_item_kind().unwrap(),
6455 TEST_ALTERNATE_PNG_KIND
6456 );
6457
6458 let handle = workspace
6459 .update(cx, |workspace, cx| {
6460 let project_path = (worktree_id, "three.txt");
6461 workspace.open_path(project_path, None, true, cx)
6462 })
6463 .await;
6464 assert!(handle.is_err());
6465 }
6466 }
6467
6468 pub fn init_test(cx: &mut TestAppContext) {
6469 cx.update(|cx| {
6470 let settings_store = SettingsStore::test(cx);
6471 cx.set_global(settings_store);
6472 theme::init(theme::LoadThemes::JustBase, cx);
6473 language::init(cx);
6474 crate::init_settings(cx);
6475 Project::init_settings(cx);
6476 });
6477 }
6478}