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