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