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