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