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