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