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::notifications::NotificationId;
98use crate::persistence::{
99 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
100 SerializedAxis,
101};
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 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
3947 } else {
3948 None
3949 }
3950 } else if let Some(dev_server_project_id) = self.project().read(cx).dev_server_project_id()
3951 {
3952 let store = dev_server_projects::Store::global(cx).read(cx);
3953 maybe!({
3954 let project = store.dev_server_project(dev_server_project_id)?;
3955 let dev_server = store.dev_server(project.dev_server_id)?;
3956
3957 let dev_server_project = SerializedDevServerProject {
3958 id: dev_server_project_id,
3959 dev_server_name: dev_server.name.to_string(),
3960 paths: project.paths.iter().map(|path| path.clone()).collect(),
3961 };
3962 Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
3963 })
3964 } else {
3965 None
3966 };
3967
3968 // don't save workspace state for the empty workspace.
3969 if let Some(location) = location {
3970 let center_group = build_serialized_pane_group(&self.center.root, cx);
3971 let docks = build_serialized_docks(self, cx);
3972 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
3973 let serialized_workspace = SerializedWorkspace {
3974 id: database_id,
3975 location,
3976 center_group,
3977 window_bounds,
3978 display: Default::default(),
3979 docks,
3980 centered_layout: self.centered_layout,
3981 };
3982 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3983 }
3984 Task::ready(())
3985 }
3986
3987 async fn serialize_items(
3988 this: &WeakView<Self>,
3989 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
3990 cx: &mut AsyncWindowContext,
3991 ) -> Result<()> {
3992 const CHUNK_SIZE: usize = 200;
3993 const THROTTLE_TIME: Duration = Duration::from_millis(200);
3994
3995 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
3996
3997 while let Some(items_received) = serializable_items.next().await {
3998 let unique_items =
3999 items_received
4000 .into_iter()
4001 .fold(HashMap::default(), |mut acc, item| {
4002 acc.entry(item.item_id()).or_insert(item);
4003 acc
4004 });
4005
4006 // We use into_iter() here so that the references to the items are moved into
4007 // the tasks and not kept alive while we're sleeping.
4008 for (_, item) in unique_items.into_iter() {
4009 if let Ok(Some(task)) =
4010 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4011 {
4012 cx.background_executor()
4013 .spawn(async move { task.await.log_err() })
4014 .detach();
4015 }
4016 }
4017
4018 cx.background_executor().timer(THROTTLE_TIME).await;
4019 }
4020
4021 Ok(())
4022 }
4023
4024 pub(crate) fn enqueue_item_serialization(
4025 &mut self,
4026 item: Box<dyn SerializableItemHandle>,
4027 ) -> Result<()> {
4028 self.serializable_items_tx
4029 .unbounded_send(item)
4030 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4031 }
4032
4033 pub(crate) fn load_workspace(
4034 serialized_workspace: SerializedWorkspace,
4035 paths_to_open: Vec<Option<ProjectPath>>,
4036 cx: &mut ViewContext<Workspace>,
4037 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4038 cx.spawn(|workspace, mut cx| async move {
4039 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4040
4041 let mut center_group = None;
4042 let mut center_items = None;
4043
4044 // Traverse the splits tree and add to things
4045 if let Some((group, active_pane, items)) = serialized_workspace
4046 .center_group
4047 .deserialize(
4048 &project,
4049 serialized_workspace.id,
4050 workspace.clone(),
4051 &mut cx,
4052 )
4053 .await
4054 {
4055 center_items = Some(items);
4056 center_group = Some((group, active_pane))
4057 }
4058
4059 let mut items_by_project_path = HashMap::default();
4060 let mut item_ids_by_kind = HashMap::default();
4061 let mut all_deserialized_items = Vec::default();
4062 cx.update(|cx| {
4063 for item in center_items.unwrap_or_default().into_iter().flatten() {
4064 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4065 item_ids_by_kind
4066 .entry(serializable_item_handle.serialized_item_kind())
4067 .or_insert(Vec::new())
4068 .push(item.item_id().as_u64() as ItemId);
4069 }
4070
4071 if let Some(project_path) = item.project_path(cx) {
4072 items_by_project_path.insert(project_path, item.clone());
4073 }
4074 all_deserialized_items.push(item);
4075 }
4076 })?;
4077
4078 let opened_items = paths_to_open
4079 .into_iter()
4080 .map(|path_to_open| {
4081 path_to_open
4082 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4083 })
4084 .collect::<Vec<_>>();
4085
4086 // Remove old panes from workspace panes list
4087 workspace.update(&mut cx, |workspace, cx| {
4088 if let Some((center_group, active_pane)) = center_group {
4089 workspace.remove_panes(workspace.center.root.clone(), cx);
4090
4091 // Swap workspace center group
4092 workspace.center = PaneGroup::with_root(center_group);
4093 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
4094 if let Some(active_pane) = active_pane {
4095 workspace.active_pane = active_pane;
4096 cx.focus_self();
4097 } else {
4098 workspace.active_pane = workspace.center.first_pane().clone();
4099 }
4100 }
4101
4102 let docks = serialized_workspace.docks;
4103
4104 for (dock, serialized_dock) in [
4105 (&mut workspace.right_dock, docks.right),
4106 (&mut workspace.left_dock, docks.left),
4107 (&mut workspace.bottom_dock, docks.bottom),
4108 ]
4109 .iter_mut()
4110 {
4111 dock.update(cx, |dock, cx| {
4112 dock.serialized_dock = Some(serialized_dock.clone());
4113 dock.restore_state(cx);
4114 });
4115 }
4116
4117 cx.notify();
4118 })?;
4119
4120 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4121 // after loading the items, we might have different items and in order to avoid
4122 // the database filling up, we delete items that haven't been loaded now.
4123 //
4124 // The items that have been loaded, have been saved after they've been added to the workspace.
4125 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4126 item_ids_by_kind
4127 .into_iter()
4128 .map(|(item_kind, loaded_items)| {
4129 SerializableItemRegistry::cleanup(
4130 item_kind,
4131 serialized_workspace.id,
4132 loaded_items,
4133 cx,
4134 )
4135 .log_err()
4136 })
4137 .collect::<Vec<_>>()
4138 })?;
4139
4140 futures::future::join_all(clean_up_tasks).await;
4141
4142 workspace
4143 .update(&mut cx, |workspace, cx| {
4144 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4145 workspace.serialize_workspace_internal(cx).detach();
4146
4147 // Ensure that we mark the window as edited if we did load dirty items
4148 workspace.update_window_edited(cx);
4149 })
4150 .ok();
4151
4152 Ok(opened_items)
4153 })
4154 }
4155
4156 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4157 self.add_workspace_actions_listeners(div, cx)
4158 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4159 .on_action(cx.listener(Self::close_all_items_and_panes))
4160 .on_action(cx.listener(Self::save_all))
4161 .on_action(cx.listener(Self::send_keystrokes))
4162 .on_action(cx.listener(Self::add_folder_to_project))
4163 .on_action(cx.listener(Self::follow_next_collaborator))
4164 .on_action(cx.listener(Self::open))
4165 .on_action(cx.listener(Self::close_window))
4166 .on_action(cx.listener(Self::activate_pane_at_index))
4167 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4168 let pane = workspace.active_pane().clone();
4169 workspace.unfollow_in_pane(&pane, cx);
4170 }))
4171 .on_action(cx.listener(|workspace, action: &Save, cx| {
4172 workspace
4173 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4174 .detach_and_log_err(cx);
4175 }))
4176 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4177 workspace
4178 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4179 .detach_and_log_err(cx);
4180 }))
4181 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4182 workspace
4183 .save_active_item(SaveIntent::SaveAs, cx)
4184 .detach_and_log_err(cx);
4185 }))
4186 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4187 workspace.activate_previous_pane(cx)
4188 }))
4189 .on_action(
4190 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4191 )
4192 .on_action(
4193 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4194 workspace.activate_pane_in_direction(action.0, cx)
4195 }),
4196 )
4197 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4198 workspace.swap_pane_in_direction(action.0, cx)
4199 }))
4200 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4201 this.toggle_dock(DockPosition::Left, cx);
4202 }))
4203 .on_action(
4204 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4205 workspace.toggle_dock(DockPosition::Right, cx);
4206 }),
4207 )
4208 .on_action(
4209 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4210 workspace.toggle_dock(DockPosition::Bottom, cx);
4211 }),
4212 )
4213 .on_action(
4214 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4215 workspace.close_all_docks(cx);
4216 }),
4217 )
4218 .on_action(
4219 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4220 workspace.clear_all_notifications(cx);
4221 }),
4222 )
4223 .on_action(
4224 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4225 workspace.reopen_closed_item(cx).detach();
4226 }),
4227 )
4228 .on_action(cx.listener(Workspace::toggle_centered_layout))
4229 }
4230
4231 #[cfg(any(test, feature = "test-support"))]
4232 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4233 use node_runtime::FakeNodeRuntime;
4234
4235 let client = project.read(cx).client();
4236 let user_store = project.read(cx).user_store();
4237
4238 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4239 cx.activate_window();
4240 let app_state = Arc::new(AppState {
4241 languages: project.read(cx).languages().clone(),
4242 workspace_store,
4243 client,
4244 user_store,
4245 fs: project.read(cx).fs().clone(),
4246 build_window_options: |_, _| Default::default(),
4247 node_runtime: FakeNodeRuntime::new(),
4248 });
4249 let workspace = Self::new(Default::default(), project, app_state, cx);
4250 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4251 workspace
4252 }
4253
4254 pub fn register_action<A: Action>(
4255 &mut self,
4256 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4257 ) -> &mut Self {
4258 let callback = Arc::new(callback);
4259
4260 self.workspace_actions.push(Box::new(move |div, cx| {
4261 let callback = callback.clone();
4262 div.on_action(
4263 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4264 )
4265 }));
4266 self
4267 }
4268
4269 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4270 for action in self.workspace_actions.iter() {
4271 div = (action)(div, cx)
4272 }
4273 div
4274 }
4275
4276 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4277 self.modal_layer.read(cx).has_active_modal()
4278 }
4279
4280 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
4281 self.modal_layer.read(cx).active_modal()
4282 }
4283
4284 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4285 where
4286 B: FnOnce(&mut ViewContext<V>) -> V,
4287 {
4288 self.modal_layer
4289 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4290 }
4291
4292 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4293 self.centered_layout = !self.centered_layout;
4294 if let Some(database_id) = self.database_id() {
4295 cx.background_executor()
4296 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4297 .detach_and_log_err(cx);
4298 }
4299 cx.notify();
4300 }
4301
4302 fn adjust_padding(padding: Option<f32>) -> f32 {
4303 padding
4304 .unwrap_or(Self::DEFAULT_PADDING)
4305 .clamp(0.0, Self::MAX_PADDING)
4306 }
4307
4308 fn render_dock(
4309 &self,
4310 position: DockPosition,
4311 dock: &View<Dock>,
4312 cx: &WindowContext,
4313 ) -> Option<Div> {
4314 if self.zoomed_position == Some(position) {
4315 return None;
4316 }
4317
4318 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4319 let pane = panel.pane(cx)?;
4320 let follower_states = &self.follower_states;
4321 leader_border_for_pane(follower_states, &pane, cx)
4322 });
4323
4324 Some(
4325 div()
4326 .flex()
4327 .flex_none()
4328 .overflow_hidden()
4329 .child(dock.clone())
4330 .children(leader_border),
4331 )
4332 }
4333}
4334
4335fn leader_border_for_pane(
4336 follower_states: &HashMap<PeerId, FollowerState>,
4337 pane: &View<Pane>,
4338 cx: &WindowContext,
4339) -> Option<Div> {
4340 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4341 if state.pane() == pane {
4342 Some((*leader_id, state))
4343 } else {
4344 None
4345 }
4346 })?;
4347
4348 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4349 let leader = room.remote_participant_for_peer_id(leader_id)?;
4350
4351 let mut leader_color = cx
4352 .theme()
4353 .players()
4354 .color_for_participant(leader.participant_index.0)
4355 .cursor;
4356 leader_color.fade_out(0.3);
4357 Some(
4358 div()
4359 .absolute()
4360 .size_full()
4361 .left_0()
4362 .top_0()
4363 .border_2()
4364 .border_color(leader_color),
4365 )
4366}
4367
4368fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4369 ZED_WINDOW_POSITION
4370 .zip(*ZED_WINDOW_SIZE)
4371 .map(|(position, size)| Bounds {
4372 origin: position,
4373 size,
4374 })
4375}
4376
4377fn open_items(
4378 serialized_workspace: Option<SerializedWorkspace>,
4379 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4380 app_state: Arc<AppState>,
4381 cx: &mut ViewContext<Workspace>,
4382) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4383 let restored_items = serialized_workspace.map(|serialized_workspace| {
4384 Workspace::load_workspace(
4385 serialized_workspace,
4386 project_paths_to_open
4387 .iter()
4388 .map(|(_, project_path)| project_path)
4389 .cloned()
4390 .collect(),
4391 cx,
4392 )
4393 });
4394
4395 cx.spawn(|workspace, mut cx| async move {
4396 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4397
4398 if let Some(restored_items) = restored_items {
4399 let restored_items = restored_items.await?;
4400
4401 let restored_project_paths = restored_items
4402 .iter()
4403 .filter_map(|item| {
4404 cx.update(|cx| item.as_ref()?.project_path(cx))
4405 .ok()
4406 .flatten()
4407 })
4408 .collect::<HashSet<_>>();
4409
4410 for restored_item in restored_items {
4411 opened_items.push(restored_item.map(Ok));
4412 }
4413
4414 project_paths_to_open
4415 .iter_mut()
4416 .for_each(|(_, project_path)| {
4417 if let Some(project_path_to_open) = project_path {
4418 if restored_project_paths.contains(project_path_to_open) {
4419 *project_path = None;
4420 }
4421 }
4422 });
4423 } else {
4424 for _ in 0..project_paths_to_open.len() {
4425 opened_items.push(None);
4426 }
4427 }
4428 assert!(opened_items.len() == project_paths_to_open.len());
4429
4430 let tasks =
4431 project_paths_to_open
4432 .into_iter()
4433 .enumerate()
4434 .map(|(ix, (abs_path, project_path))| {
4435 let workspace = workspace.clone();
4436 cx.spawn(|mut cx| {
4437 let fs = app_state.fs.clone();
4438 async move {
4439 let file_project_path = project_path?;
4440 if fs.is_dir(&abs_path).await {
4441 None
4442 } else {
4443 Some((
4444 ix,
4445 workspace
4446 .update(&mut cx, |workspace, cx| {
4447 workspace.open_path(file_project_path, None, true, cx)
4448 })
4449 .log_err()?
4450 .await,
4451 ))
4452 }
4453 }
4454 })
4455 });
4456
4457 let tasks = tasks.collect::<Vec<_>>();
4458
4459 let tasks = futures::future::join_all(tasks);
4460 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4461 opened_items[ix] = Some(path_open_result);
4462 }
4463
4464 Ok(opened_items)
4465 })
4466}
4467
4468enum ActivateInDirectionTarget {
4469 Pane(View<Pane>),
4470 Dock(View<Dock>),
4471}
4472
4473fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4474 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4475
4476 workspace
4477 .update(cx, |workspace, cx| {
4478 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4479 struct DatabaseFailedNotification;
4480
4481 workspace.show_notification_once(
4482 NotificationId::unique::<DatabaseFailedNotification>(),
4483 cx,
4484 |cx| {
4485 cx.new_view(|_| {
4486 MessageNotification::new("Failed to load the database file.")
4487 .with_click_message("Click to let us know about this error")
4488 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4489 })
4490 },
4491 );
4492 }
4493 })
4494 .log_err();
4495}
4496
4497impl FocusableView for Workspace {
4498 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4499 self.active_pane.focus_handle(cx)
4500 }
4501}
4502
4503#[derive(Clone, Render)]
4504struct DraggedDock(DockPosition);
4505
4506impl Render for Workspace {
4507 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4508 let mut context = KeyContext::new_with_defaults();
4509 context.add("Workspace");
4510 let centered_layout = self.centered_layout
4511 && self.center.panes().len() == 1
4512 && self.active_item(cx).is_some();
4513 let render_padding = |size| {
4514 (size > 0.0).then(|| {
4515 div()
4516 .h_full()
4517 .w(relative(size))
4518 .bg(cx.theme().colors().editor_background)
4519 .border_color(cx.theme().colors().pane_group_border)
4520 })
4521 };
4522 let paddings = if centered_layout {
4523 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4524 (
4525 render_padding(Self::adjust_padding(settings.left_padding)),
4526 render_padding(Self::adjust_padding(settings.right_padding)),
4527 )
4528 } else {
4529 (None, None)
4530 };
4531 let ui_font = theme::setup_ui_font(cx);
4532
4533 let theme = cx.theme().clone();
4534 let colors = theme.colors();
4535
4536 client_side_decorations(
4537 self.actions(div(), cx)
4538 .key_context(context)
4539 .relative()
4540 .size_full()
4541 .flex()
4542 .flex_col()
4543 .font(ui_font)
4544 .gap_0()
4545 .justify_start()
4546 .items_start()
4547 .text_color(colors.text)
4548 .overflow_hidden()
4549 .children(self.titlebar_item.clone())
4550 .child(
4551 div()
4552 .id("workspace")
4553 .bg(colors.background)
4554 .relative()
4555 .flex_1()
4556 .w_full()
4557 .flex()
4558 .flex_col()
4559 .overflow_hidden()
4560 .border_t_1()
4561 .border_b_1()
4562 .border_color(colors.border)
4563 .child({
4564 let this = cx.view().clone();
4565 canvas(
4566 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4567 |_, _, _| {},
4568 )
4569 .absolute()
4570 .size_full()
4571 })
4572 .when(self.zoomed.is_none(), |this| {
4573 this.on_drag_move(cx.listener(
4574 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4575 DockPosition::Left => {
4576 let size = e.event.position.x - workspace.bounds.left();
4577 workspace.left_dock.update(cx, |left_dock, cx| {
4578 left_dock.resize_active_panel(Some(size), cx);
4579 });
4580 }
4581 DockPosition::Right => {
4582 let size = workspace.bounds.right() - e.event.position.x;
4583 workspace.right_dock.update(cx, |right_dock, cx| {
4584 right_dock.resize_active_panel(Some(size), cx);
4585 });
4586 }
4587 DockPosition::Bottom => {
4588 let size = workspace.bounds.bottom() - e.event.position.y;
4589 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4590 bottom_dock.resize_active_panel(Some(size), cx);
4591 });
4592 }
4593 },
4594 ))
4595 })
4596 .child(
4597 div()
4598 .flex()
4599 .flex_row()
4600 .h_full()
4601 // Left Dock
4602 .children(self.render_dock(DockPosition::Left, &self.left_dock, cx))
4603 // Panes
4604 .child(
4605 div()
4606 .flex()
4607 .flex_col()
4608 .flex_1()
4609 .overflow_hidden()
4610 .child(
4611 h_flex()
4612 .flex_1()
4613 .when_some(paddings.0, |this, p| {
4614 this.child(p.border_r_1())
4615 })
4616 .child(self.center.render(
4617 &self.project,
4618 &self.follower_states,
4619 self.active_call(),
4620 &self.active_pane,
4621 self.zoomed.as_ref(),
4622 &self.app_state,
4623 cx,
4624 ))
4625 .when_some(paddings.1, |this, p| {
4626 this.child(p.border_l_1())
4627 }),
4628 )
4629 .children(self.render_dock(
4630 DockPosition::Bottom,
4631 &self.bottom_dock,
4632 cx,
4633 )),
4634 )
4635 // Right Dock
4636 .children(self.render_dock(
4637 DockPosition::Right,
4638 &self.right_dock,
4639 cx,
4640 )),
4641 )
4642 .children(self.zoomed.as_ref().and_then(|view| {
4643 let zoomed_view = view.upgrade()?;
4644 let div = div()
4645 .occlude()
4646 .absolute()
4647 .overflow_hidden()
4648 .border_color(colors.border)
4649 .bg(colors.background)
4650 .child(zoomed_view)
4651 .inset_0()
4652 .shadow_lg();
4653
4654 Some(match self.zoomed_position {
4655 Some(DockPosition::Left) => div.right_2().border_r_1(),
4656 Some(DockPosition::Right) => div.left_2().border_l_1(),
4657 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4658 None => div.top_2().bottom_2().left_2().right_2().border_1(),
4659 })
4660 }))
4661 .child(self.modal_layer.clone())
4662 .children(self.render_notifications(cx)),
4663 )
4664 .child(self.status_bar.clone())
4665 .children(if self.project.read(cx).is_disconnected() {
4666 if let Some(render) = self.render_disconnected_overlay.take() {
4667 let result = render(self, cx);
4668 self.render_disconnected_overlay = Some(render);
4669 Some(result)
4670 } else {
4671 None
4672 }
4673 } else {
4674 None
4675 }),
4676 cx,
4677 )
4678 }
4679}
4680
4681impl WorkspaceStore {
4682 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4683 Self {
4684 workspaces: Default::default(),
4685 _subscriptions: vec![
4686 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4687 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4688 ],
4689 client,
4690 }
4691 }
4692
4693 pub fn update_followers(
4694 &self,
4695 project_id: Option<u64>,
4696 update: proto::update_followers::Variant,
4697 cx: &AppContext,
4698 ) -> Option<()> {
4699 let active_call = ActiveCall::try_global(cx)?;
4700 let room_id = active_call.read(cx).room()?.read(cx).id();
4701 self.client
4702 .send(proto::UpdateFollowers {
4703 room_id,
4704 project_id,
4705 variant: Some(update),
4706 })
4707 .log_err()
4708 }
4709
4710 pub async fn handle_follow(
4711 this: Model<Self>,
4712 envelope: TypedEnvelope<proto::Follow>,
4713 mut cx: AsyncAppContext,
4714 ) -> Result<proto::FollowResponse> {
4715 this.update(&mut cx, |this, cx| {
4716 let follower = Follower {
4717 project_id: envelope.payload.project_id,
4718 peer_id: envelope.original_sender_id()?,
4719 };
4720
4721 let mut response = proto::FollowResponse::default();
4722 this.workspaces.retain(|workspace| {
4723 workspace
4724 .update(cx, |workspace, cx| {
4725 let handler_response = workspace.handle_follow(follower.project_id, cx);
4726 if let Some(active_view) = handler_response.active_view.clone() {
4727 if workspace.project.read(cx).remote_id() == follower.project_id {
4728 response.active_view = Some(active_view)
4729 }
4730 }
4731 })
4732 .is_ok()
4733 });
4734
4735 Ok(response)
4736 })?
4737 }
4738
4739 async fn handle_update_followers(
4740 this: Model<Self>,
4741 envelope: TypedEnvelope<proto::UpdateFollowers>,
4742 mut cx: AsyncAppContext,
4743 ) -> Result<()> {
4744 let leader_id = envelope.original_sender_id()?;
4745 let update = envelope.payload;
4746
4747 this.update(&mut cx, |this, cx| {
4748 this.workspaces.retain(|workspace| {
4749 workspace
4750 .update(cx, |workspace, cx| {
4751 let project_id = workspace.project.read(cx).remote_id();
4752 if update.project_id != project_id && update.project_id.is_some() {
4753 return;
4754 }
4755 workspace.handle_update_followers(leader_id, update.clone(), cx);
4756 })
4757 .is_ok()
4758 });
4759 Ok(())
4760 })?
4761 }
4762}
4763
4764impl ViewId {
4765 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4766 Ok(Self {
4767 creator: message
4768 .creator
4769 .ok_or_else(|| anyhow!("creator is missing"))?,
4770 id: message.id,
4771 })
4772 }
4773
4774 pub(crate) fn to_proto(&self) -> proto::ViewId {
4775 proto::ViewId {
4776 creator: Some(self.creator),
4777 id: self.id,
4778 }
4779 }
4780}
4781
4782impl FollowerState {
4783 fn pane(&self) -> &View<Pane> {
4784 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
4785 }
4786}
4787
4788pub trait WorkspaceHandle {
4789 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4790}
4791
4792impl WorkspaceHandle for View<Workspace> {
4793 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4794 self.read(cx)
4795 .worktrees(cx)
4796 .flat_map(|worktree| {
4797 let worktree_id = worktree.read(cx).id();
4798 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4799 worktree_id,
4800 path: f.path.clone(),
4801 })
4802 })
4803 .collect::<Vec<_>>()
4804 }
4805}
4806
4807impl std::fmt::Debug for OpenPaths {
4808 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4809 f.debug_struct("OpenPaths")
4810 .field("paths", &self.paths)
4811 .finish()
4812 }
4813}
4814
4815pub fn activate_workspace_for_project(
4816 cx: &mut AppContext,
4817 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4818) -> Option<WindowHandle<Workspace>> {
4819 for window in cx.windows() {
4820 let Some(workspace) = window.downcast::<Workspace>() else {
4821 continue;
4822 };
4823
4824 let predicate = workspace
4825 .update(cx, |workspace, cx| {
4826 let project = workspace.project.read(cx);
4827 if predicate(project, cx) {
4828 cx.activate_window();
4829 true
4830 } else {
4831 false
4832 }
4833 })
4834 .log_err()
4835 .unwrap_or(false);
4836
4837 if predicate {
4838 return Some(workspace);
4839 }
4840 }
4841
4842 None
4843}
4844
4845pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
4846 DB.last_workspace().await.log_err().flatten()
4847}
4848
4849actions!(collab, [OpenChannelNotes]);
4850actions!(zed, [OpenLog]);
4851
4852async fn join_channel_internal(
4853 channel_id: ChannelId,
4854 app_state: &Arc<AppState>,
4855 requesting_window: Option<WindowHandle<Workspace>>,
4856 active_call: &Model<ActiveCall>,
4857 cx: &mut AsyncAppContext,
4858) -> Result<bool> {
4859 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4860 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4861 return (false, None);
4862 };
4863
4864 let already_in_channel = room.channel_id() == Some(channel_id);
4865 let should_prompt = room.is_sharing_project()
4866 && room.remote_participants().len() > 0
4867 && !already_in_channel;
4868 let open_room = if already_in_channel {
4869 active_call.room().cloned()
4870 } else {
4871 None
4872 };
4873 (should_prompt, open_room)
4874 })?;
4875
4876 if let Some(room) = open_room {
4877 let task = room.update(cx, |room, cx| {
4878 if let Some((project, host)) = room.most_active_project(cx) {
4879 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4880 }
4881
4882 None
4883 })?;
4884 if let Some(task) = task {
4885 task.await?;
4886 }
4887 return anyhow::Ok(true);
4888 }
4889
4890 if should_prompt {
4891 if let Some(workspace) = requesting_window {
4892 let answer = workspace
4893 .update(cx, |_, cx| {
4894 cx.prompt(
4895 PromptLevel::Warning,
4896 "Do you want to switch channels?",
4897 Some("Leaving this call will unshare your current project."),
4898 &["Yes, Join Channel", "Cancel"],
4899 )
4900 })?
4901 .await;
4902
4903 if answer == Ok(1) {
4904 return Ok(false);
4905 }
4906 } else {
4907 return Ok(false); // unreachable!() hopefully
4908 }
4909 }
4910
4911 let client = cx.update(|cx| active_call.read(cx).client())?;
4912
4913 let mut client_status = client.status();
4914
4915 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4916 'outer: loop {
4917 let Some(status) = client_status.recv().await else {
4918 return Err(anyhow!("error connecting"));
4919 };
4920
4921 match status {
4922 Status::Connecting
4923 | Status::Authenticating
4924 | Status::Reconnecting
4925 | Status::Reauthenticating => continue,
4926 Status::Connected { .. } => break 'outer,
4927 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4928 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4929 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4930 return Err(ErrorCode::Disconnected.into());
4931 }
4932 }
4933 }
4934
4935 let room = active_call
4936 .update(cx, |active_call, cx| {
4937 active_call.join_channel(channel_id, cx)
4938 })?
4939 .await?;
4940
4941 let Some(room) = room else {
4942 return anyhow::Ok(true);
4943 };
4944
4945 room.update(cx, |room, _| room.room_update_completed())?
4946 .await;
4947
4948 let task = room.update(cx, |room, cx| {
4949 if let Some((project, host)) = room.most_active_project(cx) {
4950 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4951 }
4952
4953 // If you are the first to join a channel, see if you should share your project.
4954 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
4955 if let Some(workspace) = requesting_window {
4956 let project = workspace.update(cx, |workspace, cx| {
4957 let project = workspace.project.read(cx);
4958 let is_dev_server = project.dev_server_project_id().is_some();
4959
4960 if !is_dev_server && !CallSettings::get_global(cx).share_on_join {
4961 return None;
4962 }
4963
4964 if (project.is_local() || is_dev_server)
4965 && project.visible_worktrees(cx).any(|tree| {
4966 tree.read(cx)
4967 .root_entry()
4968 .map_or(false, |entry| entry.is_dir())
4969 })
4970 {
4971 Some(workspace.project.clone())
4972 } else {
4973 None
4974 }
4975 });
4976 if let Ok(Some(project)) = project {
4977 return Some(cx.spawn(|room, mut cx| async move {
4978 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4979 .await?;
4980 Ok(())
4981 }));
4982 }
4983 }
4984 }
4985
4986 None
4987 })?;
4988 if let Some(task) = task {
4989 task.await?;
4990 return anyhow::Ok(true);
4991 }
4992 anyhow::Ok(false)
4993}
4994
4995pub fn join_channel(
4996 channel_id: ChannelId,
4997 app_state: Arc<AppState>,
4998 requesting_window: Option<WindowHandle<Workspace>>,
4999 cx: &mut AppContext,
5000) -> Task<Result<()>> {
5001 let active_call = ActiveCall::global(cx);
5002 cx.spawn(|mut cx| async move {
5003 let result = join_channel_internal(
5004 channel_id,
5005 &app_state,
5006 requesting_window,
5007 &active_call,
5008 &mut cx,
5009 )
5010 .await;
5011
5012 // join channel succeeded, and opened a window
5013 if matches!(result, Ok(true)) {
5014 return anyhow::Ok(());
5015 }
5016
5017 // find an existing workspace to focus and show call controls
5018 let mut active_window =
5019 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5020 if active_window.is_none() {
5021 // no open workspaces, make one to show the error in (blergh)
5022 let (window_handle, _) = cx
5023 .update(|cx| {
5024 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
5025 })?
5026 .await?;
5027
5028 if result.is_ok() {
5029 cx.update(|cx| {
5030 cx.dispatch_action(&OpenChannelNotes);
5031 }).log_err();
5032 }
5033
5034 active_window = Some(window_handle);
5035 }
5036
5037 if let Err(err) = result {
5038 log::error!("failed to join channel: {}", err);
5039 if let Some(active_window) = active_window {
5040 active_window
5041 .update(&mut cx, |_, cx| {
5042 let detail: SharedString = match err.error_code() {
5043 ErrorCode::SignedOut => {
5044 "Please sign in to continue.".into()
5045 }
5046 ErrorCode::UpgradeRequired => {
5047 "Your are running an unsupported version of Zed. Please update to continue.".into()
5048 }
5049 ErrorCode::NoSuchChannel => {
5050 "No matching channel was found. Please check the link and try again.".into()
5051 }
5052 ErrorCode::Forbidden => {
5053 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5054 }
5055 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5056 _ => format!("{}\n\nPlease try again.", err).into(),
5057 };
5058 cx.prompt(
5059 PromptLevel::Critical,
5060 "Failed to join channel",
5061 Some(&detail),
5062 &["Ok"],
5063 )
5064 })?
5065 .await
5066 .ok();
5067 }
5068 }
5069
5070 // return ok, we showed the error to the user.
5071 return anyhow::Ok(());
5072 })
5073}
5074
5075pub async fn get_any_active_workspace(
5076 app_state: Arc<AppState>,
5077 mut cx: AsyncAppContext,
5078) -> anyhow::Result<WindowHandle<Workspace>> {
5079 // find an existing workspace to focus and show call controls
5080 let active_window = activate_any_workspace_window(&mut cx);
5081 if active_window.is_none() {
5082 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
5083 .await?;
5084 }
5085 activate_any_workspace_window(&mut cx).context("could not open zed")
5086}
5087
5088fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5089 cx.update(|cx| {
5090 if let Some(workspace_window) = cx
5091 .active_window()
5092 .and_then(|window| window.downcast::<Workspace>())
5093 {
5094 return Some(workspace_window);
5095 }
5096
5097 for window in cx.windows() {
5098 if let Some(workspace_window) = window.downcast::<Workspace>() {
5099 workspace_window
5100 .update(cx, |_, cx| cx.activate_window())
5101 .ok();
5102 return Some(workspace_window);
5103 }
5104 }
5105 None
5106 })
5107 .ok()
5108 .flatten()
5109}
5110
5111fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5112 cx.windows()
5113 .into_iter()
5114 .filter_map(|window| window.downcast::<Workspace>())
5115 .filter(|workspace| {
5116 workspace
5117 .read(cx)
5118 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5119 })
5120 .collect()
5121}
5122
5123#[derive(Default)]
5124pub struct OpenOptions {
5125 pub open_new_workspace: Option<bool>,
5126 pub replace_window: Option<WindowHandle<Workspace>>,
5127}
5128
5129#[allow(clippy::type_complexity)]
5130pub fn open_paths(
5131 abs_paths: &[PathBuf],
5132 app_state: Arc<AppState>,
5133 open_options: OpenOptions,
5134 cx: &mut AppContext,
5135) -> Task<
5136 anyhow::Result<(
5137 WindowHandle<Workspace>,
5138 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5139 )>,
5140> {
5141 let abs_paths = abs_paths.to_vec();
5142 let mut existing = None;
5143 let mut best_match = None;
5144 let mut open_visible = OpenVisible::All;
5145
5146 if open_options.open_new_workspace != Some(true) {
5147 for window in local_workspace_windows(cx) {
5148 if let Ok(workspace) = window.read(cx) {
5149 let m = workspace
5150 .project
5151 .read(cx)
5152 .visibility_for_paths(&abs_paths, cx);
5153 if m > best_match {
5154 existing = Some(window);
5155 best_match = m;
5156 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5157 existing = Some(window)
5158 }
5159 }
5160 }
5161 }
5162
5163 cx.spawn(move |mut cx| async move {
5164 if open_options.open_new_workspace.is_none() && existing.is_none() {
5165 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5166 if futures::future::join_all(all_files)
5167 .await
5168 .into_iter()
5169 .filter_map(|result| result.ok().flatten())
5170 .all(|file| !file.is_dir)
5171 {
5172 cx.update(|cx| {
5173 for window in local_workspace_windows(cx) {
5174 if let Ok(workspace) = window.read(cx) {
5175 let project = workspace.project().read(cx);
5176 if project.is_remote() {
5177 continue;
5178 }
5179 existing = Some(window);
5180 open_visible = OpenVisible::None;
5181 break;
5182 }
5183 }
5184 })?;
5185 }
5186 }
5187
5188 if let Some(existing) = existing {
5189 Ok((
5190 existing,
5191 existing
5192 .update(&mut cx, |workspace, cx| {
5193 cx.activate_window();
5194 workspace.open_paths(abs_paths, open_visible, None, cx)
5195 })?
5196 .await,
5197 ))
5198 } else {
5199 cx.update(move |cx| {
5200 Workspace::new_local(
5201 abs_paths,
5202 app_state.clone(),
5203 open_options.replace_window,
5204 cx,
5205 )
5206 })?
5207 .await
5208 }
5209 })
5210}
5211
5212pub fn open_new(
5213 app_state: Arc<AppState>,
5214 cx: &mut AppContext,
5215 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5216) -> Task<anyhow::Result<()>> {
5217 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
5218 cx.spawn(|mut cx| async move {
5219 let (workspace, opened_paths) = task.await?;
5220 workspace.update(&mut cx, |workspace, cx| {
5221 if opened_paths.is_empty() {
5222 init(workspace, cx)
5223 }
5224 })?;
5225 Ok(())
5226 })
5227}
5228
5229pub fn create_and_open_local_file(
5230 path: &'static Path,
5231 cx: &mut ViewContext<Workspace>,
5232 default_content: impl 'static + Send + FnOnce() -> Rope,
5233) -> Task<Result<Box<dyn ItemHandle>>> {
5234 cx.spawn(|workspace, mut cx| async move {
5235 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5236 if !fs.is_file(path).await {
5237 fs.create_file(path, Default::default()).await?;
5238 fs.save(path, &default_content(), Default::default())
5239 .await?;
5240 }
5241
5242 let mut items = workspace
5243 .update(&mut cx, |workspace, cx| {
5244 workspace.with_local_workspace(cx, |workspace, cx| {
5245 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5246 })
5247 })?
5248 .await?
5249 .await;
5250
5251 let item = items.pop().flatten();
5252 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5253 })
5254}
5255
5256pub fn join_hosted_project(
5257 hosted_project_id: ProjectId,
5258 app_state: Arc<AppState>,
5259 cx: &mut AppContext,
5260) -> Task<Result<()>> {
5261 cx.spawn(|mut cx| async move {
5262 let existing_window = cx.update(|cx| {
5263 cx.windows().into_iter().find_map(|window| {
5264 let workspace = window.downcast::<Workspace>()?;
5265 workspace
5266 .read(cx)
5267 .is_ok_and(|workspace| {
5268 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
5269 })
5270 .then(|| workspace)
5271 })
5272 })?;
5273
5274 let workspace = if let Some(existing_window) = existing_window {
5275 existing_window
5276 } else {
5277 let project = Project::hosted(
5278 hosted_project_id,
5279 app_state.user_store.clone(),
5280 app_state.client.clone(),
5281 app_state.languages.clone(),
5282 app_state.fs.clone(),
5283 cx.clone(),
5284 )
5285 .await?;
5286
5287 let window_bounds_override = window_bounds_env_override();
5288 cx.update(|cx| {
5289 let mut options = (app_state.build_window_options)(None, cx);
5290 options.window_bounds =
5291 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5292 cx.open_window(options, |cx| {
5293 cx.new_view(|cx| {
5294 Workspace::new(Default::default(), project, app_state.clone(), cx)
5295 })
5296 })
5297 })??
5298 };
5299
5300 workspace.update(&mut cx, |_, cx| {
5301 cx.activate(true);
5302 cx.activate_window();
5303 })?;
5304
5305 Ok(())
5306 })
5307}
5308
5309pub fn join_dev_server_project(
5310 dev_server_project_id: DevServerProjectId,
5311 project_id: ProjectId,
5312 app_state: Arc<AppState>,
5313 window_to_replace: Option<WindowHandle<Workspace>>,
5314 cx: &mut AppContext,
5315) -> Task<Result<WindowHandle<Workspace>>> {
5316 let windows = cx.windows();
5317 cx.spawn(|mut cx| async move {
5318 let existing_workspace = windows.into_iter().find_map(|window| {
5319 window.downcast::<Workspace>().and_then(|window| {
5320 window
5321 .update(&mut cx, |workspace, cx| {
5322 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
5323 Some(window)
5324 } else {
5325 None
5326 }
5327 })
5328 .unwrap_or(None)
5329 })
5330 });
5331
5332 let workspace = if let Some(existing_workspace) = existing_workspace {
5333 existing_workspace
5334 } else {
5335 let project = Project::remote(
5336 project_id.0,
5337 app_state.client.clone(),
5338 app_state.user_store.clone(),
5339 app_state.languages.clone(),
5340 app_state.fs.clone(),
5341 cx.clone(),
5342 )
5343 .await?;
5344
5345 let serialized_workspace: Option<SerializedWorkspace> =
5346 persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
5347
5348 let workspace_id = if let Some(serialized_workspace) = serialized_workspace {
5349 serialized_workspace.id
5350 } else {
5351 persistence::DB.next_id().await?
5352 };
5353
5354 if let Some(window_to_replace) = window_to_replace {
5355 cx.update_window(window_to_replace.into(), |_, cx| {
5356 cx.replace_root_view(|cx| {
5357 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5358 });
5359 })?;
5360 window_to_replace
5361 } else {
5362 let window_bounds_override = window_bounds_env_override();
5363 cx.update(|cx| {
5364 let mut options = (app_state.build_window_options)(None, cx);
5365 options.window_bounds =
5366 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5367 cx.open_window(options, |cx| {
5368 cx.new_view(|cx| {
5369 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5370 })
5371 })
5372 })??
5373 }
5374 };
5375
5376 workspace.update(&mut cx, |_, cx| {
5377 cx.activate(true);
5378 cx.activate_window();
5379 })?;
5380
5381 anyhow::Ok(workspace)
5382 })
5383}
5384
5385pub fn join_in_room_project(
5386 project_id: u64,
5387 follow_user_id: u64,
5388 app_state: Arc<AppState>,
5389 cx: &mut AppContext,
5390) -> Task<Result<()>> {
5391 let windows = cx.windows();
5392 cx.spawn(|mut cx| async move {
5393 let existing_workspace = windows.into_iter().find_map(|window| {
5394 window.downcast::<Workspace>().and_then(|window| {
5395 window
5396 .update(&mut cx, |workspace, cx| {
5397 if workspace.project().read(cx).remote_id() == Some(project_id) {
5398 Some(window)
5399 } else {
5400 None
5401 }
5402 })
5403 .unwrap_or(None)
5404 })
5405 });
5406
5407 let workspace = if let Some(existing_workspace) = existing_workspace {
5408 existing_workspace
5409 } else {
5410 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5411 let room = active_call
5412 .read_with(&cx, |call, _| call.room().cloned())?
5413 .ok_or_else(|| anyhow!("not in a call"))?;
5414 let project = room
5415 .update(&mut cx, |room, cx| {
5416 room.join_project(
5417 project_id,
5418 app_state.languages.clone(),
5419 app_state.fs.clone(),
5420 cx,
5421 )
5422 })?
5423 .await?;
5424
5425 let window_bounds_override = window_bounds_env_override();
5426 cx.update(|cx| {
5427 let mut options = (app_state.build_window_options)(None, cx);
5428 options.window_bounds =
5429 window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
5430 cx.open_window(options, |cx| {
5431 cx.new_view(|cx| {
5432 Workspace::new(Default::default(), project, app_state.clone(), cx)
5433 })
5434 })
5435 })??
5436 };
5437
5438 workspace.update(&mut cx, |workspace, cx| {
5439 cx.activate(true);
5440 cx.activate_window();
5441
5442 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5443 let follow_peer_id = room
5444 .read(cx)
5445 .remote_participants()
5446 .iter()
5447 .find(|(_, participant)| participant.user.id == follow_user_id)
5448 .map(|(_, p)| p.peer_id)
5449 .or_else(|| {
5450 // If we couldn't follow the given user, follow the host instead.
5451 let collaborator = workspace
5452 .project()
5453 .read(cx)
5454 .collaborators()
5455 .values()
5456 .find(|collaborator| collaborator.replica_id == 0)?;
5457 Some(collaborator.peer_id)
5458 });
5459
5460 if let Some(follow_peer_id) = follow_peer_id {
5461 workspace.follow(follow_peer_id, cx);
5462 }
5463 }
5464 })?;
5465
5466 anyhow::Ok(())
5467 })
5468}
5469
5470pub fn reload(reload: &Reload, cx: &mut AppContext) {
5471 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5472 let mut workspace_windows = cx
5473 .windows()
5474 .into_iter()
5475 .filter_map(|window| window.downcast::<Workspace>())
5476 .collect::<Vec<_>>();
5477
5478 // If multiple windows have unsaved changes, and need a save prompt,
5479 // prompt in the active window before switching to a different window.
5480 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5481
5482 let mut prompt = None;
5483 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5484 prompt = window
5485 .update(cx, |_, cx| {
5486 cx.prompt(
5487 PromptLevel::Info,
5488 "Are you sure you want to restart?",
5489 None,
5490 &["Restart", "Cancel"],
5491 )
5492 })
5493 .ok();
5494 }
5495
5496 let binary_path = reload.binary_path.clone();
5497 cx.spawn(|mut cx| async move {
5498 if let Some(prompt) = prompt {
5499 let answer = prompt.await?;
5500 if answer != 0 {
5501 return Ok(());
5502 }
5503 }
5504
5505 // If the user cancels any save prompt, then keep the app open.
5506 for window in workspace_windows {
5507 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5508 workspace.prepare_to_close(true, cx)
5509 }) {
5510 if !should_close.await? {
5511 return Ok(());
5512 }
5513 }
5514 }
5515
5516 cx.update(|cx| cx.restart(binary_path))
5517 })
5518 .detach_and_log_err(cx);
5519}
5520
5521fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5522 let mut parts = value.split(',');
5523 let x: usize = parts.next()?.parse().ok()?;
5524 let y: usize = parts.next()?.parse().ok()?;
5525 Some(point(px(x as f32), px(y as f32)))
5526}
5527
5528fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5529 let mut parts = value.split(',');
5530 let width: usize = parts.next()?.parse().ok()?;
5531 let height: usize = parts.next()?.parse().ok()?;
5532 Some(size(px(width as f32), px(height as f32)))
5533}
5534
5535#[cfg(test)]
5536mod tests {
5537 use std::{cell::RefCell, rc::Rc};
5538
5539 use super::*;
5540 use crate::{
5541 dock::{test::TestPanel, PanelEvent},
5542 item::{
5543 test::{TestItem, TestProjectItem},
5544 ItemEvent,
5545 },
5546 };
5547 use fs::FakeFs;
5548 use gpui::{
5549 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
5550 UpdateGlobal, VisualTestContext,
5551 };
5552 use project::{Project, ProjectEntryId};
5553 use serde_json::json;
5554 use settings::SettingsStore;
5555
5556 #[gpui::test]
5557 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
5558 init_test(cx);
5559
5560 let fs = FakeFs::new(cx.executor());
5561 let project = Project::test(fs, [], cx).await;
5562 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5563
5564 // Adding an item with no ambiguity renders the tab without detail.
5565 let item1 = cx.new_view(|cx| {
5566 let mut item = TestItem::new(cx);
5567 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
5568 item
5569 });
5570 workspace.update(cx, |workspace, cx| {
5571 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5572 });
5573 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
5574
5575 // Adding an item that creates ambiguity increases the level of detail on
5576 // both tabs.
5577 let item2 = cx.new_view(|cx| {
5578 let mut item = TestItem::new(cx);
5579 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5580 item
5581 });
5582 workspace.update(cx, |workspace, cx| {
5583 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5584 });
5585 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5586 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5587
5588 // Adding an item that creates ambiguity increases the level of detail only
5589 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
5590 // we stop at the highest detail available.
5591 let item3 = cx.new_view(|cx| {
5592 let mut item = TestItem::new(cx);
5593 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5594 item
5595 });
5596 workspace.update(cx, |workspace, cx| {
5597 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5598 });
5599 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5600 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5601 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5602 }
5603
5604 #[gpui::test]
5605 async fn test_tracking_active_path(cx: &mut TestAppContext) {
5606 init_test(cx);
5607
5608 let fs = FakeFs::new(cx.executor());
5609 fs.insert_tree(
5610 "/root1",
5611 json!({
5612 "one.txt": "",
5613 "two.txt": "",
5614 }),
5615 )
5616 .await;
5617 fs.insert_tree(
5618 "/root2",
5619 json!({
5620 "three.txt": "",
5621 }),
5622 )
5623 .await;
5624
5625 let project = Project::test(fs, ["root1".as_ref()], cx).await;
5626 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5627 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5628 let worktree_id = project.update(cx, |project, cx| {
5629 project.worktrees().next().unwrap().read(cx).id()
5630 });
5631
5632 let item1 = cx.new_view(|cx| {
5633 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5634 });
5635 let item2 = cx.new_view(|cx| {
5636 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
5637 });
5638
5639 // Add an item to an empty pane
5640 workspace.update(cx, |workspace, cx| {
5641 workspace.add_item_to_active_pane(Box::new(item1), None, cx)
5642 });
5643 project.update(cx, |project, cx| {
5644 assert_eq!(
5645 project.active_entry(),
5646 project
5647 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5648 .map(|e| e.id)
5649 );
5650 });
5651 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5652
5653 // Add a second item to a non-empty pane
5654 workspace.update(cx, |workspace, cx| {
5655 workspace.add_item_to_active_pane(Box::new(item2), None, cx)
5656 });
5657 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5658 project.update(cx, |project, cx| {
5659 assert_eq!(
5660 project.active_entry(),
5661 project
5662 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5663 .map(|e| e.id)
5664 );
5665 });
5666
5667 // Close the active item
5668 pane.update(cx, |pane, cx| {
5669 pane.close_active_item(&Default::default(), cx).unwrap()
5670 })
5671 .await
5672 .unwrap();
5673 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5674 project.update(cx, |project, cx| {
5675 assert_eq!(
5676 project.active_entry(),
5677 project
5678 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5679 .map(|e| e.id)
5680 );
5681 });
5682
5683 // Add a project folder
5684 project
5685 .update(cx, |project, cx| {
5686 project.find_or_create_worktree("root2", true, cx)
5687 })
5688 .await
5689 .unwrap();
5690 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5691
5692 // Remove a project folder
5693 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5694 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5695 }
5696
5697 #[gpui::test]
5698 async fn test_close_window(cx: &mut TestAppContext) {
5699 init_test(cx);
5700
5701 let fs = FakeFs::new(cx.executor());
5702 fs.insert_tree("/root", json!({ "one": "" })).await;
5703
5704 let project = Project::test(fs, ["root".as_ref()], cx).await;
5705 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5706
5707 // When there are no dirty items, there's nothing to do.
5708 let item1 = cx.new_view(|cx| TestItem::new(cx));
5709 workspace.update(cx, |w, cx| {
5710 w.add_item_to_active_pane(Box::new(item1.clone()), None, cx)
5711 });
5712 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5713 assert!(task.await.unwrap());
5714
5715 // When there are dirty untitled items, prompt to save each one. If the user
5716 // cancels any prompt, then abort.
5717 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5718 let item3 = cx.new_view(|cx| {
5719 TestItem::new(cx)
5720 .with_dirty(true)
5721 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5722 });
5723 workspace.update(cx, |w, cx| {
5724 w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5725 w.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5726 });
5727 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5728 cx.executor().run_until_parked();
5729 cx.simulate_prompt_answer(2); // cancel save all
5730 cx.executor().run_until_parked();
5731 cx.simulate_prompt_answer(2); // cancel save all
5732 cx.executor().run_until_parked();
5733 assert!(!cx.has_pending_prompt());
5734 assert!(!task.await.unwrap());
5735 }
5736
5737 #[gpui::test]
5738 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
5739 init_test(cx);
5740
5741 // Register TestItem as a serializable item
5742 cx.update(|cx| {
5743 register_serializable_item::<TestItem>(cx);
5744 });
5745
5746 let fs = FakeFs::new(cx.executor());
5747 fs.insert_tree("/root", json!({ "one": "" })).await;
5748
5749 let project = Project::test(fs, ["root".as_ref()], cx).await;
5750 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5751
5752 // When there are dirty untitled items, but they can serialize, then there is no prompt.
5753 let item1 = cx.new_view(|cx| {
5754 TestItem::new(cx)
5755 .with_dirty(true)
5756 .with_serialize(|| Some(Task::ready(Ok(()))))
5757 });
5758 let item2 = cx.new_view(|cx| {
5759 TestItem::new(cx)
5760 .with_dirty(true)
5761 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5762 .with_serialize(|| Some(Task::ready(Ok(()))))
5763 });
5764 workspace.update(cx, |w, cx| {
5765 w.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5766 w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5767 });
5768 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5769 assert!(task.await.unwrap());
5770 }
5771
5772 #[gpui::test]
5773 async fn test_close_pane_items(cx: &mut TestAppContext) {
5774 init_test(cx);
5775
5776 let fs = FakeFs::new(cx.executor());
5777
5778 let project = Project::test(fs, None, cx).await;
5779 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5780
5781 let item1 = cx.new_view(|cx| {
5782 TestItem::new(cx)
5783 .with_dirty(true)
5784 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5785 });
5786 let item2 = cx.new_view(|cx| {
5787 TestItem::new(cx)
5788 .with_dirty(true)
5789 .with_conflict(true)
5790 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5791 });
5792 let item3 = cx.new_view(|cx| {
5793 TestItem::new(cx)
5794 .with_dirty(true)
5795 .with_conflict(true)
5796 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5797 });
5798 let item4 = cx.new_view(|cx| {
5799 TestItem::new(cx)
5800 .with_dirty(true)
5801 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5802 });
5803 let pane = workspace.update(cx, |workspace, cx| {
5804 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
5805 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
5806 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
5807 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, cx);
5808 workspace.active_pane().clone()
5809 });
5810
5811 let close_items = pane.update(cx, |pane, cx| {
5812 pane.activate_item(1, true, true, cx);
5813 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5814 let item1_id = item1.item_id();
5815 let item3_id = item3.item_id();
5816 let item4_id = item4.item_id();
5817 pane.close_items(cx, SaveIntent::Close, move |id| {
5818 [item1_id, item3_id, item4_id].contains(&id)
5819 })
5820 });
5821 cx.executor().run_until_parked();
5822
5823 assert!(cx.has_pending_prompt());
5824 // Ignore "Save all" prompt
5825 cx.simulate_prompt_answer(2);
5826 cx.executor().run_until_parked();
5827 // There's a prompt to save item 1.
5828 pane.update(cx, |pane, _| {
5829 assert_eq!(pane.items_len(), 4);
5830 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
5831 });
5832 // Confirm saving item 1.
5833 cx.simulate_prompt_answer(0);
5834 cx.executor().run_until_parked();
5835
5836 // Item 1 is saved. There's a prompt to save item 3.
5837 pane.update(cx, |pane, cx| {
5838 assert_eq!(item1.read(cx).save_count, 1);
5839 assert_eq!(item1.read(cx).save_as_count, 0);
5840 assert_eq!(item1.read(cx).reload_count, 0);
5841 assert_eq!(pane.items_len(), 3);
5842 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
5843 });
5844 assert!(cx.has_pending_prompt());
5845
5846 // Cancel saving item 3.
5847 cx.simulate_prompt_answer(1);
5848 cx.executor().run_until_parked();
5849
5850 // Item 3 is reloaded. There's a prompt to save item 4.
5851 pane.update(cx, |pane, cx| {
5852 assert_eq!(item3.read(cx).save_count, 0);
5853 assert_eq!(item3.read(cx).save_as_count, 0);
5854 assert_eq!(item3.read(cx).reload_count, 1);
5855 assert_eq!(pane.items_len(), 2);
5856 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
5857 });
5858 assert!(cx.has_pending_prompt());
5859
5860 // Confirm saving item 4.
5861 cx.simulate_prompt_answer(0);
5862 cx.executor().run_until_parked();
5863
5864 // There's a prompt for a path for item 4.
5865 cx.simulate_new_path_selection(|_| Some(Default::default()));
5866 close_items.await.unwrap();
5867
5868 // The requested items are closed.
5869 pane.update(cx, |pane, cx| {
5870 assert_eq!(item4.read(cx).save_count, 0);
5871 assert_eq!(item4.read(cx).save_as_count, 1);
5872 assert_eq!(item4.read(cx).reload_count, 0);
5873 assert_eq!(pane.items_len(), 1);
5874 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5875 });
5876 }
5877
5878 #[gpui::test]
5879 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
5880 init_test(cx);
5881
5882 let fs = FakeFs::new(cx.executor());
5883 let project = Project::test(fs, [], cx).await;
5884 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5885
5886 // Create several workspace items with single project entries, and two
5887 // workspace items with multiple project entries.
5888 let single_entry_items = (0..=4)
5889 .map(|project_entry_id| {
5890 cx.new_view(|cx| {
5891 TestItem::new(cx)
5892 .with_dirty(true)
5893 .with_project_items(&[TestProjectItem::new(
5894 project_entry_id,
5895 &format!("{project_entry_id}.txt"),
5896 cx,
5897 )])
5898 })
5899 })
5900 .collect::<Vec<_>>();
5901 let item_2_3 = cx.new_view(|cx| {
5902 TestItem::new(cx)
5903 .with_dirty(true)
5904 .with_singleton(false)
5905 .with_project_items(&[
5906 single_entry_items[2].read(cx).project_items[0].clone(),
5907 single_entry_items[3].read(cx).project_items[0].clone(),
5908 ])
5909 });
5910 let item_3_4 = cx.new_view(|cx| {
5911 TestItem::new(cx)
5912 .with_dirty(true)
5913 .with_singleton(false)
5914 .with_project_items(&[
5915 single_entry_items[3].read(cx).project_items[0].clone(),
5916 single_entry_items[4].read(cx).project_items[0].clone(),
5917 ])
5918 });
5919
5920 // Create two panes that contain the following project entries:
5921 // left pane:
5922 // multi-entry items: (2, 3)
5923 // single-entry items: 0, 1, 2, 3, 4
5924 // right pane:
5925 // single-entry items: 1
5926 // multi-entry items: (3, 4)
5927 let left_pane = workspace.update(cx, |workspace, cx| {
5928 let left_pane = workspace.active_pane().clone();
5929 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, cx);
5930 for item in single_entry_items {
5931 workspace.add_item_to_active_pane(Box::new(item), None, cx);
5932 }
5933 left_pane.update(cx, |pane, cx| {
5934 pane.activate_item(2, true, true, cx);
5935 });
5936
5937 let right_pane = workspace
5938 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5939 .unwrap();
5940
5941 right_pane.update(cx, |pane, cx| {
5942 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5943 });
5944
5945 left_pane
5946 });
5947
5948 cx.focus_view(&left_pane);
5949
5950 // When closing all of the items in the left pane, we should be prompted twice:
5951 // once for project entry 0, and once for project entry 2. Project entries 1,
5952 // 3, and 4 are all still open in the other paten. After those two
5953 // prompts, the task should complete.
5954
5955 let close = left_pane.update(cx, |pane, cx| {
5956 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5957 });
5958 cx.executor().run_until_parked();
5959
5960 // Discard "Save all" prompt
5961 cx.simulate_prompt_answer(2);
5962
5963 cx.executor().run_until_parked();
5964 left_pane.update(cx, |pane, cx| {
5965 assert_eq!(
5966 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5967 &[ProjectEntryId::from_proto(0)]
5968 );
5969 });
5970 cx.simulate_prompt_answer(0);
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(2)]
5977 );
5978 });
5979 cx.simulate_prompt_answer(0);
5980
5981 cx.executor().run_until_parked();
5982 close.await.unwrap();
5983 left_pane.update(cx, |pane, _| {
5984 assert_eq!(pane.items_len(), 0);
5985 });
5986 }
5987
5988 #[gpui::test]
5989 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5990 init_test(cx);
5991
5992 let fs = FakeFs::new(cx.executor());
5993 let project = Project::test(fs, [], cx).await;
5994 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5995 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5996
5997 let item = cx.new_view(|cx| {
5998 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5999 });
6000 let item_id = item.entity_id();
6001 workspace.update(cx, |workspace, cx| {
6002 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
6003 });
6004
6005 // Autosave on window change.
6006 item.update(cx, |item, cx| {
6007 SettingsStore::update_global(cx, |settings, cx| {
6008 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6009 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6010 })
6011 });
6012 item.is_dirty = true;
6013 });
6014
6015 // Deactivating the window saves the file.
6016 cx.deactivate_window();
6017 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6018
6019 // Re-activating the window doesn't save the file.
6020 cx.update(|cx| cx.activate_window());
6021 cx.executor().run_until_parked();
6022 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6023
6024 // Autosave on focus change.
6025 item.update(cx, |item, cx| {
6026 cx.focus_self();
6027 SettingsStore::update_global(cx, |settings, cx| {
6028 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6029 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6030 })
6031 });
6032 item.is_dirty = true;
6033 });
6034
6035 // Blurring the item saves the file.
6036 item.update(cx, |_, cx| cx.blur());
6037 cx.executor().run_until_parked();
6038 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6039
6040 // Deactivating the window still saves the file.
6041 item.update(cx, |item, cx| {
6042 cx.focus_self();
6043 item.is_dirty = true;
6044 });
6045 cx.deactivate_window();
6046 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6047
6048 // Autosave after delay.
6049 item.update(cx, |item, cx| {
6050 SettingsStore::update_global(cx, |settings, cx| {
6051 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6052 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6053 })
6054 });
6055 item.is_dirty = true;
6056 cx.emit(ItemEvent::Edit);
6057 });
6058
6059 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6060 cx.executor().advance_clock(Duration::from_millis(250));
6061 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6062
6063 // After delay expires, the file is saved.
6064 cx.executor().advance_clock(Duration::from_millis(250));
6065 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6066
6067 // Autosave on focus change, ensuring closing the tab counts as such.
6068 item.update(cx, |item, cx| {
6069 SettingsStore::update_global(cx, |settings, cx| {
6070 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6071 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6072 })
6073 });
6074 item.is_dirty = true;
6075 });
6076
6077 pane.update(cx, |pane, cx| {
6078 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6079 })
6080 .await
6081 .unwrap();
6082 assert!(!cx.has_pending_prompt());
6083 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6084
6085 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6086 workspace.update(cx, |workspace, cx| {
6087 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
6088 });
6089 item.update(cx, |item, cx| {
6090 item.project_items[0].update(cx, |item, _| {
6091 item.entry_id = None;
6092 });
6093 item.is_dirty = true;
6094 cx.blur();
6095 });
6096 cx.run_until_parked();
6097 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6098
6099 // Ensure autosave is prevented for deleted files also when closing the buffer.
6100 let _close_items = pane.update(cx, |pane, cx| {
6101 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6102 });
6103 cx.run_until_parked();
6104 assert!(cx.has_pending_prompt());
6105 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6106 }
6107
6108 #[gpui::test]
6109 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6110 init_test(cx);
6111
6112 let fs = FakeFs::new(cx.executor());
6113
6114 let project = Project::test(fs, [], cx).await;
6115 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6116
6117 let item = cx.new_view(|cx| {
6118 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6119 });
6120 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6121 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6122 let toolbar_notify_count = Rc::new(RefCell::new(0));
6123
6124 workspace.update(cx, |workspace, cx| {
6125 workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
6126 let toolbar_notification_count = toolbar_notify_count.clone();
6127 cx.observe(&toolbar, move |_, _, _| {
6128 *toolbar_notification_count.borrow_mut() += 1
6129 })
6130 .detach();
6131 });
6132
6133 pane.update(cx, |pane, _| {
6134 assert!(!pane.can_navigate_backward());
6135 assert!(!pane.can_navigate_forward());
6136 });
6137
6138 item.update(cx, |item, cx| {
6139 item.set_state("one".to_string(), cx);
6140 });
6141
6142 // Toolbar must be notified to re-render the navigation buttons
6143 assert_eq!(*toolbar_notify_count.borrow(), 1);
6144
6145 pane.update(cx, |pane, _| {
6146 assert!(pane.can_navigate_backward());
6147 assert!(!pane.can_navigate_forward());
6148 });
6149
6150 workspace
6151 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6152 .await
6153 .unwrap();
6154
6155 assert_eq!(*toolbar_notify_count.borrow(), 2);
6156 pane.update(cx, |pane, _| {
6157 assert!(!pane.can_navigate_backward());
6158 assert!(pane.can_navigate_forward());
6159 });
6160 }
6161
6162 #[gpui::test]
6163 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6164 init_test(cx);
6165 let fs = FakeFs::new(cx.executor());
6166
6167 let project = Project::test(fs, [], cx).await;
6168 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6169
6170 let panel = workspace.update(cx, |workspace, cx| {
6171 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6172 workspace.add_panel(panel.clone(), cx);
6173
6174 workspace
6175 .right_dock()
6176 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6177
6178 panel
6179 });
6180
6181 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6182 pane.update(cx, |pane, cx| {
6183 let item = cx.new_view(|cx| TestItem::new(cx));
6184 pane.add_item(Box::new(item), true, true, None, cx);
6185 });
6186
6187 // Transfer focus from center to panel
6188 workspace.update(cx, |workspace, cx| {
6189 workspace.toggle_panel_focus::<TestPanel>(cx);
6190 });
6191
6192 workspace.update(cx, |workspace, cx| {
6193 assert!(workspace.right_dock().read(cx).is_open());
6194 assert!(!panel.is_zoomed(cx));
6195 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6196 });
6197
6198 // Transfer focus from panel to center
6199 workspace.update(cx, |workspace, cx| {
6200 workspace.toggle_panel_focus::<TestPanel>(cx);
6201 });
6202
6203 workspace.update(cx, |workspace, cx| {
6204 assert!(workspace.right_dock().read(cx).is_open());
6205 assert!(!panel.is_zoomed(cx));
6206 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6207 });
6208
6209 // Close the dock
6210 workspace.update(cx, |workspace, cx| {
6211 workspace.toggle_dock(DockPosition::Right, cx);
6212 });
6213
6214 workspace.update(cx, |workspace, cx| {
6215 assert!(!workspace.right_dock().read(cx).is_open());
6216 assert!(!panel.is_zoomed(cx));
6217 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6218 });
6219
6220 // Open the dock
6221 workspace.update(cx, |workspace, cx| {
6222 workspace.toggle_dock(DockPosition::Right, cx);
6223 });
6224
6225 workspace.update(cx, |workspace, cx| {
6226 assert!(workspace.right_dock().read(cx).is_open());
6227 assert!(!panel.is_zoomed(cx));
6228 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6229 });
6230
6231 // Focus and zoom panel
6232 panel.update(cx, |panel, cx| {
6233 cx.focus_self();
6234 panel.set_zoomed(true, cx)
6235 });
6236
6237 workspace.update(cx, |workspace, cx| {
6238 assert!(workspace.right_dock().read(cx).is_open());
6239 assert!(panel.is_zoomed(cx));
6240 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6241 });
6242
6243 // Transfer focus to the center closes the dock
6244 workspace.update(cx, |workspace, cx| {
6245 workspace.toggle_panel_focus::<TestPanel>(cx);
6246 });
6247
6248 workspace.update(cx, |workspace, cx| {
6249 assert!(!workspace.right_dock().read(cx).is_open());
6250 assert!(panel.is_zoomed(cx));
6251 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6252 });
6253
6254 // Transferring focus back to the panel keeps it zoomed
6255 workspace.update(cx, |workspace, cx| {
6256 workspace.toggle_panel_focus::<TestPanel>(cx);
6257 });
6258
6259 workspace.update(cx, |workspace, cx| {
6260 assert!(workspace.right_dock().read(cx).is_open());
6261 assert!(panel.is_zoomed(cx));
6262 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6263 });
6264
6265 // Close the dock while it is zoomed
6266 workspace.update(cx, |workspace, cx| {
6267 workspace.toggle_dock(DockPosition::Right, cx)
6268 });
6269
6270 workspace.update(cx, |workspace, cx| {
6271 assert!(!workspace.right_dock().read(cx).is_open());
6272 assert!(panel.is_zoomed(cx));
6273 assert!(workspace.zoomed.is_none());
6274 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6275 });
6276
6277 // Opening the dock, when it's zoomed, retains focus
6278 workspace.update(cx, |workspace, cx| {
6279 workspace.toggle_dock(DockPosition::Right, cx)
6280 });
6281
6282 workspace.update(cx, |workspace, cx| {
6283 assert!(workspace.right_dock().read(cx).is_open());
6284 assert!(panel.is_zoomed(cx));
6285 assert!(workspace.zoomed.is_some());
6286 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6287 });
6288
6289 // Unzoom and close the panel, zoom the active pane.
6290 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6291 workspace.update(cx, |workspace, cx| {
6292 workspace.toggle_dock(DockPosition::Right, cx)
6293 });
6294 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6295
6296 // Opening a dock unzooms the pane.
6297 workspace.update(cx, |workspace, cx| {
6298 workspace.toggle_dock(DockPosition::Right, cx)
6299 });
6300 workspace.update(cx, |workspace, cx| {
6301 let pane = pane.read(cx);
6302 assert!(!pane.is_zoomed());
6303 assert!(!pane.focus_handle(cx).is_focused(cx));
6304 assert!(workspace.right_dock().read(cx).is_open());
6305 assert!(workspace.zoomed.is_none());
6306 });
6307 }
6308
6309 struct TestModal(FocusHandle);
6310
6311 impl TestModal {
6312 fn new(cx: &mut ViewContext<Self>) -> Self {
6313 Self(cx.focus_handle())
6314 }
6315 }
6316
6317 impl EventEmitter<DismissEvent> for TestModal {}
6318
6319 impl FocusableView for TestModal {
6320 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6321 self.0.clone()
6322 }
6323 }
6324
6325 impl ModalView for TestModal {}
6326
6327 impl Render for TestModal {
6328 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
6329 div().track_focus(&self.0)
6330 }
6331 }
6332
6333 #[gpui::test]
6334 async fn test_panels(cx: &mut gpui::TestAppContext) {
6335 init_test(cx);
6336 let fs = FakeFs::new(cx.executor());
6337
6338 let project = Project::test(fs, [], cx).await;
6339 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6340
6341 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
6342 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
6343 workspace.add_panel(panel_1.clone(), cx);
6344 workspace
6345 .left_dock()
6346 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
6347 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6348 workspace.add_panel(panel_2.clone(), cx);
6349 workspace
6350 .right_dock()
6351 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6352
6353 let left_dock = workspace.left_dock();
6354 assert_eq!(
6355 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6356 panel_1.panel_id()
6357 );
6358 assert_eq!(
6359 left_dock.read(cx).active_panel_size(cx).unwrap(),
6360 panel_1.size(cx)
6361 );
6362
6363 left_dock.update(cx, |left_dock, cx| {
6364 left_dock.resize_active_panel(Some(px(1337.)), cx)
6365 });
6366 assert_eq!(
6367 workspace
6368 .right_dock()
6369 .read(cx)
6370 .visible_panel()
6371 .unwrap()
6372 .panel_id(),
6373 panel_2.panel_id(),
6374 );
6375
6376 (panel_1, panel_2)
6377 });
6378
6379 // Move panel_1 to the right
6380 panel_1.update(cx, |panel_1, cx| {
6381 panel_1.set_position(DockPosition::Right, cx)
6382 });
6383
6384 workspace.update(cx, |workspace, cx| {
6385 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
6386 // Since it was the only panel on the left, the left dock should now be closed.
6387 assert!(!workspace.left_dock().read(cx).is_open());
6388 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
6389 let right_dock = workspace.right_dock();
6390 assert_eq!(
6391 right_dock.read(cx).visible_panel().unwrap().panel_id(),
6392 panel_1.panel_id()
6393 );
6394 assert_eq!(
6395 right_dock.read(cx).active_panel_size(cx).unwrap(),
6396 px(1337.)
6397 );
6398
6399 // Now we move panel_2 to the left
6400 panel_2.set_position(DockPosition::Left, cx);
6401 });
6402
6403 workspace.update(cx, |workspace, cx| {
6404 // Since panel_2 was not visible on the right, we don't open the left dock.
6405 assert!(!workspace.left_dock().read(cx).is_open());
6406 // And the right dock is unaffected in its displaying of panel_1
6407 assert!(workspace.right_dock().read(cx).is_open());
6408 assert_eq!(
6409 workspace
6410 .right_dock()
6411 .read(cx)
6412 .visible_panel()
6413 .unwrap()
6414 .panel_id(),
6415 panel_1.panel_id(),
6416 );
6417 });
6418
6419 // Move panel_1 back to the left
6420 panel_1.update(cx, |panel_1, cx| {
6421 panel_1.set_position(DockPosition::Left, cx)
6422 });
6423
6424 workspace.update(cx, |workspace, cx| {
6425 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
6426 let left_dock = workspace.left_dock();
6427 assert!(left_dock.read(cx).is_open());
6428 assert_eq!(
6429 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6430 panel_1.panel_id()
6431 );
6432 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
6433 // And the right dock should be closed as it no longer has any panels.
6434 assert!(!workspace.right_dock().read(cx).is_open());
6435
6436 // Now we move panel_1 to the bottom
6437 panel_1.set_position(DockPosition::Bottom, cx);
6438 });
6439
6440 workspace.update(cx, |workspace, cx| {
6441 // Since panel_1 was visible on the left, we close the left dock.
6442 assert!(!workspace.left_dock().read(cx).is_open());
6443 // The bottom dock is sized based on the panel's default size,
6444 // since the panel orientation changed from vertical to horizontal.
6445 let bottom_dock = workspace.bottom_dock();
6446 assert_eq!(
6447 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
6448 panel_1.size(cx),
6449 );
6450 // Close bottom dock and move panel_1 back to the left.
6451 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
6452 panel_1.set_position(DockPosition::Left, cx);
6453 });
6454
6455 // Emit activated event on panel 1
6456 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
6457
6458 // Now the left dock is open and panel_1 is active and focused.
6459 workspace.update(cx, |workspace, cx| {
6460 let left_dock = workspace.left_dock();
6461 assert!(left_dock.read(cx).is_open());
6462 assert_eq!(
6463 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6464 panel_1.panel_id(),
6465 );
6466 assert!(panel_1.focus_handle(cx).is_focused(cx));
6467 });
6468
6469 // Emit closed event on panel 2, which is not active
6470 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6471
6472 // Wo don't close the left dock, because panel_2 wasn't the active panel
6473 workspace.update(cx, |workspace, cx| {
6474 let left_dock = workspace.left_dock();
6475 assert!(left_dock.read(cx).is_open());
6476 assert_eq!(
6477 left_dock.read(cx).visible_panel().unwrap().panel_id(),
6478 panel_1.panel_id(),
6479 );
6480 });
6481
6482 // Emitting a ZoomIn event shows the panel as zoomed.
6483 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
6484 workspace.update(cx, |workspace, _| {
6485 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6486 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
6487 });
6488
6489 // Move panel to another dock while it is zoomed
6490 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
6491 workspace.update(cx, |workspace, _| {
6492 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6493
6494 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6495 });
6496
6497 // This is a helper for getting a:
6498 // - valid focus on an element,
6499 // - that isn't a part of the panes and panels system of the Workspace,
6500 // - and doesn't trigger the 'on_focus_lost' API.
6501 let focus_other_view = {
6502 let workspace = workspace.clone();
6503 move |cx: &mut VisualTestContext| {
6504 workspace.update(cx, |workspace, cx| {
6505 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
6506 workspace.toggle_modal(cx, TestModal::new);
6507 workspace.toggle_modal(cx, TestModal::new);
6508 } else {
6509 workspace.toggle_modal(cx, TestModal::new);
6510 }
6511 })
6512 }
6513 };
6514
6515 // If focus is transferred to another view that's not a panel or another pane, we still show
6516 // the panel as zoomed.
6517 focus_other_view(cx);
6518 workspace.update(cx, |workspace, _| {
6519 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6520 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6521 });
6522
6523 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
6524 workspace.update(cx, |_, cx| cx.focus_self());
6525 workspace.update(cx, |workspace, _| {
6526 assert_eq!(workspace.zoomed, None);
6527 assert_eq!(workspace.zoomed_position, None);
6528 });
6529
6530 // If focus is transferred again to another view that's not a panel or a pane, we won't
6531 // show the panel as zoomed because it wasn't zoomed before.
6532 focus_other_view(cx);
6533 workspace.update(cx, |workspace, _| {
6534 assert_eq!(workspace.zoomed, None);
6535 assert_eq!(workspace.zoomed_position, None);
6536 });
6537
6538 // When the panel is activated, it is zoomed again.
6539 cx.dispatch_action(ToggleRightDock);
6540 workspace.update(cx, |workspace, _| {
6541 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
6542 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6543 });
6544
6545 // Emitting a ZoomOut event unzooms the panel.
6546 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
6547 workspace.update(cx, |workspace, _| {
6548 assert_eq!(workspace.zoomed, None);
6549 assert_eq!(workspace.zoomed_position, None);
6550 });
6551
6552 // Emit closed event on panel 1, which is active
6553 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6554
6555 // Now the left dock is closed, because panel_1 was the active panel
6556 workspace.update(cx, |workspace, cx| {
6557 let right_dock = workspace.right_dock();
6558 assert!(!right_dock.read(cx).is_open());
6559 });
6560 }
6561
6562 mod register_project_item_tests {
6563 use ui::Context as _;
6564
6565 use super::*;
6566
6567 // View
6568 struct TestPngItemView {
6569 focus_handle: FocusHandle,
6570 }
6571 // Model
6572 struct TestPngItem {}
6573
6574 impl project::Item for TestPngItem {
6575 fn try_open(
6576 _project: &Model<Project>,
6577 path: &ProjectPath,
6578 cx: &mut AppContext,
6579 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6580 if path.path.extension().unwrap() == "png" {
6581 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
6582 } else {
6583 None
6584 }
6585 }
6586
6587 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6588 None
6589 }
6590
6591 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6592 None
6593 }
6594 }
6595
6596 impl Item for TestPngItemView {
6597 type Event = ();
6598 }
6599 impl EventEmitter<()> for TestPngItemView {}
6600 impl FocusableView for TestPngItemView {
6601 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6602 self.focus_handle.clone()
6603 }
6604 }
6605
6606 impl Render for TestPngItemView {
6607 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6608 Empty
6609 }
6610 }
6611
6612 impl ProjectItem for TestPngItemView {
6613 type Item = TestPngItem;
6614
6615 fn for_project_item(
6616 _project: Model<Project>,
6617 _item: Model<Self::Item>,
6618 cx: &mut ViewContext<Self>,
6619 ) -> Self
6620 where
6621 Self: Sized,
6622 {
6623 Self {
6624 focus_handle: cx.focus_handle(),
6625 }
6626 }
6627 }
6628
6629 // View
6630 struct TestIpynbItemView {
6631 focus_handle: FocusHandle,
6632 }
6633 // Model
6634 struct TestIpynbItem {}
6635
6636 impl project::Item for TestIpynbItem {
6637 fn try_open(
6638 _project: &Model<Project>,
6639 path: &ProjectPath,
6640 cx: &mut AppContext,
6641 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6642 if path.path.extension().unwrap() == "ipynb" {
6643 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
6644 } else {
6645 None
6646 }
6647 }
6648
6649 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6650 None
6651 }
6652
6653 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6654 None
6655 }
6656 }
6657
6658 impl Item for TestIpynbItemView {
6659 type Event = ();
6660 }
6661 impl EventEmitter<()> for TestIpynbItemView {}
6662 impl FocusableView for TestIpynbItemView {
6663 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6664 self.focus_handle.clone()
6665 }
6666 }
6667
6668 impl Render for TestIpynbItemView {
6669 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6670 Empty
6671 }
6672 }
6673
6674 impl ProjectItem for TestIpynbItemView {
6675 type Item = TestIpynbItem;
6676
6677 fn for_project_item(
6678 _project: Model<Project>,
6679 _item: Model<Self::Item>,
6680 cx: &mut ViewContext<Self>,
6681 ) -> Self
6682 where
6683 Self: Sized,
6684 {
6685 Self {
6686 focus_handle: cx.focus_handle(),
6687 }
6688 }
6689 }
6690
6691 struct TestAlternatePngItemView {
6692 focus_handle: FocusHandle,
6693 }
6694
6695 impl Item for TestAlternatePngItemView {
6696 type Event = ();
6697 }
6698
6699 impl EventEmitter<()> for TestAlternatePngItemView {}
6700 impl FocusableView for TestAlternatePngItemView {
6701 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6702 self.focus_handle.clone()
6703 }
6704 }
6705
6706 impl Render for TestAlternatePngItemView {
6707 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6708 Empty
6709 }
6710 }
6711
6712 impl ProjectItem for TestAlternatePngItemView {
6713 type Item = TestPngItem;
6714
6715 fn for_project_item(
6716 _project: Model<Project>,
6717 _item: Model<Self::Item>,
6718 cx: &mut ViewContext<Self>,
6719 ) -> Self
6720 where
6721 Self: Sized,
6722 {
6723 Self {
6724 focus_handle: cx.focus_handle(),
6725 }
6726 }
6727 }
6728
6729 #[gpui::test]
6730 async fn test_register_project_item(cx: &mut TestAppContext) {
6731 init_test(cx);
6732
6733 cx.update(|cx| {
6734 register_project_item::<TestPngItemView>(cx);
6735 register_project_item::<TestIpynbItemView>(cx);
6736 });
6737
6738 let fs = FakeFs::new(cx.executor());
6739 fs.insert_tree(
6740 "/root1",
6741 json!({
6742 "one.png": "BINARYDATAHERE",
6743 "two.ipynb": "{ totally a notebook }",
6744 "three.txt": "editing text, sure why not?"
6745 }),
6746 )
6747 .await;
6748
6749 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6750 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6751
6752 let worktree_id = project.update(cx, |project, cx| {
6753 project.worktrees().next().unwrap().read(cx).id()
6754 });
6755
6756 let handle = workspace
6757 .update(cx, |workspace, cx| {
6758 let project_path = (worktree_id, "one.png");
6759 workspace.open_path(project_path, None, true, cx)
6760 })
6761 .await
6762 .unwrap();
6763
6764 // Now we can check if the handle we got back errored or not
6765 assert_eq!(
6766 handle.to_any().entity_type(),
6767 TypeId::of::<TestPngItemView>()
6768 );
6769
6770 let handle = workspace
6771 .update(cx, |workspace, cx| {
6772 let project_path = (worktree_id, "two.ipynb");
6773 workspace.open_path(project_path, None, true, cx)
6774 })
6775 .await
6776 .unwrap();
6777
6778 assert_eq!(
6779 handle.to_any().entity_type(),
6780 TypeId::of::<TestIpynbItemView>()
6781 );
6782
6783 let handle = workspace
6784 .update(cx, |workspace, cx| {
6785 let project_path = (worktree_id, "three.txt");
6786 workspace.open_path(project_path, None, true, cx)
6787 })
6788 .await;
6789 assert!(handle.is_err());
6790 }
6791
6792 #[gpui::test]
6793 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
6794 init_test(cx);
6795
6796 cx.update(|cx| {
6797 register_project_item::<TestPngItemView>(cx);
6798 register_project_item::<TestAlternatePngItemView>(cx);
6799 });
6800
6801 let fs = FakeFs::new(cx.executor());
6802 fs.insert_tree(
6803 "/root1",
6804 json!({
6805 "one.png": "BINARYDATAHERE",
6806 "two.ipynb": "{ totally a notebook }",
6807 "three.txt": "editing text, sure why not?"
6808 }),
6809 )
6810 .await;
6811
6812 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6813 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6814
6815 let worktree_id = project.update(cx, |project, cx| {
6816 project.worktrees().next().unwrap().read(cx).id()
6817 });
6818
6819 let handle = workspace
6820 .update(cx, |workspace, cx| {
6821 let project_path = (worktree_id, "one.png");
6822 workspace.open_path(project_path, None, true, cx)
6823 })
6824 .await
6825 .unwrap();
6826
6827 // This _must_ be the second item registered
6828 assert_eq!(
6829 handle.to_any().entity_type(),
6830 TypeId::of::<TestAlternatePngItemView>()
6831 );
6832
6833 let handle = workspace
6834 .update(cx, |workspace, cx| {
6835 let project_path = (worktree_id, "three.txt");
6836 workspace.open_path(project_path, None, true, cx)
6837 })
6838 .await;
6839 assert!(handle.is_err());
6840 }
6841 }
6842
6843 pub fn init_test(cx: &mut TestAppContext) {
6844 cx.update(|cx| {
6845 let settings_store = SettingsStore::test(cx);
6846 cx.set_global(settings_store);
6847 theme::init(theme::LoadThemes::JustBase, cx);
6848 language::init(cx);
6849 crate::init_settings(cx);
6850 Project::init_settings(cx);
6851 });
6852 }
6853}
6854
6855pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
6856 const BORDER_SIZE: Pixels = px(1.0);
6857 let decorations = cx.window_decorations();
6858
6859 if matches!(decorations, Decorations::Client { .. }) {
6860 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6861 }
6862
6863 struct GlobalResizeEdge(ResizeEdge);
6864 impl Global for GlobalResizeEdge {}
6865
6866 div()
6867 .id("window-backdrop")
6868 .bg(transparent_black())
6869 .map(|div| match decorations {
6870 Decorations::Server => div,
6871 Decorations::Client { tiling, .. } => div
6872 .when(!(tiling.top || tiling.right), |div| {
6873 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6874 })
6875 .when(!(tiling.top || tiling.left), |div| {
6876 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6877 })
6878 .when(!(tiling.bottom || tiling.right), |div| {
6879 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6880 })
6881 .when(!(tiling.bottom || tiling.left), |div| {
6882 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6883 })
6884 .when(!tiling.top, |div| {
6885 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6886 })
6887 .when(!tiling.bottom, |div| {
6888 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6889 })
6890 .when(!tiling.left, |div| {
6891 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6892 })
6893 .when(!tiling.right, |div| {
6894 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6895 })
6896 .on_mouse_move(move |e, cx| {
6897 let size = cx.window_bounds().get_bounds().size;
6898 let pos = e.position;
6899
6900 let new_edge =
6901 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6902
6903 let edge = cx.try_global::<GlobalResizeEdge>();
6904 if new_edge != edge.map(|edge| edge.0) {
6905 cx.window_handle()
6906 .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
6907 .ok();
6908 }
6909 })
6910 .on_mouse_down(MouseButton::Left, move |e, cx| {
6911 let size = cx.window_bounds().get_bounds().size;
6912 let pos = e.position;
6913
6914 let edge = match resize_edge(
6915 pos,
6916 theme::CLIENT_SIDE_DECORATION_SHADOW,
6917 size,
6918 tiling,
6919 ) {
6920 Some(value) => value,
6921 None => return,
6922 };
6923
6924 cx.start_window_resize(edge);
6925 }),
6926 })
6927 .size_full()
6928 .child(
6929 div()
6930 .cursor(CursorStyle::Arrow)
6931 .map(|div| match decorations {
6932 Decorations::Server => div,
6933 Decorations::Client { tiling } => div
6934 .border_color(cx.theme().colors().border)
6935 .when(!(tiling.top || tiling.right), |div| {
6936 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6937 })
6938 .when(!(tiling.top || tiling.left), |div| {
6939 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6940 })
6941 .when(!(tiling.bottom || tiling.right), |div| {
6942 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6943 })
6944 .when(!(tiling.bottom || tiling.left), |div| {
6945 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6946 })
6947 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6948 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6949 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6950 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6951 .when(!tiling.is_tiled(), |div| {
6952 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6953 color: Hsla {
6954 h: 0.,
6955 s: 0.,
6956 l: 0.,
6957 a: 0.4,
6958 },
6959 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6960 spread_radius: px(0.),
6961 offset: point(px(0.0), px(0.0)),
6962 }])
6963 }),
6964 })
6965 .on_mouse_move(|_e, cx| {
6966 cx.stop_propagation();
6967 })
6968 .size_full()
6969 .child(element),
6970 )
6971 .map(|div| match decorations {
6972 Decorations::Server => div,
6973 Decorations::Client { tiling, .. } => div.child(
6974 canvas(
6975 |_bounds, cx| {
6976 cx.insert_hitbox(
6977 Bounds::new(
6978 point(px(0.0), px(0.0)),
6979 cx.window_bounds().get_bounds().size,
6980 ),
6981 false,
6982 )
6983 },
6984 move |_bounds, hitbox, cx| {
6985 let mouse = cx.mouse_position();
6986 let size = cx.window_bounds().get_bounds().size;
6987 let Some(edge) =
6988 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6989 else {
6990 return;
6991 };
6992 cx.set_global(GlobalResizeEdge(edge));
6993 cx.set_cursor_style(
6994 match edge {
6995 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6996 ResizeEdge::Left | ResizeEdge::Right => {
6997 CursorStyle::ResizeLeftRight
6998 }
6999 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7000 CursorStyle::ResizeUpLeftDownRight
7001 }
7002 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7003 CursorStyle::ResizeUpRightDownLeft
7004 }
7005 },
7006 &hitbox,
7007 );
7008 },
7009 )
7010 .size_full()
7011 .absolute(),
7012 ),
7013 })
7014}
7015
7016fn resize_edge(
7017 pos: Point<Pixels>,
7018 shadow_size: Pixels,
7019 window_size: Size<Pixels>,
7020 tiling: Tiling,
7021) -> Option<ResizeEdge> {
7022 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7023 if bounds.contains(&pos) {
7024 return None;
7025 }
7026
7027 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7028 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7029 if !tiling.top && top_left_bounds.contains(&pos) {
7030 return Some(ResizeEdge::TopLeft);
7031 }
7032
7033 let top_right_bounds = Bounds::new(
7034 Point::new(window_size.width - corner_size.width, px(0.)),
7035 corner_size,
7036 );
7037 if !tiling.top && top_right_bounds.contains(&pos) {
7038 return Some(ResizeEdge::TopRight);
7039 }
7040
7041 let bottom_left_bounds = Bounds::new(
7042 Point::new(px(0.), window_size.height - corner_size.height),
7043 corner_size,
7044 );
7045 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7046 return Some(ResizeEdge::BottomLeft);
7047 }
7048
7049 let bottom_right_bounds = Bounds::new(
7050 Point::new(
7051 window_size.width - corner_size.width,
7052 window_size.height - corner_size.height,
7053 ),
7054 corner_size,
7055 );
7056 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7057 return Some(ResizeEdge::BottomRight);
7058 }
7059
7060 if !tiling.top && pos.y < shadow_size {
7061 Some(ResizeEdge::Top)
7062 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7063 Some(ResizeEdge::Bottom)
7064 } else if !tiling.left && pos.x < shadow_size {
7065 Some(ResizeEdge::Left)
7066 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7067 Some(ResizeEdge::Right)
7068 } else {
7069 None
7070 }
7071}