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