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