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