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