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 cx.on_next_frame(|_, cx| {
2964 cx.invalidate_character_coordinates();
2965 });
2966
2967 pane.model.update(cx, |pane, _| {
2968 pane.track_alternate_file_items();
2969 });
2970 if *local {
2971 self.unfollow_in_pane(&pane, cx);
2972 }
2973 if &pane == self.active_pane() {
2974 self.active_item_path_changed(cx);
2975 self.update_active_view_for_followers(cx);
2976 }
2977 }
2978 pane::Event::UserSavedItem { item, save_intent } => cx.emit(Event::UserSavedItem {
2979 pane: pane.downgrade(),
2980 item: item.boxed_clone(),
2981 save_intent: *save_intent,
2982 }),
2983 pane::Event::ChangeItemTitle => {
2984 if pane == self.active_pane {
2985 self.active_item_path_changed(cx);
2986 }
2987 self.update_window_edited(cx);
2988 }
2989 pane::Event::RemoveItem { .. } => {}
2990 pane::Event::RemovedItem { item_id } => {
2991 cx.emit(Event::ActiveItemChanged);
2992 self.update_window_edited(cx);
2993 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2994 if entry.get().entity_id() == pane.entity_id() {
2995 entry.remove();
2996 }
2997 }
2998 }
2999 pane::Event::Focus => {
3000 cx.on_next_frame(|_, cx| {
3001 cx.invalidate_character_coordinates();
3002 });
3003 self.handle_pane_focused(pane.clone(), cx);
3004 }
3005 pane::Event::ZoomIn => {
3006 if pane == self.active_pane {
3007 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3008 if pane.read(cx).has_focus(cx) {
3009 self.zoomed = Some(pane.downgrade().into());
3010 self.zoomed_position = None;
3011 cx.emit(Event::ZoomChanged);
3012 }
3013 cx.notify();
3014 }
3015 }
3016 pane::Event::ZoomOut => {
3017 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3018 if self.zoomed_position.is_none() {
3019 self.zoomed = None;
3020 cx.emit(Event::ZoomChanged);
3021 }
3022 cx.notify();
3023 }
3024 }
3025
3026 self.serialize_workspace(cx);
3027 }
3028
3029 pub fn unfollow_in_pane(
3030 &mut self,
3031 pane: &View<Pane>,
3032 cx: &mut ViewContext<Workspace>,
3033 ) -> Option<PeerId> {
3034 let leader_id = self.leader_for_pane(pane)?;
3035 self.unfollow(leader_id, cx);
3036 Some(leader_id)
3037 }
3038
3039 pub fn split_pane(
3040 &mut self,
3041 pane_to_split: View<Pane>,
3042 split_direction: SplitDirection,
3043 cx: &mut ViewContext<Self>,
3044 ) -> View<Pane> {
3045 let new_pane = self.add_pane(cx);
3046 self.center
3047 .split(&pane_to_split, &new_pane, split_direction)
3048 .unwrap();
3049 cx.notify();
3050 new_pane
3051 }
3052
3053 pub fn split_and_clone(
3054 &mut self,
3055 pane: View<Pane>,
3056 direction: SplitDirection,
3057 cx: &mut ViewContext<Self>,
3058 ) -> Option<View<Pane>> {
3059 let item = pane.read(cx).active_item()?;
3060 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3061 let new_pane = self.add_pane(cx);
3062 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3063 self.center.split(&pane, &new_pane, direction).unwrap();
3064 Some(new_pane)
3065 } else {
3066 None
3067 };
3068 cx.notify();
3069 maybe_pane_handle
3070 }
3071
3072 pub fn split_pane_with_item(
3073 &mut self,
3074 pane_to_split: WeakView<Pane>,
3075 split_direction: SplitDirection,
3076 from: WeakView<Pane>,
3077 item_id_to_move: EntityId,
3078 cx: &mut ViewContext<Self>,
3079 ) {
3080 let Some(pane_to_split) = pane_to_split.upgrade() else {
3081 return;
3082 };
3083 let Some(from) = from.upgrade() else {
3084 return;
3085 };
3086
3087 let new_pane = self.add_pane(cx);
3088 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
3089 self.center
3090 .split(&pane_to_split, &new_pane, split_direction)
3091 .unwrap();
3092 cx.notify();
3093 }
3094
3095 pub fn split_pane_with_project_entry(
3096 &mut self,
3097 pane_to_split: WeakView<Pane>,
3098 split_direction: SplitDirection,
3099 project_entry: ProjectEntryId,
3100 cx: &mut ViewContext<Self>,
3101 ) -> Option<Task<Result<()>>> {
3102 let pane_to_split = pane_to_split.upgrade()?;
3103 let new_pane = self.add_pane(cx);
3104 self.center
3105 .split(&pane_to_split, &new_pane, split_direction)
3106 .unwrap();
3107
3108 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3109 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3110 Some(cx.foreground_executor().spawn(async move {
3111 task.await?;
3112 Ok(())
3113 }))
3114 }
3115
3116 pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3117 let next_pane = self
3118 .find_pane_in_direction(SplitDirection::Right, cx)
3119 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3120 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3121 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3122 let Some(next_pane) = next_pane else {
3123 return;
3124 };
3125
3126 let item_ids: Vec<EntityId> = pane.read(cx).items().map(|item| item.item_id()).collect();
3127 for item_id in item_ids {
3128 self.move_item(pane.clone(), next_pane.clone(), item_id, 0, cx);
3129 }
3130 cx.notify();
3131 }
3132
3133 pub fn move_item(
3134 &mut self,
3135 source: View<Pane>,
3136 destination: View<Pane>,
3137 item_id_to_move: EntityId,
3138 destination_index: usize,
3139 cx: &mut ViewContext<Self>,
3140 ) {
3141 let Some((item_ix, item_handle)) = source
3142 .read(cx)
3143 .items()
3144 .enumerate()
3145 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
3146 else {
3147 // Tab was closed during drag
3148 return;
3149 };
3150
3151 let item_handle = item_handle.clone();
3152
3153 if source != destination {
3154 // Close item from previous pane
3155 source.update(cx, |source, cx| {
3156 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
3157 });
3158 }
3159
3160 // This automatically removes duplicate items in the pane
3161 destination.update(cx, |destination, cx| {
3162 destination.add_item(item_handle, true, true, Some(destination_index), cx);
3163 destination.focus(cx)
3164 });
3165 }
3166
3167 fn remove_pane(
3168 &mut self,
3169 pane: View<Pane>,
3170 focus_on: Option<View<Pane>>,
3171 cx: &mut ViewContext<Self>,
3172 ) {
3173 if self.center.remove(&pane).unwrap() {
3174 self.force_remove_pane(&pane, &focus_on, cx);
3175 self.unfollow_in_pane(&pane, cx);
3176 self.last_leaders_by_pane.remove(&pane.downgrade());
3177 for removed_item in pane.read(cx).items() {
3178 self.panes_by_item.remove(&removed_item.item_id());
3179 }
3180
3181 cx.notify();
3182 } else {
3183 self.active_item_path_changed(cx);
3184 }
3185 cx.emit(Event::PaneRemoved);
3186 }
3187
3188 pub fn panes(&self) -> &[View<Pane>] {
3189 &self.panes
3190 }
3191
3192 pub fn active_pane(&self) -> &View<Pane> {
3193 &self.active_pane
3194 }
3195
3196 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3197 self.find_pane_in_direction(SplitDirection::Right, cx)
3198 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3199 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3200 .clone()
3201 }
3202
3203 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3204 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3205 weak_pane.upgrade()
3206 }
3207
3208 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3209 self.follower_states.retain(|leader_id, state| {
3210 if *leader_id == peer_id {
3211 for item in state.items_by_leader_view_id.values() {
3212 item.view.set_leader_peer_id(None, cx);
3213 }
3214 false
3215 } else {
3216 true
3217 }
3218 });
3219 cx.notify();
3220 }
3221
3222 pub fn start_following(
3223 &mut self,
3224 leader_id: PeerId,
3225 cx: &mut ViewContext<Self>,
3226 ) -> Option<Task<Result<()>>> {
3227 let pane = self.active_pane().clone();
3228
3229 self.last_leaders_by_pane
3230 .insert(pane.downgrade(), leader_id);
3231 self.unfollow(leader_id, cx);
3232 self.unfollow_in_pane(&pane, cx);
3233 self.follower_states.insert(
3234 leader_id,
3235 FollowerState {
3236 center_pane: pane.clone(),
3237 dock_pane: None,
3238 active_view_id: None,
3239 items_by_leader_view_id: Default::default(),
3240 },
3241 );
3242 cx.notify();
3243
3244 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3245 let project_id = self.project.read(cx).remote_id();
3246 let request = self.app_state.client.request(proto::Follow {
3247 room_id,
3248 project_id,
3249 leader_id: Some(leader_id),
3250 });
3251
3252 Some(cx.spawn(|this, mut cx| async move {
3253 let response = request.await?;
3254 this.update(&mut cx, |this, _| {
3255 let state = this
3256 .follower_states
3257 .get_mut(&leader_id)
3258 .ok_or_else(|| anyhow!("following interrupted"))?;
3259 state.active_view_id = response
3260 .active_view
3261 .as_ref()
3262 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3263 Ok::<_, anyhow::Error>(())
3264 })??;
3265 if let Some(view) = response.active_view {
3266 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3267 }
3268 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3269 Ok(())
3270 }))
3271 }
3272
3273 pub fn follow_next_collaborator(
3274 &mut self,
3275 _: &FollowNextCollaborator,
3276 cx: &mut ViewContext<Self>,
3277 ) {
3278 let collaborators = self.project.read(cx).collaborators();
3279 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3280 let mut collaborators = collaborators.keys().copied();
3281 for peer_id in collaborators.by_ref() {
3282 if peer_id == leader_id {
3283 break;
3284 }
3285 }
3286 collaborators.next()
3287 } else if let Some(last_leader_id) =
3288 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3289 {
3290 if collaborators.contains_key(last_leader_id) {
3291 Some(*last_leader_id)
3292 } else {
3293 None
3294 }
3295 } else {
3296 None
3297 };
3298
3299 let pane = self.active_pane.clone();
3300 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3301 else {
3302 return;
3303 };
3304 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3305 return;
3306 }
3307 if let Some(task) = self.start_following(leader_id, cx) {
3308 task.detach_and_log_err(cx)
3309 }
3310 }
3311
3312 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3313 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3314 return;
3315 };
3316 let room = room.read(cx);
3317 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3318 return;
3319 };
3320
3321 let project = self.project.read(cx);
3322
3323 let other_project_id = match remote_participant.location {
3324 call::ParticipantLocation::External => None,
3325 call::ParticipantLocation::UnsharedProject => None,
3326 call::ParticipantLocation::SharedProject { project_id } => {
3327 if Some(project_id) == project.remote_id() {
3328 None
3329 } else {
3330 Some(project_id)
3331 }
3332 }
3333 };
3334
3335 // if they are active in another project, follow there.
3336 if let Some(project_id) = other_project_id {
3337 let app_state = self.app_state.clone();
3338 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3339 .detach_and_log_err(cx);
3340 }
3341
3342 // if you're already following, find the right pane and focus it.
3343 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3344 cx.focus_view(&follower_state.pane());
3345 return;
3346 }
3347
3348 // Otherwise, follow.
3349 if let Some(task) = self.start_following(leader_id, cx) {
3350 task.detach_and_log_err(cx)
3351 }
3352 }
3353
3354 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3355 cx.notify();
3356 let state = self.follower_states.remove(&leader_id)?;
3357 for (_, item) in state.items_by_leader_view_id {
3358 item.view.set_leader_peer_id(None, cx);
3359 }
3360
3361 let project_id = self.project.read(cx).remote_id();
3362 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3363 self.app_state
3364 .client
3365 .send(proto::Unfollow {
3366 room_id,
3367 project_id,
3368 leader_id: Some(leader_id),
3369 })
3370 .log_err();
3371
3372 Some(())
3373 }
3374
3375 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3376 self.follower_states.contains_key(&peer_id)
3377 }
3378
3379 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3380 cx.emit(Event::ActiveItemChanged);
3381 let active_entry = self.active_project_path(cx);
3382 self.project
3383 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3384
3385 self.update_window_title(cx);
3386 }
3387
3388 fn update_window_title(&mut self, cx: &mut WindowContext) {
3389 let project = self.project().read(cx);
3390 let mut title = String::new();
3391
3392 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3393 let filename = path
3394 .path
3395 .file_name()
3396 .map(|s| s.to_string_lossy())
3397 .or_else(|| {
3398 Some(Cow::Borrowed(
3399 project
3400 .worktree_for_id(path.worktree_id, cx)?
3401 .read(cx)
3402 .root_name(),
3403 ))
3404 });
3405
3406 if let Some(filename) = filename {
3407 title.push_str(filename.as_ref());
3408 title.push_str(" — ");
3409 }
3410 }
3411
3412 for (i, name) in project.worktree_root_names(cx).enumerate() {
3413 if i > 0 {
3414 title.push_str(", ");
3415 }
3416 title.push_str(name);
3417 }
3418
3419 if title.is_empty() {
3420 title = "empty project".to_string();
3421 }
3422
3423 if project.is_via_collab() {
3424 title.push_str(" ↙");
3425 } else if project.is_shared() {
3426 title.push_str(" ↗");
3427 }
3428
3429 cx.set_window_title(&title);
3430 }
3431
3432 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3433 let is_edited = !self.project.read(cx).is_disconnected()
3434 && self
3435 .items(cx)
3436 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3437 if is_edited != self.window_edited {
3438 self.window_edited = is_edited;
3439 cx.set_window_edited(self.window_edited)
3440 }
3441 }
3442
3443 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3444 if self.notifications.is_empty() {
3445 None
3446 } else {
3447 Some(
3448 div()
3449 .absolute()
3450 .right_3()
3451 .bottom_3()
3452 .w_112()
3453 .h_full()
3454 .flex()
3455 .flex_col()
3456 .justify_end()
3457 .gap_2()
3458 .children(
3459 self.notifications
3460 .iter()
3461 .map(|(_, notification)| notification.to_any()),
3462 ),
3463 )
3464 }
3465 }
3466
3467 // RPC handlers
3468
3469 fn active_view_for_follower(
3470 &self,
3471 follower_project_id: Option<u64>,
3472 cx: &mut ViewContext<Self>,
3473 ) -> Option<proto::View> {
3474 let (item, panel_id) = self.active_item_for_followers(cx);
3475 let item = item?;
3476 let leader_id = self
3477 .pane_for(&*item)
3478 .and_then(|pane| self.leader_for_pane(&pane));
3479
3480 let item_handle = item.to_followable_item_handle(cx)?;
3481 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3482 let variant = item_handle.to_state_proto(cx)?;
3483
3484 if item_handle.is_project_item(cx)
3485 && (follower_project_id.is_none()
3486 || follower_project_id != self.project.read(cx).remote_id())
3487 {
3488 return None;
3489 }
3490
3491 Some(proto::View {
3492 id: Some(id.to_proto()),
3493 leader_id,
3494 variant: Some(variant),
3495 panel_id: panel_id.map(|id| id as i32),
3496 })
3497 }
3498
3499 fn handle_follow(
3500 &mut self,
3501 follower_project_id: Option<u64>,
3502 cx: &mut ViewContext<Self>,
3503 ) -> proto::FollowResponse {
3504 let active_view = self.active_view_for_follower(follower_project_id, cx);
3505
3506 cx.notify();
3507 proto::FollowResponse {
3508 // TODO: Remove after version 0.145.x stabilizes.
3509 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3510 views: active_view.iter().cloned().collect(),
3511 active_view,
3512 }
3513 }
3514
3515 fn handle_update_followers(
3516 &mut self,
3517 leader_id: PeerId,
3518 message: proto::UpdateFollowers,
3519 _cx: &mut ViewContext<Self>,
3520 ) {
3521 self.leader_updates_tx
3522 .unbounded_send((leader_id, message))
3523 .ok();
3524 }
3525
3526 async fn process_leader_update(
3527 this: &WeakView<Self>,
3528 leader_id: PeerId,
3529 update: proto::UpdateFollowers,
3530 cx: &mut AsyncWindowContext,
3531 ) -> Result<()> {
3532 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3533 proto::update_followers::Variant::CreateView(view) => {
3534 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3535 let should_add_view = this.update(cx, |this, _| {
3536 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3537 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3538 } else {
3539 anyhow::Ok(false)
3540 }
3541 })??;
3542
3543 if should_add_view {
3544 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3545 }
3546 }
3547 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3548 let should_add_view = this.update(cx, |this, _| {
3549 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3550 state.active_view_id = update_active_view
3551 .view
3552 .as_ref()
3553 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3554
3555 if state.active_view_id.is_some_and(|view_id| {
3556 !state.items_by_leader_view_id.contains_key(&view_id)
3557 }) {
3558 anyhow::Ok(true)
3559 } else {
3560 anyhow::Ok(false)
3561 }
3562 } else {
3563 anyhow::Ok(false)
3564 }
3565 })??;
3566
3567 if should_add_view {
3568 if let Some(view) = update_active_view.view {
3569 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3570 }
3571 }
3572 }
3573 proto::update_followers::Variant::UpdateView(update_view) => {
3574 let variant = update_view
3575 .variant
3576 .ok_or_else(|| anyhow!("missing update view variant"))?;
3577 let id = update_view
3578 .id
3579 .ok_or_else(|| anyhow!("missing update view id"))?;
3580 let mut tasks = Vec::new();
3581 this.update(cx, |this, cx| {
3582 let project = this.project.clone();
3583 if let Some(state) = this.follower_states.get(&leader_id) {
3584 let view_id = ViewId::from_proto(id.clone())?;
3585 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3586 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3587 }
3588 }
3589 anyhow::Ok(())
3590 })??;
3591 try_join_all(tasks).await.log_err();
3592 }
3593 }
3594 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3595 Ok(())
3596 }
3597
3598 async fn add_view_from_leader(
3599 this: WeakView<Self>,
3600 leader_id: PeerId,
3601 view: &proto::View,
3602 cx: &mut AsyncWindowContext,
3603 ) -> Result<()> {
3604 let this = this.upgrade().context("workspace dropped")?;
3605
3606 let Some(id) = view.id.clone() else {
3607 return Err(anyhow!("no id for view"));
3608 };
3609 let id = ViewId::from_proto(id)?;
3610 let panel_id = view.panel_id.and_then(|id| proto::PanelId::from_i32(id));
3611
3612 let pane = this.update(cx, |this, _cx| {
3613 let state = this
3614 .follower_states
3615 .get(&leader_id)
3616 .context("stopped following")?;
3617 anyhow::Ok(state.pane().clone())
3618 })??;
3619 let existing_item = pane.update(cx, |pane, cx| {
3620 let client = this.read(cx).client().clone();
3621 pane.items().find_map(|item| {
3622 let item = item.to_followable_item_handle(cx)?;
3623 if item.remote_id(&client, cx) == Some(id) {
3624 Some(item)
3625 } else {
3626 None
3627 }
3628 })
3629 })?;
3630 let item = if let Some(existing_item) = existing_item {
3631 existing_item
3632 } else {
3633 let variant = view.variant.clone();
3634 if variant.is_none() {
3635 Err(anyhow!("missing view variant"))?;
3636 }
3637
3638 let task = cx.update(|cx| {
3639 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3640 })?;
3641
3642 let Some(task) = task else {
3643 return Err(anyhow!(
3644 "failed to construct view from leader (maybe from a different version of zed?)"
3645 ));
3646 };
3647
3648 let mut new_item = task.await?;
3649 pane.update(cx, |pane, cx| {
3650 let mut item_ix_to_remove = None;
3651 for (ix, item) in pane.items().enumerate() {
3652 if let Some(item) = item.to_followable_item_handle(cx) {
3653 match new_item.dedup(item.as_ref(), cx) {
3654 Some(item::Dedup::KeepExisting) => {
3655 new_item =
3656 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3657 break;
3658 }
3659 Some(item::Dedup::ReplaceExisting) => {
3660 item_ix_to_remove = Some(ix);
3661 break;
3662 }
3663 None => {}
3664 }
3665 }
3666 }
3667
3668 if let Some(ix) = item_ix_to_remove {
3669 pane.remove_item(ix, false, false, cx);
3670 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3671 }
3672 })?;
3673
3674 new_item
3675 };
3676
3677 this.update(cx, |this, cx| {
3678 let state = this.follower_states.get_mut(&leader_id)?;
3679 item.set_leader_peer_id(Some(leader_id), cx);
3680 state.items_by_leader_view_id.insert(
3681 id,
3682 FollowerView {
3683 view: item,
3684 location: panel_id,
3685 },
3686 );
3687
3688 Some(())
3689 })?;
3690
3691 Ok(())
3692 }
3693
3694 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3695 let mut is_project_item = true;
3696 let mut update = proto::UpdateActiveView::default();
3697 if cx.is_window_active() {
3698 let (active_item, panel_id) = self.active_item_for_followers(cx);
3699
3700 if let Some(item) = active_item {
3701 if item.focus_handle(cx).contains_focused(cx) {
3702 let leader_id = self
3703 .pane_for(&*item)
3704 .and_then(|pane| self.leader_for_pane(&pane));
3705
3706 if let Some(item) = item.to_followable_item_handle(cx) {
3707 let id = item
3708 .remote_id(&self.app_state.client, cx)
3709 .map(|id| id.to_proto());
3710
3711 if let Some(id) = id.clone() {
3712 if let Some(variant) = item.to_state_proto(cx) {
3713 let view = Some(proto::View {
3714 id: Some(id.clone()),
3715 leader_id,
3716 variant: Some(variant),
3717 panel_id: panel_id.map(|id| id as i32),
3718 });
3719
3720 is_project_item = item.is_project_item(cx);
3721 update = proto::UpdateActiveView {
3722 view,
3723 // TODO: Remove after version 0.145.x stabilizes.
3724 id: Some(id.clone()),
3725 leader_id,
3726 };
3727 }
3728 };
3729 }
3730 }
3731 }
3732 }
3733
3734 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3735 if active_view_id != self.last_active_view_id.as_ref() {
3736 self.last_active_view_id = active_view_id.cloned();
3737 self.update_followers(
3738 is_project_item,
3739 proto::update_followers::Variant::UpdateActiveView(update),
3740 cx,
3741 );
3742 }
3743 }
3744
3745 fn active_item_for_followers(
3746 &self,
3747 cx: &mut WindowContext,
3748 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3749 let mut active_item = None;
3750 let mut panel_id = None;
3751 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3752 if dock.focus_handle(cx).contains_focused(cx) {
3753 if let Some(panel) = dock.read(cx).active_panel() {
3754 if let Some(pane) = panel.pane(cx) {
3755 if let Some(item) = pane.read(cx).active_item() {
3756 active_item = Some(item);
3757 panel_id = panel.remote_id();
3758 break;
3759 }
3760 }
3761 }
3762 }
3763 }
3764
3765 if active_item.is_none() {
3766 active_item = self.active_pane().read(cx).active_item();
3767 }
3768 (active_item, panel_id)
3769 }
3770
3771 fn update_followers(
3772 &self,
3773 project_only: bool,
3774 update: proto::update_followers::Variant,
3775 cx: &mut WindowContext,
3776 ) -> Option<()> {
3777 // If this update only applies to for followers in the current project,
3778 // then skip it unless this project is shared. If it applies to all
3779 // followers, regardless of project, then set `project_id` to none,
3780 // indicating that it goes to all followers.
3781 let project_id = if project_only {
3782 Some(self.project.read(cx).remote_id()?)
3783 } else {
3784 None
3785 };
3786 self.app_state().workspace_store.update(cx, |store, cx| {
3787 store.update_followers(project_id, update, cx)
3788 })
3789 }
3790
3791 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3792 self.follower_states.iter().find_map(|(leader_id, state)| {
3793 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3794 Some(*leader_id)
3795 } else {
3796 None
3797 }
3798 })
3799 }
3800
3801 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3802 cx.notify();
3803
3804 let call = self.active_call()?;
3805 let room = call.read(cx).room()?.read(cx);
3806 let participant = room.remote_participant_for_peer_id(leader_id)?;
3807
3808 let leader_in_this_app;
3809 let leader_in_this_project;
3810 match participant.location {
3811 call::ParticipantLocation::SharedProject { project_id } => {
3812 leader_in_this_app = true;
3813 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3814 }
3815 call::ParticipantLocation::UnsharedProject => {
3816 leader_in_this_app = true;
3817 leader_in_this_project = false;
3818 }
3819 call::ParticipantLocation::External => {
3820 leader_in_this_app = false;
3821 leader_in_this_project = false;
3822 }
3823 };
3824
3825 let state = self.follower_states.get(&leader_id)?;
3826 let mut item_to_activate = None;
3827 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3828 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3829 if leader_in_this_project || !item.view.is_project_item(cx) {
3830 item_to_activate = Some((item.location, item.view.boxed_clone()));
3831 }
3832 }
3833 } else if let Some(shared_screen) =
3834 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3835 {
3836 item_to_activate = Some((None, Box::new(shared_screen)));
3837 }
3838
3839 let (panel_id, item) = item_to_activate?;
3840
3841 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3842 let pane;
3843 if let Some(panel_id) = panel_id {
3844 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3845 let state = self.follower_states.get_mut(&leader_id)?;
3846 state.dock_pane = Some(pane.clone());
3847 } else {
3848 pane = state.center_pane.clone();
3849 let state = self.follower_states.get_mut(&leader_id)?;
3850 if let Some(dock_pane) = state.dock_pane.take() {
3851 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3852 }
3853 }
3854
3855 pane.update(cx, |pane, cx| {
3856 let focus_active_item = pane.has_focus(cx) || transfer_focus;
3857 if let Some(index) = pane.index_for_item(item.as_ref()) {
3858 pane.activate_item(index, false, false, cx);
3859 } else {
3860 pane.add_item(item.boxed_clone(), false, false, None, cx)
3861 }
3862
3863 if focus_active_item {
3864 pane.focus_active_item(cx)
3865 }
3866 });
3867
3868 None
3869 }
3870
3871 fn shared_screen_for_peer(
3872 &self,
3873 peer_id: PeerId,
3874 pane: &View<Pane>,
3875 cx: &mut WindowContext,
3876 ) -> Option<View<SharedScreen>> {
3877 let call = self.active_call()?;
3878 let room = call.read(cx).room()?.read(cx);
3879 let participant = room.remote_participant_for_peer_id(peer_id)?;
3880 let track = participant.video_tracks.values().next()?.clone();
3881 let user = participant.user.clone();
3882
3883 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3884 if item.read(cx).peer_id == peer_id {
3885 return Some(item);
3886 }
3887 }
3888
3889 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3890 }
3891
3892 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3893 if cx.is_window_active() {
3894 self.update_active_view_for_followers(cx);
3895
3896 if let Some(database_id) = self.database_id {
3897 cx.background_executor()
3898 .spawn(persistence::DB.update_timestamp(database_id))
3899 .detach();
3900 }
3901 } else {
3902 for pane in &self.panes {
3903 pane.update(cx, |pane, cx| {
3904 if let Some(item) = pane.active_item() {
3905 item.workspace_deactivated(cx);
3906 }
3907 for item in pane.items() {
3908 if matches!(
3909 item.workspace_settings(cx).autosave,
3910 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3911 ) {
3912 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3913 .detach_and_log_err(cx);
3914 }
3915 }
3916 });
3917 }
3918 }
3919 }
3920
3921 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3922 self.active_call.as_ref().map(|(call, _)| call)
3923 }
3924
3925 fn on_active_call_event(
3926 &mut self,
3927 _: Model<ActiveCall>,
3928 event: &call::room::Event,
3929 cx: &mut ViewContext<Self>,
3930 ) {
3931 match event {
3932 call::room::Event::ParticipantLocationChanged { participant_id }
3933 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3934 self.leader_updated(*participant_id, cx);
3935 }
3936 _ => {}
3937 }
3938 }
3939
3940 pub fn database_id(&self) -> Option<WorkspaceId> {
3941 self.database_id
3942 }
3943
3944 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
3945 let project = self.project().read(cx);
3946
3947 if project.is_local_or_ssh() {
3948 Some(
3949 project
3950 .visible_worktrees(cx)
3951 .map(|worktree| worktree.read(cx).abs_path())
3952 .collect::<Vec<_>>(),
3953 )
3954 } else {
3955 None
3956 }
3957 }
3958
3959 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3960 match member {
3961 Member::Axis(PaneAxis { members, .. }) => {
3962 for child in members.iter() {
3963 self.remove_panes(child.clone(), cx)
3964 }
3965 }
3966 Member::Pane(pane) => {
3967 self.force_remove_pane(&pane, &None, cx);
3968 }
3969 }
3970 }
3971
3972 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
3973 self.session_id.take();
3974 self.serialize_workspace_internal(cx)
3975 }
3976
3977 fn force_remove_pane(
3978 &mut self,
3979 pane: &View<Pane>,
3980 focus_on: &Option<View<Pane>>,
3981 cx: &mut ViewContext<Workspace>,
3982 ) {
3983 self.panes.retain(|p| p != pane);
3984 if let Some(focus_on) = focus_on {
3985 focus_on.update(cx, |pane, cx| pane.focus(cx));
3986 } else {
3987 self.panes
3988 .last()
3989 .unwrap()
3990 .update(cx, |pane, cx| pane.focus(cx));
3991 }
3992 if self.last_active_center_pane == Some(pane.downgrade()) {
3993 self.last_active_center_pane = None;
3994 }
3995 cx.notify();
3996 }
3997
3998 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
3999 if self._schedule_serialize.is_none() {
4000 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4001 cx.background_executor()
4002 .timer(Duration::from_millis(100))
4003 .await;
4004 this.update(&mut cx, |this, cx| {
4005 this.serialize_workspace_internal(cx).detach();
4006 this._schedule_serialize.take();
4007 })
4008 .log_err();
4009 }));
4010 }
4011 }
4012
4013 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4014 let Some(database_id) = self.database_id() else {
4015 return Task::ready(());
4016 };
4017
4018 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4019 let (items, active) = {
4020 let pane = pane_handle.read(cx);
4021 let active_item_id = pane.active_item().map(|item| item.item_id());
4022 (
4023 pane.items()
4024 .filter_map(|handle| {
4025 let handle = handle.to_serializable_item_handle(cx)?;
4026
4027 Some(SerializedItem {
4028 kind: Arc::from(handle.serialized_item_kind()),
4029 item_id: handle.item_id().as_u64(),
4030 active: Some(handle.item_id()) == active_item_id,
4031 preview: pane.is_active_preview_item(handle.item_id()),
4032 })
4033 })
4034 .collect::<Vec<_>>(),
4035 pane.has_focus(cx),
4036 )
4037 };
4038
4039 SerializedPane::new(items, active)
4040 }
4041
4042 fn build_serialized_pane_group(
4043 pane_group: &Member,
4044 cx: &WindowContext,
4045 ) -> SerializedPaneGroup {
4046 match pane_group {
4047 Member::Axis(PaneAxis {
4048 axis,
4049 members,
4050 flexes,
4051 bounding_boxes: _,
4052 }) => SerializedPaneGroup::Group {
4053 axis: SerializedAxis(*axis),
4054 children: members
4055 .iter()
4056 .map(|member| build_serialized_pane_group(member, cx))
4057 .collect::<Vec<_>>(),
4058 flexes: Some(flexes.lock().clone()),
4059 },
4060 Member::Pane(pane_handle) => {
4061 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4062 }
4063 }
4064 }
4065
4066 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4067 let left_dock = this.left_dock.read(cx);
4068 let left_visible = left_dock.is_open();
4069 let left_active_panel = left_dock
4070 .visible_panel()
4071 .map(|panel| panel.persistent_name().to_string());
4072 let left_dock_zoom = left_dock
4073 .visible_panel()
4074 .map(|panel| panel.is_zoomed(cx))
4075 .unwrap_or(false);
4076
4077 let right_dock = this.right_dock.read(cx);
4078 let right_visible = right_dock.is_open();
4079 let right_active_panel = right_dock
4080 .visible_panel()
4081 .map(|panel| panel.persistent_name().to_string());
4082 let right_dock_zoom = right_dock
4083 .visible_panel()
4084 .map(|panel| panel.is_zoomed(cx))
4085 .unwrap_or(false);
4086
4087 let bottom_dock = this.bottom_dock.read(cx);
4088 let bottom_visible = bottom_dock.is_open();
4089 let bottom_active_panel = bottom_dock
4090 .visible_panel()
4091 .map(|panel| panel.persistent_name().to_string());
4092 let bottom_dock_zoom = bottom_dock
4093 .visible_panel()
4094 .map(|panel| panel.is_zoomed(cx))
4095 .unwrap_or(false);
4096
4097 DockStructure {
4098 left: DockData {
4099 visible: left_visible,
4100 active_panel: left_active_panel,
4101 zoom: left_dock_zoom,
4102 },
4103 right: DockData {
4104 visible: right_visible,
4105 active_panel: right_active_panel,
4106 zoom: right_dock_zoom,
4107 },
4108 bottom: DockData {
4109 visible: bottom_visible,
4110 active_panel: bottom_active_panel,
4111 zoom: bottom_dock_zoom,
4112 },
4113 }
4114 }
4115
4116 let location = if let Some(local_paths) = self.local_paths(cx) {
4117 if !local_paths.is_empty() {
4118 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4119 } else {
4120 None
4121 }
4122 } else if let Some(dev_server_project_id) = self.project().read(cx).dev_server_project_id()
4123 {
4124 let store = dev_server_projects::Store::global(cx).read(cx);
4125 maybe!({
4126 let project = store.dev_server_project(dev_server_project_id)?;
4127 let dev_server = store.dev_server(project.dev_server_id)?;
4128
4129 let dev_server_project = SerializedDevServerProject {
4130 id: dev_server_project_id,
4131 dev_server_name: dev_server.name.to_string(),
4132 paths: project.paths.iter().map(|path| path.clone()).collect(),
4133 };
4134 Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
4135 })
4136 } else {
4137 None
4138 };
4139
4140 if let Some(location) = location {
4141 let center_group = build_serialized_pane_group(&self.center.root, cx);
4142 let docks = build_serialized_docks(self, cx);
4143 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4144 let serialized_workspace = SerializedWorkspace {
4145 id: database_id,
4146 location,
4147 center_group,
4148 window_bounds,
4149 display: Default::default(),
4150 docks,
4151 centered_layout: self.centered_layout,
4152 session_id: self.session_id.clone(),
4153 window_id: Some(cx.window_handle().window_id().as_u64()),
4154 };
4155 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4156 }
4157 Task::ready(())
4158 }
4159
4160 async fn serialize_items(
4161 this: &WeakView<Self>,
4162 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4163 cx: &mut AsyncWindowContext,
4164 ) -> Result<()> {
4165 const CHUNK_SIZE: usize = 200;
4166 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4167
4168 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4169
4170 while let Some(items_received) = serializable_items.next().await {
4171 let unique_items =
4172 items_received
4173 .into_iter()
4174 .fold(HashMap::default(), |mut acc, item| {
4175 acc.entry(item.item_id()).or_insert(item);
4176 acc
4177 });
4178
4179 // We use into_iter() here so that the references to the items are moved into
4180 // the tasks and not kept alive while we're sleeping.
4181 for (_, item) in unique_items.into_iter() {
4182 if let Ok(Some(task)) =
4183 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4184 {
4185 cx.background_executor()
4186 .spawn(async move { task.await.log_err() })
4187 .detach();
4188 }
4189 }
4190
4191 cx.background_executor().timer(THROTTLE_TIME).await;
4192 }
4193
4194 Ok(())
4195 }
4196
4197 pub(crate) fn enqueue_item_serialization(
4198 &mut self,
4199 item: Box<dyn SerializableItemHandle>,
4200 ) -> Result<()> {
4201 self.serializable_items_tx
4202 .unbounded_send(item)
4203 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4204 }
4205
4206 pub(crate) fn load_workspace(
4207 serialized_workspace: SerializedWorkspace,
4208 paths_to_open: Vec<Option<ProjectPath>>,
4209 cx: &mut ViewContext<Workspace>,
4210 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4211 cx.spawn(|workspace, mut cx| async move {
4212 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4213
4214 let mut center_group = None;
4215 let mut center_items = None;
4216
4217 // Traverse the splits tree and add to things
4218 if let Some((group, active_pane, items)) = serialized_workspace
4219 .center_group
4220 .deserialize(
4221 &project,
4222 serialized_workspace.id,
4223 workspace.clone(),
4224 &mut cx,
4225 )
4226 .await
4227 {
4228 center_items = Some(items);
4229 center_group = Some((group, active_pane))
4230 }
4231
4232 let mut items_by_project_path = HashMap::default();
4233 let mut item_ids_by_kind = HashMap::default();
4234 let mut all_deserialized_items = Vec::default();
4235 cx.update(|cx| {
4236 for item in center_items.unwrap_or_default().into_iter().flatten() {
4237 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4238 item_ids_by_kind
4239 .entry(serializable_item_handle.serialized_item_kind())
4240 .or_insert(Vec::new())
4241 .push(item.item_id().as_u64() as ItemId);
4242 }
4243
4244 if let Some(project_path) = item.project_path(cx) {
4245 items_by_project_path.insert(project_path, item.clone());
4246 }
4247 all_deserialized_items.push(item);
4248 }
4249 })?;
4250
4251 let opened_items = paths_to_open
4252 .into_iter()
4253 .map(|path_to_open| {
4254 path_to_open
4255 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4256 })
4257 .collect::<Vec<_>>();
4258
4259 // Remove old panes from workspace panes list
4260 workspace.update(&mut cx, |workspace, cx| {
4261 if let Some((center_group, active_pane)) = center_group {
4262 workspace.remove_panes(workspace.center.root.clone(), cx);
4263
4264 // Swap workspace center group
4265 workspace.center = PaneGroup::with_root(center_group);
4266 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
4267 if let Some(active_pane) = active_pane {
4268 workspace.active_pane = active_pane;
4269 cx.focus_self();
4270 } else {
4271 workspace.active_pane = workspace.center.first_pane().clone();
4272 }
4273 }
4274
4275 let docks = serialized_workspace.docks;
4276
4277 for (dock, serialized_dock) in [
4278 (&mut workspace.right_dock, docks.right),
4279 (&mut workspace.left_dock, docks.left),
4280 (&mut workspace.bottom_dock, docks.bottom),
4281 ]
4282 .iter_mut()
4283 {
4284 dock.update(cx, |dock, cx| {
4285 dock.serialized_dock = Some(serialized_dock.clone());
4286 dock.restore_state(cx);
4287 });
4288 }
4289
4290 cx.notify();
4291 })?;
4292
4293 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4294 // after loading the items, we might have different items and in order to avoid
4295 // the database filling up, we delete items that haven't been loaded now.
4296 //
4297 // The items that have been loaded, have been saved after they've been added to the workspace.
4298 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4299 item_ids_by_kind
4300 .into_iter()
4301 .map(|(item_kind, loaded_items)| {
4302 SerializableItemRegistry::cleanup(
4303 item_kind,
4304 serialized_workspace.id,
4305 loaded_items,
4306 cx,
4307 )
4308 .log_err()
4309 })
4310 .collect::<Vec<_>>()
4311 })?;
4312
4313 futures::future::join_all(clean_up_tasks).await;
4314
4315 workspace
4316 .update(&mut cx, |workspace, cx| {
4317 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4318 workspace.serialize_workspace_internal(cx).detach();
4319
4320 // Ensure that we mark the window as edited if we did load dirty items
4321 workspace.update_window_edited(cx);
4322 })
4323 .ok();
4324
4325 Ok(opened_items)
4326 })
4327 }
4328
4329 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4330 self.add_workspace_actions_listeners(div, cx)
4331 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4332 .on_action(cx.listener(Self::close_all_items_and_panes))
4333 .on_action(cx.listener(Self::save_all))
4334 .on_action(cx.listener(Self::send_keystrokes))
4335 .on_action(cx.listener(Self::add_folder_to_project))
4336 .on_action(cx.listener(Self::follow_next_collaborator))
4337 .on_action(cx.listener(Self::open))
4338 .on_action(cx.listener(Self::close_window))
4339 .on_action(cx.listener(Self::activate_pane_at_index))
4340 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4341 let pane = workspace.active_pane().clone();
4342 workspace.unfollow_in_pane(&pane, cx);
4343 }))
4344 .on_action(cx.listener(|workspace, action: &Save, cx| {
4345 workspace
4346 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4347 .detach_and_log_err(cx);
4348 }))
4349 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4350 workspace
4351 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4352 .detach_and_log_err(cx);
4353 }))
4354 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4355 workspace
4356 .save_active_item(SaveIntent::SaveAs, cx)
4357 .detach_and_log_err(cx);
4358 }))
4359 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4360 workspace.activate_previous_pane(cx)
4361 }))
4362 .on_action(
4363 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4364 )
4365 .on_action(
4366 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4367 workspace.activate_pane_in_direction(action.0, cx)
4368 }),
4369 )
4370 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4371 workspace.swap_pane_in_direction(action.0, cx)
4372 }))
4373 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4374 this.toggle_dock(DockPosition::Left, cx);
4375 }))
4376 .on_action(
4377 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4378 workspace.toggle_dock(DockPosition::Right, cx);
4379 }),
4380 )
4381 .on_action(
4382 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4383 workspace.toggle_dock(DockPosition::Bottom, cx);
4384 }),
4385 )
4386 .on_action(
4387 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4388 workspace.close_all_docks(cx);
4389 }),
4390 )
4391 .on_action(
4392 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4393 workspace.clear_all_notifications(cx);
4394 }),
4395 )
4396 .on_action(
4397 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4398 workspace.reopen_closed_item(cx).detach();
4399 }),
4400 )
4401 .on_action(cx.listener(Workspace::toggle_centered_layout))
4402 }
4403
4404 #[cfg(any(test, feature = "test-support"))]
4405 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4406 use node_runtime::FakeNodeRuntime;
4407 use session::Session;
4408
4409 let client = project.read(cx).client();
4410 let user_store = project.read(cx).user_store();
4411
4412 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4413 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4414 cx.activate_window();
4415 let app_state = Arc::new(AppState {
4416 languages: project.read(cx).languages().clone(),
4417 workspace_store,
4418 client,
4419 user_store,
4420 fs: project.read(cx).fs().clone(),
4421 build_window_options: |_, _| Default::default(),
4422 node_runtime: FakeNodeRuntime::new(),
4423 session,
4424 });
4425 let workspace = Self::new(Default::default(), project, app_state, cx);
4426 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4427 workspace
4428 }
4429
4430 pub fn register_action<A: Action>(
4431 &mut self,
4432 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4433 ) -> &mut Self {
4434 let callback = Arc::new(callback);
4435
4436 self.workspace_actions.push(Box::new(move |div, cx| {
4437 let callback = callback.clone();
4438 div.on_action(
4439 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4440 )
4441 }));
4442 self
4443 }
4444
4445 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4446 for action in self.workspace_actions.iter() {
4447 div = (action)(div, cx)
4448 }
4449 div
4450 }
4451
4452 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4453 self.modal_layer.read(cx).has_active_modal()
4454 }
4455
4456 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
4457 self.modal_layer.read(cx).active_modal()
4458 }
4459
4460 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4461 where
4462 B: FnOnce(&mut ViewContext<V>) -> V,
4463 {
4464 self.modal_layer
4465 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4466 }
4467
4468 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4469 self.centered_layout = !self.centered_layout;
4470 if let Some(database_id) = self.database_id() {
4471 cx.background_executor()
4472 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4473 .detach_and_log_err(cx);
4474 }
4475 cx.notify();
4476 }
4477
4478 fn adjust_padding(padding: Option<f32>) -> f32 {
4479 padding
4480 .unwrap_or(Self::DEFAULT_PADDING)
4481 .clamp(0.0, Self::MAX_PADDING)
4482 }
4483
4484 fn render_dock(
4485 &self,
4486 position: DockPosition,
4487 dock: &View<Dock>,
4488 cx: &WindowContext,
4489 ) -> Option<Div> {
4490 if self.zoomed_position == Some(position) {
4491 return None;
4492 }
4493
4494 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4495 let pane = panel.pane(cx)?;
4496 let follower_states = &self.follower_states;
4497 leader_border_for_pane(follower_states, &pane, cx)
4498 });
4499
4500 Some(
4501 div()
4502 .flex()
4503 .flex_none()
4504 .overflow_hidden()
4505 .child(dock.clone())
4506 .children(leader_border),
4507 )
4508 }
4509}
4510
4511fn leader_border_for_pane(
4512 follower_states: &HashMap<PeerId, FollowerState>,
4513 pane: &View<Pane>,
4514 cx: &WindowContext,
4515) -> Option<Div> {
4516 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4517 if state.pane() == pane {
4518 Some((*leader_id, state))
4519 } else {
4520 None
4521 }
4522 })?;
4523
4524 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4525 let leader = room.remote_participant_for_peer_id(leader_id)?;
4526
4527 let mut leader_color = cx
4528 .theme()
4529 .players()
4530 .color_for_participant(leader.participant_index.0)
4531 .cursor;
4532 leader_color.fade_out(0.3);
4533 Some(
4534 div()
4535 .absolute()
4536 .size_full()
4537 .left_0()
4538 .top_0()
4539 .border_2()
4540 .border_color(leader_color),
4541 )
4542}
4543
4544fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4545 ZED_WINDOW_POSITION
4546 .zip(*ZED_WINDOW_SIZE)
4547 .map(|(position, size)| Bounds {
4548 origin: position,
4549 size,
4550 })
4551}
4552
4553fn open_items(
4554 serialized_workspace: Option<SerializedWorkspace>,
4555 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4556 app_state: Arc<AppState>,
4557 cx: &mut ViewContext<Workspace>,
4558) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4559 let restored_items = serialized_workspace.map(|serialized_workspace| {
4560 Workspace::load_workspace(
4561 serialized_workspace,
4562 project_paths_to_open
4563 .iter()
4564 .map(|(_, project_path)| project_path)
4565 .cloned()
4566 .collect(),
4567 cx,
4568 )
4569 });
4570
4571 cx.spawn(|workspace, mut cx| async move {
4572 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4573
4574 if let Some(restored_items) = restored_items {
4575 let restored_items = restored_items.await?;
4576
4577 let restored_project_paths = restored_items
4578 .iter()
4579 .filter_map(|item| {
4580 cx.update(|cx| item.as_ref()?.project_path(cx))
4581 .ok()
4582 .flatten()
4583 })
4584 .collect::<HashSet<_>>();
4585
4586 for restored_item in restored_items {
4587 opened_items.push(restored_item.map(Ok));
4588 }
4589
4590 project_paths_to_open
4591 .iter_mut()
4592 .for_each(|(_, project_path)| {
4593 if let Some(project_path_to_open) = project_path {
4594 if restored_project_paths.contains(project_path_to_open) {
4595 *project_path = None;
4596 }
4597 }
4598 });
4599 } else {
4600 for _ in 0..project_paths_to_open.len() {
4601 opened_items.push(None);
4602 }
4603 }
4604 assert!(opened_items.len() == project_paths_to_open.len());
4605
4606 let tasks =
4607 project_paths_to_open
4608 .into_iter()
4609 .enumerate()
4610 .map(|(ix, (abs_path, project_path))| {
4611 let workspace = workspace.clone();
4612 cx.spawn(|mut cx| {
4613 let fs = app_state.fs.clone();
4614 async move {
4615 let file_project_path = project_path?;
4616 if fs.is_dir(&abs_path).await {
4617 None
4618 } else {
4619 Some((
4620 ix,
4621 workspace
4622 .update(&mut cx, |workspace, cx| {
4623 workspace.open_path(file_project_path, None, true, cx)
4624 })
4625 .log_err()?
4626 .await,
4627 ))
4628 }
4629 }
4630 })
4631 });
4632
4633 let tasks = tasks.collect::<Vec<_>>();
4634
4635 let tasks = futures::future::join_all(tasks);
4636 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4637 opened_items[ix] = Some(path_open_result);
4638 }
4639
4640 Ok(opened_items)
4641 })
4642}
4643
4644enum ActivateInDirectionTarget {
4645 Pane(View<Pane>),
4646 Dock(View<Dock>),
4647}
4648
4649fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4650 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4651
4652 workspace
4653 .update(cx, |workspace, cx| {
4654 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4655 struct DatabaseFailedNotification;
4656
4657 workspace.show_notification_once(
4658 NotificationId::unique::<DatabaseFailedNotification>(),
4659 cx,
4660 |cx| {
4661 cx.new_view(|_| {
4662 MessageNotification::new("Failed to load the database file.")
4663 .with_click_message("Click to let us know about this error")
4664 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4665 })
4666 },
4667 );
4668 }
4669 })
4670 .log_err();
4671}
4672
4673impl FocusableView for Workspace {
4674 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4675 self.active_pane.focus_handle(cx)
4676 }
4677}
4678
4679#[derive(Clone, Render)]
4680struct DraggedDock(DockPosition);
4681
4682impl Render for Workspace {
4683 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4684 let mut context = KeyContext::new_with_defaults();
4685 context.add("Workspace");
4686 let centered_layout = self.centered_layout
4687 && self.center.panes().len() == 1
4688 && self.active_item(cx).is_some();
4689 let render_padding = |size| {
4690 (size > 0.0).then(|| {
4691 div()
4692 .h_full()
4693 .w(relative(size))
4694 .bg(cx.theme().colors().editor_background)
4695 .border_color(cx.theme().colors().pane_group_border)
4696 })
4697 };
4698 let paddings = if centered_layout {
4699 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4700 (
4701 render_padding(Self::adjust_padding(settings.left_padding)),
4702 render_padding(Self::adjust_padding(settings.right_padding)),
4703 )
4704 } else {
4705 (None, None)
4706 };
4707 let ui_font = theme::setup_ui_font(cx);
4708
4709 let theme = cx.theme().clone();
4710 let colors = theme.colors();
4711
4712 client_side_decorations(
4713 self.actions(div(), cx)
4714 .key_context(context)
4715 .relative()
4716 .size_full()
4717 .flex()
4718 .flex_col()
4719 .font(ui_font)
4720 .gap_0()
4721 .justify_start()
4722 .items_start()
4723 .text_color(colors.text)
4724 .overflow_hidden()
4725 .children(self.titlebar_item.clone())
4726 .child(
4727 div()
4728 .id("workspace")
4729 .bg(colors.background)
4730 .relative()
4731 .flex_1()
4732 .w_full()
4733 .flex()
4734 .flex_col()
4735 .overflow_hidden()
4736 .border_t_1()
4737 .border_b_1()
4738 .border_color(colors.border)
4739 .child({
4740 let this = cx.view().clone();
4741 canvas(
4742 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4743 |_, _, _| {},
4744 )
4745 .absolute()
4746 .size_full()
4747 })
4748 .when(self.zoomed.is_none(), |this| {
4749 this.on_drag_move(cx.listener(
4750 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4751 DockPosition::Left => {
4752 let size = e.event.position.x - workspace.bounds.left();
4753 workspace.left_dock.update(cx, |left_dock, cx| {
4754 left_dock.resize_active_panel(Some(size), cx);
4755 });
4756 }
4757 DockPosition::Right => {
4758 let size = workspace.bounds.right() - e.event.position.x;
4759 workspace.right_dock.update(cx, |right_dock, cx| {
4760 right_dock.resize_active_panel(Some(size), cx);
4761 });
4762 }
4763 DockPosition::Bottom => {
4764 let size = workspace.bounds.bottom() - e.event.position.y;
4765 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4766 bottom_dock.resize_active_panel(Some(size), cx);
4767 });
4768 }
4769 },
4770 ))
4771 })
4772 .child(
4773 div()
4774 .flex()
4775 .flex_row()
4776 .h_full()
4777 // Left Dock
4778 .children(self.render_dock(DockPosition::Left, &self.left_dock, cx))
4779 // Panes
4780 .child(
4781 div()
4782 .flex()
4783 .flex_col()
4784 .flex_1()
4785 .overflow_hidden()
4786 .child(
4787 h_flex()
4788 .flex_1()
4789 .when_some(paddings.0, |this, p| {
4790 this.child(p.border_r_1())
4791 })
4792 .child(self.center.render(
4793 &self.project,
4794 &self.follower_states,
4795 self.active_call(),
4796 &self.active_pane,
4797 self.zoomed.as_ref(),
4798 &self.app_state,
4799 cx,
4800 ))
4801 .when_some(paddings.1, |this, p| {
4802 this.child(p.border_l_1())
4803 }),
4804 )
4805 .children(self.render_dock(
4806 DockPosition::Bottom,
4807 &self.bottom_dock,
4808 cx,
4809 )),
4810 )
4811 // Right Dock
4812 .children(self.render_dock(
4813 DockPosition::Right,
4814 &self.right_dock,
4815 cx,
4816 )),
4817 )
4818 .children(self.zoomed.as_ref().and_then(|view| {
4819 let zoomed_view = view.upgrade()?;
4820 let div = div()
4821 .occlude()
4822 .absolute()
4823 .overflow_hidden()
4824 .border_color(colors.border)
4825 .bg(colors.background)
4826 .child(zoomed_view)
4827 .inset_0()
4828 .shadow_lg();
4829
4830 Some(match self.zoomed_position {
4831 Some(DockPosition::Left) => div.right_2().border_r_1(),
4832 Some(DockPosition::Right) => div.left_2().border_l_1(),
4833 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4834 None => div.top_2().bottom_2().left_2().right_2().border_1(),
4835 })
4836 }))
4837 .child(self.modal_layer.clone())
4838 .children(self.render_notifications(cx)),
4839 )
4840 .child(self.status_bar.clone())
4841 .children(if self.project.read(cx).is_disconnected() {
4842 if let Some(render) = self.render_disconnected_overlay.take() {
4843 let result = render(self, cx);
4844 self.render_disconnected_overlay = Some(render);
4845 Some(result)
4846 } else {
4847 None
4848 }
4849 } else {
4850 None
4851 }),
4852 cx,
4853 )
4854 }
4855}
4856
4857impl WorkspaceStore {
4858 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4859 Self {
4860 workspaces: Default::default(),
4861 _subscriptions: vec![
4862 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4863 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4864 ],
4865 client,
4866 }
4867 }
4868
4869 pub fn update_followers(
4870 &self,
4871 project_id: Option<u64>,
4872 update: proto::update_followers::Variant,
4873 cx: &AppContext,
4874 ) -> Option<()> {
4875 let active_call = ActiveCall::try_global(cx)?;
4876 let room_id = active_call.read(cx).room()?.read(cx).id();
4877 self.client
4878 .send(proto::UpdateFollowers {
4879 room_id,
4880 project_id,
4881 variant: Some(update),
4882 })
4883 .log_err()
4884 }
4885
4886 pub async fn handle_follow(
4887 this: Model<Self>,
4888 envelope: TypedEnvelope<proto::Follow>,
4889 mut cx: AsyncAppContext,
4890 ) -> Result<proto::FollowResponse> {
4891 this.update(&mut cx, |this, cx| {
4892 let follower = Follower {
4893 project_id: envelope.payload.project_id,
4894 peer_id: envelope.original_sender_id()?,
4895 };
4896
4897 let mut response = proto::FollowResponse::default();
4898 this.workspaces.retain(|workspace| {
4899 workspace
4900 .update(cx, |workspace, cx| {
4901 let handler_response = workspace.handle_follow(follower.project_id, cx);
4902 if let Some(active_view) = handler_response.active_view.clone() {
4903 if workspace.project.read(cx).remote_id() == follower.project_id {
4904 response.active_view = Some(active_view)
4905 }
4906 }
4907 })
4908 .is_ok()
4909 });
4910
4911 Ok(response)
4912 })?
4913 }
4914
4915 async fn handle_update_followers(
4916 this: Model<Self>,
4917 envelope: TypedEnvelope<proto::UpdateFollowers>,
4918 mut cx: AsyncAppContext,
4919 ) -> Result<()> {
4920 let leader_id = envelope.original_sender_id()?;
4921 let update = envelope.payload;
4922
4923 this.update(&mut cx, |this, cx| {
4924 this.workspaces.retain(|workspace| {
4925 workspace
4926 .update(cx, |workspace, cx| {
4927 let project_id = workspace.project.read(cx).remote_id();
4928 if update.project_id != project_id && update.project_id.is_some() {
4929 return;
4930 }
4931 workspace.handle_update_followers(leader_id, update.clone(), cx);
4932 })
4933 .is_ok()
4934 });
4935 Ok(())
4936 })?
4937 }
4938}
4939
4940impl ViewId {
4941 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4942 Ok(Self {
4943 creator: message
4944 .creator
4945 .ok_or_else(|| anyhow!("creator is missing"))?,
4946 id: message.id,
4947 })
4948 }
4949
4950 pub(crate) fn to_proto(&self) -> proto::ViewId {
4951 proto::ViewId {
4952 creator: Some(self.creator),
4953 id: self.id,
4954 }
4955 }
4956}
4957
4958impl FollowerState {
4959 fn pane(&self) -> &View<Pane> {
4960 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
4961 }
4962}
4963
4964pub trait WorkspaceHandle {
4965 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4966}
4967
4968impl WorkspaceHandle for View<Workspace> {
4969 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4970 self.read(cx)
4971 .worktrees(cx)
4972 .flat_map(|worktree| {
4973 let worktree_id = worktree.read(cx).id();
4974 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4975 worktree_id,
4976 path: f.path.clone(),
4977 })
4978 })
4979 .collect::<Vec<_>>()
4980 }
4981}
4982
4983impl std::fmt::Debug for OpenPaths {
4984 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4985 f.debug_struct("OpenPaths")
4986 .field("paths", &self.paths)
4987 .finish()
4988 }
4989}
4990
4991pub fn activate_workspace_for_project(
4992 cx: &mut AppContext,
4993 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4994) -> Option<WindowHandle<Workspace>> {
4995 for window in cx.windows() {
4996 let Some(workspace) = window.downcast::<Workspace>() else {
4997 continue;
4998 };
4999
5000 let predicate = workspace
5001 .update(cx, |workspace, cx| {
5002 let project = workspace.project.read(cx);
5003 if predicate(project, cx) {
5004 cx.activate_window();
5005 true
5006 } else {
5007 false
5008 }
5009 })
5010 .log_err()
5011 .unwrap_or(false);
5012
5013 if predicate {
5014 return Some(workspace);
5015 }
5016 }
5017
5018 None
5019}
5020
5021pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
5022 DB.last_workspace().await.log_err().flatten()
5023}
5024
5025pub fn last_session_workspace_locations(
5026 last_session_id: &str,
5027 last_session_window_stack: Option<Vec<WindowId>>,
5028) -> Option<Vec<LocalPaths>> {
5029 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5030 .log_err()
5031}
5032
5033actions!(collab, [OpenChannelNotes]);
5034actions!(zed, [OpenLog]);
5035
5036async fn join_channel_internal(
5037 channel_id: ChannelId,
5038 app_state: &Arc<AppState>,
5039 requesting_window: Option<WindowHandle<Workspace>>,
5040 active_call: &Model<ActiveCall>,
5041 cx: &mut AsyncAppContext,
5042) -> Result<bool> {
5043 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5044 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5045 return (false, None);
5046 };
5047
5048 let already_in_channel = room.channel_id() == Some(channel_id);
5049 let should_prompt = room.is_sharing_project()
5050 && room.remote_participants().len() > 0
5051 && !already_in_channel;
5052 let open_room = if already_in_channel {
5053 active_call.room().cloned()
5054 } else {
5055 None
5056 };
5057 (should_prompt, open_room)
5058 })?;
5059
5060 if let Some(room) = open_room {
5061 let task = room.update(cx, |room, cx| {
5062 if let Some((project, host)) = room.most_active_project(cx) {
5063 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5064 }
5065
5066 None
5067 })?;
5068 if let Some(task) = task {
5069 task.await?;
5070 }
5071 return anyhow::Ok(true);
5072 }
5073
5074 if should_prompt {
5075 if let Some(workspace) = requesting_window {
5076 let answer = workspace
5077 .update(cx, |_, cx| {
5078 cx.prompt(
5079 PromptLevel::Warning,
5080 "Do you want to switch channels?",
5081 Some("Leaving this call will unshare your current project."),
5082 &["Yes, Join Channel", "Cancel"],
5083 )
5084 })?
5085 .await;
5086
5087 if answer == Ok(1) {
5088 return Ok(false);
5089 }
5090 } else {
5091 return Ok(false); // unreachable!() hopefully
5092 }
5093 }
5094
5095 let client = cx.update(|cx| active_call.read(cx).client())?;
5096
5097 let mut client_status = client.status();
5098
5099 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5100 'outer: loop {
5101 let Some(status) = client_status.recv().await else {
5102 return Err(anyhow!("error connecting"));
5103 };
5104
5105 match status {
5106 Status::Connecting
5107 | Status::Authenticating
5108 | Status::Reconnecting
5109 | Status::Reauthenticating => continue,
5110 Status::Connected { .. } => break 'outer,
5111 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5112 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5113 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5114 return Err(ErrorCode::Disconnected.into());
5115 }
5116 }
5117 }
5118
5119 let room = active_call
5120 .update(cx, |active_call, cx| {
5121 active_call.join_channel(channel_id, cx)
5122 })?
5123 .await?;
5124
5125 let Some(room) = room else {
5126 return anyhow::Ok(true);
5127 };
5128
5129 room.update(cx, |room, _| room.room_update_completed())?
5130 .await;
5131
5132 let task = room.update(cx, |room, cx| {
5133 if let Some((project, host)) = room.most_active_project(cx) {
5134 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5135 }
5136
5137 // If you are the first to join a channel, see if you should share your project.
5138 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5139 if let Some(workspace) = requesting_window {
5140 let project = workspace.update(cx, |workspace, cx| {
5141 let project = workspace.project.read(cx);
5142 let is_dev_server = project.dev_server_project_id().is_some();
5143
5144 if !is_dev_server && !CallSettings::get_global(cx).share_on_join {
5145 return None;
5146 }
5147
5148 if (project.is_local_or_ssh() || is_dev_server)
5149 && project.visible_worktrees(cx).any(|tree| {
5150 tree.read(cx)
5151 .root_entry()
5152 .map_or(false, |entry| entry.is_dir())
5153 })
5154 {
5155 Some(workspace.project.clone())
5156 } else {
5157 None
5158 }
5159 });
5160 if let Ok(Some(project)) = project {
5161 return Some(cx.spawn(|room, mut cx| async move {
5162 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5163 .await?;
5164 Ok(())
5165 }));
5166 }
5167 }
5168 }
5169
5170 None
5171 })?;
5172 if let Some(task) = task {
5173 task.await?;
5174 return anyhow::Ok(true);
5175 }
5176 anyhow::Ok(false)
5177}
5178
5179pub fn join_channel(
5180 channel_id: ChannelId,
5181 app_state: Arc<AppState>,
5182 requesting_window: Option<WindowHandle<Workspace>>,
5183 cx: &mut AppContext,
5184) -> Task<Result<()>> {
5185 let active_call = ActiveCall::global(cx);
5186 cx.spawn(|mut cx| async move {
5187 let result = join_channel_internal(
5188 channel_id,
5189 &app_state,
5190 requesting_window,
5191 &active_call,
5192 &mut cx,
5193 )
5194 .await;
5195
5196 // join channel succeeded, and opened a window
5197 if matches!(result, Ok(true)) {
5198 return anyhow::Ok(());
5199 }
5200
5201 // find an existing workspace to focus and show call controls
5202 let mut active_window =
5203 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5204 if active_window.is_none() {
5205 // no open workspaces, make one to show the error in (blergh)
5206 let (window_handle, _) = cx
5207 .update(|cx| {
5208 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
5209 })?
5210 .await?;
5211
5212 if result.is_ok() {
5213 cx.update(|cx| {
5214 cx.dispatch_action(&OpenChannelNotes);
5215 }).log_err();
5216 }
5217
5218 active_window = Some(window_handle);
5219 }
5220
5221 if let Err(err) = result {
5222 log::error!("failed to join channel: {}", err);
5223 if let Some(active_window) = active_window {
5224 active_window
5225 .update(&mut cx, |_, cx| {
5226 let detail: SharedString = match err.error_code() {
5227 ErrorCode::SignedOut => {
5228 "Please sign in to continue.".into()
5229 }
5230 ErrorCode::UpgradeRequired => {
5231 "Your are running an unsupported version of Zed. Please update to continue.".into()
5232 }
5233 ErrorCode::NoSuchChannel => {
5234 "No matching channel was found. Please check the link and try again.".into()
5235 }
5236 ErrorCode::Forbidden => {
5237 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5238 }
5239 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5240 _ => format!("{}\n\nPlease try again.", err).into(),
5241 };
5242 cx.prompt(
5243 PromptLevel::Critical,
5244 "Failed to join channel",
5245 Some(&detail),
5246 &["Ok"],
5247 )
5248 })?
5249 .await
5250 .ok();
5251 }
5252 }
5253
5254 // return ok, we showed the error to the user.
5255 return anyhow::Ok(());
5256 })
5257}
5258
5259pub async fn get_any_active_workspace(
5260 app_state: Arc<AppState>,
5261 mut cx: AsyncAppContext,
5262) -> anyhow::Result<WindowHandle<Workspace>> {
5263 // find an existing workspace to focus and show call controls
5264 let active_window = activate_any_workspace_window(&mut cx);
5265 if active_window.is_none() {
5266 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
5267 .await?;
5268 }
5269 activate_any_workspace_window(&mut cx).context("could not open zed")
5270}
5271
5272fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5273 cx.update(|cx| {
5274 if let Some(workspace_window) = cx
5275 .active_window()
5276 .and_then(|window| window.downcast::<Workspace>())
5277 {
5278 return Some(workspace_window);
5279 }
5280
5281 for window in cx.windows() {
5282 if let Some(workspace_window) = window.downcast::<Workspace>() {
5283 workspace_window
5284 .update(cx, |_, cx| cx.activate_window())
5285 .ok();
5286 return Some(workspace_window);
5287 }
5288 }
5289 None
5290 })
5291 .ok()
5292 .flatten()
5293}
5294
5295pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5296 cx.windows()
5297 .into_iter()
5298 .filter_map(|window| window.downcast::<Workspace>())
5299 .filter(|workspace| {
5300 workspace
5301 .read(cx)
5302 .is_ok_and(|workspace| workspace.project.read(cx).is_local_or_ssh())
5303 })
5304 .collect()
5305}
5306
5307#[derive(Default)]
5308pub struct OpenOptions {
5309 pub open_new_workspace: Option<bool>,
5310 pub replace_window: Option<WindowHandle<Workspace>>,
5311}
5312
5313#[allow(clippy::type_complexity)]
5314pub fn open_paths(
5315 abs_paths: &[PathBuf],
5316 app_state: Arc<AppState>,
5317 open_options: OpenOptions,
5318 cx: &mut AppContext,
5319) -> Task<
5320 anyhow::Result<(
5321 WindowHandle<Workspace>,
5322 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5323 )>,
5324> {
5325 let abs_paths = abs_paths.to_vec();
5326 let mut existing = None;
5327 let mut best_match = None;
5328 let mut open_visible = OpenVisible::All;
5329
5330 if open_options.open_new_workspace != Some(true) {
5331 for window in local_workspace_windows(cx) {
5332 if let Ok(workspace) = window.read(cx) {
5333 let m = workspace
5334 .project
5335 .read(cx)
5336 .visibility_for_paths(&abs_paths, cx);
5337 if m > best_match {
5338 existing = Some(window);
5339 best_match = m;
5340 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5341 existing = Some(window)
5342 }
5343 }
5344 }
5345 }
5346
5347 cx.spawn(move |mut cx| async move {
5348 if open_options.open_new_workspace.is_none() && existing.is_none() {
5349 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5350 if futures::future::join_all(all_files)
5351 .await
5352 .into_iter()
5353 .filter_map(|result| result.ok().flatten())
5354 .all(|file| !file.is_dir)
5355 {
5356 cx.update(|cx| {
5357 for window in local_workspace_windows(cx) {
5358 if let Ok(workspace) = window.read(cx) {
5359 let project = workspace.project().read(cx);
5360 if project.is_via_collab() {
5361 continue;
5362 }
5363 existing = Some(window);
5364 open_visible = OpenVisible::None;
5365 break;
5366 }
5367 }
5368 })?;
5369 }
5370 }
5371
5372 if let Some(existing) = existing {
5373 Ok((
5374 existing,
5375 existing
5376 .update(&mut cx, |workspace, cx| {
5377 cx.activate_window();
5378 workspace.open_paths(abs_paths, open_visible, None, cx)
5379 })?
5380 .await,
5381 ))
5382 } else {
5383 cx.update(move |cx| {
5384 Workspace::new_local(
5385 abs_paths,
5386 app_state.clone(),
5387 open_options.replace_window,
5388 cx,
5389 )
5390 })?
5391 .await
5392 }
5393 })
5394}
5395
5396pub fn open_new(
5397 app_state: Arc<AppState>,
5398 cx: &mut AppContext,
5399 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5400) -> Task<anyhow::Result<()>> {
5401 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
5402 cx.spawn(|mut cx| async move {
5403 let (workspace, opened_paths) = task.await?;
5404 workspace.update(&mut cx, |workspace, cx| {
5405 if opened_paths.is_empty() {
5406 init(workspace, cx)
5407 }
5408 })?;
5409 Ok(())
5410 })
5411}
5412
5413pub fn create_and_open_local_file(
5414 path: &'static Path,
5415 cx: &mut ViewContext<Workspace>,
5416 default_content: impl 'static + Send + FnOnce() -> Rope,
5417) -> Task<Result<Box<dyn ItemHandle>>> {
5418 cx.spawn(|workspace, mut cx| async move {
5419 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5420 if !fs.is_file(path).await {
5421 fs.create_file(path, Default::default()).await?;
5422 fs.save(path, &default_content(), Default::default())
5423 .await?;
5424 }
5425
5426 let mut items = workspace
5427 .update(&mut cx, |workspace, cx| {
5428 workspace.with_local_workspace(cx, |workspace, cx| {
5429 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5430 })
5431 })?
5432 .await?
5433 .await;
5434
5435 let item = items.pop().flatten();
5436 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5437 })
5438}
5439
5440pub fn join_hosted_project(
5441 hosted_project_id: ProjectId,
5442 app_state: Arc<AppState>,
5443 cx: &mut AppContext,
5444) -> Task<Result<()>> {
5445 cx.spawn(|mut cx| async move {
5446 let existing_window = cx.update(|cx| {
5447 cx.windows().into_iter().find_map(|window| {
5448 let workspace = window.downcast::<Workspace>()?;
5449 workspace
5450 .read(cx)
5451 .is_ok_and(|workspace| {
5452 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
5453 })
5454 .then(|| workspace)
5455 })
5456 })?;
5457
5458 let workspace = if let Some(existing_window) = existing_window {
5459 existing_window
5460 } else {
5461 let project = Project::hosted(
5462 hosted_project_id,
5463 app_state.user_store.clone(),
5464 app_state.client.clone(),
5465 app_state.languages.clone(),
5466 app_state.fs.clone(),
5467 cx.clone(),
5468 )
5469 .await?;
5470
5471 let window_bounds_override = window_bounds_env_override();
5472 cx.update(|cx| {
5473 let mut options = (app_state.build_window_options)(None, cx);
5474 options.window_bounds =
5475 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5476 cx.open_window(options, |cx| {
5477 cx.new_view(|cx| {
5478 Workspace::new(Default::default(), project, app_state.clone(), cx)
5479 })
5480 })
5481 })??
5482 };
5483
5484 workspace.update(&mut cx, |_, cx| {
5485 cx.activate(true);
5486 cx.activate_window();
5487 })?;
5488
5489 Ok(())
5490 })
5491}
5492
5493pub fn join_dev_server_project(
5494 dev_server_project_id: DevServerProjectId,
5495 project_id: ProjectId,
5496 app_state: Arc<AppState>,
5497 window_to_replace: Option<WindowHandle<Workspace>>,
5498 cx: &mut AppContext,
5499) -> Task<Result<WindowHandle<Workspace>>> {
5500 let windows = cx.windows();
5501 cx.spawn(|mut cx| async move {
5502 let existing_workspace = windows.into_iter().find_map(|window| {
5503 window.downcast::<Workspace>().and_then(|window| {
5504 window
5505 .update(&mut cx, |workspace, cx| {
5506 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
5507 Some(window)
5508 } else {
5509 None
5510 }
5511 })
5512 .unwrap_or(None)
5513 })
5514 });
5515
5516 let workspace = if let Some(existing_workspace) = existing_workspace {
5517 existing_workspace
5518 } else {
5519 let project = Project::remote(
5520 project_id.0,
5521 app_state.client.clone(),
5522 app_state.user_store.clone(),
5523 app_state.languages.clone(),
5524 app_state.fs.clone(),
5525 cx.clone(),
5526 )
5527 .await?;
5528
5529 let serialized_workspace: Option<SerializedWorkspace> =
5530 persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
5531
5532 let workspace_id = if let Some(serialized_workspace) = serialized_workspace {
5533 serialized_workspace.id
5534 } else {
5535 persistence::DB.next_id().await?
5536 };
5537
5538 if let Some(window_to_replace) = window_to_replace {
5539 cx.update_window(window_to_replace.into(), |_, cx| {
5540 cx.replace_root_view(|cx| {
5541 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5542 });
5543 })?;
5544 window_to_replace
5545 } else {
5546 let window_bounds_override = window_bounds_env_override();
5547 cx.update(|cx| {
5548 let mut options = (app_state.build_window_options)(None, cx);
5549 options.window_bounds =
5550 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5551 cx.open_window(options, |cx| {
5552 cx.new_view(|cx| {
5553 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5554 })
5555 })
5556 })??
5557 }
5558 };
5559
5560 workspace.update(&mut cx, |_, cx| {
5561 cx.activate(true);
5562 cx.activate_window();
5563 })?;
5564
5565 anyhow::Ok(workspace)
5566 })
5567}
5568
5569pub fn join_in_room_project(
5570 project_id: u64,
5571 follow_user_id: u64,
5572 app_state: Arc<AppState>,
5573 cx: &mut AppContext,
5574) -> Task<Result<()>> {
5575 let windows = cx.windows();
5576 cx.spawn(|mut cx| async move {
5577 let existing_workspace = windows.into_iter().find_map(|window| {
5578 window.downcast::<Workspace>().and_then(|window| {
5579 window
5580 .update(&mut cx, |workspace, cx| {
5581 if workspace.project().read(cx).remote_id() == Some(project_id) {
5582 Some(window)
5583 } else {
5584 None
5585 }
5586 })
5587 .unwrap_or(None)
5588 })
5589 });
5590
5591 let workspace = if let Some(existing_workspace) = existing_workspace {
5592 existing_workspace
5593 } else {
5594 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5595 let room = active_call
5596 .read_with(&cx, |call, _| call.room().cloned())?
5597 .ok_or_else(|| anyhow!("not in a call"))?;
5598 let project = room
5599 .update(&mut cx, |room, cx| {
5600 room.join_project(
5601 project_id,
5602 app_state.languages.clone(),
5603 app_state.fs.clone(),
5604 cx,
5605 )
5606 })?
5607 .await?;
5608
5609 let window_bounds_override = window_bounds_env_override();
5610 cx.update(|cx| {
5611 let mut options = (app_state.build_window_options)(None, cx);
5612 options.window_bounds =
5613 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5614 cx.open_window(options, |cx| {
5615 cx.new_view(|cx| {
5616 Workspace::new(Default::default(), project, app_state.clone(), cx)
5617 })
5618 })
5619 })??
5620 };
5621
5622 workspace.update(&mut cx, |workspace, cx| {
5623 cx.activate(true);
5624 cx.activate_window();
5625
5626 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5627 let follow_peer_id = room
5628 .read(cx)
5629 .remote_participants()
5630 .iter()
5631 .find(|(_, participant)| participant.user.id == follow_user_id)
5632 .map(|(_, p)| p.peer_id)
5633 .or_else(|| {
5634 // If we couldn't follow the given user, follow the host instead.
5635 let collaborator = workspace
5636 .project()
5637 .read(cx)
5638 .collaborators()
5639 .values()
5640 .find(|collaborator| collaborator.replica_id == 0)?;
5641 Some(collaborator.peer_id)
5642 });
5643
5644 if let Some(follow_peer_id) = follow_peer_id {
5645 workspace.follow(follow_peer_id, cx);
5646 }
5647 }
5648 })?;
5649
5650 anyhow::Ok(())
5651 })
5652}
5653
5654pub fn reload(reload: &Reload, cx: &mut AppContext) {
5655 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5656 let mut workspace_windows = cx
5657 .windows()
5658 .into_iter()
5659 .filter_map(|window| window.downcast::<Workspace>())
5660 .collect::<Vec<_>>();
5661
5662 // If multiple windows have unsaved changes, and need a save prompt,
5663 // prompt in the active window before switching to a different window.
5664 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5665
5666 let mut prompt = None;
5667 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5668 prompt = window
5669 .update(cx, |_, cx| {
5670 cx.prompt(
5671 PromptLevel::Info,
5672 "Are you sure you want to restart?",
5673 None,
5674 &["Restart", "Cancel"],
5675 )
5676 })
5677 .ok();
5678 }
5679
5680 let binary_path = reload.binary_path.clone();
5681 cx.spawn(|mut cx| async move {
5682 if let Some(prompt) = prompt {
5683 let answer = prompt.await?;
5684 if answer != 0 {
5685 return Ok(());
5686 }
5687 }
5688
5689 // If the user cancels any save prompt, then keep the app open.
5690 for window in workspace_windows {
5691 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5692 workspace.prepare_to_close(CloseIntent::Quit, cx)
5693 }) {
5694 if !should_close.await? {
5695 return Ok(());
5696 }
5697 }
5698 }
5699
5700 cx.update(|cx| cx.restart(binary_path))
5701 })
5702 .detach_and_log_err(cx);
5703}
5704
5705fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5706 let mut parts = value.split(',');
5707 let x: usize = parts.next()?.parse().ok()?;
5708 let y: usize = parts.next()?.parse().ok()?;
5709 Some(point(px(x as f32), px(y as f32)))
5710}
5711
5712fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5713 let mut parts = value.split(',');
5714 let width: usize = parts.next()?.parse().ok()?;
5715 let height: usize = parts.next()?.parse().ok()?;
5716 Some(size(px(width as f32), px(height as f32)))
5717}
5718
5719#[cfg(test)]
5720mod tests {
5721 use std::{cell::RefCell, rc::Rc};
5722
5723 use super::*;
5724 use crate::{
5725 dock::{test::TestPanel, PanelEvent},
5726 item::{
5727 test::{TestItem, TestProjectItem},
5728 ItemEvent,
5729 },
5730 };
5731 use fs::FakeFs;
5732 use gpui::{
5733 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
5734 UpdateGlobal, VisualTestContext,
5735 };
5736 use project::{Project, ProjectEntryId};
5737 use serde_json::json;
5738 use settings::SettingsStore;
5739
5740 #[gpui::test]
5741 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
5742 init_test(cx);
5743
5744 let fs = FakeFs::new(cx.executor());
5745 let project = Project::test(fs, [], cx).await;
5746 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5747
5748 // Adding an item with no ambiguity renders the tab without detail.
5749 let item1 = cx.new_view(|cx| {
5750 let mut item = TestItem::new(cx);
5751 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
5752 item
5753 });
5754 workspace.update(cx, |workspace, cx| {
5755 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
5756 });
5757 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
5758
5759 // Adding an item that creates ambiguity increases the level of detail on
5760 // both tabs.
5761 let item2 = cx.new_view(|cx| {
5762 let mut item = TestItem::new(cx);
5763 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5764 item
5765 });
5766 workspace.update(cx, |workspace, cx| {
5767 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
5768 });
5769 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5770 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5771
5772 // Adding an item that creates ambiguity increases the level of detail only
5773 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
5774 // we stop at the highest detail available.
5775 let item3 = cx.new_view(|cx| {
5776 let mut item = TestItem::new(cx);
5777 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5778 item
5779 });
5780 workspace.update(cx, |workspace, cx| {
5781 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
5782 });
5783 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5784 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5785 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5786 }
5787
5788 #[gpui::test]
5789 async fn test_tracking_active_path(cx: &mut TestAppContext) {
5790 init_test(cx);
5791
5792 let fs = FakeFs::new(cx.executor());
5793 fs.insert_tree(
5794 "/root1",
5795 json!({
5796 "one.txt": "",
5797 "two.txt": "",
5798 }),
5799 )
5800 .await;
5801 fs.insert_tree(
5802 "/root2",
5803 json!({
5804 "three.txt": "",
5805 }),
5806 )
5807 .await;
5808
5809 let project = Project::test(fs, ["root1".as_ref()], cx).await;
5810 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5811 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5812 let worktree_id = project.update(cx, |project, cx| {
5813 project.worktrees(cx).next().unwrap().read(cx).id()
5814 });
5815
5816 let item1 = cx.new_view(|cx| {
5817 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5818 });
5819 let item2 = cx.new_view(|cx| {
5820 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
5821 });
5822
5823 // Add an item to an empty pane
5824 workspace.update(cx, |workspace, cx| {
5825 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
5826 });
5827 project.update(cx, |project, cx| {
5828 assert_eq!(
5829 project.active_entry(),
5830 project
5831 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5832 .map(|e| e.id)
5833 );
5834 });
5835 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5836
5837 // Add a second item to a non-empty pane
5838 workspace.update(cx, |workspace, cx| {
5839 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
5840 });
5841 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5842 project.update(cx, |project, cx| {
5843 assert_eq!(
5844 project.active_entry(),
5845 project
5846 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5847 .map(|e| e.id)
5848 );
5849 });
5850
5851 // Close the active item
5852 pane.update(cx, |pane, cx| {
5853 pane.close_active_item(&Default::default(), cx).unwrap()
5854 })
5855 .await
5856 .unwrap();
5857 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5858 project.update(cx, |project, cx| {
5859 assert_eq!(
5860 project.active_entry(),
5861 project
5862 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5863 .map(|e| e.id)
5864 );
5865 });
5866
5867 // Add a project folder
5868 project
5869 .update(cx, |project, cx| {
5870 project.find_or_create_worktree("root2", true, cx)
5871 })
5872 .await
5873 .unwrap();
5874 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5875
5876 // Remove a project folder
5877 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5878 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5879 }
5880
5881 #[gpui::test]
5882 async fn test_close_window(cx: &mut TestAppContext) {
5883 init_test(cx);
5884
5885 let fs = FakeFs::new(cx.executor());
5886 fs.insert_tree("/root", json!({ "one": "" })).await;
5887
5888 let project = Project::test(fs, ["root".as_ref()], cx).await;
5889 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5890
5891 // When there are no dirty items, there's nothing to do.
5892 let item1 = cx.new_view(|cx| TestItem::new(cx));
5893 workspace.update(cx, |w, cx| {
5894 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
5895 });
5896 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
5897 assert!(task.await.unwrap());
5898
5899 // When there are dirty untitled items, prompt to save each one. If the user
5900 // cancels any prompt, then abort.
5901 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5902 let item3 = cx.new_view(|cx| {
5903 TestItem::new(cx)
5904 .with_dirty(true)
5905 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5906 });
5907 workspace.update(cx, |w, cx| {
5908 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
5909 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
5910 });
5911 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
5912 cx.executor().run_until_parked();
5913 cx.simulate_prompt_answer(2); // cancel save all
5914 cx.executor().run_until_parked();
5915 cx.simulate_prompt_answer(2); // cancel save all
5916 cx.executor().run_until_parked();
5917 assert!(!cx.has_pending_prompt());
5918 assert!(!task.await.unwrap());
5919 }
5920
5921 #[gpui::test]
5922 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
5923 init_test(cx);
5924
5925 // Register TestItem as a serializable item
5926 cx.update(|cx| {
5927 register_serializable_item::<TestItem>(cx);
5928 });
5929
5930 let fs = FakeFs::new(cx.executor());
5931 fs.insert_tree("/root", json!({ "one": "" })).await;
5932
5933 let project = Project::test(fs, ["root".as_ref()], cx).await;
5934 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5935
5936 // When there are dirty untitled items, but they can serialize, then there is no prompt.
5937 let item1 = cx.new_view(|cx| {
5938 TestItem::new(cx)
5939 .with_dirty(true)
5940 .with_serialize(|| Some(Task::ready(Ok(()))))
5941 });
5942 let item2 = cx.new_view(|cx| {
5943 TestItem::new(cx)
5944 .with_dirty(true)
5945 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5946 .with_serialize(|| Some(Task::ready(Ok(()))))
5947 });
5948 workspace.update(cx, |w, cx| {
5949 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
5950 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
5951 });
5952 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
5953 assert!(task.await.unwrap());
5954 }
5955
5956 #[gpui::test]
5957 async fn test_close_pane_items(cx: &mut TestAppContext) {
5958 init_test(cx);
5959
5960 let fs = FakeFs::new(cx.executor());
5961
5962 let project = Project::test(fs, None, cx).await;
5963 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5964
5965 let item1 = cx.new_view(|cx| {
5966 TestItem::new(cx)
5967 .with_dirty(true)
5968 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5969 });
5970 let item2 = cx.new_view(|cx| {
5971 TestItem::new(cx)
5972 .with_dirty(true)
5973 .with_conflict(true)
5974 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5975 });
5976 let item3 = cx.new_view(|cx| {
5977 TestItem::new(cx)
5978 .with_dirty(true)
5979 .with_conflict(true)
5980 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5981 });
5982 let item4 = cx.new_view(|cx| {
5983 TestItem::new(cx)
5984 .with_dirty(true)
5985 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5986 });
5987 let pane = workspace.update(cx, |workspace, cx| {
5988 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
5989 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
5990 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
5991 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
5992 workspace.active_pane().clone()
5993 });
5994
5995 let close_items = pane.update(cx, |pane, cx| {
5996 pane.activate_item(1, true, true, cx);
5997 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5998 let item1_id = item1.item_id();
5999 let item3_id = item3.item_id();
6000 let item4_id = item4.item_id();
6001 pane.close_items(cx, SaveIntent::Close, move |id| {
6002 [item1_id, item3_id, item4_id].contains(&id)
6003 })
6004 });
6005 cx.executor().run_until_parked();
6006
6007 assert!(cx.has_pending_prompt());
6008 // Ignore "Save all" prompt
6009 cx.simulate_prompt_answer(2);
6010 cx.executor().run_until_parked();
6011 // There's a prompt to save item 1.
6012 pane.update(cx, |pane, _| {
6013 assert_eq!(pane.items_len(), 4);
6014 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6015 });
6016 // Confirm saving item 1.
6017 cx.simulate_prompt_answer(0);
6018 cx.executor().run_until_parked();
6019
6020 // Item 1 is saved. There's a prompt to save item 3.
6021 pane.update(cx, |pane, cx| {
6022 assert_eq!(item1.read(cx).save_count, 1);
6023 assert_eq!(item1.read(cx).save_as_count, 0);
6024 assert_eq!(item1.read(cx).reload_count, 0);
6025 assert_eq!(pane.items_len(), 3);
6026 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6027 });
6028 assert!(cx.has_pending_prompt());
6029
6030 // Cancel saving item 3.
6031 cx.simulate_prompt_answer(1);
6032 cx.executor().run_until_parked();
6033
6034 // Item 3 is reloaded. There's a prompt to save item 4.
6035 pane.update(cx, |pane, cx| {
6036 assert_eq!(item3.read(cx).save_count, 0);
6037 assert_eq!(item3.read(cx).save_as_count, 0);
6038 assert_eq!(item3.read(cx).reload_count, 1);
6039 assert_eq!(pane.items_len(), 2);
6040 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6041 });
6042 assert!(cx.has_pending_prompt());
6043
6044 // Confirm saving item 4.
6045 cx.simulate_prompt_answer(0);
6046 cx.executor().run_until_parked();
6047
6048 // There's a prompt for a path for item 4.
6049 cx.simulate_new_path_selection(|_| Some(Default::default()));
6050 close_items.await.unwrap();
6051
6052 // The requested items are closed.
6053 pane.update(cx, |pane, cx| {
6054 assert_eq!(item4.read(cx).save_count, 0);
6055 assert_eq!(item4.read(cx).save_as_count, 1);
6056 assert_eq!(item4.read(cx).reload_count, 0);
6057 assert_eq!(pane.items_len(), 1);
6058 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6059 });
6060 }
6061
6062 #[gpui::test]
6063 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6064 init_test(cx);
6065
6066 let fs = FakeFs::new(cx.executor());
6067 let project = Project::test(fs, [], cx).await;
6068 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6069
6070 // Create several workspace items with single project entries, and two
6071 // workspace items with multiple project entries.
6072 let single_entry_items = (0..=4)
6073 .map(|project_entry_id| {
6074 cx.new_view(|cx| {
6075 TestItem::new(cx)
6076 .with_dirty(true)
6077 .with_project_items(&[TestProjectItem::new(
6078 project_entry_id,
6079 &format!("{project_entry_id}.txt"),
6080 cx,
6081 )])
6082 })
6083 })
6084 .collect::<Vec<_>>();
6085 let item_2_3 = cx.new_view(|cx| {
6086 TestItem::new(cx)
6087 .with_dirty(true)
6088 .with_singleton(false)
6089 .with_project_items(&[
6090 single_entry_items[2].read(cx).project_items[0].clone(),
6091 single_entry_items[3].read(cx).project_items[0].clone(),
6092 ])
6093 });
6094 let item_3_4 = cx.new_view(|cx| {
6095 TestItem::new(cx)
6096 .with_dirty(true)
6097 .with_singleton(false)
6098 .with_project_items(&[
6099 single_entry_items[3].read(cx).project_items[0].clone(),
6100 single_entry_items[4].read(cx).project_items[0].clone(),
6101 ])
6102 });
6103
6104 // Create two panes that contain the following project entries:
6105 // left pane:
6106 // multi-entry items: (2, 3)
6107 // single-entry items: 0, 1, 2, 3, 4
6108 // right pane:
6109 // single-entry items: 1
6110 // multi-entry items: (3, 4)
6111 let left_pane = workspace.update(cx, |workspace, cx| {
6112 let left_pane = workspace.active_pane().clone();
6113 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6114 for item in single_entry_items {
6115 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6116 }
6117 left_pane.update(cx, |pane, cx| {
6118 pane.activate_item(2, true, true, cx);
6119 });
6120
6121 let right_pane = workspace
6122 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6123 .unwrap();
6124
6125 right_pane.update(cx, |pane, cx| {
6126 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6127 });
6128
6129 left_pane
6130 });
6131
6132 cx.focus_view(&left_pane);
6133
6134 // When closing all of the items in the left pane, we should be prompted twice:
6135 // once for project entry 0, and once for project entry 2. Project entries 1,
6136 // 3, and 4 are all still open in the other paten. After those two
6137 // prompts, the task should complete.
6138
6139 let close = left_pane.update(cx, |pane, cx| {
6140 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6141 });
6142 cx.executor().run_until_parked();
6143
6144 // Discard "Save all" prompt
6145 cx.simulate_prompt_answer(2);
6146
6147 cx.executor().run_until_parked();
6148 left_pane.update(cx, |pane, cx| {
6149 assert_eq!(
6150 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6151 &[ProjectEntryId::from_proto(0)]
6152 );
6153 });
6154 cx.simulate_prompt_answer(0);
6155
6156 cx.executor().run_until_parked();
6157 left_pane.update(cx, |pane, cx| {
6158 assert_eq!(
6159 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6160 &[ProjectEntryId::from_proto(2)]
6161 );
6162 });
6163 cx.simulate_prompt_answer(0);
6164
6165 cx.executor().run_until_parked();
6166 close.await.unwrap();
6167 left_pane.update(cx, |pane, _| {
6168 assert_eq!(pane.items_len(), 0);
6169 });
6170 }
6171
6172 #[gpui::test]
6173 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6174 init_test(cx);
6175
6176 let fs = FakeFs::new(cx.executor());
6177 let project = Project::test(fs, [], cx).await;
6178 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6179 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6180
6181 let item = cx.new_view(|cx| {
6182 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6183 });
6184 let item_id = item.entity_id();
6185 workspace.update(cx, |workspace, cx| {
6186 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6187 });
6188
6189 // Autosave on window change.
6190 item.update(cx, |item, cx| {
6191 SettingsStore::update_global(cx, |settings, cx| {
6192 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6193 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6194 })
6195 });
6196 item.is_dirty = true;
6197 });
6198
6199 // Deactivating the window saves the file.
6200 cx.deactivate_window();
6201 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6202
6203 // Re-activating the window doesn't save the file.
6204 cx.update(|cx| cx.activate_window());
6205 cx.executor().run_until_parked();
6206 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6207
6208 // Autosave on focus change.
6209 item.update(cx, |item, cx| {
6210 cx.focus_self();
6211 SettingsStore::update_global(cx, |settings, cx| {
6212 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6213 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6214 })
6215 });
6216 item.is_dirty = true;
6217 });
6218
6219 // Blurring the item saves the file.
6220 item.update(cx, |_, cx| cx.blur());
6221 cx.executor().run_until_parked();
6222 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6223
6224 // Deactivating the window still saves the file.
6225 item.update(cx, |item, cx| {
6226 cx.focus_self();
6227 item.is_dirty = true;
6228 });
6229 cx.deactivate_window();
6230 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6231
6232 // Autosave after delay.
6233 item.update(cx, |item, cx| {
6234 SettingsStore::update_global(cx, |settings, cx| {
6235 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6236 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6237 })
6238 });
6239 item.is_dirty = true;
6240 cx.emit(ItemEvent::Edit);
6241 });
6242
6243 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6244 cx.executor().advance_clock(Duration::from_millis(250));
6245 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6246
6247 // After delay expires, the file is saved.
6248 cx.executor().advance_clock(Duration::from_millis(250));
6249 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6250
6251 // Autosave on focus change, ensuring closing the tab counts as such.
6252 item.update(cx, |item, cx| {
6253 SettingsStore::update_global(cx, |settings, cx| {
6254 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6255 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6256 })
6257 });
6258 item.is_dirty = true;
6259 });
6260
6261 pane.update(cx, |pane, cx| {
6262 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6263 })
6264 .await
6265 .unwrap();
6266 assert!(!cx.has_pending_prompt());
6267 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6268
6269 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6270 workspace.update(cx, |workspace, cx| {
6271 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6272 });
6273 item.update(cx, |item, cx| {
6274 item.project_items[0].update(cx, |item, _| {
6275 item.entry_id = None;
6276 });
6277 item.is_dirty = true;
6278 cx.blur();
6279 });
6280 cx.run_until_parked();
6281 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6282
6283 // Ensure autosave is prevented for deleted files also when closing the buffer.
6284 let _close_items = pane.update(cx, |pane, cx| {
6285 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6286 });
6287 cx.run_until_parked();
6288 assert!(cx.has_pending_prompt());
6289 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6290 }
6291
6292 #[gpui::test]
6293 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6294 init_test(cx);
6295
6296 let fs = FakeFs::new(cx.executor());
6297
6298 let project = Project::test(fs, [], cx).await;
6299 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6300
6301 let item = cx.new_view(|cx| {
6302 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6303 });
6304 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6305 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6306 let toolbar_notify_count = Rc::new(RefCell::new(0));
6307
6308 workspace.update(cx, |workspace, cx| {
6309 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6310 let toolbar_notification_count = toolbar_notify_count.clone();
6311 cx.observe(&toolbar, move |_, _, _| {
6312 *toolbar_notification_count.borrow_mut() += 1
6313 })
6314 .detach();
6315 });
6316
6317 pane.update(cx, |pane, _| {
6318 assert!(!pane.can_navigate_backward());
6319 assert!(!pane.can_navigate_forward());
6320 });
6321
6322 item.update(cx, |item, cx| {
6323 item.set_state("one".to_string(), cx);
6324 });
6325
6326 // Toolbar must be notified to re-render the navigation buttons
6327 assert_eq!(*toolbar_notify_count.borrow(), 1);
6328
6329 pane.update(cx, |pane, _| {
6330 assert!(pane.can_navigate_backward());
6331 assert!(!pane.can_navigate_forward());
6332 });
6333
6334 workspace
6335 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6336 .await
6337 .unwrap();
6338
6339 assert_eq!(*toolbar_notify_count.borrow(), 2);
6340 pane.update(cx, |pane, _| {
6341 assert!(!pane.can_navigate_backward());
6342 assert!(pane.can_navigate_forward());
6343 });
6344 }
6345
6346 #[gpui::test]
6347 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6348 init_test(cx);
6349 let fs = FakeFs::new(cx.executor());
6350
6351 let project = Project::test(fs, [], cx).await;
6352 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6353
6354 let panel = workspace.update(cx, |workspace, cx| {
6355 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6356 workspace.add_panel(panel.clone(), cx);
6357
6358 workspace
6359 .right_dock()
6360 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6361
6362 panel
6363 });
6364
6365 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6366 pane.update(cx, |pane, cx| {
6367 let item = cx.new_view(|cx| TestItem::new(cx));
6368 pane.add_item(Box::new(item), true, true, None, cx);
6369 });
6370
6371 // Transfer focus from center to panel
6372 workspace.update(cx, |workspace, cx| {
6373 workspace.toggle_panel_focus::<TestPanel>(cx);
6374 });
6375
6376 workspace.update(cx, |workspace, cx| {
6377 assert!(workspace.right_dock().read(cx).is_open());
6378 assert!(!panel.is_zoomed(cx));
6379 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6380 });
6381
6382 // Transfer focus from panel to center
6383 workspace.update(cx, |workspace, cx| {
6384 workspace.toggle_panel_focus::<TestPanel>(cx);
6385 });
6386
6387 workspace.update(cx, |workspace, cx| {
6388 assert!(workspace.right_dock().read(cx).is_open());
6389 assert!(!panel.is_zoomed(cx));
6390 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6391 });
6392
6393 // Close the dock
6394 workspace.update(cx, |workspace, cx| {
6395 workspace.toggle_dock(DockPosition::Right, cx);
6396 });
6397
6398 workspace.update(cx, |workspace, cx| {
6399 assert!(!workspace.right_dock().read(cx).is_open());
6400 assert!(!panel.is_zoomed(cx));
6401 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6402 });
6403
6404 // Open the dock
6405 workspace.update(cx, |workspace, cx| {
6406 workspace.toggle_dock(DockPosition::Right, cx);
6407 });
6408
6409 workspace.update(cx, |workspace, cx| {
6410 assert!(workspace.right_dock().read(cx).is_open());
6411 assert!(!panel.is_zoomed(cx));
6412 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6413 });
6414
6415 // Focus and zoom panel
6416 panel.update(cx, |panel, cx| {
6417 cx.focus_self();
6418 panel.set_zoomed(true, cx)
6419 });
6420
6421 workspace.update(cx, |workspace, cx| {
6422 assert!(workspace.right_dock().read(cx).is_open());
6423 assert!(panel.is_zoomed(cx));
6424 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6425 });
6426
6427 // Transfer focus to the center closes the dock
6428 workspace.update(cx, |workspace, cx| {
6429 workspace.toggle_panel_focus::<TestPanel>(cx);
6430 });
6431
6432 workspace.update(cx, |workspace, cx| {
6433 assert!(!workspace.right_dock().read(cx).is_open());
6434 assert!(panel.is_zoomed(cx));
6435 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6436 });
6437
6438 // Transferring focus back to the panel keeps it zoomed
6439 workspace.update(cx, |workspace, cx| {
6440 workspace.toggle_panel_focus::<TestPanel>(cx);
6441 });
6442
6443 workspace.update(cx, |workspace, cx| {
6444 assert!(workspace.right_dock().read(cx).is_open());
6445 assert!(panel.is_zoomed(cx));
6446 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6447 });
6448
6449 // Close the dock while it is zoomed
6450 workspace.update(cx, |workspace, cx| {
6451 workspace.toggle_dock(DockPosition::Right, cx)
6452 });
6453
6454 workspace.update(cx, |workspace, cx| {
6455 assert!(!workspace.right_dock().read(cx).is_open());
6456 assert!(panel.is_zoomed(cx));
6457 assert!(workspace.zoomed.is_none());
6458 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6459 });
6460
6461 // Opening the dock, when it's zoomed, retains focus
6462 workspace.update(cx, |workspace, cx| {
6463 workspace.toggle_dock(DockPosition::Right, cx)
6464 });
6465
6466 workspace.update(cx, |workspace, cx| {
6467 assert!(workspace.right_dock().read(cx).is_open());
6468 assert!(panel.is_zoomed(cx));
6469 assert!(workspace.zoomed.is_some());
6470 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6471 });
6472
6473 // Unzoom and close the panel, zoom the active pane.
6474 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6475 workspace.update(cx, |workspace, cx| {
6476 workspace.toggle_dock(DockPosition::Right, cx)
6477 });
6478 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6479
6480 // Opening a dock unzooms the pane.
6481 workspace.update(cx, |workspace, cx| {
6482 workspace.toggle_dock(DockPosition::Right, cx)
6483 });
6484 workspace.update(cx, |workspace, cx| {
6485 let pane = pane.read(cx);
6486 assert!(!pane.is_zoomed());
6487 assert!(!pane.focus_handle(cx).is_focused(cx));
6488 assert!(workspace.right_dock().read(cx).is_open());
6489 assert!(workspace.zoomed.is_none());
6490 });
6491 }
6492
6493 #[gpui::test]
6494 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6495 init_test(cx);
6496
6497 let fs = FakeFs::new(cx.executor());
6498
6499 let project = Project::test(fs, None, cx).await;
6500 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6501
6502 // Let's arrange the panes like this:
6503 //
6504 // +-----------------------+
6505 // | top |
6506 // +------+--------+-------+
6507 // | left | center | right |
6508 // +------+--------+-------+
6509 // | bottom |
6510 // +-----------------------+
6511
6512 let top_item = cx.new_view(|cx| {
6513 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6514 });
6515 let bottom_item = cx.new_view(|cx| {
6516 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6517 });
6518 let left_item = cx.new_view(|cx| {
6519 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6520 });
6521 let right_item = cx.new_view(|cx| {
6522 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6523 });
6524 let center_item = cx.new_view(|cx| {
6525 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6526 });
6527
6528 let top_pane_id = workspace.update(cx, |workspace, cx| {
6529 let top_pane_id = workspace.active_pane().entity_id();
6530 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6531 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6532 top_pane_id
6533 });
6534 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6535 let bottom_pane_id = workspace.active_pane().entity_id();
6536 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6537 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6538 bottom_pane_id
6539 });
6540 let left_pane_id = workspace.update(cx, |workspace, cx| {
6541 let left_pane_id = workspace.active_pane().entity_id();
6542 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6543 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6544 left_pane_id
6545 });
6546 let right_pane_id = workspace.update(cx, |workspace, cx| {
6547 let right_pane_id = workspace.active_pane().entity_id();
6548 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6549 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6550 right_pane_id
6551 });
6552 let center_pane_id = workspace.update(cx, |workspace, cx| {
6553 let center_pane_id = workspace.active_pane().entity_id();
6554 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6555 center_pane_id
6556 });
6557 cx.executor().run_until_parked();
6558
6559 workspace.update(cx, |workspace, cx| {
6560 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6561
6562 // Join into next from center pane into right
6563 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6564 });
6565
6566 workspace.update(cx, |workspace, cx| {
6567 let active_pane = workspace.active_pane();
6568 assert_eq!(right_pane_id, active_pane.entity_id());
6569 assert_eq!(2, active_pane.read(cx).items_len());
6570 let item_ids_in_pane =
6571 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6572 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6573 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6574
6575 // Join into next from right pane into bottom
6576 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6577 });
6578
6579 workspace.update(cx, |workspace, cx| {
6580 let active_pane = workspace.active_pane();
6581 assert_eq!(bottom_pane_id, active_pane.entity_id());
6582 assert_eq!(3, active_pane.read(cx).items_len());
6583 let item_ids_in_pane =
6584 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6585 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6586 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6587 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6588
6589 // Join into next from bottom pane into left
6590 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6591 });
6592
6593 workspace.update(cx, |workspace, cx| {
6594 let active_pane = workspace.active_pane();
6595 assert_eq!(left_pane_id, active_pane.entity_id());
6596 assert_eq!(4, active_pane.read(cx).items_len());
6597 let item_ids_in_pane =
6598 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6599 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6600 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6601 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6602 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6603
6604 // Join into next from left pane into top
6605 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6606 });
6607
6608 workspace.update(cx, |workspace, cx| {
6609 let active_pane = workspace.active_pane();
6610 assert_eq!(top_pane_id, active_pane.entity_id());
6611 assert_eq!(5, active_pane.read(cx).items_len());
6612 let item_ids_in_pane =
6613 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6614 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6615 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6616 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6617 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6618 assert!(item_ids_in_pane.contains(&top_item.item_id()));
6619
6620 // Single pane left: no-op
6621 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
6622 });
6623
6624 workspace.update(cx, |workspace, _cx| {
6625 let active_pane = workspace.active_pane();
6626 assert_eq!(top_pane_id, active_pane.entity_id());
6627 });
6628 }
6629
6630 struct TestModal(FocusHandle);
6631
6632 impl TestModal {
6633 fn new(cx: &mut ViewContext<Self>) -> Self {
6634 Self(cx.focus_handle())
6635 }
6636 }
6637
6638 impl EventEmitter<DismissEvent> for TestModal {}
6639
6640 impl FocusableView for TestModal {
6641 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6642 self.0.clone()
6643 }
6644 }
6645
6646 impl ModalView for TestModal {}
6647
6648 impl Render for TestModal {
6649 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
6650 div().track_focus(&self.0)
6651 }
6652 }
6653
6654 #[gpui::test]
6655 async fn test_panels(cx: &mut gpui::TestAppContext) {
6656 init_test(cx);
6657 let fs = FakeFs::new(cx.executor());
6658
6659 let project = Project::test(fs, [], cx).await;
6660 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6661
6662 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
6663 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
6664 workspace.add_panel(panel_1.clone(), cx);
6665 workspace
6666 .left_dock()
6667 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
6668 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6669 workspace.add_panel(panel_2.clone(), cx);
6670 workspace
6671 .right_dock()
6672 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6673
6674 let left_dock = workspace.left_dock();
6675 assert_eq!(
6676 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6677 panel_1.panel_id()
6678 );
6679 assert_eq!(
6680 left_dock.read(cx).active_panel_size(cx).unwrap(),
6681 panel_1.size(cx)
6682 );
6683
6684 left_dock.update(cx, |left_dock, cx| {
6685 left_dock.resize_active_panel(Some(px(1337.)), cx)
6686 });
6687 assert_eq!(
6688 workspace
6689 .right_dock()
6690 .read(cx)
6691 .visible_panel()
6692 .unwrap()
6693 .panel_id(),
6694 panel_2.panel_id(),
6695 );
6696
6697 (panel_1, panel_2)
6698 });
6699
6700 // Move panel_1 to the right
6701 panel_1.update(cx, |panel_1, cx| {
6702 panel_1.set_position(DockPosition::Right, cx)
6703 });
6704
6705 workspace.update(cx, |workspace, cx| {
6706 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
6707 // Since it was the only panel on the left, the left dock should now be closed.
6708 assert!(!workspace.left_dock().read(cx).is_open());
6709 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
6710 let right_dock = workspace.right_dock();
6711 assert_eq!(
6712 right_dock.read(cx).visible_panel().unwrap().panel_id(),
6713 panel_1.panel_id()
6714 );
6715 assert_eq!(
6716 right_dock.read(cx).active_panel_size(cx).unwrap(),
6717 px(1337.)
6718 );
6719
6720 // Now we move panel_2 to the left
6721 panel_2.set_position(DockPosition::Left, cx);
6722 });
6723
6724 workspace.update(cx, |workspace, cx| {
6725 // Since panel_2 was not visible on the right, we don't open the left dock.
6726 assert!(!workspace.left_dock().read(cx).is_open());
6727 // And the right dock is unaffected in its displaying of panel_1
6728 assert!(workspace.right_dock().read(cx).is_open());
6729 assert_eq!(
6730 workspace
6731 .right_dock()
6732 .read(cx)
6733 .visible_panel()
6734 .unwrap()
6735 .panel_id(),
6736 panel_1.panel_id(),
6737 );
6738 });
6739
6740 // Move panel_1 back to the left
6741 panel_1.update(cx, |panel_1, cx| {
6742 panel_1.set_position(DockPosition::Left, cx)
6743 });
6744
6745 workspace.update(cx, |workspace, cx| {
6746 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
6747 let left_dock = workspace.left_dock();
6748 assert!(left_dock.read(cx).is_open());
6749 assert_eq!(
6750 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6751 panel_1.panel_id()
6752 );
6753 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
6754 // And the right dock should be closed as it no longer has any panels.
6755 assert!(!workspace.right_dock().read(cx).is_open());
6756
6757 // Now we move panel_1 to the bottom
6758 panel_1.set_position(DockPosition::Bottom, cx);
6759 });
6760
6761 workspace.update(cx, |workspace, cx| {
6762 // Since panel_1 was visible on the left, we close the left dock.
6763 assert!(!workspace.left_dock().read(cx).is_open());
6764 // The bottom dock is sized based on the panel's default size,
6765 // since the panel orientation changed from vertical to horizontal.
6766 let bottom_dock = workspace.bottom_dock();
6767 assert_eq!(
6768 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
6769 panel_1.size(cx),
6770 );
6771 // Close bottom dock and move panel_1 back to the left.
6772 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
6773 panel_1.set_position(DockPosition::Left, cx);
6774 });
6775
6776 // Emit activated event on panel 1
6777 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
6778
6779 // Now the left dock is open and panel_1 is active and focused.
6780 workspace.update(cx, |workspace, cx| {
6781 let left_dock = workspace.left_dock();
6782 assert!(left_dock.read(cx).is_open());
6783 assert_eq!(
6784 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6785 panel_1.panel_id(),
6786 );
6787 assert!(panel_1.focus_handle(cx).is_focused(cx));
6788 });
6789
6790 // Emit closed event on panel 2, which is not active
6791 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6792
6793 // Wo don't close the left dock, because panel_2 wasn't the active panel
6794 workspace.update(cx, |workspace, cx| {
6795 let left_dock = workspace.left_dock();
6796 assert!(left_dock.read(cx).is_open());
6797 assert_eq!(
6798 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6799 panel_1.panel_id(),
6800 );
6801 });
6802
6803 // Emitting a ZoomIn event shows the panel as zoomed.
6804 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
6805 workspace.update(cx, |workspace, _| {
6806 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6807 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
6808 });
6809
6810 // Move panel to another dock while it is zoomed
6811 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
6812 workspace.update(cx, |workspace, _| {
6813 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6814
6815 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6816 });
6817
6818 // This is a helper for getting a:
6819 // - valid focus on an element,
6820 // - that isn't a part of the panes and panels system of the Workspace,
6821 // - and doesn't trigger the 'on_focus_lost' API.
6822 let focus_other_view = {
6823 let workspace = workspace.clone();
6824 move |cx: &mut VisualTestContext| {
6825 workspace.update(cx, |workspace, cx| {
6826 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
6827 workspace.toggle_modal(cx, TestModal::new);
6828 workspace.toggle_modal(cx, TestModal::new);
6829 } else {
6830 workspace.toggle_modal(cx, TestModal::new);
6831 }
6832 })
6833 }
6834 };
6835
6836 // If focus is transferred to another view that's not a panel or another pane, we still show
6837 // the panel as zoomed.
6838 focus_other_view(cx);
6839 workspace.update(cx, |workspace, _| {
6840 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6841 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6842 });
6843
6844 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
6845 workspace.update(cx, |_, cx| cx.focus_self());
6846 workspace.update(cx, |workspace, _| {
6847 assert_eq!(workspace.zoomed, None);
6848 assert_eq!(workspace.zoomed_position, None);
6849 });
6850
6851 // If focus is transferred again to another view that's not a panel or a pane, we won't
6852 // show the panel as zoomed because it wasn't zoomed before.
6853 focus_other_view(cx);
6854 workspace.update(cx, |workspace, _| {
6855 assert_eq!(workspace.zoomed, None);
6856 assert_eq!(workspace.zoomed_position, None);
6857 });
6858
6859 // When the panel is activated, it is zoomed again.
6860 cx.dispatch_action(ToggleRightDock);
6861 workspace.update(cx, |workspace, _| {
6862 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6863 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6864 });
6865
6866 // Emitting a ZoomOut event unzooms the panel.
6867 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
6868 workspace.update(cx, |workspace, _| {
6869 assert_eq!(workspace.zoomed, None);
6870 assert_eq!(workspace.zoomed_position, None);
6871 });
6872
6873 // Emit closed event on panel 1, which is active
6874 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6875
6876 // Now the left dock is closed, because panel_1 was the active panel
6877 workspace.update(cx, |workspace, cx| {
6878 let right_dock = workspace.right_dock();
6879 assert!(!right_dock.read(cx).is_open());
6880 });
6881 }
6882
6883 mod register_project_item_tests {
6884 use ui::Context as _;
6885
6886 use super::*;
6887
6888 // View
6889 struct TestPngItemView {
6890 focus_handle: FocusHandle,
6891 }
6892 // Model
6893 struct TestPngItem {}
6894
6895 impl project::Item for TestPngItem {
6896 fn try_open(
6897 _project: &Model<Project>,
6898 path: &ProjectPath,
6899 cx: &mut AppContext,
6900 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6901 if path.path.extension().unwrap() == "png" {
6902 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
6903 } else {
6904 None
6905 }
6906 }
6907
6908 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6909 None
6910 }
6911
6912 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6913 None
6914 }
6915 }
6916
6917 impl Item for TestPngItemView {
6918 type Event = ();
6919 }
6920 impl EventEmitter<()> for TestPngItemView {}
6921 impl FocusableView for TestPngItemView {
6922 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6923 self.focus_handle.clone()
6924 }
6925 }
6926
6927 impl Render for TestPngItemView {
6928 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6929 Empty
6930 }
6931 }
6932
6933 impl ProjectItem for TestPngItemView {
6934 type Item = TestPngItem;
6935
6936 fn for_project_item(
6937 _project: Model<Project>,
6938 _item: Model<Self::Item>,
6939 cx: &mut ViewContext<Self>,
6940 ) -> Self
6941 where
6942 Self: Sized,
6943 {
6944 Self {
6945 focus_handle: cx.focus_handle(),
6946 }
6947 }
6948 }
6949
6950 // View
6951 struct TestIpynbItemView {
6952 focus_handle: FocusHandle,
6953 }
6954 // Model
6955 struct TestIpynbItem {}
6956
6957 impl project::Item for TestIpynbItem {
6958 fn try_open(
6959 _project: &Model<Project>,
6960 path: &ProjectPath,
6961 cx: &mut AppContext,
6962 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6963 if path.path.extension().unwrap() == "ipynb" {
6964 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
6965 } else {
6966 None
6967 }
6968 }
6969
6970 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6971 None
6972 }
6973
6974 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6975 None
6976 }
6977 }
6978
6979 impl Item for TestIpynbItemView {
6980 type Event = ();
6981 }
6982 impl EventEmitter<()> for TestIpynbItemView {}
6983 impl FocusableView for TestIpynbItemView {
6984 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6985 self.focus_handle.clone()
6986 }
6987 }
6988
6989 impl Render for TestIpynbItemView {
6990 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6991 Empty
6992 }
6993 }
6994
6995 impl ProjectItem for TestIpynbItemView {
6996 type Item = TestIpynbItem;
6997
6998 fn for_project_item(
6999 _project: Model<Project>,
7000 _item: Model<Self::Item>,
7001 cx: &mut ViewContext<Self>,
7002 ) -> Self
7003 where
7004 Self: Sized,
7005 {
7006 Self {
7007 focus_handle: cx.focus_handle(),
7008 }
7009 }
7010 }
7011
7012 struct TestAlternatePngItemView {
7013 focus_handle: FocusHandle,
7014 }
7015
7016 impl Item for TestAlternatePngItemView {
7017 type Event = ();
7018 }
7019
7020 impl EventEmitter<()> for TestAlternatePngItemView {}
7021 impl FocusableView for TestAlternatePngItemView {
7022 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7023 self.focus_handle.clone()
7024 }
7025 }
7026
7027 impl Render for TestAlternatePngItemView {
7028 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7029 Empty
7030 }
7031 }
7032
7033 impl ProjectItem for TestAlternatePngItemView {
7034 type Item = TestPngItem;
7035
7036 fn for_project_item(
7037 _project: Model<Project>,
7038 _item: Model<Self::Item>,
7039 cx: &mut ViewContext<Self>,
7040 ) -> Self
7041 where
7042 Self: Sized,
7043 {
7044 Self {
7045 focus_handle: cx.focus_handle(),
7046 }
7047 }
7048 }
7049
7050 #[gpui::test]
7051 async fn test_register_project_item(cx: &mut TestAppContext) {
7052 init_test(cx);
7053
7054 cx.update(|cx| {
7055 register_project_item::<TestPngItemView>(cx);
7056 register_project_item::<TestIpynbItemView>(cx);
7057 });
7058
7059 let fs = FakeFs::new(cx.executor());
7060 fs.insert_tree(
7061 "/root1",
7062 json!({
7063 "one.png": "BINARYDATAHERE",
7064 "two.ipynb": "{ totally a notebook }",
7065 "three.txt": "editing text, sure why not?"
7066 }),
7067 )
7068 .await;
7069
7070 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7071 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7072
7073 let worktree_id = project.update(cx, |project, cx| {
7074 project.worktrees(cx).next().unwrap().read(cx).id()
7075 });
7076
7077 let handle = workspace
7078 .update(cx, |workspace, cx| {
7079 let project_path = (worktree_id, "one.png");
7080 workspace.open_path(project_path, None, true, cx)
7081 })
7082 .await
7083 .unwrap();
7084
7085 // Now we can check if the handle we got back errored or not
7086 assert_eq!(
7087 handle.to_any().entity_type(),
7088 TypeId::of::<TestPngItemView>()
7089 );
7090
7091 let handle = workspace
7092 .update(cx, |workspace, cx| {
7093 let project_path = (worktree_id, "two.ipynb");
7094 workspace.open_path(project_path, None, true, cx)
7095 })
7096 .await
7097 .unwrap();
7098
7099 assert_eq!(
7100 handle.to_any().entity_type(),
7101 TypeId::of::<TestIpynbItemView>()
7102 );
7103
7104 let handle = workspace
7105 .update(cx, |workspace, cx| {
7106 let project_path = (worktree_id, "three.txt");
7107 workspace.open_path(project_path, None, true, cx)
7108 })
7109 .await;
7110 assert!(handle.is_err());
7111 }
7112
7113 #[gpui::test]
7114 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7115 init_test(cx);
7116
7117 cx.update(|cx| {
7118 register_project_item::<TestPngItemView>(cx);
7119 register_project_item::<TestAlternatePngItemView>(cx);
7120 });
7121
7122 let fs = FakeFs::new(cx.executor());
7123 fs.insert_tree(
7124 "/root1",
7125 json!({
7126 "one.png": "BINARYDATAHERE",
7127 "two.ipynb": "{ totally a notebook }",
7128 "three.txt": "editing text, sure why not?"
7129 }),
7130 )
7131 .await;
7132
7133 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7134 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7135
7136 let worktree_id = project.update(cx, |project, cx| {
7137 project.worktrees(cx).next().unwrap().read(cx).id()
7138 });
7139
7140 let handle = workspace
7141 .update(cx, |workspace, cx| {
7142 let project_path = (worktree_id, "one.png");
7143 workspace.open_path(project_path, None, true, cx)
7144 })
7145 .await
7146 .unwrap();
7147
7148 // This _must_ be the second item registered
7149 assert_eq!(
7150 handle.to_any().entity_type(),
7151 TypeId::of::<TestAlternatePngItemView>()
7152 );
7153
7154 let handle = workspace
7155 .update(cx, |workspace, cx| {
7156 let project_path = (worktree_id, "three.txt");
7157 workspace.open_path(project_path, None, true, cx)
7158 })
7159 .await;
7160 assert!(handle.is_err());
7161 }
7162 }
7163
7164 pub fn init_test(cx: &mut TestAppContext) {
7165 cx.update(|cx| {
7166 let settings_store = SettingsStore::test(cx);
7167 cx.set_global(settings_store);
7168 theme::init(theme::LoadThemes::JustBase, cx);
7169 language::init(cx);
7170 crate::init_settings(cx);
7171 Project::init_settings(cx);
7172 });
7173 }
7174}
7175
7176pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
7177 const BORDER_SIZE: Pixels = px(1.0);
7178 let decorations = cx.window_decorations();
7179
7180 if matches!(decorations, Decorations::Client { .. }) {
7181 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7182 }
7183
7184 struct GlobalResizeEdge(ResizeEdge);
7185 impl Global for GlobalResizeEdge {}
7186
7187 div()
7188 .id("window-backdrop")
7189 .bg(transparent_black())
7190 .map(|div| match decorations {
7191 Decorations::Server => div,
7192 Decorations::Client { tiling, .. } => div
7193 .when(!(tiling.top || tiling.right), |div| {
7194 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7195 })
7196 .when(!(tiling.top || tiling.left), |div| {
7197 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7198 })
7199 .when(!(tiling.bottom || tiling.right), |div| {
7200 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7201 })
7202 .when(!(tiling.bottom || tiling.left), |div| {
7203 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7204 })
7205 .when(!tiling.top, |div| {
7206 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7207 })
7208 .when(!tiling.bottom, |div| {
7209 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7210 })
7211 .when(!tiling.left, |div| {
7212 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7213 })
7214 .when(!tiling.right, |div| {
7215 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7216 })
7217 .on_mouse_move(move |e, cx| {
7218 let size = cx.window_bounds().get_bounds().size;
7219 let pos = e.position;
7220
7221 let new_edge =
7222 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7223
7224 let edge = cx.try_global::<GlobalResizeEdge>();
7225 if new_edge != edge.map(|edge| edge.0) {
7226 cx.window_handle()
7227 .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
7228 .ok();
7229 }
7230 })
7231 .on_mouse_down(MouseButton::Left, move |e, cx| {
7232 let size = cx.window_bounds().get_bounds().size;
7233 let pos = e.position;
7234
7235 let edge = match resize_edge(
7236 pos,
7237 theme::CLIENT_SIDE_DECORATION_SHADOW,
7238 size,
7239 tiling,
7240 ) {
7241 Some(value) => value,
7242 None => return,
7243 };
7244
7245 cx.start_window_resize(edge);
7246 }),
7247 })
7248 .size_full()
7249 .child(
7250 div()
7251 .cursor(CursorStyle::Arrow)
7252 .map(|div| match decorations {
7253 Decorations::Server => div,
7254 Decorations::Client { tiling } => div
7255 .border_color(cx.theme().colors().border)
7256 .when(!(tiling.top || tiling.right), |div| {
7257 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7258 })
7259 .when(!(tiling.top || tiling.left), |div| {
7260 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7261 })
7262 .when(!(tiling.bottom || tiling.right), |div| {
7263 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7264 })
7265 .when(!(tiling.bottom || tiling.left), |div| {
7266 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7267 })
7268 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7269 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7270 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7271 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7272 .when(!tiling.is_tiled(), |div| {
7273 div.shadow(smallvec::smallvec![gpui::BoxShadow {
7274 color: Hsla {
7275 h: 0.,
7276 s: 0.,
7277 l: 0.,
7278 a: 0.4,
7279 },
7280 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7281 spread_radius: px(0.),
7282 offset: point(px(0.0), px(0.0)),
7283 }])
7284 }),
7285 })
7286 .on_mouse_move(|_e, cx| {
7287 cx.stop_propagation();
7288 })
7289 .size_full()
7290 .child(element),
7291 )
7292 .map(|div| match decorations {
7293 Decorations::Server => div,
7294 Decorations::Client { tiling, .. } => div.child(
7295 canvas(
7296 |_bounds, cx| {
7297 cx.insert_hitbox(
7298 Bounds::new(
7299 point(px(0.0), px(0.0)),
7300 cx.window_bounds().get_bounds().size,
7301 ),
7302 false,
7303 )
7304 },
7305 move |_bounds, hitbox, cx| {
7306 let mouse = cx.mouse_position();
7307 let size = cx.window_bounds().get_bounds().size;
7308 let Some(edge) =
7309 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7310 else {
7311 return;
7312 };
7313 cx.set_global(GlobalResizeEdge(edge));
7314 cx.set_cursor_style(
7315 match edge {
7316 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7317 ResizeEdge::Left | ResizeEdge::Right => {
7318 CursorStyle::ResizeLeftRight
7319 }
7320 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7321 CursorStyle::ResizeUpLeftDownRight
7322 }
7323 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7324 CursorStyle::ResizeUpRightDownLeft
7325 }
7326 },
7327 &hitbox,
7328 );
7329 },
7330 )
7331 .size_full()
7332 .absolute(),
7333 ),
7334 })
7335}
7336
7337fn resize_edge(
7338 pos: Point<Pixels>,
7339 shadow_size: Pixels,
7340 window_size: Size<Pixels>,
7341 tiling: Tiling,
7342) -> Option<ResizeEdge> {
7343 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7344 if bounds.contains(&pos) {
7345 return None;
7346 }
7347
7348 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7349 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7350 if !tiling.top && top_left_bounds.contains(&pos) {
7351 return Some(ResizeEdge::TopLeft);
7352 }
7353
7354 let top_right_bounds = Bounds::new(
7355 Point::new(window_size.width - corner_size.width, px(0.)),
7356 corner_size,
7357 );
7358 if !tiling.top && top_right_bounds.contains(&pos) {
7359 return Some(ResizeEdge::TopRight);
7360 }
7361
7362 let bottom_left_bounds = Bounds::new(
7363 Point::new(px(0.), window_size.height - corner_size.height),
7364 corner_size,
7365 );
7366 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7367 return Some(ResizeEdge::BottomLeft);
7368 }
7369
7370 let bottom_right_bounds = Bounds::new(
7371 Point::new(
7372 window_size.width - corner_size.width,
7373 window_size.height - corner_size.height,
7374 ),
7375 corner_size,
7376 );
7377 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7378 return Some(ResizeEdge::BottomRight);
7379 }
7380
7381 if !tiling.top && pos.y < shadow_size {
7382 Some(ResizeEdge::Top)
7383 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7384 Some(ResizeEdge::Bottom)
7385 } else if !tiling.left && pos.x < shadow_size {
7386 Some(ResizeEdge::Left)
7387 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7388 Some(ResizeEdge::Right)
7389 } else {
7390 None
7391 }
7392}