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