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