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