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