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