1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11pub mod tasks;
12mod toolbar;
13mod workspace_settings;
14
15use anyhow::{anyhow, Context as _, Result};
16use call::{call_settings::CallSettings, ActiveCall};
17use client::{
18 proto::{self, ErrorCode, PeerId},
19 ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
20};
21use collections::{hash_map, HashMap, HashSet};
22use derive_more::{Deref, DerefMut};
23use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
24use futures::{
25 channel::{mpsc, oneshot},
26 future::try_join_all,
27 Future, FutureExt, StreamExt,
28};
29use gpui::{
30 actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView,
31 AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent,
32 ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
33 GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext,
34 PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView,
35 WindowBounds, WindowHandle, WindowOptions,
36};
37use item::{
38 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
39 ProjectItem,
40};
41use itertools::Itertools;
42use language::{LanguageRegistry, Rope};
43use lazy_static::lazy_static;
44pub use modal_layer::*;
45use node_runtime::NodeRuntime;
46use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
47pub use pane::*;
48pub use pane_group::*;
49use persistence::{model::SerializedWorkspace, SerializedWindowBounds, DB};
50pub use persistence::{
51 model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation},
52 WorkspaceDb, DB as WORKSPACE_DB,
53};
54use postage::stream::Stream;
55use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
56use serde::Deserialize;
57use settings::Settings;
58use shared_screen::SharedScreen;
59use sqlez::{
60 bindable::{Bind, Column, StaticColumnCount},
61 statement::Statement,
62};
63use status_bar::StatusBar;
64pub use status_bar::StatusItemView;
65use std::{
66 any::TypeId,
67 borrow::Cow,
68 cell::RefCell,
69 cmp,
70 collections::hash_map::DefaultHasher,
71 env,
72 hash::{Hash, Hasher},
73 path::{Path, PathBuf},
74 rc::Rc,
75 sync::{atomic::AtomicUsize, Arc, Weak},
76 time::Duration,
77};
78use task::SpawnInTerminal;
79use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
80pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
81pub use ui;
82use ui::{
83 div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement,
84 Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
85 WindowContext,
86};
87use util::{maybe, ResultExt};
88use uuid::Uuid;
89pub use workspace_settings::{
90 AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
91};
92
93use crate::notifications::NotificationId;
94use crate::persistence::{
95 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
96 SerializedAxis,
97};
98
99lazy_static! {
100 static ref ZED_WINDOW_SIZE: Option<Size<DevicePixels>> = env::var("ZED_WINDOW_SIZE")
101 .ok()
102 .as_deref()
103 .and_then(parse_pixel_size_env_var);
104 static ref ZED_WINDOW_POSITION: Option<Point<DevicePixels>> = env::var("ZED_WINDOW_POSITION")
105 .ok()
106 .as_deref()
107 .and_then(parse_pixel_position_env_var);
108}
109
110#[derive(Clone, PartialEq)]
111pub struct RemoveWorktreeFromProject(pub WorktreeId);
112
113actions!(
114 workspace,
115 [
116 Open,
117 OpenInTerminal,
118 NewFile,
119 NewWindow,
120 CloseWindow,
121 AddFolderToProject,
122 Unfollow,
123 SaveAs,
124 SaveWithoutFormat,
125 ReloadActiveItem,
126 ActivatePreviousPane,
127 ActivateNextPane,
128 FollowNextCollaborator,
129 NewTerminal,
130 NewCenterTerminal,
131 NewSearch,
132 Feedback,
133 Welcome,
134 ToggleZoom,
135 ToggleLeftDock,
136 ToggleRightDock,
137 ToggleBottomDock,
138 ToggleCenteredLayout,
139 CloseAllDocks,
140 ]
141);
142
143#[derive(Clone, PartialEq)]
144pub struct OpenPaths {
145 pub paths: Vec<PathBuf>,
146}
147
148#[derive(Clone, Deserialize, PartialEq)]
149pub struct ActivatePane(pub usize);
150
151#[derive(Clone, Deserialize, PartialEq)]
152pub struct ActivatePaneInDirection(pub SplitDirection);
153
154#[derive(Clone, Deserialize, PartialEq)]
155pub struct SwapPaneInDirection(pub SplitDirection);
156
157#[derive(Clone, Deserialize, PartialEq)]
158pub struct NewFileInDirection(pub SplitDirection);
159
160#[derive(Clone, PartialEq, Debug, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct SaveAll {
163 pub save_intent: Option<SaveIntent>,
164}
165
166#[derive(Clone, PartialEq, Debug, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct Save {
169 pub save_intent: Option<SaveIntent>,
170}
171
172#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
173#[serde(rename_all = "camelCase")]
174pub struct CloseAllItemsAndPanes {
175 pub save_intent: Option<SaveIntent>,
176}
177
178#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
179#[serde(rename_all = "camelCase")]
180pub struct CloseInactiveTabsAndPanes {
181 pub save_intent: Option<SaveIntent>,
182}
183
184#[derive(Clone, Deserialize, PartialEq)]
185pub struct SendKeystrokes(pub String);
186
187#[derive(Clone, Deserialize, PartialEq, Default)]
188pub struct Restart {
189 pub binary_path: Option<PathBuf>,
190}
191
192impl_actions!(
193 workspace,
194 [
195 ActivatePane,
196 ActivatePaneInDirection,
197 CloseAllItemsAndPanes,
198 CloseInactiveTabsAndPanes,
199 NewFileInDirection,
200 OpenTerminal,
201 Restart,
202 Save,
203 SaveAll,
204 SwapPaneInDirection,
205 SendKeystrokes,
206 ]
207);
208
209pub struct Toast {
210 id: NotificationId,
211 msg: Cow<'static, str>,
212 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
213}
214
215impl Toast {
216 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
217 Toast {
218 id,
219 msg: msg.into(),
220 on_click: None,
221 }
222 }
223
224 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
225 where
226 M: Into<Cow<'static, str>>,
227 F: Fn(&mut WindowContext) + 'static,
228 {
229 self.on_click = Some((message.into(), Arc::new(on_click)));
230 self
231 }
232}
233
234impl PartialEq for Toast {
235 fn eq(&self, other: &Self) -> bool {
236 self.id == other.id
237 && self.msg == other.msg
238 && self.on_click.is_some() == other.on_click.is_some()
239 }
240}
241
242impl Clone for Toast {
243 fn clone(&self) -> Self {
244 Toast {
245 id: self.id.clone(),
246 msg: self.msg.clone(),
247 on_click: self.on_click.clone(),
248 }
249 }
250}
251
252#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
253pub struct OpenTerminal {
254 pub working_directory: PathBuf,
255}
256
257#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
258pub struct WorkspaceId(i64);
259
260impl StaticColumnCount for WorkspaceId {}
261impl Bind for WorkspaceId {
262 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
263 self.0.bind(statement, start_index)
264 }
265}
266impl Column for WorkspaceId {
267 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
268 i64::column(statement, start_index)
269 .map(|(i, next_index)| (Self(i), next_index))
270 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
271 }
272}
273pub fn init_settings(cx: &mut AppContext) {
274 WorkspaceSettings::register(cx);
275 ItemSettings::register(cx);
276 PreviewTabsSettings::register(cx);
277 TabBarSettings::register(cx);
278}
279
280pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
281 init_settings(cx);
282 notifications::init(cx);
283
284 cx.on_action(Workspace::close_global);
285 cx.on_action(restart);
286
287 cx.on_action({
288 let app_state = Arc::downgrade(&app_state);
289 move |_: &Open, cx: &mut AppContext| {
290 let paths = cx.prompt_for_paths(PathPromptOptions {
291 files: true,
292 directories: true,
293 multiple: true,
294 });
295
296 if let Some(app_state) = app_state.upgrade() {
297 cx.spawn(move |cx| async move {
298 if let Some(paths) = paths.await.log_err().flatten() {
299 cx.update(|cx| {
300 open_paths(&paths, app_state, OpenOptions::default(), cx)
301 .detach_and_log_err(cx)
302 })
303 .ok();
304 }
305 })
306 .detach();
307 }
308 }
309 });
310}
311
312#[derive(Clone, Default, Deref, DerefMut)]
313struct ProjectItemOpeners(Vec<ProjectItemOpener>);
314
315type ProjectItemOpener = fn(
316 &Model<Project>,
317 &ProjectPath,
318 &mut WindowContext,
319)
320 -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
321
322type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
323
324impl Global for ProjectItemOpeners {}
325
326/// Registers a [ProjectItem] for the app. When opening a file, all the registered
327/// items will get a chance to open the file, starting from the project item that
328/// was added last.
329pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
330 let builders = cx.default_global::<ProjectItemOpeners>();
331 builders.push(|project, project_path, cx| {
332 let project_item = <I::Item as project::Item>::try_open(&project, project_path, cx)?;
333 let project = project.clone();
334 Some(cx.spawn(|cx| async move {
335 let project_item = project_item.await?;
336 let project_entry_id: Option<ProjectEntryId> =
337 project_item.read_with(&cx, |item, cx| project::Item::entry_id(item, cx))?;
338 let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
339 Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
340 as Box<dyn ItemHandle>
341 }) as Box<_>;
342 Ok((project_entry_id, build_workspace_item))
343 }))
344 });
345}
346
347type FollowableItemBuilder = fn(
348 View<Pane>,
349 View<Workspace>,
350 ViewId,
351 &mut Option<proto::view::Variant>,
352 &mut WindowContext,
353) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
354
355#[derive(Default, Deref, DerefMut)]
356struct FollowableItemBuilders(
357 HashMap<
358 TypeId,
359 (
360 FollowableItemBuilder,
361 fn(&AnyView) -> Box<dyn FollowableItemHandle>,
362 ),
363 >,
364);
365
366impl Global for FollowableItemBuilders {}
367
368pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
369 let builders = cx.default_global::<FollowableItemBuilders>();
370 builders.insert(
371 TypeId::of::<I>(),
372 (
373 |pane, workspace, id, state, cx| {
374 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
375 cx.foreground_executor()
376 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
377 })
378 },
379 |this| Box::new(this.clone().downcast::<I>().unwrap()),
380 ),
381 );
382}
383
384#[derive(Default, Deref, DerefMut)]
385struct ItemDeserializers(
386 HashMap<
387 Arc<str>,
388 fn(
389 Model<Project>,
390 WeakView<Workspace>,
391 WorkspaceId,
392 ItemId,
393 &mut ViewContext<Pane>,
394 ) -> Task<Result<Box<dyn ItemHandle>>>,
395 >,
396);
397
398impl Global for ItemDeserializers {}
399
400pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
401 if let Some(serialized_item_kind) = I::serialized_item_kind() {
402 let deserializers = cx.default_global::<ItemDeserializers>();
403 deserializers.insert(
404 Arc::from(serialized_item_kind),
405 |project, workspace, workspace_id, item_id, cx| {
406 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
407 cx.foreground_executor()
408 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
409 },
410 );
411 }
412}
413
414pub struct AppState {
415 pub languages: Arc<LanguageRegistry>,
416 pub client: Arc<Client>,
417 pub user_store: Model<UserStore>,
418 pub workspace_store: Model<WorkspaceStore>,
419 pub fs: Arc<dyn fs::Fs>,
420 pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
421 pub node_runtime: Arc<dyn NodeRuntime>,
422}
423
424struct GlobalAppState(Weak<AppState>);
425
426impl Global for GlobalAppState {}
427
428pub struct WorkspaceStore {
429 workspaces: HashSet<WindowHandle<Workspace>>,
430 client: Arc<Client>,
431 _subscriptions: Vec<client::Subscription>,
432}
433
434#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
435struct Follower {
436 project_id: Option<u64>,
437 peer_id: PeerId,
438}
439
440impl AppState {
441 pub fn global(cx: &AppContext) -> Weak<Self> {
442 cx.global::<GlobalAppState>().0.clone()
443 }
444 pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
445 cx.try_global::<GlobalAppState>()
446 .map(|state| state.0.clone())
447 }
448 pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
449 cx.set_global(GlobalAppState(state));
450 }
451
452 #[cfg(any(test, feature = "test-support"))]
453 pub fn test(cx: &mut AppContext) -> Arc<Self> {
454 use node_runtime::FakeNodeRuntime;
455 use settings::SettingsStore;
456 use ui::Context as _;
457
458 if !cx.has_global::<SettingsStore>() {
459 let settings_store = SettingsStore::test(cx);
460 cx.set_global(settings_store);
461 }
462
463 let fs = fs::FakeFs::new(cx.background_executor().clone());
464 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
465 let clock = Arc::new(clock::FakeSystemClock::default());
466 let http_client = util::http::FakeHttpClient::with_404_response();
467 let client = Client::new(clock, http_client.clone(), cx);
468 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
469 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
470
471 theme::init(theme::LoadThemes::JustBase, cx);
472 client::init(&client, cx);
473 crate::init_settings(cx);
474
475 Arc::new(Self {
476 client,
477 fs,
478 languages,
479 user_store,
480 workspace_store,
481 node_runtime: FakeNodeRuntime::new(),
482 build_window_options: |_, _| Default::default(),
483 })
484 }
485}
486
487struct DelayedDebouncedEditAction {
488 task: Option<Task<()>>,
489 cancel_channel: Option<oneshot::Sender<()>>,
490}
491
492impl DelayedDebouncedEditAction {
493 fn new() -> DelayedDebouncedEditAction {
494 DelayedDebouncedEditAction {
495 task: None,
496 cancel_channel: None,
497 }
498 }
499
500 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
501 where
502 F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
503 {
504 if let Some(channel) = self.cancel_channel.take() {
505 _ = channel.send(());
506 }
507
508 let (sender, mut receiver) = oneshot::channel::<()>();
509 self.cancel_channel = Some(sender);
510
511 let previous_task = self.task.take();
512 self.task = Some(cx.spawn(move |workspace, mut cx| async move {
513 let mut timer = cx.background_executor().timer(delay).fuse();
514 if let Some(previous_task) = previous_task {
515 previous_task.await;
516 }
517
518 futures::select_biased! {
519 _ = receiver => return,
520 _ = timer => {}
521 }
522
523 if let Some(result) = workspace
524 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
525 .log_err()
526 {
527 result.await.log_err();
528 }
529 }));
530 }
531}
532
533pub enum Event {
534 PaneAdded(View<Pane>),
535 ActiveItemChanged,
536 ContactRequestedJoin(u64),
537 WorkspaceCreated(WeakView<Workspace>),
538 SpawnTask(SpawnInTerminal),
539 OpenBundledFile {
540 text: Cow<'static, str>,
541 title: &'static str,
542 language: &'static str,
543 },
544 ZoomChanged,
545}
546
547pub enum OpenVisible {
548 All,
549 None,
550 OnlyFiles,
551 OnlyDirectories,
552}
553
554type PromptForNewPath = Box<
555 dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
556>;
557
558/// Collects everything project-related for a certain window opened.
559/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
560///
561/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
562/// The `Workspace` owns everybody's state and serves as a default, "global context",
563/// that can be used to register a global action to be triggered from any place in the window.
564pub struct Workspace {
565 weak_self: WeakView<Self>,
566 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
567 zoomed: Option<AnyWeakView>,
568 zoomed_position: Option<DockPosition>,
569 center: PaneGroup,
570 left_dock: View<Dock>,
571 bottom_dock: View<Dock>,
572 right_dock: View<Dock>,
573 panes: Vec<View<Pane>>,
574 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
575 active_pane: View<Pane>,
576 last_active_center_pane: Option<WeakView<Pane>>,
577 last_active_view_id: Option<proto::ViewId>,
578 status_bar: View<StatusBar>,
579 modal_layer: View<ModalLayer>,
580 titlebar_item: Option<AnyView>,
581 notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
582 project: Model<Project>,
583 follower_states: HashMap<View<Pane>, FollowerState>,
584 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
585 window_edited: bool,
586 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
587 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
588 database_id: WorkspaceId,
589 app_state: Arc<AppState>,
590 dispatching_keystrokes: Rc<RefCell<Vec<Keystroke>>>,
591 _subscriptions: Vec<Subscription>,
592 _apply_leader_updates: Task<Result<()>>,
593 _observe_current_user: Task<Result<()>>,
594 _schedule_serialize: Option<Task<()>>,
595 pane_history_timestamp: Arc<AtomicUsize>,
596 bounds: Bounds<Pixels>,
597 centered_layout: bool,
598 bounds_save_task_queued: Option<Task<()>>,
599 on_prompt_for_new_path: Option<PromptForNewPath>,
600}
601
602impl EventEmitter<Event> for Workspace {}
603
604#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
605pub struct ViewId {
606 pub creator: PeerId,
607 pub id: u64,
608}
609
610#[derive(Default)]
611struct FollowerState {
612 leader_id: PeerId,
613 active_view_id: Option<ViewId>,
614 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
615}
616
617impl Workspace {
618 const DEFAULT_PADDING: f32 = 0.2;
619 const MAX_PADDING: f32 = 0.4;
620
621 pub fn new(
622 workspace_id: WorkspaceId,
623 project: Model<Project>,
624 app_state: Arc<AppState>,
625 cx: &mut ViewContext<Self>,
626 ) -> Self {
627 cx.observe(&project, |_, _, cx| cx.notify()).detach();
628 cx.subscribe(&project, move |this, _, event, cx| {
629 match event {
630 project::Event::RemoteIdChanged(_) => {
631 this.update_window_title(cx);
632 }
633
634 project::Event::CollaboratorLeft(peer_id) => {
635 this.collaborator_left(*peer_id, cx);
636 }
637
638 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
639 this.update_window_title(cx);
640 this.serialize_workspace(cx);
641 }
642
643 project::Event::DisconnectedFromHost => {
644 this.update_window_edited(cx);
645 let panes_to_unfollow: Vec<View<Pane>> =
646 this.follower_states.keys().map(|k| k.clone()).collect();
647 for pane in panes_to_unfollow {
648 this.unfollow(&pane, cx);
649 }
650 cx.disable_focus();
651 }
652
653 project::Event::Closed => {
654 cx.remove_window();
655 }
656
657 project::Event::DeletedEntry(entry_id) => {
658 for pane in this.panes.iter() {
659 pane.update(cx, |pane, cx| {
660 pane.handle_deleted_project_item(*entry_id, cx)
661 });
662 }
663 }
664
665 project::Event::Notification(message) => {
666 struct ProjectNotification;
667
668 this.show_notification(
669 NotificationId::unique::<ProjectNotification>(),
670 cx,
671 |cx| cx.new_view(|_| MessageNotification::new(message.clone())),
672 )
673 }
674
675 project::Event::LanguageServerPrompt(request) => {
676 struct LanguageServerPrompt;
677
678 let mut hasher = DefaultHasher::new();
679 request.lsp_name.as_str().hash(&mut hasher);
680 let id = hasher.finish();
681
682 this.show_notification(
683 NotificationId::identified::<LanguageServerPrompt>(id as usize),
684 cx,
685 |cx| {
686 cx.new_view(|_| {
687 notifications::LanguageServerPrompt::new(request.clone())
688 })
689 },
690 );
691 }
692
693 _ => {}
694 }
695 cx.notify()
696 })
697 .detach();
698
699 cx.on_focus_lost(|this, cx| {
700 let focus_handle = this.focus_handle(cx);
701 cx.focus(&focus_handle);
702 })
703 .detach();
704
705 let weak_handle = cx.view().downgrade();
706 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
707
708 let center_pane = cx.new_view(|cx| {
709 Pane::new(
710 weak_handle.clone(),
711 project.clone(),
712 pane_history_timestamp.clone(),
713 None,
714 NewFile.boxed_clone(),
715 cx,
716 )
717 });
718 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
719
720 cx.focus_view(¢er_pane);
721 cx.emit(Event::PaneAdded(center_pane.clone()));
722
723 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
724 app_state.workspace_store.update(cx, |store, _| {
725 store.workspaces.insert(window_handle);
726 });
727
728 let mut current_user = app_state.user_store.read(cx).watch_current_user();
729 let mut connection_status = app_state.client.status();
730 let _observe_current_user = cx.spawn(|this, mut cx| async move {
731 current_user.next().await;
732 connection_status.next().await;
733 let mut stream =
734 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
735
736 while stream.recv().await.is_some() {
737 this.update(&mut cx, |_, cx| cx.notify())?;
738 }
739 anyhow::Ok(())
740 });
741
742 // All leader updates are enqueued and then processed in a single task, so
743 // that each asynchronous operation can be run in order.
744 let (leader_updates_tx, mut leader_updates_rx) =
745 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
746 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
747 while let Some((leader_id, update)) = leader_updates_rx.next().await {
748 Self::process_leader_update(&this, leader_id, update, &mut cx)
749 .await
750 .log_err();
751 }
752
753 Ok(())
754 });
755
756 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
757
758 let left_dock = Dock::new(DockPosition::Left, cx);
759 let bottom_dock = Dock::new(DockPosition::Bottom, cx);
760 let right_dock = Dock::new(DockPosition::Right, cx);
761 let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
762 let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
763 let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
764 let status_bar = cx.new_view(|cx| {
765 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
766 status_bar.add_left_item(left_dock_buttons, cx);
767 status_bar.add_right_item(right_dock_buttons, cx);
768 status_bar.add_right_item(bottom_dock_buttons, cx);
769 status_bar
770 });
771
772 let modal_layer = cx.new_view(|_| ModalLayer::new());
773
774 let mut active_call = None;
775 if let Some(call) = ActiveCall::try_global(cx) {
776 let call = call.clone();
777 let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
778 active_call = Some((call, subscriptions));
779 }
780
781 let subscriptions = vec![
782 cx.observe_window_activation(Self::on_window_activation_changed),
783 cx.observe_window_bounds(move |this, cx| {
784 if this.bounds_save_task_queued.is_some() {
785 return;
786 }
787 this.bounds_save_task_queued = Some(cx.spawn(|this, mut cx| async move {
788 cx.background_executor()
789 .timer(Duration::from_millis(100))
790 .await;
791 this.update(&mut cx, |this, cx| {
792 if let Some(display) = cx.display() {
793 if let Some(display_uuid) = display.uuid().log_err() {
794 let window_bounds = cx.window_bounds();
795 cx.background_executor()
796 .spawn(DB.set_window_open_status(
797 workspace_id,
798 SerializedWindowBounds(window_bounds),
799 display_uuid,
800 ))
801 .detach_and_log_err(cx);
802 }
803 }
804 this.bounds_save_task_queued.take();
805 })
806 .ok();
807 }));
808 cx.notify();
809 }),
810 cx.observe_window_appearance(|_, cx| {
811 let window_appearance = cx.appearance();
812
813 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
814
815 ThemeSettings::reload_current_theme(cx);
816 }),
817 cx.observe(&left_dock, |this, _, cx| {
818 this.serialize_workspace(cx);
819 cx.notify();
820 }),
821 cx.observe(&bottom_dock, |this, _, cx| {
822 this.serialize_workspace(cx);
823 cx.notify();
824 }),
825 cx.observe(&right_dock, |this, _, cx| {
826 this.serialize_workspace(cx);
827 cx.notify();
828 }),
829 cx.on_release(|this, window, cx| {
830 this.app_state.workspace_store.update(cx, |store, _| {
831 let window = window.downcast::<Self>().unwrap();
832 store.workspaces.remove(&window);
833 })
834 }),
835 ];
836
837 cx.defer(|this, cx| {
838 this.update_window_title(cx);
839 });
840 Workspace {
841 weak_self: weak_handle.clone(),
842 zoomed: None,
843 zoomed_position: None,
844 center: PaneGroup::new(center_pane.clone()),
845 panes: vec![center_pane.clone()],
846 panes_by_item: Default::default(),
847 active_pane: center_pane.clone(),
848 last_active_center_pane: Some(center_pane.downgrade()),
849 last_active_view_id: None,
850 status_bar,
851 modal_layer,
852 titlebar_item: None,
853 notifications: Default::default(),
854 left_dock,
855 bottom_dock,
856 right_dock,
857 project: project.clone(),
858 follower_states: Default::default(),
859 last_leaders_by_pane: Default::default(),
860 dispatching_keystrokes: Default::default(),
861 window_edited: false,
862 active_call,
863 database_id: workspace_id,
864 app_state,
865 _observe_current_user,
866 _apply_leader_updates,
867 _schedule_serialize: None,
868 leader_updates_tx,
869 _subscriptions: subscriptions,
870 pane_history_timestamp,
871 workspace_actions: Default::default(),
872 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
873 bounds: Default::default(),
874 centered_layout: false,
875 bounds_save_task_queued: None,
876 on_prompt_for_new_path: None,
877 }
878 }
879
880 fn new_local(
881 abs_paths: Vec<PathBuf>,
882 app_state: Arc<AppState>,
883 requesting_window: Option<WindowHandle<Workspace>>,
884 cx: &mut AppContext,
885 ) -> Task<
886 anyhow::Result<(
887 WindowHandle<Workspace>,
888 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
889 )>,
890 > {
891 let project_handle = Project::local(
892 app_state.client.clone(),
893 app_state.node_runtime.clone(),
894 app_state.user_store.clone(),
895 app_state.languages.clone(),
896 app_state.fs.clone(),
897 cx,
898 );
899
900 cx.spawn(|mut cx| async move {
901 let serialized_workspace: Option<SerializedWorkspace> =
902 persistence::DB.workspace_for_roots(abs_paths.as_slice());
903
904 let paths_to_open = Arc::new(abs_paths);
905
906 // Get project paths for all of the abs_paths
907 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
908 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
909 Vec::with_capacity(paths_to_open.len());
910 for path in paths_to_open.iter().cloned() {
911 if let Some((worktree, project_entry)) = cx
912 .update(|cx| {
913 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
914 })?
915 .await
916 .log_err()
917 {
918 worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
919 project_paths.push((path, Some(project_entry)));
920 } else {
921 project_paths.push((path, None));
922 }
923 }
924
925 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
926 serialized_workspace.id
927 } else {
928 DB.next_id().await.unwrap_or_else(|_| Default::default())
929 };
930
931 let window = if let Some(window) = requesting_window {
932 cx.update_window(window.into(), |_, cx| {
933 cx.replace_root_view(|cx| {
934 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
935 });
936 })?;
937 window
938 } else {
939 let window_bounds_override = window_bounds_env_override();
940
941 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
942 (Some(WindowBounds::Windowed(bounds)), None)
943 } else {
944 let restorable_bounds = serialized_workspace
945 .as_ref()
946 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
947 .or_else(|| {
948 let (display, window_bounds) = DB.last_window().log_err()?;
949 Some((display?, window_bounds?))
950 });
951
952 if let Some((serialized_display, serialized_status)) = restorable_bounds {
953 (Some(serialized_status.0), Some(serialized_display))
954 } else {
955 (None, None)
956 }
957 };
958
959 // Use the serialized workspace to construct the new window
960 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
961 options.window_bounds = window_bounds;
962 let centered_layout = serialized_workspace
963 .as_ref()
964 .map(|w| w.centered_layout)
965 .unwrap_or(false);
966 cx.open_window(options, {
967 let app_state = app_state.clone();
968 let project_handle = project_handle.clone();
969 move |cx| {
970 cx.new_view(|cx| {
971 let mut workspace =
972 Workspace::new(workspace_id, project_handle, app_state, cx);
973 workspace.centered_layout = centered_layout;
974 workspace
975 })
976 }
977 })?
978 };
979
980 notify_if_database_failed(window, &mut cx);
981 let opened_items = window
982 .update(&mut cx, |_workspace, cx| {
983 open_items(serialized_workspace, project_paths, app_state, cx)
984 })?
985 .await
986 .unwrap_or_default();
987
988 window
989 .update(&mut cx, |_, cx| cx.activate_window())
990 .log_err();
991 Ok((window, opened_items))
992 })
993 }
994
995 pub fn weak_handle(&self) -> WeakView<Self> {
996 self.weak_self.clone()
997 }
998
999 pub fn left_dock(&self) -> &View<Dock> {
1000 &self.left_dock
1001 }
1002
1003 pub fn bottom_dock(&self) -> &View<Dock> {
1004 &self.bottom_dock
1005 }
1006
1007 pub fn right_dock(&self) -> &View<Dock> {
1008 &self.right_dock
1009 }
1010
1011 pub fn is_edited(&self) -> bool {
1012 self.window_edited
1013 }
1014
1015 pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut WindowContext) {
1016 let dock = match panel.position(cx) {
1017 DockPosition::Left => &self.left_dock,
1018 DockPosition::Bottom => &self.bottom_dock,
1019 DockPosition::Right => &self.right_dock,
1020 };
1021
1022 dock.update(cx, |dock, cx| {
1023 dock.add_panel(panel, self.weak_self.clone(), cx)
1024 });
1025 }
1026
1027 pub fn status_bar(&self) -> &View<StatusBar> {
1028 &self.status_bar
1029 }
1030
1031 pub fn app_state(&self) -> &Arc<AppState> {
1032 &self.app_state
1033 }
1034
1035 pub fn user_store(&self) -> &Model<UserStore> {
1036 &self.app_state.user_store
1037 }
1038
1039 pub fn project(&self) -> &Model<Project> {
1040 &self.project
1041 }
1042
1043 pub fn recent_navigation_history(
1044 &self,
1045 limit: Option<usize>,
1046 cx: &AppContext,
1047 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1048 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1049 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1050 for pane in &self.panes {
1051 let pane = pane.read(cx);
1052 pane.nav_history()
1053 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1054 if let Some(fs_path) = &fs_path {
1055 abs_paths_opened
1056 .entry(fs_path.clone())
1057 .or_default()
1058 .insert(project_path.clone());
1059 }
1060 let timestamp = entry.timestamp;
1061 match history.entry(project_path) {
1062 hash_map::Entry::Occupied(mut entry) => {
1063 let (_, old_timestamp) = entry.get();
1064 if ×tamp > old_timestamp {
1065 entry.insert((fs_path, timestamp));
1066 }
1067 }
1068 hash_map::Entry::Vacant(entry) => {
1069 entry.insert((fs_path, timestamp));
1070 }
1071 }
1072 });
1073 }
1074
1075 history
1076 .into_iter()
1077 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1078 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1079 .rev()
1080 .filter(|(history_path, abs_path)| {
1081 let latest_project_path_opened = abs_path
1082 .as_ref()
1083 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1084 .and_then(|project_paths| {
1085 project_paths
1086 .iter()
1087 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1088 });
1089
1090 match latest_project_path_opened {
1091 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1092 None => true,
1093 }
1094 })
1095 .take(limit.unwrap_or(usize::MAX))
1096 .collect()
1097 }
1098
1099 fn navigate_history(
1100 &mut self,
1101 pane: WeakView<Pane>,
1102 mode: NavigationMode,
1103 cx: &mut ViewContext<Workspace>,
1104 ) -> Task<Result<()>> {
1105 let to_load = if let Some(pane) = pane.upgrade() {
1106 pane.update(cx, |pane, cx| {
1107 pane.focus(cx);
1108 loop {
1109 // Retrieve the weak item handle from the history.
1110 let entry = pane.nav_history_mut().pop(mode, cx)?;
1111
1112 // If the item is still present in this pane, then activate it.
1113 if let Some(index) = entry
1114 .item
1115 .upgrade()
1116 .and_then(|v| pane.index_for_item(v.as_ref()))
1117 {
1118 let prev_active_item_index = pane.active_item_index();
1119 pane.nav_history_mut().set_mode(mode);
1120 pane.activate_item(index, true, true, cx);
1121 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1122
1123 let mut navigated = prev_active_item_index != pane.active_item_index();
1124 if let Some(data) = entry.data {
1125 navigated |= pane.active_item()?.navigate(data, cx);
1126 }
1127
1128 if navigated {
1129 break None;
1130 }
1131 }
1132 // If the item is no longer present in this pane, then retrieve its
1133 // project path in order to reopen it.
1134 else {
1135 break pane
1136 .nav_history()
1137 .path_for_item(entry.item.id())
1138 .map(|(project_path, _)| (project_path, entry));
1139 }
1140 }
1141 })
1142 } else {
1143 None
1144 };
1145
1146 if let Some((project_path, entry)) = to_load {
1147 // If the item was no longer present, then load it again from its previous path.
1148 let task = self.load_path(project_path, cx);
1149 cx.spawn(|workspace, mut cx| async move {
1150 let task = task.await;
1151 let mut navigated = false;
1152 if let Some((project_entry_id, build_item)) = task.log_err() {
1153 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1154 pane.nav_history_mut().set_mode(mode);
1155 pane.active_item().map(|p| p.item_id())
1156 })?;
1157
1158 pane.update(&mut cx, |pane, cx| {
1159 let item = pane.open_item(
1160 project_entry_id,
1161 true,
1162 entry.is_preview,
1163 cx,
1164 build_item,
1165 );
1166 navigated |= Some(item.item_id()) != prev_active_item_id;
1167 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1168 if let Some(data) = entry.data {
1169 navigated |= item.navigate(data, cx);
1170 }
1171 })?;
1172 }
1173
1174 if !navigated {
1175 workspace
1176 .update(&mut cx, |workspace, cx| {
1177 Self::navigate_history(workspace, pane, mode, cx)
1178 })?
1179 .await?;
1180 }
1181
1182 Ok(())
1183 })
1184 } else {
1185 Task::ready(Ok(()))
1186 }
1187 }
1188
1189 pub fn go_back(
1190 &mut self,
1191 pane: WeakView<Pane>,
1192 cx: &mut ViewContext<Workspace>,
1193 ) -> Task<Result<()>> {
1194 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1195 }
1196
1197 pub fn go_forward(
1198 &mut self,
1199 pane: WeakView<Pane>,
1200 cx: &mut ViewContext<Workspace>,
1201 ) -> Task<Result<()>> {
1202 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1203 }
1204
1205 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1206 self.navigate_history(
1207 self.active_pane().downgrade(),
1208 NavigationMode::ReopeningClosedItem,
1209 cx,
1210 )
1211 }
1212
1213 pub fn client(&self) -> &Arc<Client> {
1214 &self.app_state.client
1215 }
1216
1217 pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1218 self.titlebar_item = Some(item);
1219 cx.notify();
1220 }
1221
1222 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1223 self.on_prompt_for_new_path = Some(prompt)
1224 }
1225
1226 pub fn prompt_for_new_path(
1227 &mut self,
1228 cx: &mut ViewContext<Self>,
1229 ) -> oneshot::Receiver<Option<ProjectPath>> {
1230 if let Some(prompt) = self.on_prompt_for_new_path.take() {
1231 let rx = prompt(self, cx);
1232 self.on_prompt_for_new_path = Some(prompt);
1233 rx
1234 } else {
1235 let start_abs_path = self
1236 .project
1237 .update(cx, |project, cx| {
1238 let worktree = project.visible_worktrees(cx).next()?;
1239 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1240 })
1241 .unwrap_or_else(|| Path::new("").into());
1242
1243 let (tx, rx) = oneshot::channel();
1244 let abs_path = cx.prompt_for_new_path(&start_abs_path);
1245 cx.spawn(|this, mut cx| async move {
1246 let abs_path = abs_path.await?;
1247 let project_path = abs_path.and_then(|abs_path| {
1248 this.update(&mut cx, |this, cx| {
1249 this.project.update(cx, |project, cx| {
1250 project.find_or_create_local_worktree(abs_path, true, cx)
1251 })
1252 })
1253 .ok()
1254 });
1255
1256 if let Some(project_path) = project_path {
1257 let (worktree, path) = project_path.await?;
1258 let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1259 tx.send(Some(ProjectPath {
1260 worktree_id,
1261 path: path.into(),
1262 }))
1263 .ok();
1264 } else {
1265 tx.send(None).ok();
1266 }
1267 anyhow::Ok(())
1268 })
1269 .detach_and_log_err(cx);
1270
1271 rx
1272 }
1273 }
1274
1275 pub fn titlebar_item(&self) -> Option<AnyView> {
1276 self.titlebar_item.clone()
1277 }
1278
1279 /// Call the given callback with a workspace whose project is local.
1280 ///
1281 /// If the given workspace has a local project, then it will be passed
1282 /// to the callback. Otherwise, a new empty window will be created.
1283 pub fn with_local_workspace<T, F>(
1284 &mut self,
1285 cx: &mut ViewContext<Self>,
1286 callback: F,
1287 ) -> Task<Result<T>>
1288 where
1289 T: 'static,
1290 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1291 {
1292 if self.project.read(cx).is_local() {
1293 Task::Ready(Some(Ok(callback(self, cx))))
1294 } else {
1295 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1296 cx.spawn(|_vh, mut cx| async move {
1297 let (workspace, _) = task.await?;
1298 workspace.update(&mut cx, callback)
1299 })
1300 }
1301 }
1302
1303 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1304 self.project.read(cx).worktrees()
1305 }
1306
1307 pub fn visible_worktrees<'a>(
1308 &self,
1309 cx: &'a AppContext,
1310 ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1311 self.project.read(cx).visible_worktrees(cx)
1312 }
1313
1314 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1315 let futures = self
1316 .worktrees(cx)
1317 .filter_map(|worktree| worktree.read(cx).as_local())
1318 .map(|worktree| worktree.scan_complete())
1319 .collect::<Vec<_>>();
1320 async move {
1321 for future in futures {
1322 future.await;
1323 }
1324 }
1325 }
1326
1327 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1328 cx.defer(|cx| {
1329 cx.windows().iter().find(|window| {
1330 window
1331 .update(cx, |_, window| {
1332 if window.is_window_active() {
1333 //This can only get called when the window's project connection has been lost
1334 //so we don't need to prompt the user for anything and instead just close the window
1335 window.remove_window();
1336 true
1337 } else {
1338 false
1339 }
1340 })
1341 .unwrap_or(false)
1342 });
1343 });
1344 }
1345
1346 pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1347 let window = cx.window_handle();
1348 let prepare = self.prepare_to_close(false, cx);
1349 cx.spawn(|_, mut cx| async move {
1350 if prepare.await? {
1351 window.update(&mut cx, |_, cx| {
1352 cx.remove_window();
1353 })?;
1354 }
1355 anyhow::Ok(())
1356 })
1357 .detach_and_log_err(cx)
1358 }
1359
1360 pub fn prepare_to_close(
1361 &mut self,
1362 quitting: bool,
1363 cx: &mut ViewContext<Self>,
1364 ) -> Task<Result<bool>> {
1365 let active_call = self.active_call().cloned();
1366 let window = cx.window_handle();
1367
1368 cx.spawn(|this, mut cx| async move {
1369 let workspace_count = (*cx).update(|cx| {
1370 cx.windows()
1371 .iter()
1372 .filter(|window| window.downcast::<Workspace>().is_some())
1373 .count()
1374 })?;
1375
1376 if let Some(active_call) = active_call {
1377 if !quitting
1378 && workspace_count == 1
1379 && active_call.read_with(&cx, |call, _| call.room().is_some())?
1380 {
1381 let answer = window.update(&mut cx, |_, cx| {
1382 cx.prompt(
1383 PromptLevel::Warning,
1384 "Do you want to leave the current call?",
1385 None,
1386 &["Close window and hang up", "Cancel"],
1387 )
1388 })?;
1389
1390 if answer.await.log_err() == Some(1) {
1391 return anyhow::Ok(false);
1392 } else {
1393 active_call
1394 .update(&mut cx, |call, cx| call.hang_up(cx))?
1395 .await
1396 .log_err();
1397 }
1398 }
1399 }
1400
1401 this.update(&mut cx, |this, cx| {
1402 this.save_all_internal(SaveIntent::Close, cx)
1403 })?
1404 .await
1405 })
1406 }
1407
1408 fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1409 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1410 .detach_and_log_err(cx);
1411 }
1412
1413 fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1414 let mut keystrokes: Vec<Keystroke> = action
1415 .0
1416 .split(' ')
1417 .flat_map(|k| Keystroke::parse(k).log_err())
1418 .collect();
1419 keystrokes.reverse();
1420
1421 self.dispatching_keystrokes
1422 .borrow_mut()
1423 .append(&mut keystrokes);
1424
1425 let keystrokes = self.dispatching_keystrokes.clone();
1426 cx.window_context()
1427 .spawn(|mut cx| async move {
1428 // limit to 100 keystrokes to avoid infinite recursion.
1429 for _ in 0..100 {
1430 let Some(keystroke) = keystrokes.borrow_mut().pop() else {
1431 return Ok(());
1432 };
1433 cx.update(|cx| {
1434 let focused = cx.focused();
1435 cx.dispatch_keystroke(keystroke.clone());
1436 if cx.focused() != focused {
1437 // dispatch_keystroke may cause the focus to change.
1438 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1439 // And we need that to happen before the next keystroke to keep vim mode happy...
1440 // (Note that the tests always do this implicitly, so you must manually test with something like:
1441 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1442 // )
1443 cx.draw();
1444 }
1445 })?;
1446 }
1447 keystrokes.borrow_mut().clear();
1448 Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1449 })
1450 .detach_and_log_err(cx);
1451 }
1452
1453 fn save_all_internal(
1454 &mut self,
1455 mut save_intent: SaveIntent,
1456 cx: &mut ViewContext<Self>,
1457 ) -> Task<Result<bool>> {
1458 if self.project.read(cx).is_disconnected() {
1459 return Task::ready(Ok(true));
1460 }
1461 let dirty_items = self
1462 .panes
1463 .iter()
1464 .flat_map(|pane| {
1465 pane.read(cx).items().filter_map(|item| {
1466 if item.is_dirty(cx) {
1467 Some((pane.downgrade(), item.boxed_clone()))
1468 } else {
1469 None
1470 }
1471 })
1472 })
1473 .collect::<Vec<_>>();
1474
1475 let project = self.project.clone();
1476 cx.spawn(|workspace, mut cx| async move {
1477 // Override save mode and display "Save all files" prompt
1478 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1479 let answer = workspace.update(&mut cx, |_, cx| {
1480 let (prompt, detail) = Pane::file_names_for_prompt(
1481 &mut dirty_items.iter().map(|(_, handle)| handle),
1482 dirty_items.len(),
1483 cx,
1484 );
1485 cx.prompt(
1486 PromptLevel::Warning,
1487 &prompt,
1488 Some(&detail),
1489 &["Save all", "Discard all", "Cancel"],
1490 )
1491 })?;
1492 match answer.await.log_err() {
1493 Some(0) => save_intent = SaveIntent::SaveAll,
1494 Some(1) => save_intent = SaveIntent::Skip,
1495 _ => {}
1496 }
1497 }
1498 for (pane, item) in dirty_items {
1499 let (singleton, project_entry_ids) =
1500 cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1501 if singleton || !project_entry_ids.is_empty() {
1502 if let Some(ix) =
1503 pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1504 {
1505 if !Pane::save_item(
1506 project.clone(),
1507 &pane,
1508 ix,
1509 &*item,
1510 save_intent,
1511 &mut cx,
1512 )
1513 .await?
1514 {
1515 return Ok(false);
1516 }
1517 }
1518 }
1519 }
1520 Ok(true)
1521 })
1522 }
1523
1524 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
1525 self.client()
1526 .telemetry()
1527 .report_app_event("open project".to_string());
1528 let paths = cx.prompt_for_paths(PathPromptOptions {
1529 files: true,
1530 directories: true,
1531 multiple: true,
1532 });
1533
1534 cx.spawn(|this, mut cx| async move {
1535 let Some(paths) = paths.await.log_err().flatten() else {
1536 return;
1537 };
1538
1539 if let Some(task) = this
1540 .update(&mut cx, |this, cx| {
1541 this.open_workspace_for_paths(false, paths, cx)
1542 })
1543 .log_err()
1544 {
1545 task.await.log_err();
1546 }
1547 })
1548 .detach()
1549 }
1550
1551 pub fn open_workspace_for_paths(
1552 &mut self,
1553 replace_current_window: bool,
1554 paths: Vec<PathBuf>,
1555 cx: &mut ViewContext<Self>,
1556 ) -> Task<Result<()>> {
1557 let window = cx.window_handle().downcast::<Self>();
1558 let is_remote = self.project.read(cx).is_remote();
1559 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1560 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1561
1562 let window_to_replace = if replace_current_window {
1563 window
1564 } else if is_remote || has_worktree || has_dirty_items {
1565 None
1566 } else {
1567 window
1568 };
1569 let app_state = self.app_state.clone();
1570
1571 cx.spawn(|_, mut cx| async move {
1572 cx.update(|cx| {
1573 open_paths(
1574 &paths,
1575 app_state,
1576 OpenOptions {
1577 replace_window: window_to_replace,
1578 ..Default::default()
1579 },
1580 cx,
1581 )
1582 })?
1583 .await?;
1584 Ok(())
1585 })
1586 }
1587
1588 #[allow(clippy::type_complexity)]
1589 pub fn open_paths(
1590 &mut self,
1591 mut abs_paths: Vec<PathBuf>,
1592 visible: OpenVisible,
1593 pane: Option<WeakView<Pane>>,
1594 cx: &mut ViewContext<Self>,
1595 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1596 log::info!("open paths {abs_paths:?}");
1597
1598 let fs = self.app_state.fs.clone();
1599
1600 // Sort the paths to ensure we add worktrees for parents before their children.
1601 abs_paths.sort_unstable();
1602 cx.spawn(move |this, mut cx| async move {
1603 let mut tasks = Vec::with_capacity(abs_paths.len());
1604
1605 for abs_path in &abs_paths {
1606 let visible = match visible {
1607 OpenVisible::All => Some(true),
1608 OpenVisible::None => Some(false),
1609 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
1610 Some(Some(metadata)) => Some(!metadata.is_dir),
1611 Some(None) => Some(true),
1612 None => None,
1613 },
1614 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
1615 Some(Some(metadata)) => Some(metadata.is_dir),
1616 Some(None) => Some(false),
1617 None => None,
1618 },
1619 };
1620 let project_path = match visible {
1621 Some(visible) => match this
1622 .update(&mut cx, |this, cx| {
1623 Workspace::project_path_for_path(
1624 this.project.clone(),
1625 abs_path,
1626 visible,
1627 cx,
1628 )
1629 })
1630 .log_err()
1631 {
1632 Some(project_path) => project_path.await.log_err(),
1633 None => None,
1634 },
1635 None => None,
1636 };
1637
1638 let this = this.clone();
1639 let abs_path = abs_path.clone();
1640 let fs = fs.clone();
1641 let pane = pane.clone();
1642 let task = cx.spawn(move |mut cx| async move {
1643 let (worktree, project_path) = project_path?;
1644 if fs.is_dir(&abs_path).await {
1645 this.update(&mut cx, |workspace, cx| {
1646 let worktree = worktree.read(cx);
1647 let worktree_abs_path = worktree.abs_path();
1648 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1649 worktree.root_entry()
1650 } else {
1651 abs_path
1652 .strip_prefix(worktree_abs_path.as_ref())
1653 .ok()
1654 .and_then(|relative_path| {
1655 worktree.entry_for_path(relative_path)
1656 })
1657 }
1658 .map(|entry| entry.id);
1659 if let Some(entry_id) = entry_id {
1660 workspace.project.update(cx, |_, cx| {
1661 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1662 })
1663 }
1664 })
1665 .log_err()?;
1666 None
1667 } else {
1668 Some(
1669 this.update(&mut cx, |this, cx| {
1670 this.open_path(project_path, pane, true, cx)
1671 })
1672 .log_err()?
1673 .await,
1674 )
1675 }
1676 });
1677 tasks.push(task);
1678 }
1679
1680 futures::future::join_all(tasks).await
1681 })
1682 }
1683
1684 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1685 if self.project.read(cx).is_remote() {
1686 self.show_error(
1687 &anyhow!("Folders cannot yet be added to remote projects"),
1688 cx,
1689 );
1690 return;
1691 }
1692 let paths = cx.prompt_for_paths(PathPromptOptions {
1693 files: false,
1694 directories: true,
1695 multiple: true,
1696 });
1697 cx.spawn(|this, mut cx| async move {
1698 if let Some(paths) = paths.await.log_err().flatten() {
1699 let results = this
1700 .update(&mut cx, |this, cx| {
1701 this.open_paths(paths, OpenVisible::All, None, cx)
1702 })?
1703 .await;
1704 for result in results.into_iter().flatten() {
1705 result.log_err();
1706 }
1707 }
1708 anyhow::Ok(())
1709 })
1710 .detach_and_log_err(cx);
1711 }
1712
1713 fn project_path_for_path(
1714 project: Model<Project>,
1715 abs_path: &Path,
1716 visible: bool,
1717 cx: &mut AppContext,
1718 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1719 let entry = project.update(cx, |project, cx| {
1720 project.find_or_create_local_worktree(abs_path, visible, cx)
1721 });
1722 cx.spawn(|mut cx| async move {
1723 let (worktree, path) = entry.await?;
1724 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1725 Ok((
1726 worktree,
1727 ProjectPath {
1728 worktree_id,
1729 path: path.into(),
1730 },
1731 ))
1732 })
1733 }
1734
1735 pub fn items<'a>(
1736 &'a self,
1737 cx: &'a AppContext,
1738 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1739 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1740 }
1741
1742 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1743 self.items_of_type(cx).max_by_key(|item| item.item_id())
1744 }
1745
1746 pub fn items_of_type<'a, T: Item>(
1747 &'a self,
1748 cx: &'a AppContext,
1749 ) -> impl 'a + Iterator<Item = View<T>> {
1750 self.panes
1751 .iter()
1752 .flat_map(|pane| pane.read(cx).items_of_type())
1753 }
1754
1755 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1756 self.active_pane().read(cx).active_item()
1757 }
1758
1759 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1760 let item = self.active_item(cx)?;
1761 item.to_any().downcast::<I>().ok()
1762 }
1763
1764 fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
1765 self.active_item(cx).and_then(|item| item.project_path(cx))
1766 }
1767
1768 pub fn save_active_item(
1769 &mut self,
1770 save_intent: SaveIntent,
1771 cx: &mut WindowContext,
1772 ) -> Task<Result<()>> {
1773 let project = self.project.clone();
1774 let pane = self.active_pane();
1775 let item_ix = pane.read(cx).active_item_index();
1776 let item = pane.read(cx).active_item();
1777 let pane = pane.downgrade();
1778
1779 cx.spawn(|mut cx| async move {
1780 if let Some(item) = item {
1781 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1782 .await
1783 .map(|_| ())
1784 } else {
1785 Ok(())
1786 }
1787 })
1788 }
1789
1790 pub fn close_inactive_items_and_panes(
1791 &mut self,
1792 action: &CloseInactiveTabsAndPanes,
1793 cx: &mut ViewContext<Self>,
1794 ) {
1795 if let Some(task) =
1796 self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1797 {
1798 task.detach_and_log_err(cx)
1799 }
1800 }
1801
1802 pub fn close_all_items_and_panes(
1803 &mut self,
1804 action: &CloseAllItemsAndPanes,
1805 cx: &mut ViewContext<Self>,
1806 ) {
1807 if let Some(task) =
1808 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1809 {
1810 task.detach_and_log_err(cx)
1811 }
1812 }
1813
1814 fn close_all_internal(
1815 &mut self,
1816 retain_active_pane: bool,
1817 save_intent: SaveIntent,
1818 cx: &mut ViewContext<Self>,
1819 ) -> Option<Task<Result<()>>> {
1820 let current_pane = self.active_pane();
1821
1822 let mut tasks = Vec::new();
1823
1824 if retain_active_pane {
1825 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1826 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
1827 }) {
1828 tasks.push(current_pane_close);
1829 };
1830 }
1831
1832 for pane in self.panes() {
1833 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
1834 continue;
1835 }
1836
1837 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1838 pane.close_all_items(
1839 &CloseAllItems {
1840 save_intent: Some(save_intent),
1841 },
1842 cx,
1843 )
1844 }) {
1845 tasks.push(close_pane_items)
1846 }
1847 }
1848
1849 if tasks.is_empty() {
1850 None
1851 } else {
1852 Some(cx.spawn(|_, _| async move {
1853 for task in tasks {
1854 task.await?
1855 }
1856 Ok(())
1857 }))
1858 }
1859 }
1860
1861 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1862 let dock = match dock_side {
1863 DockPosition::Left => &self.left_dock,
1864 DockPosition::Bottom => &self.bottom_dock,
1865 DockPosition::Right => &self.right_dock,
1866 };
1867 let mut focus_center = false;
1868 let mut reveal_dock = false;
1869 dock.update(cx, |dock, cx| {
1870 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1871 let was_visible = dock.is_open() && !other_is_zoomed;
1872 dock.set_open(!was_visible, cx);
1873
1874 if let Some(active_panel) = dock.active_panel() {
1875 if was_visible {
1876 if active_panel.focus_handle(cx).contains_focused(cx) {
1877 focus_center = true;
1878 }
1879 } else {
1880 let focus_handle = &active_panel.focus_handle(cx);
1881 cx.focus(focus_handle);
1882 reveal_dock = true;
1883 }
1884 }
1885 });
1886
1887 if reveal_dock {
1888 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1889 }
1890
1891 if focus_center {
1892 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1893 }
1894
1895 cx.notify();
1896 self.serialize_workspace(cx);
1897 }
1898
1899 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1900 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1901
1902 for dock in docks {
1903 dock.update(cx, |dock, cx| {
1904 dock.set_open(false, cx);
1905 });
1906 }
1907
1908 cx.focus_self();
1909 cx.notify();
1910 self.serialize_workspace(cx);
1911 }
1912
1913 /// Transfer focus to the panel of the given type.
1914 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
1915 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
1916 panel.to_any().downcast().ok()
1917 }
1918
1919 /// Focus the panel of the given type if it isn't already focused. If it is
1920 /// already focused, then transfer focus back to the workspace center.
1921 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1922 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
1923 !panel.focus_handle(cx).contains_focused(cx)
1924 });
1925 }
1926
1927 /// Focus or unfocus the given panel type, depending on the given callback.
1928 fn focus_or_unfocus_panel<T: Panel>(
1929 &mut self,
1930 cx: &mut ViewContext<Self>,
1931 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1932 ) -> Option<Arc<dyn PanelHandle>> {
1933 let mut result_panel = None;
1934 let mut serialize = false;
1935 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1936 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1937 let mut focus_center = false;
1938 let panel = dock.update(cx, |dock, cx| {
1939 dock.activate_panel(panel_index, cx);
1940
1941 let panel = dock.active_panel().cloned();
1942 if let Some(panel) = panel.as_ref() {
1943 if should_focus(&**panel, cx) {
1944 dock.set_open(true, cx);
1945 panel.focus_handle(cx).focus(cx);
1946 } else {
1947 focus_center = true;
1948 }
1949 }
1950 panel
1951 });
1952
1953 if focus_center {
1954 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1955 }
1956
1957 result_panel = panel;
1958 serialize = true;
1959 break;
1960 }
1961 }
1962
1963 if serialize {
1964 self.serialize_workspace(cx);
1965 }
1966
1967 cx.notify();
1968 result_panel
1969 }
1970
1971 /// Open the panel of the given type
1972 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1973 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1974 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1975 dock.update(cx, |dock, cx| {
1976 dock.activate_panel(panel_index, cx);
1977 dock.set_open(true, cx);
1978 });
1979 }
1980 }
1981 }
1982
1983 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1984 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1985 let dock = dock.read(cx);
1986 if let Some(panel) = dock.panel::<T>() {
1987 return Some(panel);
1988 }
1989 }
1990 None
1991 }
1992
1993 fn dismiss_zoomed_items_to_reveal(
1994 &mut self,
1995 dock_to_reveal: Option<DockPosition>,
1996 cx: &mut ViewContext<Self>,
1997 ) {
1998 // If a center pane is zoomed, unzoom it.
1999 for pane in &self.panes {
2000 if pane != &self.active_pane || dock_to_reveal.is_some() {
2001 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2002 }
2003 }
2004
2005 // If another dock is zoomed, hide it.
2006 let mut focus_center = false;
2007 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2008 dock.update(cx, |dock, cx| {
2009 if Some(dock.position()) != dock_to_reveal {
2010 if let Some(panel) = dock.active_panel() {
2011 if panel.is_zoomed(cx) {
2012 focus_center |= panel.focus_handle(cx).contains_focused(cx);
2013 dock.set_open(false, cx);
2014 }
2015 }
2016 }
2017 });
2018 }
2019
2020 if focus_center {
2021 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2022 }
2023
2024 if self.zoomed_position != dock_to_reveal {
2025 self.zoomed = None;
2026 self.zoomed_position = None;
2027 cx.emit(Event::ZoomChanged);
2028 }
2029
2030 cx.notify();
2031 }
2032
2033 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2034 let pane = cx.new_view(|cx| {
2035 Pane::new(
2036 self.weak_handle(),
2037 self.project.clone(),
2038 self.pane_history_timestamp.clone(),
2039 None,
2040 NewFile.boxed_clone(),
2041 cx,
2042 )
2043 });
2044 cx.subscribe(&pane, Self::handle_pane_event).detach();
2045 self.panes.push(pane.clone());
2046 cx.focus_view(&pane);
2047 cx.emit(Event::PaneAdded(pane.clone()));
2048 pane
2049 }
2050
2051 pub fn add_item_to_center(
2052 &mut self,
2053 item: Box<dyn ItemHandle>,
2054 cx: &mut ViewContext<Self>,
2055 ) -> bool {
2056 if let Some(center_pane) = self.last_active_center_pane.clone() {
2057 if let Some(center_pane) = center_pane.upgrade() {
2058 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2059 true
2060 } else {
2061 false
2062 }
2063 } else {
2064 false
2065 }
2066 }
2067
2068 pub fn add_item_to_active_pane(
2069 &mut self,
2070 item: Box<dyn ItemHandle>,
2071 destination_index: Option<usize>,
2072 cx: &mut WindowContext,
2073 ) {
2074 self.add_item(self.active_pane.clone(), item, destination_index, cx)
2075 }
2076
2077 pub fn add_item(
2078 &mut self,
2079 pane: View<Pane>,
2080 item: Box<dyn ItemHandle>,
2081 destination_index: Option<usize>,
2082 cx: &mut WindowContext,
2083 ) {
2084 if let Some(text) = item.telemetry_event_text(cx) {
2085 self.client()
2086 .telemetry()
2087 .report_app_event(format!("{}: open", text));
2088 }
2089
2090 pane.update(cx, |pane, cx| {
2091 pane.add_item(item, true, true, destination_index, cx)
2092 });
2093 }
2094
2095 pub fn split_item(
2096 &mut self,
2097 split_direction: SplitDirection,
2098 item: Box<dyn ItemHandle>,
2099 cx: &mut ViewContext<Self>,
2100 ) {
2101 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2102 self.add_item(new_pane, item, None, cx);
2103 }
2104
2105 pub fn open_abs_path(
2106 &mut self,
2107 abs_path: PathBuf,
2108 visible: bool,
2109 cx: &mut ViewContext<Self>,
2110 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2111 cx.spawn(|workspace, mut cx| async move {
2112 let open_paths_task_result = workspace
2113 .update(&mut cx, |workspace, cx| {
2114 workspace.open_paths(
2115 vec![abs_path.clone()],
2116 if visible {
2117 OpenVisible::All
2118 } else {
2119 OpenVisible::None
2120 },
2121 None,
2122 cx,
2123 )
2124 })
2125 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2126 .await;
2127 anyhow::ensure!(
2128 open_paths_task_result.len() == 1,
2129 "open abs path {abs_path:?} task returned incorrect number of results"
2130 );
2131 match open_paths_task_result
2132 .into_iter()
2133 .next()
2134 .expect("ensured single task result")
2135 {
2136 Some(open_result) => {
2137 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2138 }
2139 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2140 }
2141 })
2142 }
2143
2144 pub fn split_abs_path(
2145 &mut self,
2146 abs_path: PathBuf,
2147 visible: bool,
2148 cx: &mut ViewContext<Self>,
2149 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2150 let project_path_task =
2151 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2152 cx.spawn(|this, mut cx| async move {
2153 let (_, path) = project_path_task.await?;
2154 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2155 .await
2156 })
2157 }
2158
2159 pub fn open_path(
2160 &mut self,
2161 path: impl Into<ProjectPath>,
2162 pane: Option<WeakView<Pane>>,
2163 focus_item: bool,
2164 cx: &mut WindowContext,
2165 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2166 self.open_path_preview(path, pane, focus_item, false, cx)
2167 }
2168
2169 pub fn open_path_preview(
2170 &mut self,
2171 path: impl Into<ProjectPath>,
2172 pane: Option<WeakView<Pane>>,
2173 focus_item: bool,
2174 allow_preview: bool,
2175 cx: &mut WindowContext,
2176 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2177 let pane = pane.unwrap_or_else(|| {
2178 self.last_active_center_pane.clone().unwrap_or_else(|| {
2179 self.panes
2180 .first()
2181 .expect("There must be an active pane")
2182 .downgrade()
2183 })
2184 });
2185
2186 let task = self.load_path(path.into(), cx);
2187 cx.spawn(move |mut cx| async move {
2188 let (project_entry_id, build_item) = task.await?;
2189 pane.update(&mut cx, |pane, cx| {
2190 pane.open_item(project_entry_id, focus_item, allow_preview, cx, build_item)
2191 })
2192 })
2193 }
2194
2195 pub fn split_path(
2196 &mut self,
2197 path: impl Into<ProjectPath>,
2198 cx: &mut ViewContext<Self>,
2199 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2200 self.split_path_preview(path, false, cx)
2201 }
2202
2203 pub fn split_path_preview(
2204 &mut self,
2205 path: impl Into<ProjectPath>,
2206 allow_preview: bool,
2207 cx: &mut ViewContext<Self>,
2208 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2209 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2210 self.panes
2211 .first()
2212 .expect("There must be an active pane")
2213 .downgrade()
2214 });
2215
2216 if let Member::Pane(center_pane) = &self.center.root {
2217 if center_pane.read(cx).items_len() == 0 {
2218 return self.open_path(path, Some(pane), true, cx);
2219 }
2220 }
2221
2222 let task = self.load_path(path.into(), cx);
2223 cx.spawn(|this, mut cx| async move {
2224 let (project_entry_id, build_item) = task.await?;
2225 this.update(&mut cx, move |this, cx| -> Option<_> {
2226 let pane = pane.upgrade()?;
2227 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2228 new_pane.update(cx, |new_pane, cx| {
2229 Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item))
2230 })
2231 })
2232 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2233 })
2234 }
2235
2236 fn load_path(
2237 &mut self,
2238 path: ProjectPath,
2239 cx: &mut WindowContext,
2240 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2241 let project = self.project().clone();
2242 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2243 let Some(open_project_item) = project_item_builders
2244 .iter()
2245 .rev()
2246 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2247 else {
2248 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2249 };
2250 open_project_item
2251 }
2252
2253 pub fn open_project_item<T>(
2254 &mut self,
2255 pane: View<Pane>,
2256 project_item: Model<T::Item>,
2257 cx: &mut ViewContext<Self>,
2258 ) -> View<T>
2259 where
2260 T: ProjectItem,
2261 {
2262 use project::Item as _;
2263
2264 let entry_id = project_item.read(cx).entry_id(cx);
2265 if let Some(item) = entry_id
2266 .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
2267 .and_then(|item| item.downcast())
2268 {
2269 self.activate_item(&item, cx);
2270 return item;
2271 }
2272
2273 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2274
2275 let item_id = item.item_id();
2276 let mut destination_index = None;
2277 pane.update(cx, |pane, cx| {
2278 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2279 if let Some(preview_item_id) = pane.preview_item_id() {
2280 if preview_item_id != item_id {
2281 destination_index = pane.close_current_preview_item(cx);
2282 }
2283 }
2284 }
2285 pane.set_preview_item_id(Some(item.item_id()), cx)
2286 });
2287
2288 self.add_item(pane, Box::new(item.clone()), destination_index, cx);
2289 item
2290 }
2291
2292 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2293 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2294 self.active_pane.update(cx, |pane, cx| {
2295 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2296 });
2297 }
2298 }
2299
2300 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2301 let result = self.panes.iter().find_map(|pane| {
2302 pane.read(cx)
2303 .index_for_item(item)
2304 .map(|ix| (pane.clone(), ix))
2305 });
2306 if let Some((pane, ix)) = result {
2307 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2308 true
2309 } else {
2310 false
2311 }
2312 }
2313
2314 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2315 let panes = self.center.panes();
2316 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2317 cx.focus_view(&pane);
2318 } else {
2319 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2320 }
2321 }
2322
2323 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2324 let panes = self.center.panes();
2325 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2326 let next_ix = (ix + 1) % panes.len();
2327 let next_pane = panes[next_ix].clone();
2328 cx.focus_view(&next_pane);
2329 }
2330 }
2331
2332 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2333 let panes = self.center.panes();
2334 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2335 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2336 let prev_pane = panes[prev_ix].clone();
2337 cx.focus_view(&prev_pane);
2338 }
2339 }
2340
2341 pub fn activate_pane_in_direction(
2342 &mut self,
2343 direction: SplitDirection,
2344 cx: &mut WindowContext,
2345 ) {
2346 use ActivateInDirectionTarget as Target;
2347 enum Origin {
2348 LeftDock,
2349 RightDock,
2350 BottomDock,
2351 Center,
2352 }
2353
2354 let origin: Origin = [
2355 (&self.left_dock, Origin::LeftDock),
2356 (&self.right_dock, Origin::RightDock),
2357 (&self.bottom_dock, Origin::BottomDock),
2358 ]
2359 .into_iter()
2360 .find_map(|(dock, origin)| {
2361 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2362 Some(origin)
2363 } else {
2364 None
2365 }
2366 })
2367 .unwrap_or(Origin::Center);
2368
2369 let get_last_active_pane = || {
2370 self.last_active_center_pane.as_ref().and_then(|p| {
2371 let p = p.upgrade()?;
2372 (p.read(cx).items_len() != 0).then_some(p)
2373 })
2374 };
2375
2376 let try_dock =
2377 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2378
2379 let target = match (origin, direction) {
2380 // We're in the center, so we first try to go to a different pane,
2381 // otherwise try to go to a dock.
2382 (Origin::Center, direction) => {
2383 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2384 Some(Target::Pane(pane))
2385 } else {
2386 match direction {
2387 SplitDirection::Up => None,
2388 SplitDirection::Down => try_dock(&self.bottom_dock),
2389 SplitDirection::Left => try_dock(&self.left_dock),
2390 SplitDirection::Right => try_dock(&self.right_dock),
2391 }
2392 }
2393 }
2394
2395 (Origin::LeftDock, SplitDirection::Right) => {
2396 if let Some(last_active_pane) = get_last_active_pane() {
2397 Some(Target::Pane(last_active_pane))
2398 } else {
2399 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2400 }
2401 }
2402
2403 (Origin::LeftDock, SplitDirection::Down)
2404 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2405
2406 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2407 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2408 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2409
2410 (Origin::RightDock, SplitDirection::Left) => {
2411 if let Some(last_active_pane) = get_last_active_pane() {
2412 Some(Target::Pane(last_active_pane))
2413 } else {
2414 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2415 }
2416 }
2417
2418 _ => None,
2419 };
2420
2421 match target {
2422 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2423 Some(ActivateInDirectionTarget::Dock(dock)) => {
2424 if let Some(panel) = dock.read(cx).active_panel() {
2425 panel.focus_handle(cx).focus(cx);
2426 } else {
2427 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2428 }
2429 }
2430 None => {}
2431 }
2432 }
2433
2434 pub fn find_pane_in_direction(
2435 &mut self,
2436 direction: SplitDirection,
2437 cx: &WindowContext,
2438 ) -> Option<View<Pane>> {
2439 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2440 return None;
2441 };
2442 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2443 let center = match cursor {
2444 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2445 _ => bounding_box.center(),
2446 };
2447
2448 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2449
2450 let target = match direction {
2451 SplitDirection::Left => {
2452 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2453 }
2454 SplitDirection::Right => {
2455 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2456 }
2457 SplitDirection::Up => {
2458 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2459 }
2460 SplitDirection::Down => {
2461 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2462 }
2463 };
2464 self.center.pane_at_pixel_position(target).cloned()
2465 }
2466
2467 pub fn swap_pane_in_direction(
2468 &mut self,
2469 direction: SplitDirection,
2470 cx: &mut ViewContext<Self>,
2471 ) {
2472 if let Some(to) = self
2473 .find_pane_in_direction(direction, cx)
2474 .map(|pane| pane.clone())
2475 {
2476 self.center.swap(&self.active_pane.clone(), &to);
2477 cx.notify();
2478 }
2479 }
2480
2481 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2482 // This is explicitly hoisted out of the following check for pane identity as
2483 // terminal panel panes are not registered as a center panes.
2484 self.status_bar.update(cx, |status_bar, cx| {
2485 status_bar.set_active_pane(&pane, cx);
2486 });
2487 if self.active_pane != pane {
2488 self.active_pane = pane.clone();
2489 self.active_item_path_changed(cx);
2490 self.last_active_center_pane = Some(pane.downgrade());
2491 }
2492
2493 self.dismiss_zoomed_items_to_reveal(None, cx);
2494 if pane.read(cx).is_zoomed() {
2495 self.zoomed = Some(pane.downgrade().into());
2496 } else {
2497 self.zoomed = None;
2498 }
2499 self.zoomed_position = None;
2500 cx.emit(Event::ZoomChanged);
2501 self.update_active_view_for_followers(cx);
2502 pane.model.update(cx, |pane, _| {
2503 pane.track_alternate_file_items();
2504 });
2505
2506 cx.notify();
2507 }
2508
2509 fn handle_pane_event(
2510 &mut self,
2511 pane: View<Pane>,
2512 event: &pane::Event,
2513 cx: &mut ViewContext<Self>,
2514 ) {
2515 match event {
2516 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2517 pane::Event::Split(direction) => {
2518 self.split_and_clone(pane, *direction, cx);
2519 }
2520 pane::Event::Remove => self.remove_pane(pane, cx),
2521 pane::Event::ActivateItem { local } => {
2522 pane.model.update(cx, |pane, _| {
2523 pane.track_alternate_file_items();
2524 });
2525 if *local {
2526 self.unfollow(&pane, cx);
2527 }
2528 if &pane == self.active_pane() {
2529 self.active_item_path_changed(cx);
2530 self.update_active_view_for_followers(cx);
2531 }
2532 }
2533 pane::Event::ChangeItemTitle => {
2534 if pane == self.active_pane {
2535 self.active_item_path_changed(cx);
2536 }
2537 self.update_window_edited(cx);
2538 }
2539 pane::Event::RemoveItem { item_id } => {
2540 cx.emit(Event::ActiveItemChanged);
2541 self.update_window_edited(cx);
2542 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2543 if entry.get().entity_id() == pane.entity_id() {
2544 entry.remove();
2545 }
2546 }
2547 }
2548 pane::Event::Focus => {
2549 self.handle_pane_focused(pane.clone(), cx);
2550 }
2551 pane::Event::ZoomIn => {
2552 if pane == self.active_pane {
2553 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2554 if pane.read(cx).has_focus(cx) {
2555 self.zoomed = Some(pane.downgrade().into());
2556 self.zoomed_position = None;
2557 cx.emit(Event::ZoomChanged);
2558 }
2559 cx.notify();
2560 }
2561 }
2562 pane::Event::ZoomOut => {
2563 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2564 if self.zoomed_position.is_none() {
2565 self.zoomed = None;
2566 cx.emit(Event::ZoomChanged);
2567 }
2568 cx.notify();
2569 }
2570 }
2571
2572 self.serialize_workspace(cx);
2573 }
2574
2575 pub fn split_pane(
2576 &mut self,
2577 pane_to_split: View<Pane>,
2578 split_direction: SplitDirection,
2579 cx: &mut ViewContext<Self>,
2580 ) -> View<Pane> {
2581 let new_pane = self.add_pane(cx);
2582 self.center
2583 .split(&pane_to_split, &new_pane, split_direction)
2584 .unwrap();
2585 cx.notify();
2586 new_pane
2587 }
2588
2589 pub fn split_and_clone(
2590 &mut self,
2591 pane: View<Pane>,
2592 direction: SplitDirection,
2593 cx: &mut ViewContext<Self>,
2594 ) -> Option<View<Pane>> {
2595 let item = pane.read(cx).active_item()?;
2596 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2597 let new_pane = self.add_pane(cx);
2598 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2599 self.center.split(&pane, &new_pane, direction).unwrap();
2600 Some(new_pane)
2601 } else {
2602 None
2603 };
2604 cx.notify();
2605 maybe_pane_handle
2606 }
2607
2608 pub fn split_pane_with_item(
2609 &mut self,
2610 pane_to_split: WeakView<Pane>,
2611 split_direction: SplitDirection,
2612 from: WeakView<Pane>,
2613 item_id_to_move: EntityId,
2614 cx: &mut ViewContext<Self>,
2615 ) {
2616 let Some(pane_to_split) = pane_to_split.upgrade() else {
2617 return;
2618 };
2619 let Some(from) = from.upgrade() else {
2620 return;
2621 };
2622
2623 let new_pane = self.add_pane(cx);
2624 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2625 self.center
2626 .split(&pane_to_split, &new_pane, split_direction)
2627 .unwrap();
2628 cx.notify();
2629 }
2630
2631 pub fn split_pane_with_project_entry(
2632 &mut self,
2633 pane_to_split: WeakView<Pane>,
2634 split_direction: SplitDirection,
2635 project_entry: ProjectEntryId,
2636 cx: &mut ViewContext<Self>,
2637 ) -> Option<Task<Result<()>>> {
2638 let pane_to_split = pane_to_split.upgrade()?;
2639 let new_pane = self.add_pane(cx);
2640 self.center
2641 .split(&pane_to_split, &new_pane, split_direction)
2642 .unwrap();
2643
2644 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2645 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2646 Some(cx.foreground_executor().spawn(async move {
2647 task.await?;
2648 Ok(())
2649 }))
2650 }
2651
2652 pub fn move_item(
2653 &mut self,
2654 source: View<Pane>,
2655 destination: View<Pane>,
2656 item_id_to_move: EntityId,
2657 destination_index: usize,
2658 cx: &mut ViewContext<Self>,
2659 ) {
2660 let Some((item_ix, item_handle)) = source
2661 .read(cx)
2662 .items()
2663 .enumerate()
2664 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2665 else {
2666 // Tab was closed during drag
2667 return;
2668 };
2669
2670 let item_handle = item_handle.clone();
2671
2672 if source != destination {
2673 // Close item from previous pane
2674 source.update(cx, |source, cx| {
2675 source.remove_item(item_ix, false, true, cx);
2676 });
2677 }
2678
2679 // This automatically removes duplicate items in the pane
2680 destination.update(cx, |destination, cx| {
2681 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2682 destination.focus(cx)
2683 });
2684 }
2685
2686 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2687 if self.center.remove(&pane).unwrap() {
2688 self.force_remove_pane(&pane, cx);
2689 self.unfollow(&pane, cx);
2690 self.last_leaders_by_pane.remove(&pane.downgrade());
2691 for removed_item in pane.read(cx).items() {
2692 self.panes_by_item.remove(&removed_item.item_id());
2693 }
2694
2695 cx.notify();
2696 } else {
2697 self.active_item_path_changed(cx);
2698 }
2699 }
2700
2701 pub fn panes(&self) -> &[View<Pane>] {
2702 &self.panes
2703 }
2704
2705 pub fn active_pane(&self) -> &View<Pane> {
2706 &self.active_pane
2707 }
2708
2709 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2710 self.find_pane_in_direction(SplitDirection::Right, cx)
2711 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
2712 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
2713 .clone()
2714 }
2715
2716 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2717 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2718 weak_pane.upgrade()
2719 }
2720
2721 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2722 self.follower_states.retain(|_, state| {
2723 if state.leader_id == peer_id {
2724 for item in state.items_by_leader_view_id.values() {
2725 item.set_leader_peer_id(None, cx);
2726 }
2727 false
2728 } else {
2729 true
2730 }
2731 });
2732 cx.notify();
2733 }
2734
2735 pub fn start_following(
2736 &mut self,
2737 leader_id: PeerId,
2738 cx: &mut ViewContext<Self>,
2739 ) -> Option<Task<Result<()>>> {
2740 let pane = self.active_pane().clone();
2741
2742 self.last_leaders_by_pane
2743 .insert(pane.downgrade(), leader_id);
2744 self.unfollow(&pane, cx);
2745 self.follower_states.insert(
2746 pane.clone(),
2747 FollowerState {
2748 leader_id,
2749 active_view_id: None,
2750 items_by_leader_view_id: Default::default(),
2751 },
2752 );
2753 cx.notify();
2754
2755 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2756 let project_id = self.project.read(cx).remote_id();
2757 let request = self.app_state.client.request(proto::Follow {
2758 room_id,
2759 project_id,
2760 leader_id: Some(leader_id),
2761 });
2762
2763 Some(cx.spawn(|this, mut cx| async move {
2764 let response = request.await?;
2765 this.update(&mut cx, |this, _| {
2766 let state = this
2767 .follower_states
2768 .get_mut(&pane)
2769 .ok_or_else(|| anyhow!("following interrupted"))?;
2770 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2771 Some(ViewId::from_proto(active_view_id)?)
2772 } else {
2773 None
2774 };
2775 Ok::<_, anyhow::Error>(())
2776 })??;
2777 if let Some(view) = response.active_view {
2778 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
2779 .await?;
2780 }
2781 Self::add_views_from_leader(
2782 this.clone(),
2783 leader_id,
2784 vec![pane],
2785 response.views,
2786 &mut cx,
2787 )
2788 .await?;
2789 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2790 Ok(())
2791 }))
2792 }
2793
2794 pub fn follow_next_collaborator(
2795 &mut self,
2796 _: &FollowNextCollaborator,
2797 cx: &mut ViewContext<Self>,
2798 ) {
2799 let collaborators = self.project.read(cx).collaborators();
2800 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2801 let mut collaborators = collaborators.keys().copied();
2802 for peer_id in collaborators.by_ref() {
2803 if peer_id == leader_id {
2804 break;
2805 }
2806 }
2807 collaborators.next()
2808 } else if let Some(last_leader_id) =
2809 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2810 {
2811 if collaborators.contains_key(last_leader_id) {
2812 Some(*last_leader_id)
2813 } else {
2814 None
2815 }
2816 } else {
2817 None
2818 };
2819
2820 let pane = self.active_pane.clone();
2821 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2822 else {
2823 return;
2824 };
2825 if Some(leader_id) == self.unfollow(&pane, cx) {
2826 return;
2827 }
2828 if let Some(task) = self.start_following(leader_id, cx) {
2829 task.detach_and_log_err(cx)
2830 }
2831 }
2832
2833 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2834 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2835 return;
2836 };
2837 let room = room.read(cx);
2838 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2839 return;
2840 };
2841
2842 let project = self.project.read(cx);
2843
2844 let other_project_id = match remote_participant.location {
2845 call::ParticipantLocation::External => None,
2846 call::ParticipantLocation::UnsharedProject => None,
2847 call::ParticipantLocation::SharedProject { project_id } => {
2848 if Some(project_id) == project.remote_id() {
2849 None
2850 } else {
2851 Some(project_id)
2852 }
2853 }
2854 };
2855
2856 // if they are active in another project, follow there.
2857 if let Some(project_id) = other_project_id {
2858 let app_state = self.app_state.clone();
2859 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
2860 .detach_and_log_err(cx);
2861 }
2862
2863 // if you're already following, find the right pane and focus it.
2864 for (pane, state) in &self.follower_states {
2865 if leader_id == state.leader_id {
2866 cx.focus_view(pane);
2867 return;
2868 }
2869 }
2870
2871 // Otherwise, follow.
2872 if let Some(task) = self.start_following(leader_id, cx) {
2873 task.detach_and_log_err(cx)
2874 }
2875 }
2876
2877 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2878 let state = self.follower_states.remove(pane)?;
2879 let leader_id = state.leader_id;
2880 for (_, item) in state.items_by_leader_view_id {
2881 item.set_leader_peer_id(None, cx);
2882 }
2883
2884 if self
2885 .follower_states
2886 .values()
2887 .all(|state| state.leader_id != leader_id)
2888 {
2889 let project_id = self.project.read(cx).remote_id();
2890 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2891 self.app_state
2892 .client
2893 .send(proto::Unfollow {
2894 room_id,
2895 project_id,
2896 leader_id: Some(leader_id),
2897 })
2898 .log_err();
2899 }
2900
2901 cx.notify();
2902 Some(leader_id)
2903 }
2904
2905 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2906 self.follower_states
2907 .values()
2908 .any(|state| state.leader_id == peer_id)
2909 }
2910
2911 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2912 cx.emit(Event::ActiveItemChanged);
2913 let active_entry = self.active_project_path(cx);
2914 self.project
2915 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2916
2917 self.update_window_title(cx);
2918 }
2919
2920 fn update_window_title(&mut self, cx: &mut WindowContext) {
2921 let project = self.project().read(cx);
2922 let mut title = String::new();
2923
2924 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2925 let filename = path
2926 .path
2927 .file_name()
2928 .map(|s| s.to_string_lossy())
2929 .or_else(|| {
2930 Some(Cow::Borrowed(
2931 project
2932 .worktree_for_id(path.worktree_id, cx)?
2933 .read(cx)
2934 .root_name(),
2935 ))
2936 });
2937
2938 if let Some(filename) = filename {
2939 title.push_str(filename.as_ref());
2940 title.push_str(" — ");
2941 }
2942 }
2943
2944 for (i, name) in project.worktree_root_names(cx).enumerate() {
2945 if i > 0 {
2946 title.push_str(", ");
2947 }
2948 title.push_str(name);
2949 }
2950
2951 if title.is_empty() {
2952 title = "empty project".to_string();
2953 }
2954
2955 if project.is_remote() {
2956 title.push_str(" ↙");
2957 } else if project.is_shared() {
2958 title.push_str(" ↗");
2959 }
2960
2961 cx.set_window_title(&title);
2962 }
2963
2964 fn update_window_edited(&mut self, cx: &mut WindowContext) {
2965 let is_edited = !self.project.read(cx).is_disconnected()
2966 && self
2967 .items(cx)
2968 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2969 if is_edited != self.window_edited {
2970 self.window_edited = is_edited;
2971 cx.set_window_edited(self.window_edited)
2972 }
2973 }
2974
2975 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2976 if self.notifications.is_empty() {
2977 None
2978 } else {
2979 Some(
2980 div()
2981 .absolute()
2982 .right_3()
2983 .bottom_3()
2984 .w_112()
2985 .h_full()
2986 .flex()
2987 .flex_col()
2988 .justify_end()
2989 .gap_2()
2990 .children(
2991 self.notifications
2992 .iter()
2993 .map(|(_, notification)| notification.to_any()),
2994 ),
2995 )
2996 }
2997 }
2998
2999 // RPC handlers
3000
3001 fn active_view_for_follower(
3002 &self,
3003 follower_project_id: Option<u64>,
3004 cx: &mut ViewContext<Self>,
3005 ) -> Option<proto::View> {
3006 let item = self.active_item(cx)?;
3007 let leader_id = self
3008 .pane_for(&*item)
3009 .and_then(|pane| self.leader_for_pane(&pane));
3010
3011 let item_handle = item.to_followable_item_handle(cx)?;
3012 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3013 let variant = item_handle.to_state_proto(cx)?;
3014
3015 if item_handle.is_project_item(cx)
3016 && (follower_project_id.is_none()
3017 || follower_project_id != self.project.read(cx).remote_id())
3018 {
3019 return None;
3020 }
3021
3022 Some(proto::View {
3023 id: Some(id.to_proto()),
3024 leader_id,
3025 variant: Some(variant),
3026 })
3027 }
3028
3029 fn handle_follow(
3030 &mut self,
3031 follower_project_id: Option<u64>,
3032 cx: &mut ViewContext<Self>,
3033 ) -> proto::FollowResponse {
3034 let client = &self.app_state.client;
3035 let project_id = self.project.read(cx).remote_id();
3036
3037 let active_view = self.active_view_for_follower(follower_project_id, cx);
3038 let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
3039
3040 cx.notify();
3041
3042 proto::FollowResponse {
3043 active_view,
3044 // TODO: once v0.124.0 is retired we can stop sending these
3045 active_view_id,
3046 views: self
3047 .panes()
3048 .iter()
3049 .flat_map(|pane| {
3050 let leader_id = self.leader_for_pane(pane);
3051 pane.read(cx).items().filter_map({
3052 let cx = &cx;
3053 move |item| {
3054 let item = item.to_followable_item_handle(cx)?;
3055
3056 // If the item belongs to a particular project, then it should
3057 // only be included if this project is shared, and the follower
3058 // is in the project.
3059 //
3060 // Some items, like channel notes, do not belong to a particular
3061 // project, so they should be included regardless of whether the
3062 // current project is shared, or what project the follower is in.
3063 if item.is_project_item(cx)
3064 && (project_id.is_none() || project_id != follower_project_id)
3065 {
3066 return None;
3067 }
3068
3069 let id = item.remote_id(client, cx)?.to_proto();
3070 let variant = item.to_state_proto(cx)?;
3071 Some(proto::View {
3072 id: Some(id),
3073 leader_id,
3074 variant: Some(variant),
3075 })
3076 }
3077 })
3078 })
3079 .collect(),
3080 }
3081 }
3082
3083 fn handle_update_followers(
3084 &mut self,
3085 leader_id: PeerId,
3086 message: proto::UpdateFollowers,
3087 _cx: &mut ViewContext<Self>,
3088 ) {
3089 self.leader_updates_tx
3090 .unbounded_send((leader_id, message))
3091 .ok();
3092 }
3093
3094 async fn process_leader_update(
3095 this: &WeakView<Self>,
3096 leader_id: PeerId,
3097 update: proto::UpdateFollowers,
3098 cx: &mut AsyncWindowContext,
3099 ) -> Result<()> {
3100 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3101 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3102 let panes_missing_view = this.update(cx, |this, _| {
3103 let mut panes = vec![];
3104 for (pane, state) in &mut this.follower_states {
3105 if state.leader_id != leader_id {
3106 continue;
3107 }
3108
3109 state.active_view_id =
3110 if let Some(active_view_id) = update_active_view.id.clone() {
3111 Some(ViewId::from_proto(active_view_id)?)
3112 } else {
3113 None
3114 };
3115
3116 if state.active_view_id.is_some_and(|view_id| {
3117 !state.items_by_leader_view_id.contains_key(&view_id)
3118 }) {
3119 panes.push(pane.clone())
3120 }
3121 }
3122 anyhow::Ok(panes)
3123 })??;
3124
3125 if let Some(view) = update_active_view.view {
3126 for pane in panes_missing_view {
3127 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
3128 .await?
3129 }
3130 }
3131 }
3132 proto::update_followers::Variant::UpdateView(update_view) => {
3133 let variant = update_view
3134 .variant
3135 .ok_or_else(|| anyhow!("missing update view variant"))?;
3136 let id = update_view
3137 .id
3138 .ok_or_else(|| anyhow!("missing update view id"))?;
3139 let mut tasks = Vec::new();
3140 this.update(cx, |this, cx| {
3141 let project = this.project.clone();
3142 for (_, state) in &mut this.follower_states {
3143 if state.leader_id == leader_id {
3144 let view_id = ViewId::from_proto(id.clone())?;
3145 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3146 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
3147 }
3148 }
3149 }
3150 anyhow::Ok(())
3151 })??;
3152 try_join_all(tasks).await.log_err();
3153 }
3154 proto::update_followers::Variant::CreateView(view) => {
3155 let panes = this.update(cx, |this, _| {
3156 this.follower_states
3157 .iter()
3158 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
3159 .cloned()
3160 .collect()
3161 })?;
3162 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
3163 }
3164 }
3165 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3166 Ok(())
3167 }
3168
3169 async fn add_view_from_leader(
3170 this: WeakView<Self>,
3171 leader_id: PeerId,
3172 pane: View<Pane>,
3173 view: &proto::View,
3174 cx: &mut AsyncWindowContext,
3175 ) -> Result<()> {
3176 let this = this.upgrade().context("workspace dropped")?;
3177
3178 let item_builders = cx.update(|cx| {
3179 cx.default_global::<FollowableItemBuilders>()
3180 .values()
3181 .map(|b| b.0)
3182 .collect::<Vec<_>>()
3183 })?;
3184
3185 let Some(id) = view.id.clone() else {
3186 return Err(anyhow!("no id for view"));
3187 };
3188 let id = ViewId::from_proto(id)?;
3189
3190 let mut variant = view.variant.clone();
3191 if variant.is_none() {
3192 Err(anyhow!("missing view variant"))?;
3193 }
3194
3195 let task = item_builders.iter().find_map(|build_item| {
3196 cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
3197 .log_err()
3198 .flatten()
3199 });
3200 let Some(task) = task else {
3201 return Err(anyhow!(
3202 "failed to construct view from leader (maybe from a different version of zed?)"
3203 ));
3204 };
3205
3206 let item = task.await?;
3207
3208 this.update(cx, |this, cx| {
3209 let state = this.follower_states.get_mut(&pane)?;
3210 item.set_leader_peer_id(Some(leader_id), cx);
3211 state.items_by_leader_view_id.insert(id, item);
3212
3213 Some(())
3214 })?;
3215
3216 Ok(())
3217 }
3218
3219 async fn add_views_from_leader(
3220 this: WeakView<Self>,
3221 leader_id: PeerId,
3222 panes: Vec<View<Pane>>,
3223 views: Vec<proto::View>,
3224 cx: &mut AsyncWindowContext,
3225 ) -> Result<()> {
3226 let this = this.upgrade().context("workspace dropped")?;
3227
3228 let item_builders = cx.update(|cx| {
3229 cx.default_global::<FollowableItemBuilders>()
3230 .values()
3231 .map(|b| b.0)
3232 .collect::<Vec<_>>()
3233 })?;
3234
3235 let mut item_tasks_by_pane = HashMap::default();
3236 for pane in panes {
3237 let mut item_tasks = Vec::new();
3238 let mut leader_view_ids = Vec::new();
3239 for view in &views {
3240 let Some(id) = &view.id else {
3241 continue;
3242 };
3243 let id = ViewId::from_proto(id.clone())?;
3244 let mut variant = view.variant.clone();
3245 if variant.is_none() {
3246 Err(anyhow!("missing view variant"))?;
3247 }
3248 for build_item in &item_builders {
3249 let task = cx.update(|cx| {
3250 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3251 })?;
3252 if let Some(task) = task {
3253 item_tasks.push(task);
3254 leader_view_ids.push(id);
3255 break;
3256 } else if variant.is_none() {
3257 Err(anyhow!(
3258 "failed to construct view from leader (maybe from a different version of zed?)"
3259 ))?;
3260 }
3261 }
3262 }
3263
3264 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3265 }
3266
3267 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3268 let items = futures::future::try_join_all(item_tasks).await?;
3269 this.update(cx, |this, cx| {
3270 let state = this.follower_states.get_mut(&pane)?;
3271 for (id, item) in leader_view_ids.into_iter().zip(items) {
3272 item.set_leader_peer_id(Some(leader_id), cx);
3273 state.items_by_leader_view_id.insert(id, item);
3274 }
3275
3276 Some(())
3277 })?;
3278 }
3279 Ok(())
3280 }
3281
3282 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3283 let mut is_project_item = true;
3284 let mut update = proto::UpdateActiveView::default();
3285 if cx.is_window_active() {
3286 if let Some(item) = self.active_item(cx) {
3287 if item.focus_handle(cx).contains_focused(cx) {
3288 let leader_id = self
3289 .pane_for(&*item)
3290 .and_then(|pane| self.leader_for_pane(&pane));
3291
3292 if let Some(item) = item.to_followable_item_handle(cx) {
3293 let id = item
3294 .remote_id(&self.app_state.client, cx)
3295 .map(|id| id.to_proto());
3296
3297 if let Some(id) = id.clone() {
3298 if let Some(variant) = item.to_state_proto(cx) {
3299 let view = Some(proto::View {
3300 id: Some(id.clone()),
3301 leader_id,
3302 variant: Some(variant),
3303 });
3304
3305 is_project_item = item.is_project_item(cx);
3306 update = proto::UpdateActiveView {
3307 view,
3308 // TODO: once v0.124.0 is retired we can stop sending these
3309 id: Some(id),
3310 leader_id,
3311 };
3312 }
3313 };
3314 }
3315 }
3316 }
3317 }
3318
3319 if &update.id != &self.last_active_view_id {
3320 self.last_active_view_id.clone_from(&update.id);
3321 self.update_followers(
3322 is_project_item,
3323 proto::update_followers::Variant::UpdateActiveView(update),
3324 cx,
3325 );
3326 }
3327 }
3328
3329 fn update_followers(
3330 &self,
3331 project_only: bool,
3332 update: proto::update_followers::Variant,
3333 cx: &mut WindowContext,
3334 ) -> Option<()> {
3335 // If this update only applies to for followers in the current project,
3336 // then skip it unless this project is shared. If it applies to all
3337 // followers, regardless of project, then set `project_id` to none,
3338 // indicating that it goes to all followers.
3339 let project_id = if project_only {
3340 Some(self.project.read(cx).remote_id()?)
3341 } else {
3342 None
3343 };
3344 self.app_state().workspace_store.update(cx, |store, cx| {
3345 store.update_followers(project_id, update, cx)
3346 })
3347 }
3348
3349 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3350 self.follower_states.get(pane).map(|state| state.leader_id)
3351 }
3352
3353 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3354 cx.notify();
3355
3356 let call = self.active_call()?;
3357 let room = call.read(cx).room()?.read(cx);
3358 let participant = room.remote_participant_for_peer_id(leader_id)?;
3359 let mut items_to_activate = Vec::new();
3360
3361 let leader_in_this_app;
3362 let leader_in_this_project;
3363 match participant.location {
3364 call::ParticipantLocation::SharedProject { project_id } => {
3365 leader_in_this_app = true;
3366 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3367 }
3368 call::ParticipantLocation::UnsharedProject => {
3369 leader_in_this_app = true;
3370 leader_in_this_project = false;
3371 }
3372 call::ParticipantLocation::External => {
3373 leader_in_this_app = false;
3374 leader_in_this_project = false;
3375 }
3376 };
3377
3378 for (pane, state) in &self.follower_states {
3379 if state.leader_id != leader_id {
3380 continue;
3381 }
3382 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3383 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3384 if leader_in_this_project || !item.is_project_item(cx) {
3385 items_to_activate.push((pane.clone(), item.boxed_clone()));
3386 }
3387 }
3388 continue;
3389 }
3390
3391 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3392 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3393 }
3394 }
3395
3396 for (pane, item) in items_to_activate {
3397 let pane_was_focused = pane.read(cx).has_focus(cx);
3398 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3399 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3400 } else {
3401 pane.update(cx, |pane, cx| {
3402 pane.add_item(item.boxed_clone(), false, false, None, cx)
3403 });
3404 }
3405
3406 if pane_was_focused {
3407 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3408 }
3409 }
3410
3411 None
3412 }
3413
3414 fn shared_screen_for_peer(
3415 &self,
3416 peer_id: PeerId,
3417 pane: &View<Pane>,
3418 cx: &mut WindowContext,
3419 ) -> Option<View<SharedScreen>> {
3420 let call = self.active_call()?;
3421 let room = call.read(cx).room()?.read(cx);
3422 let participant = room.remote_participant_for_peer_id(peer_id)?;
3423 let track = participant.video_tracks.values().next()?.clone();
3424 let user = participant.user.clone();
3425
3426 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3427 if item.read(cx).peer_id == peer_id {
3428 return Some(item);
3429 }
3430 }
3431
3432 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3433 }
3434
3435 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3436 if cx.is_window_active() {
3437 self.update_active_view_for_followers(cx);
3438 cx.background_executor()
3439 .spawn(persistence::DB.update_timestamp(self.database_id()))
3440 .detach();
3441 } else {
3442 for pane in &self.panes {
3443 pane.update(cx, |pane, cx| {
3444 if let Some(item) = pane.active_item() {
3445 item.workspace_deactivated(cx);
3446 }
3447 if matches!(
3448 WorkspaceSettings::get_global(cx).autosave,
3449 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3450 ) {
3451 for item in pane.items() {
3452 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3453 .detach_and_log_err(cx);
3454 }
3455 }
3456 });
3457 }
3458 }
3459 }
3460
3461 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3462 self.active_call.as_ref().map(|(call, _)| call)
3463 }
3464
3465 fn on_active_call_event(
3466 &mut self,
3467 _: Model<ActiveCall>,
3468 event: &call::room::Event,
3469 cx: &mut ViewContext<Self>,
3470 ) {
3471 match event {
3472 call::room::Event::ParticipantLocationChanged { participant_id }
3473 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3474 self.leader_updated(*participant_id, cx);
3475 }
3476 _ => {}
3477 }
3478 }
3479
3480 pub fn database_id(&self) -> WorkspaceId {
3481 self.database_id
3482 }
3483
3484 fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
3485 let project = self.project().read(cx);
3486
3487 if project.is_local() {
3488 Some(LocalPaths::new(
3489 project
3490 .visible_worktrees(cx)
3491 .map(|worktree| worktree.read(cx).abs_path())
3492 .collect::<Vec<_>>(),
3493 ))
3494 } else {
3495 None
3496 }
3497 }
3498
3499 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3500 match member {
3501 Member::Axis(PaneAxis { members, .. }) => {
3502 for child in members.iter() {
3503 self.remove_panes(child.clone(), cx)
3504 }
3505 }
3506 Member::Pane(pane) => {
3507 self.force_remove_pane(&pane, cx);
3508 }
3509 }
3510 }
3511
3512 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3513 self.panes.retain(|p| p != pane);
3514 self.panes
3515 .last()
3516 .unwrap()
3517 .update(cx, |pane, cx| pane.focus(cx));
3518 if self.last_active_center_pane == Some(pane.downgrade()) {
3519 self.last_active_center_pane = None;
3520 }
3521 cx.notify();
3522 }
3523
3524 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
3525 if self._schedule_serialize.is_none() {
3526 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3527 cx.background_executor()
3528 .timer(Duration::from_millis(100))
3529 .await;
3530 this.update(&mut cx, |this, cx| {
3531 this.serialize_workspace_internal(cx).detach();
3532 this._schedule_serialize.take();
3533 })
3534 .log_err();
3535 }));
3536 }
3537 }
3538
3539 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
3540 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3541 let (items, active) = {
3542 let pane = pane_handle.read(cx);
3543 let active_item_id = pane.active_item().map(|item| item.item_id());
3544 (
3545 pane.items()
3546 .filter_map(|item_handle| {
3547 Some(SerializedItem {
3548 kind: Arc::from(item_handle.serialized_item_kind()?),
3549 item_id: item_handle.item_id().as_u64(),
3550 active: Some(item_handle.item_id()) == active_item_id,
3551 preview: pane.is_active_preview_item(item_handle.item_id()),
3552 })
3553 })
3554 .collect::<Vec<_>>(),
3555 pane.has_focus(cx),
3556 )
3557 };
3558
3559 SerializedPane::new(items, active)
3560 }
3561
3562 fn build_serialized_pane_group(
3563 pane_group: &Member,
3564 cx: &WindowContext,
3565 ) -> SerializedPaneGroup {
3566 match pane_group {
3567 Member::Axis(PaneAxis {
3568 axis,
3569 members,
3570 flexes,
3571 bounding_boxes: _,
3572 }) => SerializedPaneGroup::Group {
3573 axis: SerializedAxis(*axis),
3574 children: members
3575 .iter()
3576 .map(|member| build_serialized_pane_group(member, cx))
3577 .collect::<Vec<_>>(),
3578 flexes: Some(flexes.lock().clone()),
3579 },
3580 Member::Pane(pane_handle) => {
3581 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3582 }
3583 }
3584 }
3585
3586 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3587 let left_dock = this.left_dock.read(cx);
3588 let left_visible = left_dock.is_open();
3589 let left_active_panel = left_dock
3590 .visible_panel()
3591 .map(|panel| panel.persistent_name().to_string());
3592 let left_dock_zoom = left_dock
3593 .visible_panel()
3594 .map(|panel| panel.is_zoomed(cx))
3595 .unwrap_or(false);
3596
3597 let right_dock = this.right_dock.read(cx);
3598 let right_visible = right_dock.is_open();
3599 let right_active_panel = right_dock
3600 .visible_panel()
3601 .map(|panel| panel.persistent_name().to_string());
3602 let right_dock_zoom = right_dock
3603 .visible_panel()
3604 .map(|panel| panel.is_zoomed(cx))
3605 .unwrap_or(false);
3606
3607 let bottom_dock = this.bottom_dock.read(cx);
3608 let bottom_visible = bottom_dock.is_open();
3609 let bottom_active_panel = bottom_dock
3610 .visible_panel()
3611 .map(|panel| panel.persistent_name().to_string());
3612 let bottom_dock_zoom = bottom_dock
3613 .visible_panel()
3614 .map(|panel| panel.is_zoomed(cx))
3615 .unwrap_or(false);
3616
3617 DockStructure {
3618 left: DockData {
3619 visible: left_visible,
3620 active_panel: left_active_panel,
3621 zoom: left_dock_zoom,
3622 },
3623 right: DockData {
3624 visible: right_visible,
3625 active_panel: right_active_panel,
3626 zoom: right_dock_zoom,
3627 },
3628 bottom: DockData {
3629 visible: bottom_visible,
3630 active_panel: bottom_active_panel,
3631 zoom: bottom_dock_zoom,
3632 },
3633 }
3634 }
3635
3636 let location = if let Some(local_paths) = self.local_paths(cx) {
3637 if !local_paths.paths().is_empty() {
3638 Some(SerializedWorkspaceLocation::Local(local_paths))
3639 } else {
3640 None
3641 }
3642 } else if let Some(dev_server_project_id) = self.project().read(cx).dev_server_project_id()
3643 {
3644 let store = dev_server_projects::Store::global(cx).read(cx);
3645 maybe!({
3646 let project = store.dev_server_project(dev_server_project_id)?;
3647 let dev_server = store.dev_server(project.dev_server_id)?;
3648
3649 let dev_server_project = SerializedDevServerProject {
3650 id: dev_server_project_id,
3651 dev_server_name: dev_server.name.to_string(),
3652 path: project.path.to_string(),
3653 };
3654 Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
3655 })
3656 } else {
3657 None
3658 };
3659
3660 // don't save workspace state for the empty workspace.
3661 if let Some(location) = location {
3662 let center_group = build_serialized_pane_group(&self.center.root, cx);
3663 let docks = build_serialized_docks(self, cx);
3664 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
3665 let serialized_workspace = SerializedWorkspace {
3666 id: self.database_id,
3667 location,
3668 center_group,
3669 window_bounds,
3670 display: Default::default(),
3671 docks,
3672 centered_layout: self.centered_layout,
3673 };
3674 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3675 }
3676 Task::ready(())
3677 }
3678
3679 pub(crate) fn load_workspace(
3680 serialized_workspace: SerializedWorkspace,
3681 paths_to_open: Vec<Option<ProjectPath>>,
3682 cx: &mut ViewContext<Workspace>,
3683 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3684 cx.spawn(|workspace, mut cx| async move {
3685 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3686
3687 let mut center_group = None;
3688 let mut center_items = None;
3689
3690 // Traverse the splits tree and add to things
3691 if let Some((group, active_pane, items)) = serialized_workspace
3692 .center_group
3693 .deserialize(
3694 &project,
3695 serialized_workspace.id,
3696 workspace.clone(),
3697 &mut cx,
3698 )
3699 .await
3700 {
3701 center_items = Some(items);
3702 center_group = Some((group, active_pane))
3703 }
3704
3705 let mut items_by_project_path = cx.update(|cx| {
3706 center_items
3707 .unwrap_or_default()
3708 .into_iter()
3709 .filter_map(|item| {
3710 let item = item?;
3711 let project_path = item.project_path(cx)?;
3712 Some((project_path, item))
3713 })
3714 .collect::<HashMap<_, _>>()
3715 })?;
3716
3717 let opened_items = paths_to_open
3718 .into_iter()
3719 .map(|path_to_open| {
3720 path_to_open
3721 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3722 })
3723 .collect::<Vec<_>>();
3724
3725 // Remove old panes from workspace panes list
3726 workspace.update(&mut cx, |workspace, cx| {
3727 if let Some((center_group, active_pane)) = center_group {
3728 workspace.remove_panes(workspace.center.root.clone(), cx);
3729
3730 // Swap workspace center group
3731 workspace.center = PaneGroup::with_root(center_group);
3732 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3733 if let Some(active_pane) = active_pane {
3734 workspace.active_pane = active_pane;
3735 cx.focus_self();
3736 } else {
3737 workspace.active_pane = workspace.center.first_pane().clone();
3738 }
3739 }
3740
3741 let docks = serialized_workspace.docks;
3742
3743 let right = docks.right.clone();
3744 workspace
3745 .right_dock
3746 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3747 let left = docks.left.clone();
3748 workspace
3749 .left_dock
3750 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3751 let bottom = docks.bottom.clone();
3752 workspace
3753 .bottom_dock
3754 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3755
3756 cx.notify();
3757 })?;
3758
3759 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3760 workspace
3761 .update(&mut cx, |workspace, cx| {
3762 workspace.serialize_workspace_internal(cx).detach();
3763 })
3764 .ok();
3765
3766 Ok(opened_items)
3767 })
3768 }
3769
3770 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3771 self.add_workspace_actions_listeners(div, cx)
3772 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3773 .on_action(cx.listener(Self::close_all_items_and_panes))
3774 .on_action(cx.listener(Self::save_all))
3775 .on_action(cx.listener(Self::send_keystrokes))
3776 .on_action(cx.listener(Self::add_folder_to_project))
3777 .on_action(cx.listener(Self::follow_next_collaborator))
3778 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3779 let pane = workspace.active_pane().clone();
3780 workspace.unfollow(&pane, cx);
3781 }))
3782 .on_action(cx.listener(|workspace, action: &Save, cx| {
3783 workspace
3784 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3785 .detach_and_log_err(cx);
3786 }))
3787 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
3788 workspace
3789 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
3790 .detach_and_log_err(cx);
3791 }))
3792 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3793 workspace
3794 .save_active_item(SaveIntent::SaveAs, cx)
3795 .detach_and_log_err(cx);
3796 }))
3797 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3798 workspace.activate_previous_pane(cx)
3799 }))
3800 .on_action(
3801 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3802 )
3803 .on_action(
3804 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3805 workspace.activate_pane_in_direction(action.0, cx)
3806 }),
3807 )
3808 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3809 workspace.swap_pane_in_direction(action.0, cx)
3810 }))
3811 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3812 this.toggle_dock(DockPosition::Left, cx);
3813 }))
3814 .on_action(
3815 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3816 workspace.toggle_dock(DockPosition::Right, cx);
3817 }),
3818 )
3819 .on_action(
3820 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3821 workspace.toggle_dock(DockPosition::Bottom, cx);
3822 }),
3823 )
3824 .on_action(
3825 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3826 workspace.close_all_docks(cx);
3827 }),
3828 )
3829 .on_action(cx.listener(Workspace::open))
3830 .on_action(cx.listener(Workspace::close_window))
3831 .on_action(cx.listener(Workspace::activate_pane_at_index))
3832 .on_action(
3833 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3834 workspace.reopen_closed_item(cx).detach();
3835 }),
3836 )
3837 .on_action(cx.listener(Workspace::toggle_centered_layout))
3838 }
3839
3840 #[cfg(any(test, feature = "test-support"))]
3841 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3842 use node_runtime::FakeNodeRuntime;
3843
3844 let client = project.read(cx).client();
3845 let user_store = project.read(cx).user_store();
3846
3847 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3848 cx.activate_window();
3849 let app_state = Arc::new(AppState {
3850 languages: project.read(cx).languages().clone(),
3851 workspace_store,
3852 client,
3853 user_store,
3854 fs: project.read(cx).fs().clone(),
3855 build_window_options: |_, _| Default::default(),
3856 node_runtime: FakeNodeRuntime::new(),
3857 });
3858 let workspace = Self::new(Default::default(), project, app_state, cx);
3859 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3860 workspace
3861 }
3862
3863 pub fn register_action<A: Action>(
3864 &mut self,
3865 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3866 ) -> &mut Self {
3867 let callback = Arc::new(callback);
3868
3869 self.workspace_actions.push(Box::new(move |div, cx| {
3870 let callback = callback.clone();
3871 div.on_action(
3872 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3873 )
3874 }));
3875 self
3876 }
3877
3878 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3879 let mut div = div
3880 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3881 .on_action(cx.listener(Self::close_all_items_and_panes))
3882 .on_action(cx.listener(Self::add_folder_to_project))
3883 .on_action(cx.listener(Self::save_all))
3884 .on_action(cx.listener(Self::open));
3885 for action in self.workspace_actions.iter() {
3886 div = (action)(div, cx)
3887 }
3888 div
3889 }
3890
3891 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3892 self.modal_layer.read(cx).has_active_modal()
3893 }
3894
3895 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3896 self.modal_layer.read(cx).active_modal()
3897 }
3898
3899 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3900 where
3901 B: FnOnce(&mut ViewContext<V>) -> V,
3902 {
3903 self.modal_layer
3904 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3905 }
3906
3907 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
3908 self.centered_layout = !self.centered_layout;
3909 cx.background_executor()
3910 .spawn(DB.set_centered_layout(self.database_id, self.centered_layout))
3911 .detach_and_log_err(cx);
3912 cx.notify();
3913 }
3914
3915 fn adjust_padding(padding: Option<f32>) -> f32 {
3916 padding
3917 .unwrap_or(Self::DEFAULT_PADDING)
3918 .min(Self::MAX_PADDING)
3919 .max(0.0)
3920 }
3921}
3922
3923fn window_bounds_env_override() -> Option<Bounds<DevicePixels>> {
3924 ZED_WINDOW_POSITION
3925 .zip(*ZED_WINDOW_SIZE)
3926 .map(|(position, size)| Bounds {
3927 origin: position,
3928 size,
3929 })
3930}
3931
3932fn open_items(
3933 serialized_workspace: Option<SerializedWorkspace>,
3934 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3935 app_state: Arc<AppState>,
3936 cx: &mut ViewContext<Workspace>,
3937) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3938 let restored_items = serialized_workspace.map(|serialized_workspace| {
3939 Workspace::load_workspace(
3940 serialized_workspace,
3941 project_paths_to_open
3942 .iter()
3943 .map(|(_, project_path)| project_path)
3944 .cloned()
3945 .collect(),
3946 cx,
3947 )
3948 });
3949
3950 cx.spawn(|workspace, mut cx| async move {
3951 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3952
3953 if let Some(restored_items) = restored_items {
3954 let restored_items = restored_items.await?;
3955
3956 let restored_project_paths = restored_items
3957 .iter()
3958 .filter_map(|item| {
3959 cx.update(|cx| item.as_ref()?.project_path(cx))
3960 .ok()
3961 .flatten()
3962 })
3963 .collect::<HashSet<_>>();
3964
3965 for restored_item in restored_items {
3966 opened_items.push(restored_item.map(Ok));
3967 }
3968
3969 project_paths_to_open
3970 .iter_mut()
3971 .for_each(|(_, project_path)| {
3972 if let Some(project_path_to_open) = project_path {
3973 if restored_project_paths.contains(project_path_to_open) {
3974 *project_path = None;
3975 }
3976 }
3977 });
3978 } else {
3979 for _ in 0..project_paths_to_open.len() {
3980 opened_items.push(None);
3981 }
3982 }
3983 assert!(opened_items.len() == project_paths_to_open.len());
3984
3985 let tasks =
3986 project_paths_to_open
3987 .into_iter()
3988 .enumerate()
3989 .map(|(ix, (abs_path, project_path))| {
3990 let workspace = workspace.clone();
3991 cx.spawn(|mut cx| {
3992 let fs = app_state.fs.clone();
3993 async move {
3994 let file_project_path = project_path?;
3995 if fs.is_dir(&abs_path).await {
3996 None
3997 } else {
3998 Some((
3999 ix,
4000 workspace
4001 .update(&mut cx, |workspace, cx| {
4002 workspace.open_path(file_project_path, None, true, cx)
4003 })
4004 .log_err()?
4005 .await,
4006 ))
4007 }
4008 }
4009 })
4010 });
4011
4012 let tasks = tasks.collect::<Vec<_>>();
4013
4014 let tasks = futures::future::join_all(tasks);
4015 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4016 opened_items[ix] = Some(path_open_result);
4017 }
4018
4019 Ok(opened_items)
4020 })
4021}
4022
4023enum ActivateInDirectionTarget {
4024 Pane(View<Pane>),
4025 Dock(View<Dock>),
4026}
4027
4028fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4029 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4030
4031 workspace
4032 .update(cx, |workspace, cx| {
4033 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4034 struct DatabaseFailedNotification;
4035
4036 workspace.show_notification_once(
4037 NotificationId::unique::<DatabaseFailedNotification>(),
4038 cx,
4039 |cx| {
4040 cx.new_view(|_| {
4041 MessageNotification::new("Failed to load the database file.")
4042 .with_click_message("Click to let us know about this error")
4043 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4044 })
4045 },
4046 );
4047 }
4048 })
4049 .log_err();
4050}
4051
4052impl FocusableView for Workspace {
4053 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4054 self.active_pane.focus_handle(cx)
4055 }
4056}
4057
4058#[derive(Clone, Render)]
4059struct DraggedDock(DockPosition);
4060
4061impl Render for Workspace {
4062 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4063 let mut context = KeyContext::new_with_defaults();
4064 context.add("Workspace");
4065 let centered_layout = self.centered_layout
4066 && self.center.panes().len() == 1
4067 && self.active_item(cx).is_some();
4068 let render_padding = |size| {
4069 (size > 0.0).then(|| {
4070 div()
4071 .h_full()
4072 .w(relative(size))
4073 .bg(cx.theme().colors().editor_background)
4074 .border_color(cx.theme().colors().pane_group_border)
4075 })
4076 };
4077 let paddings = if centered_layout {
4078 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4079 (
4080 render_padding(Self::adjust_padding(settings.left_padding)),
4081 render_padding(Self::adjust_padding(settings.right_padding)),
4082 )
4083 } else {
4084 (None, None)
4085 };
4086 let (ui_font, ui_font_size) = {
4087 let theme_settings = ThemeSettings::get_global(cx);
4088 (
4089 theme_settings.ui_font.family.clone(),
4090 theme_settings.ui_font_size,
4091 )
4092 };
4093
4094 let theme = cx.theme().clone();
4095 let colors = theme.colors();
4096 cx.set_rem_size(ui_font_size);
4097
4098 self.actions(div(), cx)
4099 .key_context(context)
4100 .relative()
4101 .size_full()
4102 .flex()
4103 .flex_col()
4104 .font_family(ui_font)
4105 .gap_0()
4106 .justify_start()
4107 .items_start()
4108 .text_color(colors.text)
4109 .bg(colors.background)
4110 .children(self.titlebar_item.clone())
4111 .child(
4112 div()
4113 .id("workspace")
4114 .relative()
4115 .flex_1()
4116 .w_full()
4117 .flex()
4118 .flex_col()
4119 .overflow_hidden()
4120 .border_t_1()
4121 .border_b_1()
4122 .border_color(colors.border)
4123 .child({
4124 let this = cx.view().clone();
4125 canvas(
4126 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4127 |_, _, _| {},
4128 )
4129 .absolute()
4130 .size_full()
4131 })
4132 .when(self.zoomed.is_none(), |this| {
4133 this.on_drag_move(cx.listener(
4134 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4135 DockPosition::Left => {
4136 let size = workspace.bounds.left() + e.event.position.x;
4137 workspace.left_dock.update(cx, |left_dock, cx| {
4138 left_dock.resize_active_panel(Some(size), cx);
4139 });
4140 }
4141 DockPosition::Right => {
4142 let size = workspace.bounds.right() - e.event.position.x;
4143 workspace.right_dock.update(cx, |right_dock, cx| {
4144 right_dock.resize_active_panel(Some(size), cx);
4145 });
4146 }
4147 DockPosition::Bottom => {
4148 let size = workspace.bounds.bottom() - e.event.position.y;
4149 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4150 bottom_dock.resize_active_panel(Some(size), cx);
4151 });
4152 }
4153 },
4154 ))
4155 })
4156 .child(
4157 div()
4158 .flex()
4159 .flex_row()
4160 .h_full()
4161 // Left Dock
4162 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
4163 || {
4164 div()
4165 .flex()
4166 .flex_none()
4167 .overflow_hidden()
4168 .child(self.left_dock.clone())
4169 },
4170 ))
4171 // Panes
4172 .child(
4173 div()
4174 .flex()
4175 .flex_col()
4176 .flex_1()
4177 .overflow_hidden()
4178 .child(
4179 h_flex()
4180 .flex_1()
4181 .when_some(paddings.0, |this, p| {
4182 this.child(p.border_r_1())
4183 })
4184 .child(self.center.render(
4185 &self.project,
4186 &self.follower_states,
4187 self.active_call(),
4188 &self.active_pane,
4189 self.zoomed.as_ref(),
4190 &self.app_state,
4191 cx,
4192 ))
4193 .when_some(paddings.1, |this, p| {
4194 this.child(p.border_l_1())
4195 }),
4196 )
4197 .children(
4198 self.zoomed_position
4199 .ne(&Some(DockPosition::Bottom))
4200 .then(|| self.bottom_dock.clone()),
4201 ),
4202 )
4203 // Right Dock
4204 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
4205 || {
4206 div()
4207 .flex()
4208 .flex_none()
4209 .overflow_hidden()
4210 .child(self.right_dock.clone())
4211 },
4212 )),
4213 )
4214 .children(self.zoomed.as_ref().and_then(|view| {
4215 let zoomed_view = view.upgrade()?;
4216 let div = div()
4217 .occlude()
4218 .absolute()
4219 .overflow_hidden()
4220 .border_color(colors.border)
4221 .bg(colors.background)
4222 .child(zoomed_view)
4223 .inset_0()
4224 .shadow_lg();
4225
4226 Some(match self.zoomed_position {
4227 Some(DockPosition::Left) => div.right_2().border_r_1(),
4228 Some(DockPosition::Right) => div.left_2().border_l_1(),
4229 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4230 None => div.top_2().bottom_2().left_2().right_2().border_1(),
4231 })
4232 }))
4233 .child(self.modal_layer.clone())
4234 .children(self.render_notifications(cx)),
4235 )
4236 .child(self.status_bar.clone())
4237 .children(if self.project.read(cx).is_disconnected() {
4238 Some(DisconnectedOverlay)
4239 } else {
4240 None
4241 })
4242 }
4243}
4244
4245impl WorkspaceStore {
4246 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4247 Self {
4248 workspaces: Default::default(),
4249 _subscriptions: vec![
4250 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4251 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4252 ],
4253 client,
4254 }
4255 }
4256
4257 pub fn update_followers(
4258 &self,
4259 project_id: Option<u64>,
4260 update: proto::update_followers::Variant,
4261 cx: &AppContext,
4262 ) -> Option<()> {
4263 let active_call = ActiveCall::try_global(cx)?;
4264 let room_id = active_call.read(cx).room()?.read(cx).id();
4265 self.client
4266 .send(proto::UpdateFollowers {
4267 room_id,
4268 project_id,
4269 variant: Some(update),
4270 })
4271 .log_err()
4272 }
4273
4274 pub async fn handle_follow(
4275 this: Model<Self>,
4276 envelope: TypedEnvelope<proto::Follow>,
4277 _: Arc<Client>,
4278 mut cx: AsyncAppContext,
4279 ) -> Result<proto::FollowResponse> {
4280 this.update(&mut cx, |this, cx| {
4281 let follower = Follower {
4282 project_id: envelope.payload.project_id,
4283 peer_id: envelope.original_sender_id()?,
4284 };
4285
4286 let mut response = proto::FollowResponse::default();
4287 this.workspaces.retain(|workspace| {
4288 workspace
4289 .update(cx, |workspace, cx| {
4290 let handler_response = workspace.handle_follow(follower.project_id, cx);
4291 if response.views.is_empty() {
4292 response.views = handler_response.views;
4293 } else {
4294 response.views.extend_from_slice(&handler_response.views);
4295 }
4296
4297 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4298 if response.active_view_id.is_none()
4299 || workspace.project.read(cx).remote_id() == follower.project_id
4300 {
4301 response.active_view_id = Some(active_view_id);
4302 }
4303 }
4304
4305 if let Some(active_view) = handler_response.active_view.clone() {
4306 if response.active_view_id.is_none()
4307 || workspace.project.read(cx).remote_id() == follower.project_id
4308 {
4309 response.active_view = Some(active_view)
4310 }
4311 }
4312 })
4313 .is_ok()
4314 });
4315
4316 Ok(response)
4317 })?
4318 }
4319
4320 async fn handle_update_followers(
4321 this: Model<Self>,
4322 envelope: TypedEnvelope<proto::UpdateFollowers>,
4323 _: Arc<Client>,
4324 mut cx: AsyncAppContext,
4325 ) -> Result<()> {
4326 let leader_id = envelope.original_sender_id()?;
4327 let update = envelope.payload;
4328
4329 this.update(&mut cx, |this, cx| {
4330 this.workspaces.retain(|workspace| {
4331 workspace
4332 .update(cx, |workspace, cx| {
4333 let project_id = workspace.project.read(cx).remote_id();
4334 if update.project_id != project_id && update.project_id.is_some() {
4335 return;
4336 }
4337 workspace.handle_update_followers(leader_id, update.clone(), cx);
4338 })
4339 .is_ok()
4340 });
4341 Ok(())
4342 })?
4343 }
4344}
4345
4346impl ViewId {
4347 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4348 Ok(Self {
4349 creator: message
4350 .creator
4351 .ok_or_else(|| anyhow!("creator is missing"))?,
4352 id: message.id,
4353 })
4354 }
4355
4356 pub(crate) fn to_proto(&self) -> proto::ViewId {
4357 proto::ViewId {
4358 creator: Some(self.creator),
4359 id: self.id,
4360 }
4361 }
4362}
4363
4364pub trait WorkspaceHandle {
4365 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4366}
4367
4368impl WorkspaceHandle for View<Workspace> {
4369 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4370 self.read(cx)
4371 .worktrees(cx)
4372 .flat_map(|worktree| {
4373 let worktree_id = worktree.read(cx).id();
4374 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4375 worktree_id,
4376 path: f.path.clone(),
4377 })
4378 })
4379 .collect::<Vec<_>>()
4380 }
4381}
4382
4383impl std::fmt::Debug for OpenPaths {
4384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4385 f.debug_struct("OpenPaths")
4386 .field("paths", &self.paths)
4387 .finish()
4388 }
4389}
4390
4391pub fn activate_workspace_for_project(
4392 cx: &mut AppContext,
4393 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4394) -> Option<WindowHandle<Workspace>> {
4395 for window in cx.windows() {
4396 let Some(workspace) = window.downcast::<Workspace>() else {
4397 continue;
4398 };
4399
4400 let predicate = workspace
4401 .update(cx, |workspace, cx| {
4402 let project = workspace.project.read(cx);
4403 if predicate(project, cx) {
4404 cx.activate_window();
4405 true
4406 } else {
4407 false
4408 }
4409 })
4410 .log_err()
4411 .unwrap_or(false);
4412
4413 if predicate {
4414 return Some(workspace);
4415 }
4416 }
4417
4418 None
4419}
4420
4421pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
4422 DB.last_workspace().await.log_err().flatten()
4423}
4424
4425actions!(collab, [OpenChannelNotes]);
4426actions!(zed, [OpenLog]);
4427
4428async fn join_channel_internal(
4429 channel_id: ChannelId,
4430 app_state: &Arc<AppState>,
4431 requesting_window: Option<WindowHandle<Workspace>>,
4432 active_call: &Model<ActiveCall>,
4433 cx: &mut AsyncAppContext,
4434) -> Result<bool> {
4435 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4436 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4437 return (false, None);
4438 };
4439
4440 let already_in_channel = room.channel_id() == Some(channel_id);
4441 let should_prompt = room.is_sharing_project()
4442 && room.remote_participants().len() > 0
4443 && !already_in_channel;
4444 let open_room = if already_in_channel {
4445 active_call.room().cloned()
4446 } else {
4447 None
4448 };
4449 (should_prompt, open_room)
4450 })?;
4451
4452 if let Some(room) = open_room {
4453 let task = room.update(cx, |room, cx| {
4454 if let Some((project, host)) = room.most_active_project(cx) {
4455 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4456 }
4457
4458 None
4459 })?;
4460 if let Some(task) = task {
4461 task.await?;
4462 }
4463 return anyhow::Ok(true);
4464 }
4465
4466 if should_prompt {
4467 if let Some(workspace) = requesting_window {
4468 let answer = workspace
4469 .update(cx, |_, cx| {
4470 cx.prompt(
4471 PromptLevel::Warning,
4472 "Do you want to switch channels?",
4473 Some("Leaving this call will unshare your current project."),
4474 &["Yes, Join Channel", "Cancel"],
4475 )
4476 })?
4477 .await;
4478
4479 if answer == Ok(1) {
4480 return Ok(false);
4481 }
4482 } else {
4483 return Ok(false); // unreachable!() hopefully
4484 }
4485 }
4486
4487 let client = cx.update(|cx| active_call.read(cx).client())?;
4488
4489 let mut client_status = client.status();
4490
4491 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4492 'outer: loop {
4493 let Some(status) = client_status.recv().await else {
4494 return Err(anyhow!("error connecting"));
4495 };
4496
4497 match status {
4498 Status::Connecting
4499 | Status::Authenticating
4500 | Status::Reconnecting
4501 | Status::Reauthenticating => continue,
4502 Status::Connected { .. } => break 'outer,
4503 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4504 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4505 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4506 return Err(ErrorCode::Disconnected.into());
4507 }
4508 }
4509 }
4510
4511 let room = active_call
4512 .update(cx, |active_call, cx| {
4513 active_call.join_channel(channel_id, cx)
4514 })?
4515 .await?;
4516
4517 let Some(room) = room else {
4518 return anyhow::Ok(true);
4519 };
4520
4521 room.update(cx, |room, _| room.room_update_completed())?
4522 .await;
4523
4524 let task = room.update(cx, |room, cx| {
4525 if let Some((project, host)) = room.most_active_project(cx) {
4526 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4527 }
4528 // if you are the first to join a channel, share your project
4529 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4530 if let Some(workspace) = requesting_window {
4531 let project = workspace.update(cx, |workspace, cx| {
4532 if !CallSettings::get_global(cx).share_on_join {
4533 return None;
4534 }
4535 let project = workspace.project.read(cx);
4536 if (project.is_local() || project.dev_server_project_id().is_some())
4537 && project.visible_worktrees(cx).any(|tree| {
4538 tree.read(cx)
4539 .root_entry()
4540 .map_or(false, |entry| entry.is_dir())
4541 })
4542 {
4543 Some(workspace.project.clone())
4544 } else {
4545 None
4546 }
4547 });
4548 if let Ok(Some(project)) = project {
4549 return Some(cx.spawn(|room, mut cx| async move {
4550 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4551 .await?;
4552 Ok(())
4553 }));
4554 }
4555 }
4556 }
4557
4558 None
4559 })?;
4560 if let Some(task) = task {
4561 task.await?;
4562 return anyhow::Ok(true);
4563 }
4564 anyhow::Ok(false)
4565}
4566
4567pub fn join_channel(
4568 channel_id: ChannelId,
4569 app_state: Arc<AppState>,
4570 requesting_window: Option<WindowHandle<Workspace>>,
4571 cx: &mut AppContext,
4572) -> Task<Result<()>> {
4573 let active_call = ActiveCall::global(cx);
4574 cx.spawn(|mut cx| async move {
4575 let result = join_channel_internal(
4576 channel_id,
4577 &app_state,
4578 requesting_window,
4579 &active_call,
4580 &mut cx,
4581 )
4582 .await;
4583
4584 // join channel succeeded, and opened a window
4585 if matches!(result, Ok(true)) {
4586 return anyhow::Ok(());
4587 }
4588
4589 // find an existing workspace to focus and show call controls
4590 let mut active_window =
4591 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4592 if active_window.is_none() {
4593 // no open workspaces, make one to show the error in (blergh)
4594 let (window_handle, _) = cx
4595 .update(|cx| {
4596 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4597 })?
4598 .await?;
4599
4600 if result.is_ok() {
4601 cx.update(|cx| {
4602 cx.dispatch_action(&OpenChannelNotes);
4603 }).log_err();
4604 }
4605
4606 active_window = Some(window_handle);
4607 }
4608
4609 if let Err(err) = result {
4610 log::error!("failed to join channel: {}", err);
4611 if let Some(active_window) = active_window {
4612 active_window
4613 .update(&mut cx, |_, cx| {
4614 let detail: SharedString = match err.error_code() {
4615 ErrorCode::SignedOut => {
4616 "Please sign in to continue.".into()
4617 }
4618 ErrorCode::UpgradeRequired => {
4619 "Your are running an unsupported version of Zed. Please update to continue.".into()
4620 }
4621 ErrorCode::NoSuchChannel => {
4622 "No matching channel was found. Please check the link and try again.".into()
4623 }
4624 ErrorCode::Forbidden => {
4625 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4626 }
4627 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4628 _ => format!("{}\n\nPlease try again.", err).into(),
4629 };
4630 cx.prompt(
4631 PromptLevel::Critical,
4632 "Failed to join channel",
4633 Some(&detail),
4634 &["Ok"],
4635 )
4636 })?
4637 .await
4638 .ok();
4639 }
4640 }
4641
4642 // return ok, we showed the error to the user.
4643 return anyhow::Ok(());
4644 })
4645}
4646
4647pub async fn get_any_active_workspace(
4648 app_state: Arc<AppState>,
4649 mut cx: AsyncAppContext,
4650) -> anyhow::Result<WindowHandle<Workspace>> {
4651 // find an existing workspace to focus and show call controls
4652 let active_window = activate_any_workspace_window(&mut cx);
4653 if active_window.is_none() {
4654 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4655 .await?;
4656 }
4657 activate_any_workspace_window(&mut cx).context("could not open zed")
4658}
4659
4660fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4661 cx.update(|cx| {
4662 if let Some(workspace_window) = cx
4663 .active_window()
4664 .and_then(|window| window.downcast::<Workspace>())
4665 {
4666 return Some(workspace_window);
4667 }
4668
4669 for window in cx.windows() {
4670 if let Some(workspace_window) = window.downcast::<Workspace>() {
4671 workspace_window
4672 .update(cx, |_, cx| cx.activate_window())
4673 .ok();
4674 return Some(workspace_window);
4675 }
4676 }
4677 None
4678 })
4679 .ok()
4680 .flatten()
4681}
4682
4683fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
4684 cx.windows()
4685 .into_iter()
4686 .filter_map(|window| window.downcast::<Workspace>())
4687 .filter(|workspace| {
4688 workspace
4689 .read(cx)
4690 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
4691 })
4692 .collect()
4693}
4694
4695#[derive(Default)]
4696pub struct OpenOptions {
4697 pub open_new_workspace: Option<bool>,
4698 pub replace_window: Option<WindowHandle<Workspace>>,
4699}
4700
4701#[allow(clippy::type_complexity)]
4702pub fn open_paths(
4703 abs_paths: &[PathBuf],
4704 app_state: Arc<AppState>,
4705 open_options: OpenOptions,
4706 cx: &mut AppContext,
4707) -> Task<
4708 anyhow::Result<(
4709 WindowHandle<Workspace>,
4710 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4711 )>,
4712> {
4713 let abs_paths = abs_paths.to_vec();
4714 let mut existing = None;
4715 let mut best_match = None;
4716 let mut open_visible = OpenVisible::All;
4717
4718 if open_options.open_new_workspace != Some(true) {
4719 for window in local_workspace_windows(cx) {
4720 if let Ok(workspace) = window.read(cx) {
4721 let m = workspace
4722 .project
4723 .read(cx)
4724 .visibility_for_paths(&abs_paths, cx);
4725 if m > best_match {
4726 existing = Some(window);
4727 best_match = m;
4728 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
4729 existing = Some(window)
4730 }
4731 }
4732 }
4733 }
4734
4735 cx.spawn(move |mut cx| async move {
4736 if open_options.open_new_workspace.is_none() && existing.is_none() {
4737 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
4738 if futures::future::join_all(all_files)
4739 .await
4740 .into_iter()
4741 .filter_map(|result| result.ok().flatten())
4742 .all(|file| !file.is_dir)
4743 {
4744 cx.update(|cx| {
4745 for window in local_workspace_windows(cx) {
4746 if let Ok(workspace) = window.read(cx) {
4747 let project = workspace.project().read(cx);
4748 if project.is_remote() {
4749 continue;
4750 }
4751 existing = Some(window);
4752 open_visible = OpenVisible::None;
4753 break;
4754 }
4755 }
4756 })?;
4757 }
4758 }
4759
4760 if let Some(existing) = existing {
4761 Ok((
4762 existing,
4763 existing
4764 .update(&mut cx, |workspace, cx| {
4765 cx.activate_window();
4766 workspace.open_paths(abs_paths, open_visible, None, cx)
4767 })?
4768 .await,
4769 ))
4770 } else {
4771 cx.update(move |cx| {
4772 Workspace::new_local(
4773 abs_paths,
4774 app_state.clone(),
4775 open_options.replace_window,
4776 cx,
4777 )
4778 })?
4779 .await
4780 }
4781 })
4782}
4783
4784pub fn open_new(
4785 app_state: Arc<AppState>,
4786 cx: &mut AppContext,
4787 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4788) -> Task<()> {
4789 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
4790 cx.spawn(|mut cx| async move {
4791 if let Some((workspace, opened_paths)) = task.await.log_err() {
4792 workspace
4793 .update(&mut cx, |workspace, cx| {
4794 if opened_paths.is_empty() {
4795 init(workspace, cx)
4796 }
4797 })
4798 .log_err();
4799 }
4800 })
4801}
4802
4803pub fn create_and_open_local_file(
4804 path: &'static Path,
4805 cx: &mut ViewContext<Workspace>,
4806 default_content: impl 'static + Send + FnOnce() -> Rope,
4807) -> Task<Result<Box<dyn ItemHandle>>> {
4808 cx.spawn(|workspace, mut cx| async move {
4809 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4810 if !fs.is_file(path).await {
4811 fs.create_file(path, Default::default()).await?;
4812 fs.save(path, &default_content(), Default::default())
4813 .await?;
4814 }
4815
4816 let mut items = workspace
4817 .update(&mut cx, |workspace, cx| {
4818 workspace.with_local_workspace(cx, |workspace, cx| {
4819 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4820 })
4821 })?
4822 .await?
4823 .await;
4824
4825 let item = items.pop().flatten();
4826 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4827 })
4828}
4829
4830pub fn join_hosted_project(
4831 hosted_project_id: ProjectId,
4832 app_state: Arc<AppState>,
4833 cx: &mut AppContext,
4834) -> Task<Result<()>> {
4835 cx.spawn(|mut cx| async move {
4836 let existing_window = cx.update(|cx| {
4837 cx.windows().into_iter().find_map(|window| {
4838 let workspace = window.downcast::<Workspace>()?;
4839 workspace
4840 .read(cx)
4841 .is_ok_and(|workspace| {
4842 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
4843 })
4844 .then(|| workspace)
4845 })
4846 })?;
4847
4848 let workspace = if let Some(existing_window) = existing_window {
4849 existing_window
4850 } else {
4851 let project = Project::hosted(
4852 hosted_project_id,
4853 app_state.user_store.clone(),
4854 app_state.client.clone(),
4855 app_state.languages.clone(),
4856 app_state.fs.clone(),
4857 cx.clone(),
4858 )
4859 .await?;
4860
4861 let window_bounds_override = window_bounds_env_override();
4862 cx.update(|cx| {
4863 let mut options = (app_state.build_window_options)(None, cx);
4864 options.window_bounds =
4865 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
4866 cx.open_window(options, |cx| {
4867 cx.new_view(|cx| {
4868 Workspace::new(Default::default(), project, app_state.clone(), cx)
4869 })
4870 })
4871 })?
4872 };
4873
4874 workspace.update(&mut cx, |_, cx| {
4875 cx.activate(true);
4876 cx.activate_window();
4877 })?;
4878
4879 Ok(())
4880 })
4881}
4882
4883pub fn join_dev_server_project(
4884 project_id: ProjectId,
4885 app_state: Arc<AppState>,
4886 window_to_replace: Option<WindowHandle<Workspace>>,
4887 cx: &mut AppContext,
4888) -> Task<Result<WindowHandle<Workspace>>> {
4889 let windows = cx.windows();
4890 cx.spawn(|mut cx| async move {
4891 let existing_workspace = windows.into_iter().find_map(|window| {
4892 window.downcast::<Workspace>().and_then(|window| {
4893 window
4894 .update(&mut cx, |workspace, cx| {
4895 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
4896 Some(window)
4897 } else {
4898 None
4899 }
4900 })
4901 .unwrap_or(None)
4902 })
4903 });
4904
4905 let workspace = if let Some(existing_workspace) = existing_workspace {
4906 existing_workspace
4907 } else {
4908 let project = Project::remote(
4909 project_id.0,
4910 app_state.client.clone(),
4911 app_state.user_store.clone(),
4912 app_state.languages.clone(),
4913 app_state.fs.clone(),
4914 cx.clone(),
4915 )
4916 .await?;
4917
4918 if let Some(window_to_replace) = window_to_replace {
4919 cx.update_window(window_to_replace.into(), |_, cx| {
4920 cx.replace_root_view(|cx| {
4921 Workspace::new(Default::default(), project, app_state.clone(), cx)
4922 });
4923 })?;
4924 window_to_replace
4925 } else {
4926 let window_bounds_override = window_bounds_env_override();
4927 cx.update(|cx| {
4928 let mut options = (app_state.build_window_options)(None, cx);
4929 options.window_bounds =
4930 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
4931 cx.open_window(options, |cx| {
4932 cx.new_view(|cx| {
4933 Workspace::new(Default::default(), project, app_state.clone(), cx)
4934 })
4935 })
4936 })?
4937 }
4938 };
4939
4940 workspace.update(&mut cx, |_, cx| {
4941 cx.activate(true);
4942 cx.activate_window();
4943 })?;
4944
4945 anyhow::Ok(workspace)
4946 })
4947}
4948
4949pub fn join_in_room_project(
4950 project_id: u64,
4951 follow_user_id: u64,
4952 app_state: Arc<AppState>,
4953 cx: &mut AppContext,
4954) -> Task<Result<()>> {
4955 let windows = cx.windows();
4956 cx.spawn(|mut cx| async move {
4957 let existing_workspace = windows.into_iter().find_map(|window| {
4958 window.downcast::<Workspace>().and_then(|window| {
4959 window
4960 .update(&mut cx, |workspace, cx| {
4961 if workspace.project().read(cx).remote_id() == Some(project_id) {
4962 Some(window)
4963 } else {
4964 None
4965 }
4966 })
4967 .unwrap_or(None)
4968 })
4969 });
4970
4971 let workspace = if let Some(existing_workspace) = existing_workspace {
4972 existing_workspace
4973 } else {
4974 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4975 let room = active_call
4976 .read_with(&cx, |call, _| call.room().cloned())?
4977 .ok_or_else(|| anyhow!("not in a call"))?;
4978 let project = room
4979 .update(&mut cx, |room, cx| {
4980 room.join_project(
4981 project_id,
4982 app_state.languages.clone(),
4983 app_state.fs.clone(),
4984 cx,
4985 )
4986 })?
4987 .await?;
4988
4989 let window_bounds_override = window_bounds_env_override();
4990 cx.update(|cx| {
4991 let mut options = (app_state.build_window_options)(None, cx);
4992 options.window_bounds =
4993 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
4994 cx.open_window(options, |cx| {
4995 cx.new_view(|cx| {
4996 Workspace::new(Default::default(), project, app_state.clone(), cx)
4997 })
4998 })
4999 })?
5000 };
5001
5002 workspace.update(&mut cx, |workspace, cx| {
5003 cx.activate(true);
5004 cx.activate_window();
5005
5006 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5007 let follow_peer_id = room
5008 .read(cx)
5009 .remote_participants()
5010 .iter()
5011 .find(|(_, participant)| participant.user.id == follow_user_id)
5012 .map(|(_, p)| p.peer_id)
5013 .or_else(|| {
5014 // If we couldn't follow the given user, follow the host instead.
5015 let collaborator = workspace
5016 .project()
5017 .read(cx)
5018 .collaborators()
5019 .values()
5020 .find(|collaborator| collaborator.replica_id == 0)?;
5021 Some(collaborator.peer_id)
5022 });
5023
5024 if let Some(follow_peer_id) = follow_peer_id {
5025 workspace.follow(follow_peer_id, cx);
5026 }
5027 }
5028 })?;
5029
5030 anyhow::Ok(())
5031 })
5032}
5033
5034pub fn restart(restart: &Restart, cx: &mut AppContext) {
5035 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5036 let mut workspace_windows = cx
5037 .windows()
5038 .into_iter()
5039 .filter_map(|window| window.downcast::<Workspace>())
5040 .collect::<Vec<_>>();
5041
5042 // If multiple windows have unsaved changes, and need a save prompt,
5043 // prompt in the active window before switching to a different window.
5044 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5045
5046 let mut prompt = None;
5047 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5048 prompt = window
5049 .update(cx, |_, cx| {
5050 cx.prompt(
5051 PromptLevel::Info,
5052 "Are you sure you want to restart?",
5053 None,
5054 &["Restart", "Cancel"],
5055 )
5056 })
5057 .ok();
5058 }
5059
5060 let binary_path = restart.binary_path.clone();
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(binary_path))
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}