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