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