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