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