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