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