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