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