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