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
4552fn leader_border_for_pane(
4553 follower_states: &HashMap<PeerId, FollowerState>,
4554 pane: &View<Pane>,
4555 cx: &WindowContext,
4556) -> Option<Div> {
4557 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4558 if state.pane() == pane {
4559 Some((*leader_id, state))
4560 } else {
4561 None
4562 }
4563 })?;
4564
4565 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4566 let leader = room.remote_participant_for_peer_id(leader_id)?;
4567
4568 let mut leader_color = cx
4569 .theme()
4570 .players()
4571 .color_for_participant(leader.participant_index.0)
4572 .cursor;
4573 leader_color.fade_out(0.3);
4574 Some(
4575 div()
4576 .absolute()
4577 .size_full()
4578 .left_0()
4579 .top_0()
4580 .border_2()
4581 .border_color(leader_color),
4582 )
4583}
4584
4585fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4586 ZED_WINDOW_POSITION
4587 .zip(*ZED_WINDOW_SIZE)
4588 .map(|(position, size)| Bounds {
4589 origin: position,
4590 size,
4591 })
4592}
4593
4594fn open_items(
4595 serialized_workspace: Option<SerializedWorkspace>,
4596 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4597 cx: &mut ViewContext<Workspace>,
4598) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4599 let restored_items = serialized_workspace.map(|serialized_workspace| {
4600 Workspace::load_workspace(
4601 serialized_workspace,
4602 project_paths_to_open
4603 .iter()
4604 .map(|(_, project_path)| project_path)
4605 .cloned()
4606 .collect(),
4607 cx,
4608 )
4609 });
4610
4611 cx.spawn(|workspace, mut cx| async move {
4612 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4613
4614 if let Some(restored_items) = restored_items {
4615 let restored_items = restored_items.await?;
4616
4617 let restored_project_paths = restored_items
4618 .iter()
4619 .filter_map(|item| {
4620 cx.update(|cx| item.as_ref()?.project_path(cx))
4621 .ok()
4622 .flatten()
4623 })
4624 .collect::<HashSet<_>>();
4625
4626 for restored_item in restored_items {
4627 opened_items.push(restored_item.map(Ok));
4628 }
4629
4630 project_paths_to_open
4631 .iter_mut()
4632 .for_each(|(_, project_path)| {
4633 if let Some(project_path_to_open) = project_path {
4634 if restored_project_paths.contains(project_path_to_open) {
4635 *project_path = None;
4636 }
4637 }
4638 });
4639 } else {
4640 for _ in 0..project_paths_to_open.len() {
4641 opened_items.push(None);
4642 }
4643 }
4644 assert!(opened_items.len() == project_paths_to_open.len());
4645
4646 let tasks =
4647 project_paths_to_open
4648 .into_iter()
4649 .enumerate()
4650 .map(|(ix, (abs_path, project_path))| {
4651 let workspace = workspace.clone();
4652 cx.spawn(|mut cx| async move {
4653 let file_project_path = project_path?;
4654 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4655 workspace.project().update(cx, |project, cx| {
4656 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4657 })
4658 });
4659
4660 // We only want to open file paths here. If one of the items
4661 // here is a directory, it was already opened further above
4662 // with a `find_or_create_worktree`.
4663 if let Ok(task) = abs_path_task {
4664 if task.await.map_or(true, |p| p.is_file()) {
4665 return Some((
4666 ix,
4667 workspace
4668 .update(&mut cx, |workspace, cx| {
4669 workspace.open_path(file_project_path, None, true, cx)
4670 })
4671 .log_err()?
4672 .await,
4673 ));
4674 }
4675 }
4676 None
4677 })
4678 });
4679
4680 let tasks = tasks.collect::<Vec<_>>();
4681
4682 let tasks = futures::future::join_all(tasks);
4683 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4684 opened_items[ix] = Some(path_open_result);
4685 }
4686
4687 Ok(opened_items)
4688 })
4689}
4690
4691enum ActivateInDirectionTarget {
4692 Pane(View<Pane>),
4693 Dock(View<Dock>),
4694}
4695
4696fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4697 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";
4698
4699 workspace
4700 .update(cx, |workspace, cx| {
4701 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4702 struct DatabaseFailedNotification;
4703
4704 workspace.show_notification_once(
4705 NotificationId::unique::<DatabaseFailedNotification>(),
4706 cx,
4707 |cx| {
4708 cx.new_view(|_| {
4709 MessageNotification::new("Failed to load the database file.")
4710 .with_click_message("File an issue")
4711 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4712 })
4713 },
4714 );
4715 }
4716 })
4717 .log_err();
4718}
4719
4720impl FocusableView for Workspace {
4721 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4722 self.active_pane.focus_handle(cx)
4723 }
4724}
4725
4726#[derive(Clone, Render)]
4727struct DraggedDock(DockPosition);
4728
4729impl Render for Workspace {
4730 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4731 let mut context = KeyContext::new_with_defaults();
4732 context.add("Workspace");
4733 let centered_layout = self.centered_layout
4734 && self.center.panes().len() == 1
4735 && self.active_item(cx).is_some();
4736 let render_padding = |size| {
4737 (size > 0.0).then(|| {
4738 div()
4739 .h_full()
4740 .w(relative(size))
4741 .bg(cx.theme().colors().editor_background)
4742 .border_color(cx.theme().colors().pane_group_border)
4743 })
4744 };
4745 let paddings = if centered_layout {
4746 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4747 (
4748 render_padding(Self::adjust_padding(settings.left_padding)),
4749 render_padding(Self::adjust_padding(settings.right_padding)),
4750 )
4751 } else {
4752 (None, None)
4753 };
4754 let ui_font = theme::setup_ui_font(cx);
4755
4756 let theme = cx.theme().clone();
4757 let colors = theme.colors();
4758
4759 client_side_decorations(
4760 self.actions(div(), cx)
4761 .key_context(context)
4762 .relative()
4763 .size_full()
4764 .flex()
4765 .flex_col()
4766 .font(ui_font)
4767 .gap_0()
4768 .justify_start()
4769 .items_start()
4770 .text_color(colors.text)
4771 .overflow_hidden()
4772 .children(self.titlebar_item.clone())
4773 .child(
4774 div()
4775 .size_full()
4776 .relative()
4777 .flex_1()
4778 .flex()
4779 .flex_col()
4780 .child(
4781 div()
4782 .id("workspace")
4783 .bg(colors.background)
4784 .relative()
4785 .flex_1()
4786 .w_full()
4787 .flex()
4788 .flex_col()
4789 .overflow_hidden()
4790 .border_t_1()
4791 .border_b_1()
4792 .border_color(colors.border)
4793 .child({
4794 let this = cx.view().clone();
4795 canvas(
4796 move |bounds, cx| {
4797 this.update(cx, |this, _cx| this.bounds = bounds)
4798 },
4799 |_, _, _| {},
4800 )
4801 .absolute()
4802 .size_full()
4803 })
4804 .when(self.zoomed.is_none(), |this| {
4805 this.on_drag_move(cx.listener(
4806 |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4807 match e.drag(cx).0 {
4808 DockPosition::Left => {
4809 let size = e.event.position.x
4810 - workspace.bounds.left();
4811 workspace.left_dock.update(
4812 cx,
4813 |left_dock, cx| {
4814 left_dock.resize_active_panel(
4815 Some(size),
4816 cx,
4817 );
4818 },
4819 );
4820 }
4821 DockPosition::Right => {
4822 let size = workspace.bounds.right()
4823 - e.event.position.x;
4824 workspace.right_dock.update(
4825 cx,
4826 |right_dock, cx| {
4827 right_dock.resize_active_panel(
4828 Some(size),
4829 cx,
4830 );
4831 },
4832 );
4833 }
4834 DockPosition::Bottom => {
4835 let size = workspace.bounds.bottom()
4836 - e.event.position.y;
4837 workspace.bottom_dock.update(
4838 cx,
4839 |bottom_dock, cx| {
4840 bottom_dock.resize_active_panel(
4841 Some(size),
4842 cx,
4843 );
4844 },
4845 );
4846 }
4847 }
4848 },
4849 ))
4850 })
4851 .child(
4852 div()
4853 .flex()
4854 .flex_row()
4855 .h_full()
4856 // Left Dock
4857 .children(self.render_dock(
4858 DockPosition::Left,
4859 &self.left_dock,
4860 cx,
4861 ))
4862 // Panes
4863 .child(
4864 div()
4865 .flex()
4866 .flex_col()
4867 .flex_1()
4868 .overflow_hidden()
4869 .child(
4870 h_flex()
4871 .flex_1()
4872 .when_some(paddings.0, |this, p| {
4873 this.child(p.border_r_1())
4874 })
4875 .child(self.center.render(
4876 &self.project,
4877 &self.follower_states,
4878 self.active_call(),
4879 &self.active_pane,
4880 self.zoomed.as_ref(),
4881 &self.app_state,
4882 cx,
4883 ))
4884 .when_some(paddings.1, |this, p| {
4885 this.child(p.border_l_1())
4886 }),
4887 )
4888 .children(self.render_dock(
4889 DockPosition::Bottom,
4890 &self.bottom_dock,
4891 cx,
4892 )),
4893 )
4894 // Right Dock
4895 .children(self.render_dock(
4896 DockPosition::Right,
4897 &self.right_dock,
4898 cx,
4899 )),
4900 )
4901 .children(self.zoomed.as_ref().and_then(|view| {
4902 let zoomed_view = view.upgrade()?;
4903 let div = div()
4904 .occlude()
4905 .absolute()
4906 .overflow_hidden()
4907 .border_color(colors.border)
4908 .bg(colors.background)
4909 .child(zoomed_view)
4910 .inset_0()
4911 .shadow_lg();
4912
4913 Some(match self.zoomed_position {
4914 Some(DockPosition::Left) => div.right_2().border_r_1(),
4915 Some(DockPosition::Right) => div.left_2().border_l_1(),
4916 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4917 None => {
4918 div.top_2().bottom_2().left_2().right_2().border_1()
4919 }
4920 })
4921 }))
4922 .children(self.render_notifications(cx)),
4923 )
4924 .child(self.status_bar.clone())
4925 .child(self.modal_layer.clone()),
4926 ),
4927 cx,
4928 )
4929 }
4930}
4931
4932impl WorkspaceStore {
4933 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4934 Self {
4935 workspaces: Default::default(),
4936 _subscriptions: vec![
4937 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4938 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4939 ],
4940 client,
4941 }
4942 }
4943
4944 pub fn update_followers(
4945 &self,
4946 project_id: Option<u64>,
4947 update: proto::update_followers::Variant,
4948 cx: &AppContext,
4949 ) -> Option<()> {
4950 let active_call = ActiveCall::try_global(cx)?;
4951 let room_id = active_call.read(cx).room()?.read(cx).id();
4952 self.client
4953 .send(proto::UpdateFollowers {
4954 room_id,
4955 project_id,
4956 variant: Some(update),
4957 })
4958 .log_err()
4959 }
4960
4961 pub async fn handle_follow(
4962 this: Model<Self>,
4963 envelope: TypedEnvelope<proto::Follow>,
4964 mut cx: AsyncAppContext,
4965 ) -> Result<proto::FollowResponse> {
4966 this.update(&mut cx, |this, cx| {
4967 let follower = Follower {
4968 project_id: envelope.payload.project_id,
4969 peer_id: envelope.original_sender_id()?,
4970 };
4971
4972 let mut response = proto::FollowResponse::default();
4973 this.workspaces.retain(|workspace| {
4974 workspace
4975 .update(cx, |workspace, cx| {
4976 let handler_response = workspace.handle_follow(follower.project_id, cx);
4977 if let Some(active_view) = handler_response.active_view.clone() {
4978 if workspace.project.read(cx).remote_id() == follower.project_id {
4979 response.active_view = Some(active_view)
4980 }
4981 }
4982 })
4983 .is_ok()
4984 });
4985
4986 Ok(response)
4987 })?
4988 }
4989
4990 async fn handle_update_followers(
4991 this: Model<Self>,
4992 envelope: TypedEnvelope<proto::UpdateFollowers>,
4993 mut cx: AsyncAppContext,
4994 ) -> Result<()> {
4995 let leader_id = envelope.original_sender_id()?;
4996 let update = envelope.payload;
4997
4998 this.update(&mut cx, |this, cx| {
4999 this.workspaces.retain(|workspace| {
5000 workspace
5001 .update(cx, |workspace, cx| {
5002 let project_id = workspace.project.read(cx).remote_id();
5003 if update.project_id != project_id && update.project_id.is_some() {
5004 return;
5005 }
5006 workspace.handle_update_followers(leader_id, update.clone(), cx);
5007 })
5008 .is_ok()
5009 });
5010 Ok(())
5011 })?
5012 }
5013}
5014
5015impl ViewId {
5016 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5017 Ok(Self {
5018 creator: message
5019 .creator
5020 .ok_or_else(|| anyhow!("creator is missing"))?,
5021 id: message.id,
5022 })
5023 }
5024
5025 pub(crate) fn to_proto(self) -> proto::ViewId {
5026 proto::ViewId {
5027 creator: Some(self.creator),
5028 id: self.id,
5029 }
5030 }
5031}
5032
5033impl FollowerState {
5034 fn pane(&self) -> &View<Pane> {
5035 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5036 }
5037}
5038
5039pub trait WorkspaceHandle {
5040 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5041}
5042
5043impl WorkspaceHandle for View<Workspace> {
5044 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5045 self.read(cx)
5046 .worktrees(cx)
5047 .flat_map(|worktree| {
5048 let worktree_id = worktree.read(cx).id();
5049 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5050 worktree_id,
5051 path: f.path.clone(),
5052 })
5053 })
5054 .collect::<Vec<_>>()
5055 }
5056}
5057
5058impl std::fmt::Debug for OpenPaths {
5059 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5060 f.debug_struct("OpenPaths")
5061 .field("paths", &self.paths)
5062 .finish()
5063 }
5064}
5065
5066pub fn activate_workspace_for_project(
5067 cx: &mut AppContext,
5068 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5069) -> Option<WindowHandle<Workspace>> {
5070 for window in cx.windows() {
5071 let Some(workspace) = window.downcast::<Workspace>() else {
5072 continue;
5073 };
5074
5075 let predicate = workspace
5076 .update(cx, |workspace, cx| {
5077 let project = workspace.project.read(cx);
5078 if predicate(project, cx) {
5079 cx.activate_window();
5080 true
5081 } else {
5082 false
5083 }
5084 })
5085 .log_err()
5086 .unwrap_or(false);
5087
5088 if predicate {
5089 return Some(workspace);
5090 }
5091 }
5092
5093 None
5094}
5095
5096pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5097 DB.last_workspace().await.log_err().flatten()
5098}
5099
5100pub fn last_session_workspace_locations(
5101 last_session_id: &str,
5102 last_session_window_stack: Option<Vec<WindowId>>,
5103) -> Option<Vec<SerializedWorkspaceLocation>> {
5104 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5105 .log_err()
5106}
5107
5108actions!(collab, [OpenChannelNotes]);
5109actions!(zed, [OpenLog]);
5110
5111async fn join_channel_internal(
5112 channel_id: ChannelId,
5113 app_state: &Arc<AppState>,
5114 requesting_window: Option<WindowHandle<Workspace>>,
5115 active_call: &Model<ActiveCall>,
5116 cx: &mut AsyncAppContext,
5117) -> Result<bool> {
5118 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5119 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5120 return (false, None);
5121 };
5122
5123 let already_in_channel = room.channel_id() == Some(channel_id);
5124 let should_prompt = room.is_sharing_project()
5125 && !room.remote_participants().is_empty()
5126 && !already_in_channel;
5127 let open_room = if already_in_channel {
5128 active_call.room().cloned()
5129 } else {
5130 None
5131 };
5132 (should_prompt, open_room)
5133 })?;
5134
5135 if let Some(room) = open_room {
5136 let task = room.update(cx, |room, cx| {
5137 if let Some((project, host)) = room.most_active_project(cx) {
5138 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5139 }
5140
5141 None
5142 })?;
5143 if let Some(task) = task {
5144 task.await?;
5145 }
5146 return anyhow::Ok(true);
5147 }
5148
5149 if should_prompt {
5150 if let Some(workspace) = requesting_window {
5151 let answer = workspace
5152 .update(cx, |_, cx| {
5153 cx.prompt(
5154 PromptLevel::Warning,
5155 "Do you want to switch channels?",
5156 Some("Leaving this call will unshare your current project."),
5157 &["Yes, Join Channel", "Cancel"],
5158 )
5159 })?
5160 .await;
5161
5162 if answer == Ok(1) {
5163 return Ok(false);
5164 }
5165 } else {
5166 return Ok(false); // unreachable!() hopefully
5167 }
5168 }
5169
5170 let client = cx.update(|cx| active_call.read(cx).client())?;
5171
5172 let mut client_status = client.status();
5173
5174 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5175 'outer: loop {
5176 let Some(status) = client_status.recv().await else {
5177 return Err(anyhow!("error connecting"));
5178 };
5179
5180 match status {
5181 Status::Connecting
5182 | Status::Authenticating
5183 | Status::Reconnecting
5184 | Status::Reauthenticating => continue,
5185 Status::Connected { .. } => break 'outer,
5186 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5187 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5188 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5189 return Err(ErrorCode::Disconnected.into());
5190 }
5191 }
5192 }
5193
5194 let room = active_call
5195 .update(cx, |active_call, cx| {
5196 active_call.join_channel(channel_id, cx)
5197 })?
5198 .await?;
5199
5200 let Some(room) = room else {
5201 return anyhow::Ok(true);
5202 };
5203
5204 room.update(cx, |room, _| room.room_update_completed())?
5205 .await;
5206
5207 let task = room.update(cx, |room, cx| {
5208 if let Some((project, host)) = room.most_active_project(cx) {
5209 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5210 }
5211
5212 // If you are the first to join a channel, see if you should share your project.
5213 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5214 if let Some(workspace) = requesting_window {
5215 let project = workspace.update(cx, |workspace, cx| {
5216 let project = workspace.project.read(cx);
5217
5218 if !CallSettings::get_global(cx).share_on_join {
5219 return None;
5220 }
5221
5222 if (project.is_local() || project.is_via_ssh())
5223 && project.visible_worktrees(cx).any(|tree| {
5224 tree.read(cx)
5225 .root_entry()
5226 .map_or(false, |entry| entry.is_dir())
5227 })
5228 {
5229 Some(workspace.project.clone())
5230 } else {
5231 None
5232 }
5233 });
5234 if let Ok(Some(project)) = project {
5235 return Some(cx.spawn(|room, mut cx| async move {
5236 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5237 .await?;
5238 Ok(())
5239 }));
5240 }
5241 }
5242 }
5243
5244 None
5245 })?;
5246 if let Some(task) = task {
5247 task.await?;
5248 return anyhow::Ok(true);
5249 }
5250 anyhow::Ok(false)
5251}
5252
5253pub fn join_channel(
5254 channel_id: ChannelId,
5255 app_state: Arc<AppState>,
5256 requesting_window: Option<WindowHandle<Workspace>>,
5257 cx: &mut AppContext,
5258) -> Task<Result<()>> {
5259 let active_call = ActiveCall::global(cx);
5260 cx.spawn(|mut cx| async move {
5261 let result = join_channel_internal(
5262 channel_id,
5263 &app_state,
5264 requesting_window,
5265 &active_call,
5266 &mut cx,
5267 )
5268 .await;
5269
5270 // join channel succeeded, and opened a window
5271 if matches!(result, Ok(true)) {
5272 return anyhow::Ok(());
5273 }
5274
5275 // find an existing workspace to focus and show call controls
5276 let mut active_window =
5277 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5278 if active_window.is_none() {
5279 // no open workspaces, make one to show the error in (blergh)
5280 let (window_handle, _) = cx
5281 .update(|cx| {
5282 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5283 })?
5284 .await?;
5285
5286 if result.is_ok() {
5287 cx.update(|cx| {
5288 cx.dispatch_action(&OpenChannelNotes);
5289 }).log_err();
5290 }
5291
5292 active_window = Some(window_handle);
5293 }
5294
5295 if let Err(err) = result {
5296 log::error!("failed to join channel: {}", err);
5297 if let Some(active_window) = active_window {
5298 active_window
5299 .update(&mut cx, |_, cx| {
5300 let detail: SharedString = match err.error_code() {
5301 ErrorCode::SignedOut => {
5302 "Please sign in to continue.".into()
5303 }
5304 ErrorCode::UpgradeRequired => {
5305 "Your are running an unsupported version of Zed. Please update to continue.".into()
5306 }
5307 ErrorCode::NoSuchChannel => {
5308 "No matching channel was found. Please check the link and try again.".into()
5309 }
5310 ErrorCode::Forbidden => {
5311 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5312 }
5313 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5314 _ => format!("{}\n\nPlease try again.", err).into(),
5315 };
5316 cx.prompt(
5317 PromptLevel::Critical,
5318 "Failed to join channel",
5319 Some(&detail),
5320 &["Ok"],
5321 )
5322 })?
5323 .await
5324 .ok();
5325 }
5326 }
5327
5328 // return ok, we showed the error to the user.
5329 anyhow::Ok(())
5330 })
5331}
5332
5333pub async fn get_any_active_workspace(
5334 app_state: Arc<AppState>,
5335 mut cx: AsyncAppContext,
5336) -> anyhow::Result<WindowHandle<Workspace>> {
5337 // find an existing workspace to focus and show call controls
5338 let active_window = activate_any_workspace_window(&mut cx);
5339 if active_window.is_none() {
5340 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5341 .await?;
5342 }
5343 activate_any_workspace_window(&mut cx).context("could not open zed")
5344}
5345
5346fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5347 cx.update(|cx| {
5348 if let Some(workspace_window) = cx
5349 .active_window()
5350 .and_then(|window| window.downcast::<Workspace>())
5351 {
5352 return Some(workspace_window);
5353 }
5354
5355 for window in cx.windows() {
5356 if let Some(workspace_window) = window.downcast::<Workspace>() {
5357 workspace_window
5358 .update(cx, |_, cx| cx.activate_window())
5359 .ok();
5360 return Some(workspace_window);
5361 }
5362 }
5363 None
5364 })
5365 .ok()
5366 .flatten()
5367}
5368
5369pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5370 cx.windows()
5371 .into_iter()
5372 .filter_map(|window| window.downcast::<Workspace>())
5373 .filter(|workspace| {
5374 workspace
5375 .read(cx)
5376 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5377 })
5378 .collect()
5379}
5380
5381#[derive(Default)]
5382pub struct OpenOptions {
5383 pub open_new_workspace: Option<bool>,
5384 pub replace_window: Option<WindowHandle<Workspace>>,
5385 pub env: Option<HashMap<String, String>>,
5386}
5387
5388#[allow(clippy::type_complexity)]
5389pub fn open_paths(
5390 abs_paths: &[PathBuf],
5391 app_state: Arc<AppState>,
5392 open_options: OpenOptions,
5393 cx: &mut AppContext,
5394) -> Task<
5395 anyhow::Result<(
5396 WindowHandle<Workspace>,
5397 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5398 )>,
5399> {
5400 let abs_paths = abs_paths.to_vec();
5401 let mut existing = None;
5402 let mut best_match = None;
5403 let mut open_visible = OpenVisible::All;
5404
5405 if open_options.open_new_workspace != Some(true) {
5406 for window in local_workspace_windows(cx) {
5407 if let Ok(workspace) = window.read(cx) {
5408 let m = workspace
5409 .project
5410 .read(cx)
5411 .visibility_for_paths(&abs_paths, cx);
5412 if m > best_match {
5413 existing = Some(window);
5414 best_match = m;
5415 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5416 existing = Some(window)
5417 }
5418 }
5419 }
5420 }
5421
5422 cx.spawn(move |mut cx| async move {
5423 if open_options.open_new_workspace.is_none() && existing.is_none() {
5424 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5425 if futures::future::join_all(all_files)
5426 .await
5427 .into_iter()
5428 .filter_map(|result| result.ok().flatten())
5429 .all(|file| !file.is_dir)
5430 {
5431 cx.update(|cx| {
5432 for window in local_workspace_windows(cx) {
5433 if let Ok(workspace) = window.read(cx) {
5434 let project = workspace.project().read(cx);
5435 if project.is_via_collab() {
5436 continue;
5437 }
5438 existing = Some(window);
5439 open_visible = OpenVisible::None;
5440 break;
5441 }
5442 }
5443 })?;
5444 }
5445 }
5446
5447 if let Some(existing) = existing {
5448 Ok((
5449 existing,
5450 existing
5451 .update(&mut cx, |workspace, cx| {
5452 cx.activate_window();
5453 workspace.open_paths(abs_paths, open_visible, None, cx)
5454 })?
5455 .await,
5456 ))
5457 } else {
5458 cx.update(move |cx| {
5459 Workspace::new_local(
5460 abs_paths,
5461 app_state.clone(),
5462 open_options.replace_window,
5463 open_options.env,
5464 cx,
5465 )
5466 })?
5467 .await
5468 }
5469 })
5470}
5471
5472pub fn open_new(
5473 open_options: OpenOptions,
5474 app_state: Arc<AppState>,
5475 cx: &mut AppContext,
5476 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5477) -> Task<anyhow::Result<()>> {
5478 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5479 cx.spawn(|mut cx| async move {
5480 let (workspace, opened_paths) = task.await?;
5481 workspace.update(&mut cx, |workspace, cx| {
5482 if opened_paths.is_empty() {
5483 init(workspace, cx)
5484 }
5485 })?;
5486 Ok(())
5487 })
5488}
5489
5490pub fn create_and_open_local_file(
5491 path: &'static Path,
5492 cx: &mut ViewContext<Workspace>,
5493 default_content: impl 'static + Send + FnOnce() -> Rope,
5494) -> Task<Result<Box<dyn ItemHandle>>> {
5495 cx.spawn(|workspace, mut cx| async move {
5496 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5497 if !fs.is_file(path).await {
5498 fs.create_file(path, Default::default()).await?;
5499 fs.save(path, &default_content(), Default::default())
5500 .await?;
5501 }
5502
5503 let mut items = workspace
5504 .update(&mut cx, |workspace, cx| {
5505 workspace.with_local_workspace(cx, |workspace, cx| {
5506 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5507 })
5508 })?
5509 .await?
5510 .await;
5511
5512 let item = items.pop().flatten();
5513 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5514 })
5515}
5516
5517pub fn open_ssh_project(
5518 window: WindowHandle<Workspace>,
5519 connection_options: SshConnectionOptions,
5520 cancel_rx: oneshot::Receiver<()>,
5521 delegate: Arc<dyn SshClientDelegate>,
5522 app_state: Arc<AppState>,
5523 paths: Vec<PathBuf>,
5524 cx: &mut AppContext,
5525) -> Task<Result<()>> {
5526 cx.spawn(|mut cx| async move {
5527 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5528 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5529
5530 let session = match cx
5531 .update(|cx| {
5532 remote::SshRemoteClient::new(
5533 ConnectionIdentifier::Workspace(workspace_id.0),
5534 connection_options,
5535 cancel_rx,
5536 delegate,
5537 cx,
5538 )
5539 })?
5540 .await?
5541 {
5542 Some(result) => result,
5543 None => return Ok(()),
5544 };
5545
5546 let project = cx.update(|cx| {
5547 project::Project::ssh(
5548 session,
5549 app_state.client.clone(),
5550 app_state.node_runtime.clone(),
5551 app_state.user_store.clone(),
5552 app_state.languages.clone(),
5553 app_state.fs.clone(),
5554 cx,
5555 )
5556 })?;
5557
5558 let toolchains = DB.toolchains(workspace_id).await?;
5559 for (toolchain, worktree_id) in toolchains {
5560 project
5561 .update(&mut cx, |this, cx| {
5562 this.activate_toolchain(worktree_id, toolchain, cx)
5563 })?
5564 .await;
5565 }
5566 let mut project_paths_to_open = vec![];
5567 let mut project_path_errors = vec![];
5568
5569 for path in paths {
5570 let result = cx
5571 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5572 .await;
5573 match result {
5574 Ok((_, project_path)) => {
5575 project_paths_to_open.push((path.clone(), Some(project_path)));
5576 }
5577 Err(error) => {
5578 project_path_errors.push(error);
5579 }
5580 };
5581 }
5582
5583 if project_paths_to_open.is_empty() {
5584 return Err(project_path_errors
5585 .pop()
5586 .unwrap_or_else(|| anyhow!("no paths given")));
5587 }
5588
5589 cx.update_window(window.into(), |_, cx| {
5590 cx.replace_root_view(|cx| {
5591 let mut workspace =
5592 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5593
5594 workspace
5595 .client()
5596 .telemetry()
5597 .report_app_event("open ssh project".to_string());
5598
5599 workspace.set_serialized_ssh_project(serialized_ssh_project);
5600 workspace
5601 });
5602 })?;
5603
5604 window
5605 .update(&mut cx, |_, cx| {
5606 cx.activate_window();
5607
5608 open_items(serialized_workspace, project_paths_to_open, cx)
5609 })?
5610 .await?;
5611
5612 window.update(&mut cx, |workspace, cx| {
5613 for error in project_path_errors {
5614 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5615 if let Some(path) = error.error_tag("path") {
5616 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5617 }
5618 } else {
5619 workspace.show_error(&error, cx)
5620 }
5621 }
5622 })
5623 })
5624}
5625
5626fn serialize_ssh_project(
5627 connection_options: SshConnectionOptions,
5628 paths: Vec<PathBuf>,
5629 cx: &AsyncAppContext,
5630) -> Task<
5631 Result<(
5632 SerializedSshProject,
5633 WorkspaceId,
5634 Option<SerializedWorkspace>,
5635 )>,
5636> {
5637 cx.background_executor().spawn(async move {
5638 let serialized_ssh_project = persistence::DB
5639 .get_or_create_ssh_project(
5640 connection_options.host.clone(),
5641 connection_options.port,
5642 paths
5643 .iter()
5644 .map(|path| path.to_string_lossy().to_string())
5645 .collect::<Vec<_>>(),
5646 connection_options.username.clone(),
5647 )
5648 .await?;
5649
5650 let serialized_workspace =
5651 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5652
5653 let workspace_id = if let Some(workspace_id) =
5654 serialized_workspace.as_ref().map(|workspace| workspace.id)
5655 {
5656 workspace_id
5657 } else {
5658 persistence::DB.next_id().await?
5659 };
5660
5661 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5662 })
5663}
5664
5665pub fn join_in_room_project(
5666 project_id: u64,
5667 follow_user_id: u64,
5668 app_state: Arc<AppState>,
5669 cx: &mut AppContext,
5670) -> Task<Result<()>> {
5671 let windows = cx.windows();
5672 cx.spawn(|mut cx| async move {
5673 let existing_workspace = windows.into_iter().find_map(|window| {
5674 window.downcast::<Workspace>().and_then(|window| {
5675 window
5676 .update(&mut cx, |workspace, cx| {
5677 if workspace.project().read(cx).remote_id() == Some(project_id) {
5678 Some(window)
5679 } else {
5680 None
5681 }
5682 })
5683 .unwrap_or(None)
5684 })
5685 });
5686
5687 let workspace = if let Some(existing_workspace) = existing_workspace {
5688 existing_workspace
5689 } else {
5690 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5691 let room = active_call
5692 .read_with(&cx, |call, _| call.room().cloned())?
5693 .ok_or_else(|| anyhow!("not in a call"))?;
5694 let project = room
5695 .update(&mut cx, |room, cx| {
5696 room.join_project(
5697 project_id,
5698 app_state.languages.clone(),
5699 app_state.fs.clone(),
5700 cx,
5701 )
5702 })?
5703 .await?;
5704
5705 let window_bounds_override = window_bounds_env_override();
5706 cx.update(|cx| {
5707 let mut options = (app_state.build_window_options)(None, cx);
5708 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5709 cx.open_window(options, |cx| {
5710 cx.new_view(|cx| {
5711 Workspace::new(Default::default(), project, app_state.clone(), cx)
5712 })
5713 })
5714 })??
5715 };
5716
5717 workspace.update(&mut cx, |workspace, cx| {
5718 cx.activate(true);
5719 cx.activate_window();
5720
5721 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5722 let follow_peer_id = room
5723 .read(cx)
5724 .remote_participants()
5725 .iter()
5726 .find(|(_, participant)| participant.user.id == follow_user_id)
5727 .map(|(_, p)| p.peer_id)
5728 .or_else(|| {
5729 // If we couldn't follow the given user, follow the host instead.
5730 let collaborator = workspace
5731 .project()
5732 .read(cx)
5733 .collaborators()
5734 .values()
5735 .find(|collaborator| collaborator.is_host)?;
5736 Some(collaborator.peer_id)
5737 });
5738
5739 if let Some(follow_peer_id) = follow_peer_id {
5740 workspace.follow(follow_peer_id, cx);
5741 }
5742 }
5743 })?;
5744
5745 anyhow::Ok(())
5746 })
5747}
5748
5749pub fn reload(reload: &Reload, cx: &mut AppContext) {
5750 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5751 let mut workspace_windows = cx
5752 .windows()
5753 .into_iter()
5754 .filter_map(|window| window.downcast::<Workspace>())
5755 .collect::<Vec<_>>();
5756
5757 // If multiple windows have unsaved changes, and need a save prompt,
5758 // prompt in the active window before switching to a different window.
5759 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5760
5761 let mut prompt = None;
5762 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5763 prompt = window
5764 .update(cx, |_, cx| {
5765 cx.prompt(
5766 PromptLevel::Info,
5767 "Are you sure you want to restart?",
5768 None,
5769 &["Restart", "Cancel"],
5770 )
5771 })
5772 .ok();
5773 }
5774
5775 let binary_path = reload.binary_path.clone();
5776 cx.spawn(|mut cx| async move {
5777 if let Some(prompt) = prompt {
5778 let answer = prompt.await?;
5779 if answer != 0 {
5780 return Ok(());
5781 }
5782 }
5783
5784 // If the user cancels any save prompt, then keep the app open.
5785 for window in workspace_windows {
5786 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5787 workspace.prepare_to_close(CloseIntent::Quit, cx)
5788 }) {
5789 if !should_close.await? {
5790 return Ok(());
5791 }
5792 }
5793 }
5794
5795 cx.update(|cx| cx.restart(binary_path))
5796 })
5797 .detach_and_log_err(cx);
5798}
5799
5800fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5801 let mut parts = value.split(',');
5802 let x: usize = parts.next()?.parse().ok()?;
5803 let y: usize = parts.next()?.parse().ok()?;
5804 Some(point(px(x as f32), px(y as f32)))
5805}
5806
5807fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5808 let mut parts = value.split(',');
5809 let width: usize = parts.next()?.parse().ok()?;
5810 let height: usize = parts.next()?.parse().ok()?;
5811 Some(size(px(width as f32), px(height as f32)))
5812}
5813
5814pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5815 const BORDER_SIZE: Pixels = px(1.0);
5816 let decorations = cx.window_decorations();
5817
5818 if matches!(decorations, Decorations::Client { .. }) {
5819 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5820 }
5821
5822 struct GlobalResizeEdge(ResizeEdge);
5823 impl Global for GlobalResizeEdge {}
5824
5825 div()
5826 .id("window-backdrop")
5827 .bg(transparent_black())
5828 .map(|div| match decorations {
5829 Decorations::Server => div,
5830 Decorations::Client { tiling, .. } => div
5831 .when(!(tiling.top || tiling.right), |div| {
5832 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5833 })
5834 .when(!(tiling.top || tiling.left), |div| {
5835 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5836 })
5837 .when(!(tiling.bottom || tiling.right), |div| {
5838 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5839 })
5840 .when(!(tiling.bottom || tiling.left), |div| {
5841 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5842 })
5843 .when(!tiling.top, |div| {
5844 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5845 })
5846 .when(!tiling.bottom, |div| {
5847 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5848 })
5849 .when(!tiling.left, |div| {
5850 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5851 })
5852 .when(!tiling.right, |div| {
5853 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5854 })
5855 .on_mouse_move(move |e, cx| {
5856 let size = cx.window_bounds().get_bounds().size;
5857 let pos = e.position;
5858
5859 let new_edge =
5860 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5861
5862 let edge = cx.try_global::<GlobalResizeEdge>();
5863 if new_edge != edge.map(|edge| edge.0) {
5864 cx.window_handle()
5865 .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5866 .ok();
5867 }
5868 })
5869 .on_mouse_down(MouseButton::Left, move |e, cx| {
5870 let size = cx.window_bounds().get_bounds().size;
5871 let pos = e.position;
5872
5873 let edge = match resize_edge(
5874 pos,
5875 theme::CLIENT_SIDE_DECORATION_SHADOW,
5876 size,
5877 tiling,
5878 ) {
5879 Some(value) => value,
5880 None => return,
5881 };
5882
5883 cx.start_window_resize(edge);
5884 }),
5885 })
5886 .size_full()
5887 .child(
5888 div()
5889 .cursor(CursorStyle::Arrow)
5890 .map(|div| match decorations {
5891 Decorations::Server => div,
5892 Decorations::Client { tiling } => div
5893 .border_color(cx.theme().colors().border)
5894 .when(!(tiling.top || tiling.right), |div| {
5895 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5896 })
5897 .when(!(tiling.top || tiling.left), |div| {
5898 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5899 })
5900 .when(!(tiling.bottom || tiling.right), |div| {
5901 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5902 })
5903 .when(!(tiling.bottom || tiling.left), |div| {
5904 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5905 })
5906 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5907 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5908 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5909 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5910 .when(!tiling.is_tiled(), |div| {
5911 div.shadow(smallvec::smallvec![gpui::BoxShadow {
5912 color: Hsla {
5913 h: 0.,
5914 s: 0.,
5915 l: 0.,
5916 a: 0.4,
5917 },
5918 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5919 spread_radius: px(0.),
5920 offset: point(px(0.0), px(0.0)),
5921 }])
5922 }),
5923 })
5924 .on_mouse_move(|_e, cx| {
5925 cx.stop_propagation();
5926 })
5927 .size_full()
5928 .child(element),
5929 )
5930 .map(|div| match decorations {
5931 Decorations::Server => div,
5932 Decorations::Client { tiling, .. } => div.child(
5933 canvas(
5934 |_bounds, cx| {
5935 cx.insert_hitbox(
5936 Bounds::new(
5937 point(px(0.0), px(0.0)),
5938 cx.window_bounds().get_bounds().size,
5939 ),
5940 false,
5941 )
5942 },
5943 move |_bounds, hitbox, cx| {
5944 let mouse = cx.mouse_position();
5945 let size = cx.window_bounds().get_bounds().size;
5946 let Some(edge) =
5947 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5948 else {
5949 return;
5950 };
5951 cx.set_global(GlobalResizeEdge(edge));
5952 cx.set_cursor_style(
5953 match edge {
5954 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5955 ResizeEdge::Left | ResizeEdge::Right => {
5956 CursorStyle::ResizeLeftRight
5957 }
5958 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5959 CursorStyle::ResizeUpLeftDownRight
5960 }
5961 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5962 CursorStyle::ResizeUpRightDownLeft
5963 }
5964 },
5965 &hitbox,
5966 );
5967 },
5968 )
5969 .size_full()
5970 .absolute(),
5971 ),
5972 })
5973}
5974
5975fn resize_edge(
5976 pos: Point<Pixels>,
5977 shadow_size: Pixels,
5978 window_size: Size<Pixels>,
5979 tiling: Tiling,
5980) -> Option<ResizeEdge> {
5981 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
5982 if bounds.contains(&pos) {
5983 return None;
5984 }
5985
5986 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
5987 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
5988 if !tiling.top && top_left_bounds.contains(&pos) {
5989 return Some(ResizeEdge::TopLeft);
5990 }
5991
5992 let top_right_bounds = Bounds::new(
5993 Point::new(window_size.width - corner_size.width, px(0.)),
5994 corner_size,
5995 );
5996 if !tiling.top && top_right_bounds.contains(&pos) {
5997 return Some(ResizeEdge::TopRight);
5998 }
5999
6000 let bottom_left_bounds = Bounds::new(
6001 Point::new(px(0.), window_size.height - corner_size.height),
6002 corner_size,
6003 );
6004 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6005 return Some(ResizeEdge::BottomLeft);
6006 }
6007
6008 let bottom_right_bounds = Bounds::new(
6009 Point::new(
6010 window_size.width - corner_size.width,
6011 window_size.height - corner_size.height,
6012 ),
6013 corner_size,
6014 );
6015 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6016 return Some(ResizeEdge::BottomRight);
6017 }
6018
6019 if !tiling.top && pos.y < shadow_size {
6020 Some(ResizeEdge::Top)
6021 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6022 Some(ResizeEdge::Bottom)
6023 } else if !tiling.left && pos.x < shadow_size {
6024 Some(ResizeEdge::Left)
6025 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6026 Some(ResizeEdge::Right)
6027 } else {
6028 None
6029 }
6030}
6031
6032fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6033 if pane == active_pane {
6034 return;
6035 } else if pane.read(cx).items_len() == 0 {
6036 pane.update(cx, |_, cx| {
6037 cx.emit(pane::Event::Remove {
6038 focus_on_pane: None,
6039 });
6040 })
6041 } else {
6042 move_all_items(pane, active_pane, cx);
6043 }
6044}
6045
6046fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6047 let destination_is_different = from_pane != to_pane;
6048 let mut moved_items = 0;
6049 for (item_ix, item_handle) in from_pane
6050 .read(cx)
6051 .items()
6052 .enumerate()
6053 .map(|(ix, item)| (ix, item.clone()))
6054 .collect::<Vec<_>>()
6055 {
6056 let ix = item_ix - moved_items;
6057 if destination_is_different {
6058 // Close item from previous pane
6059 from_pane.update(cx, |source, cx| {
6060 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6061 });
6062 moved_items += 1;
6063 }
6064
6065 // This automatically removes duplicate items in the pane
6066 to_pane.update(cx, |destination, cx| {
6067 destination.add_item(item_handle, true, true, None, cx);
6068 destination.focus(cx)
6069 });
6070 }
6071}
6072
6073pub fn move_item(
6074 source: &View<Pane>,
6075 destination: &View<Pane>,
6076 item_id_to_move: EntityId,
6077 destination_index: usize,
6078 cx: &mut WindowContext<'_>,
6079) {
6080 let Some((item_ix, item_handle)) = source
6081 .read(cx)
6082 .items()
6083 .enumerate()
6084 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6085 .map(|(ix, item)| (ix, item.clone()))
6086 else {
6087 // Tab was closed during drag
6088 return;
6089 };
6090
6091 if source != destination {
6092 // Close item from previous pane
6093 source.update(cx, |source, cx| {
6094 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6095 });
6096 }
6097
6098 // This automatically removes duplicate items in the pane
6099 destination.update(cx, |destination, cx| {
6100 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6101 destination.focus(cx)
6102 });
6103}
6104
6105#[cfg(test)]
6106mod tests {
6107 use std::{cell::RefCell, rc::Rc};
6108
6109 use super::*;
6110 use crate::{
6111 dock::{test::TestPanel, PanelEvent},
6112 item::{
6113 test::{TestItem, TestProjectItem},
6114 ItemEvent,
6115 },
6116 };
6117 use fs::FakeFs;
6118 use gpui::{
6119 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6120 UpdateGlobal, VisualTestContext,
6121 };
6122 use project::{Project, ProjectEntryId};
6123 use serde_json::json;
6124 use settings::SettingsStore;
6125
6126 #[gpui::test]
6127 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6128 init_test(cx);
6129
6130 let fs = FakeFs::new(cx.executor());
6131 let project = Project::test(fs, [], cx).await;
6132 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6133
6134 // Adding an item with no ambiguity renders the tab without detail.
6135 let item1 = cx.new_view(|cx| {
6136 let mut item = TestItem::new(cx);
6137 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6138 item
6139 });
6140 workspace.update(cx, |workspace, cx| {
6141 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6142 });
6143 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6144
6145 // Adding an item that creates ambiguity increases the level of detail on
6146 // both tabs.
6147 let item2 = cx.new_view(|cx| {
6148 let mut item = TestItem::new(cx);
6149 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6150 item
6151 });
6152 workspace.update(cx, |workspace, cx| {
6153 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6154 });
6155 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6156 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6157
6158 // Adding an item that creates ambiguity increases the level of detail only
6159 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6160 // we stop at the highest detail available.
6161 let item3 = cx.new_view(|cx| {
6162 let mut item = TestItem::new(cx);
6163 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6164 item
6165 });
6166 workspace.update(cx, |workspace, cx| {
6167 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6168 });
6169 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6170 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6171 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6172 }
6173
6174 #[gpui::test]
6175 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6176 init_test(cx);
6177
6178 let fs = FakeFs::new(cx.executor());
6179 fs.insert_tree(
6180 "/root1",
6181 json!({
6182 "one.txt": "",
6183 "two.txt": "",
6184 }),
6185 )
6186 .await;
6187 fs.insert_tree(
6188 "/root2",
6189 json!({
6190 "three.txt": "",
6191 }),
6192 )
6193 .await;
6194
6195 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6196 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6197 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6198 let worktree_id = project.update(cx, |project, cx| {
6199 project.worktrees(cx).next().unwrap().read(cx).id()
6200 });
6201
6202 let item1 = cx.new_view(|cx| {
6203 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6204 });
6205 let item2 = cx.new_view(|cx| {
6206 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6207 });
6208
6209 // Add an item to an empty pane
6210 workspace.update(cx, |workspace, cx| {
6211 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6212 });
6213 project.update(cx, |project, cx| {
6214 assert_eq!(
6215 project.active_entry(),
6216 project
6217 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6218 .map(|e| e.id)
6219 );
6220 });
6221 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6222
6223 // Add a second item to a non-empty pane
6224 workspace.update(cx, |workspace, cx| {
6225 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6226 });
6227 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6228 project.update(cx, |project, cx| {
6229 assert_eq!(
6230 project.active_entry(),
6231 project
6232 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6233 .map(|e| e.id)
6234 );
6235 });
6236
6237 // Close the active item
6238 pane.update(cx, |pane, cx| {
6239 pane.close_active_item(&Default::default(), cx).unwrap()
6240 })
6241 .await
6242 .unwrap();
6243 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6244 project.update(cx, |project, cx| {
6245 assert_eq!(
6246 project.active_entry(),
6247 project
6248 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6249 .map(|e| e.id)
6250 );
6251 });
6252
6253 // Add a project folder
6254 project
6255 .update(cx, |project, cx| {
6256 project.find_or_create_worktree("root2", true, cx)
6257 })
6258 .await
6259 .unwrap();
6260 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6261
6262 // Remove a project folder
6263 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6264 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6265 }
6266
6267 #[gpui::test]
6268 async fn test_close_window(cx: &mut TestAppContext) {
6269 init_test(cx);
6270
6271 let fs = FakeFs::new(cx.executor());
6272 fs.insert_tree("/root", json!({ "one": "" })).await;
6273
6274 let project = Project::test(fs, ["root".as_ref()], cx).await;
6275 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6276
6277 // When there are no dirty items, there's nothing to do.
6278 let item1 = cx.new_view(TestItem::new);
6279 workspace.update(cx, |w, cx| {
6280 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6281 });
6282 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6283 assert!(task.await.unwrap());
6284
6285 // When there are dirty untitled items, prompt to save each one. If the user
6286 // cancels any prompt, then abort.
6287 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6288 let item3 = cx.new_view(|cx| {
6289 TestItem::new(cx)
6290 .with_dirty(true)
6291 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6292 });
6293 workspace.update(cx, |w, cx| {
6294 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6295 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6296 });
6297 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6298 cx.executor().run_until_parked();
6299 cx.simulate_prompt_answer(2); // cancel save all
6300 cx.executor().run_until_parked();
6301 cx.simulate_prompt_answer(2); // cancel save all
6302 cx.executor().run_until_parked();
6303 assert!(!cx.has_pending_prompt());
6304 assert!(!task.await.unwrap());
6305 }
6306
6307 #[gpui::test]
6308 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6309 init_test(cx);
6310
6311 // Register TestItem as a serializable item
6312 cx.update(|cx| {
6313 register_serializable_item::<TestItem>(cx);
6314 });
6315
6316 let fs = FakeFs::new(cx.executor());
6317 fs.insert_tree("/root", json!({ "one": "" })).await;
6318
6319 let project = Project::test(fs, ["root".as_ref()], cx).await;
6320 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6321
6322 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6323 let item1 = cx.new_view(|cx| {
6324 TestItem::new(cx)
6325 .with_dirty(true)
6326 .with_serialize(|| Some(Task::ready(Ok(()))))
6327 });
6328 let item2 = cx.new_view(|cx| {
6329 TestItem::new(cx)
6330 .with_dirty(true)
6331 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6332 .with_serialize(|| Some(Task::ready(Ok(()))))
6333 });
6334 workspace.update(cx, |w, cx| {
6335 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6336 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6337 });
6338 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6339 assert!(task.await.unwrap());
6340 }
6341
6342 #[gpui::test]
6343 async fn test_close_pane_items(cx: &mut TestAppContext) {
6344 init_test(cx);
6345
6346 let fs = FakeFs::new(cx.executor());
6347
6348 let project = Project::test(fs, None, cx).await;
6349 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6350
6351 let item1 = cx.new_view(|cx| {
6352 TestItem::new(cx)
6353 .with_dirty(true)
6354 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6355 });
6356 let item2 = cx.new_view(|cx| {
6357 TestItem::new(cx)
6358 .with_dirty(true)
6359 .with_conflict(true)
6360 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6361 });
6362 let item3 = cx.new_view(|cx| {
6363 TestItem::new(cx)
6364 .with_dirty(true)
6365 .with_conflict(true)
6366 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6367 });
6368 let item4 = cx.new_view(|cx| {
6369 TestItem::new(cx)
6370 .with_dirty(true)
6371 .with_project_items(&[TestProjectItem::new_untitled(cx)])
6372 });
6373 let pane = workspace.update(cx, |workspace, cx| {
6374 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6375 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6376 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6377 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6378 workspace.active_pane().clone()
6379 });
6380
6381 let close_items = pane.update(cx, |pane, cx| {
6382 pane.activate_item(1, true, true, cx);
6383 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6384 let item1_id = item1.item_id();
6385 let item3_id = item3.item_id();
6386 let item4_id = item4.item_id();
6387 pane.close_items(cx, SaveIntent::Close, move |id| {
6388 [item1_id, item3_id, item4_id].contains(&id)
6389 })
6390 });
6391 cx.executor().run_until_parked();
6392
6393 assert!(cx.has_pending_prompt());
6394 // Ignore "Save all" prompt
6395 cx.simulate_prompt_answer(2);
6396 cx.executor().run_until_parked();
6397 // There's a prompt to save item 1.
6398 pane.update(cx, |pane, _| {
6399 assert_eq!(pane.items_len(), 4);
6400 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6401 });
6402 // Confirm saving item 1.
6403 cx.simulate_prompt_answer(0);
6404 cx.executor().run_until_parked();
6405
6406 // Item 1 is saved. There's a prompt to save item 3.
6407 pane.update(cx, |pane, cx| {
6408 assert_eq!(item1.read(cx).save_count, 1);
6409 assert_eq!(item1.read(cx).save_as_count, 0);
6410 assert_eq!(item1.read(cx).reload_count, 0);
6411 assert_eq!(pane.items_len(), 3);
6412 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6413 });
6414 assert!(cx.has_pending_prompt());
6415
6416 // Cancel saving item 3.
6417 cx.simulate_prompt_answer(1);
6418 cx.executor().run_until_parked();
6419
6420 // Item 3 is reloaded. There's a prompt to save item 4.
6421 pane.update(cx, |pane, cx| {
6422 assert_eq!(item3.read(cx).save_count, 0);
6423 assert_eq!(item3.read(cx).save_as_count, 0);
6424 assert_eq!(item3.read(cx).reload_count, 1);
6425 assert_eq!(pane.items_len(), 2);
6426 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6427 });
6428 assert!(cx.has_pending_prompt());
6429
6430 // Confirm saving item 4.
6431 cx.simulate_prompt_answer(0);
6432 cx.executor().run_until_parked();
6433
6434 // There's a prompt for a path for item 4.
6435 cx.simulate_new_path_selection(|_| Some(Default::default()));
6436 close_items.await.unwrap();
6437
6438 // The requested items are closed.
6439 pane.update(cx, |pane, cx| {
6440 assert_eq!(item4.read(cx).save_count, 0);
6441 assert_eq!(item4.read(cx).save_as_count, 1);
6442 assert_eq!(item4.read(cx).reload_count, 0);
6443 assert_eq!(pane.items_len(), 1);
6444 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6445 });
6446 }
6447
6448 #[gpui::test]
6449 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6450 init_test(cx);
6451
6452 let fs = FakeFs::new(cx.executor());
6453 let project = Project::test(fs, [], cx).await;
6454 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6455
6456 // Create several workspace items with single project entries, and two
6457 // workspace items with multiple project entries.
6458 let single_entry_items = (0..=4)
6459 .map(|project_entry_id| {
6460 cx.new_view(|cx| {
6461 TestItem::new(cx)
6462 .with_dirty(true)
6463 .with_project_items(&[TestProjectItem::new(
6464 project_entry_id,
6465 &format!("{project_entry_id}.txt"),
6466 cx,
6467 )])
6468 })
6469 })
6470 .collect::<Vec<_>>();
6471 let item_2_3 = cx.new_view(|cx| {
6472 TestItem::new(cx)
6473 .with_dirty(true)
6474 .with_singleton(false)
6475 .with_project_items(&[
6476 single_entry_items[2].read(cx).project_items[0].clone(),
6477 single_entry_items[3].read(cx).project_items[0].clone(),
6478 ])
6479 });
6480 let item_3_4 = cx.new_view(|cx| {
6481 TestItem::new(cx)
6482 .with_dirty(true)
6483 .with_singleton(false)
6484 .with_project_items(&[
6485 single_entry_items[3].read(cx).project_items[0].clone(),
6486 single_entry_items[4].read(cx).project_items[0].clone(),
6487 ])
6488 });
6489
6490 // Create two panes that contain the following project entries:
6491 // left pane:
6492 // multi-entry items: (2, 3)
6493 // single-entry items: 0, 1, 2, 3, 4
6494 // right pane:
6495 // single-entry items: 1
6496 // multi-entry items: (3, 4)
6497 let left_pane = workspace.update(cx, |workspace, cx| {
6498 let left_pane = workspace.active_pane().clone();
6499 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6500 for item in single_entry_items {
6501 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6502 }
6503 left_pane.update(cx, |pane, cx| {
6504 pane.activate_item(2, true, true, cx);
6505 });
6506
6507 let right_pane = workspace
6508 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6509 .unwrap();
6510
6511 right_pane.update(cx, |pane, cx| {
6512 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6513 });
6514
6515 left_pane
6516 });
6517
6518 cx.focus_view(&left_pane);
6519
6520 // When closing all of the items in the left pane, we should be prompted twice:
6521 // once for project entry 0, and once for project entry 2. Project entries 1,
6522 // 3, and 4 are all still open in the other paten. After those two
6523 // prompts, the task should complete.
6524
6525 let close = left_pane.update(cx, |pane, cx| {
6526 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6527 });
6528 cx.executor().run_until_parked();
6529
6530 // Discard "Save all" prompt
6531 cx.simulate_prompt_answer(2);
6532
6533 cx.executor().run_until_parked();
6534 left_pane.update(cx, |pane, cx| {
6535 assert_eq!(
6536 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6537 &[ProjectEntryId::from_proto(0)]
6538 );
6539 });
6540 cx.simulate_prompt_answer(0);
6541
6542 cx.executor().run_until_parked();
6543 left_pane.update(cx, |pane, cx| {
6544 assert_eq!(
6545 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6546 &[ProjectEntryId::from_proto(2)]
6547 );
6548 });
6549 cx.simulate_prompt_answer(0);
6550
6551 cx.executor().run_until_parked();
6552 close.await.unwrap();
6553 left_pane.update(cx, |pane, _| {
6554 assert_eq!(pane.items_len(), 0);
6555 });
6556 }
6557
6558 #[gpui::test]
6559 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6560 init_test(cx);
6561
6562 let fs = FakeFs::new(cx.executor());
6563 let project = Project::test(fs, [], cx).await;
6564 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6565 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6566
6567 let item = cx.new_view(|cx| {
6568 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6569 });
6570 let item_id = item.entity_id();
6571 workspace.update(cx, |workspace, cx| {
6572 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6573 });
6574
6575 // Autosave on window change.
6576 item.update(cx, |item, cx| {
6577 SettingsStore::update_global(cx, |settings, cx| {
6578 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6579 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6580 })
6581 });
6582 item.is_dirty = true;
6583 });
6584
6585 // Deactivating the window saves the file.
6586 cx.deactivate_window();
6587 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6588
6589 // Re-activating the window doesn't save the file.
6590 cx.update(|cx| cx.activate_window());
6591 cx.executor().run_until_parked();
6592 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6593
6594 // Autosave on focus change.
6595 item.update(cx, |item, cx| {
6596 cx.focus_self();
6597 SettingsStore::update_global(cx, |settings, cx| {
6598 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6599 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6600 })
6601 });
6602 item.is_dirty = true;
6603 });
6604
6605 // Blurring the item saves the file.
6606 item.update(cx, |_, cx| cx.blur());
6607 cx.executor().run_until_parked();
6608 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6609
6610 // Deactivating the window still saves the file.
6611 item.update(cx, |item, cx| {
6612 cx.focus_self();
6613 item.is_dirty = true;
6614 });
6615 cx.deactivate_window();
6616 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6617
6618 // Autosave after delay.
6619 item.update(cx, |item, cx| {
6620 SettingsStore::update_global(cx, |settings, cx| {
6621 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6622 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6623 })
6624 });
6625 item.is_dirty = true;
6626 cx.emit(ItemEvent::Edit);
6627 });
6628
6629 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6630 cx.executor().advance_clock(Duration::from_millis(250));
6631 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6632
6633 // After delay expires, the file is saved.
6634 cx.executor().advance_clock(Duration::from_millis(250));
6635 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6636
6637 // Autosave on focus change, ensuring closing the tab counts as such.
6638 item.update(cx, |item, cx| {
6639 SettingsStore::update_global(cx, |settings, cx| {
6640 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6641 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6642 })
6643 });
6644 item.is_dirty = true;
6645 });
6646
6647 pane.update(cx, |pane, cx| {
6648 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6649 })
6650 .await
6651 .unwrap();
6652 assert!(!cx.has_pending_prompt());
6653 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6654
6655 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6656 workspace.update(cx, |workspace, cx| {
6657 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6658 });
6659 item.update(cx, |item, cx| {
6660 item.project_items[0].update(cx, |item, _| {
6661 item.entry_id = None;
6662 });
6663 item.is_dirty = true;
6664 cx.blur();
6665 });
6666 cx.run_until_parked();
6667 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6668
6669 // Ensure autosave is prevented for deleted files also when closing the buffer.
6670 let _close_items = pane.update(cx, |pane, cx| {
6671 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6672 });
6673 cx.run_until_parked();
6674 assert!(cx.has_pending_prompt());
6675 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6676 }
6677
6678 #[gpui::test]
6679 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6680 init_test(cx);
6681
6682 let fs = FakeFs::new(cx.executor());
6683
6684 let project = Project::test(fs, [], cx).await;
6685 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6686
6687 let item = cx.new_view(|cx| {
6688 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6689 });
6690 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6691 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6692 let toolbar_notify_count = Rc::new(RefCell::new(0));
6693
6694 workspace.update(cx, |workspace, cx| {
6695 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6696 let toolbar_notification_count = toolbar_notify_count.clone();
6697 cx.observe(&toolbar, move |_, _, _| {
6698 *toolbar_notification_count.borrow_mut() += 1
6699 })
6700 .detach();
6701 });
6702
6703 pane.update(cx, |pane, _| {
6704 assert!(!pane.can_navigate_backward());
6705 assert!(!pane.can_navigate_forward());
6706 });
6707
6708 item.update(cx, |item, cx| {
6709 item.set_state("one".to_string(), cx);
6710 });
6711
6712 // Toolbar must be notified to re-render the navigation buttons
6713 assert_eq!(*toolbar_notify_count.borrow(), 1);
6714
6715 pane.update(cx, |pane, _| {
6716 assert!(pane.can_navigate_backward());
6717 assert!(!pane.can_navigate_forward());
6718 });
6719
6720 workspace
6721 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6722 .await
6723 .unwrap();
6724
6725 assert_eq!(*toolbar_notify_count.borrow(), 2);
6726 pane.update(cx, |pane, _| {
6727 assert!(!pane.can_navigate_backward());
6728 assert!(pane.can_navigate_forward());
6729 });
6730 }
6731
6732 #[gpui::test]
6733 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6734 init_test(cx);
6735 let fs = FakeFs::new(cx.executor());
6736
6737 let project = Project::test(fs, [], cx).await;
6738 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6739
6740 let panel = workspace.update(cx, |workspace, cx| {
6741 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6742 workspace.add_panel(panel.clone(), cx);
6743
6744 workspace
6745 .right_dock()
6746 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6747
6748 panel
6749 });
6750
6751 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6752 pane.update(cx, |pane, cx| {
6753 let item = cx.new_view(TestItem::new);
6754 pane.add_item(Box::new(item), true, true, None, cx);
6755 });
6756
6757 // Transfer focus from center to panel
6758 workspace.update(cx, |workspace, cx| {
6759 workspace.toggle_panel_focus::<TestPanel>(cx);
6760 });
6761
6762 workspace.update(cx, |workspace, cx| {
6763 assert!(workspace.right_dock().read(cx).is_open());
6764 assert!(!panel.is_zoomed(cx));
6765 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6766 });
6767
6768 // Transfer focus from panel to center
6769 workspace.update(cx, |workspace, cx| {
6770 workspace.toggle_panel_focus::<TestPanel>(cx);
6771 });
6772
6773 workspace.update(cx, |workspace, cx| {
6774 assert!(workspace.right_dock().read(cx).is_open());
6775 assert!(!panel.is_zoomed(cx));
6776 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6777 });
6778
6779 // Close the dock
6780 workspace.update(cx, |workspace, cx| {
6781 workspace.toggle_dock(DockPosition::Right, cx);
6782 });
6783
6784 workspace.update(cx, |workspace, cx| {
6785 assert!(!workspace.right_dock().read(cx).is_open());
6786 assert!(!panel.is_zoomed(cx));
6787 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6788 });
6789
6790 // Open the dock
6791 workspace.update(cx, |workspace, cx| {
6792 workspace.toggle_dock(DockPosition::Right, cx);
6793 });
6794
6795 workspace.update(cx, |workspace, cx| {
6796 assert!(workspace.right_dock().read(cx).is_open());
6797 assert!(!panel.is_zoomed(cx));
6798 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6799 });
6800
6801 // Focus and zoom panel
6802 panel.update(cx, |panel, cx| {
6803 cx.focus_self();
6804 panel.set_zoomed(true, cx)
6805 });
6806
6807 workspace.update(cx, |workspace, cx| {
6808 assert!(workspace.right_dock().read(cx).is_open());
6809 assert!(panel.is_zoomed(cx));
6810 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6811 });
6812
6813 // Transfer focus to the center closes the dock
6814 workspace.update(cx, |workspace, cx| {
6815 workspace.toggle_panel_focus::<TestPanel>(cx);
6816 });
6817
6818 workspace.update(cx, |workspace, cx| {
6819 assert!(!workspace.right_dock().read(cx).is_open());
6820 assert!(panel.is_zoomed(cx));
6821 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6822 });
6823
6824 // Transferring focus back to the panel keeps it zoomed
6825 workspace.update(cx, |workspace, cx| {
6826 workspace.toggle_panel_focus::<TestPanel>(cx);
6827 });
6828
6829 workspace.update(cx, |workspace, cx| {
6830 assert!(workspace.right_dock().read(cx).is_open());
6831 assert!(panel.is_zoomed(cx));
6832 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6833 });
6834
6835 // Close the dock while it is zoomed
6836 workspace.update(cx, |workspace, cx| {
6837 workspace.toggle_dock(DockPosition::Right, cx)
6838 });
6839
6840 workspace.update(cx, |workspace, cx| {
6841 assert!(!workspace.right_dock().read(cx).is_open());
6842 assert!(panel.is_zoomed(cx));
6843 assert!(workspace.zoomed.is_none());
6844 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6845 });
6846
6847 // Opening the dock, when it's zoomed, retains focus
6848 workspace.update(cx, |workspace, cx| {
6849 workspace.toggle_dock(DockPosition::Right, cx)
6850 });
6851
6852 workspace.update(cx, |workspace, cx| {
6853 assert!(workspace.right_dock().read(cx).is_open());
6854 assert!(panel.is_zoomed(cx));
6855 assert!(workspace.zoomed.is_some());
6856 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6857 });
6858
6859 // Unzoom and close the panel, zoom the active pane.
6860 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6861 workspace.update(cx, |workspace, cx| {
6862 workspace.toggle_dock(DockPosition::Right, cx)
6863 });
6864 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6865
6866 // Opening a dock unzooms the pane.
6867 workspace.update(cx, |workspace, cx| {
6868 workspace.toggle_dock(DockPosition::Right, cx)
6869 });
6870 workspace.update(cx, |workspace, cx| {
6871 let pane = pane.read(cx);
6872 assert!(!pane.is_zoomed());
6873 assert!(!pane.focus_handle(cx).is_focused(cx));
6874 assert!(workspace.right_dock().read(cx).is_open());
6875 assert!(workspace.zoomed.is_none());
6876 });
6877 }
6878
6879 #[gpui::test]
6880 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6881 init_test(cx);
6882
6883 let fs = FakeFs::new(cx.executor());
6884
6885 let project = Project::test(fs, None, cx).await;
6886 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6887
6888 // Let's arrange the panes like this:
6889 //
6890 // +-----------------------+
6891 // | top |
6892 // +------+--------+-------+
6893 // | left | center | right |
6894 // +------+--------+-------+
6895 // | bottom |
6896 // +-----------------------+
6897
6898 let top_item = cx.new_view(|cx| {
6899 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6900 });
6901 let bottom_item = cx.new_view(|cx| {
6902 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6903 });
6904 let left_item = cx.new_view(|cx| {
6905 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6906 });
6907 let right_item = cx.new_view(|cx| {
6908 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6909 });
6910 let center_item = cx.new_view(|cx| {
6911 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6912 });
6913
6914 let top_pane_id = workspace.update(cx, |workspace, cx| {
6915 let top_pane_id = workspace.active_pane().entity_id();
6916 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6917 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6918 top_pane_id
6919 });
6920 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6921 let bottom_pane_id = workspace.active_pane().entity_id();
6922 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6923 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6924 bottom_pane_id
6925 });
6926 let left_pane_id = workspace.update(cx, |workspace, cx| {
6927 let left_pane_id = workspace.active_pane().entity_id();
6928 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6929 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6930 left_pane_id
6931 });
6932 let right_pane_id = workspace.update(cx, |workspace, cx| {
6933 let right_pane_id = workspace.active_pane().entity_id();
6934 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6935 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6936 right_pane_id
6937 });
6938 let center_pane_id = workspace.update(cx, |workspace, cx| {
6939 let center_pane_id = workspace.active_pane().entity_id();
6940 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6941 center_pane_id
6942 });
6943 cx.executor().run_until_parked();
6944
6945 workspace.update(cx, |workspace, cx| {
6946 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6947
6948 // Join into next from center pane into right
6949 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6950 });
6951
6952 workspace.update(cx, |workspace, cx| {
6953 let active_pane = workspace.active_pane();
6954 assert_eq!(right_pane_id, active_pane.entity_id());
6955 assert_eq!(2, active_pane.read(cx).items_len());
6956 let item_ids_in_pane =
6957 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6958 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6959 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6960
6961 // Join into next from right pane into bottom
6962 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6963 });
6964
6965 workspace.update(cx, |workspace, cx| {
6966 let active_pane = workspace.active_pane();
6967 assert_eq!(bottom_pane_id, active_pane.entity_id());
6968 assert_eq!(3, active_pane.read(cx).items_len());
6969 let item_ids_in_pane =
6970 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6971 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6972 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6973 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6974
6975 // Join into next from bottom pane into left
6976 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6977 });
6978
6979 workspace.update(cx, |workspace, cx| {
6980 let active_pane = workspace.active_pane();
6981 assert_eq!(left_pane_id, active_pane.entity_id());
6982 assert_eq!(4, active_pane.read(cx).items_len());
6983 let item_ids_in_pane =
6984 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6985 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6986 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6987 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6988 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6989
6990 // Join into next from left pane into top
6991 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6992 });
6993
6994 workspace.update(cx, |workspace, cx| {
6995 let active_pane = workspace.active_pane();
6996 assert_eq!(top_pane_id, active_pane.entity_id());
6997 assert_eq!(5, active_pane.read(cx).items_len());
6998 let item_ids_in_pane =
6999 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7000 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7001 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7002 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7003 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7004 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7005
7006 // Single pane left: no-op
7007 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7008 });
7009
7010 workspace.update(cx, |workspace, _cx| {
7011 let active_pane = workspace.active_pane();
7012 assert_eq!(top_pane_id, active_pane.entity_id());
7013 });
7014 }
7015
7016 fn add_an_item_to_active_pane(
7017 cx: &mut VisualTestContext,
7018 workspace: &View<Workspace>,
7019 item_id: u64,
7020 ) -> View<TestItem> {
7021 let item = cx.new_view(|cx| {
7022 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7023 item_id,
7024 "item{item_id}.txt",
7025 cx,
7026 )])
7027 });
7028 workspace.update(cx, |workspace, cx| {
7029 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7030 });
7031 return item;
7032 }
7033
7034 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7035 return workspace.update(cx, |workspace, cx| {
7036 let new_pane =
7037 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7038 new_pane
7039 });
7040 }
7041
7042 #[gpui::test]
7043 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7044 init_test(cx);
7045 let fs = FakeFs::new(cx.executor());
7046 let project = Project::test(fs, None, cx).await;
7047 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7048
7049 add_an_item_to_active_pane(cx, &workspace, 1);
7050 split_pane(cx, &workspace);
7051 add_an_item_to_active_pane(cx, &workspace, 2);
7052 split_pane(cx, &workspace); // empty pane
7053 split_pane(cx, &workspace);
7054 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7055
7056 cx.executor().run_until_parked();
7057
7058 workspace.update(cx, |workspace, cx| {
7059 let num_panes = workspace.panes().len();
7060 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7061 let active_item = workspace
7062 .active_pane()
7063 .read(cx)
7064 .active_item()
7065 .expect("item is in focus");
7066
7067 assert_eq!(num_panes, 4);
7068 assert_eq!(num_items_in_current_pane, 1);
7069 assert_eq!(active_item.item_id(), last_item.item_id());
7070 });
7071
7072 workspace.update(cx, |workspace, cx| {
7073 workspace.join_all_panes(cx);
7074 });
7075
7076 workspace.update(cx, |workspace, cx| {
7077 let num_panes = workspace.panes().len();
7078 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7079 let active_item = workspace
7080 .active_pane()
7081 .read(cx)
7082 .active_item()
7083 .expect("item is in focus");
7084
7085 assert_eq!(num_panes, 1);
7086 assert_eq!(num_items_in_current_pane, 3);
7087 assert_eq!(active_item.item_id(), last_item.item_id());
7088 });
7089 }
7090 struct TestModal(FocusHandle);
7091
7092 impl TestModal {
7093 fn new(cx: &mut ViewContext<Self>) -> Self {
7094 Self(cx.focus_handle())
7095 }
7096 }
7097
7098 impl EventEmitter<DismissEvent> for TestModal {}
7099
7100 impl FocusableView for TestModal {
7101 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7102 self.0.clone()
7103 }
7104 }
7105
7106 impl ModalView for TestModal {}
7107
7108 impl Render for TestModal {
7109 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7110 div().track_focus(&self.0)
7111 }
7112 }
7113
7114 #[gpui::test]
7115 async fn test_panels(cx: &mut gpui::TestAppContext) {
7116 init_test(cx);
7117 let fs = FakeFs::new(cx.executor());
7118
7119 let project = Project::test(fs, [], cx).await;
7120 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7121
7122 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7123 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7124 workspace.add_panel(panel_1.clone(), cx);
7125 workspace
7126 .left_dock()
7127 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7128 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7129 workspace.add_panel(panel_2.clone(), cx);
7130 workspace
7131 .right_dock()
7132 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7133
7134 let left_dock = workspace.left_dock();
7135 assert_eq!(
7136 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7137 panel_1.panel_id()
7138 );
7139 assert_eq!(
7140 left_dock.read(cx).active_panel_size(cx).unwrap(),
7141 panel_1.size(cx)
7142 );
7143
7144 left_dock.update(cx, |left_dock, cx| {
7145 left_dock.resize_active_panel(Some(px(1337.)), cx)
7146 });
7147 assert_eq!(
7148 workspace
7149 .right_dock()
7150 .read(cx)
7151 .visible_panel()
7152 .unwrap()
7153 .panel_id(),
7154 panel_2.panel_id(),
7155 );
7156
7157 (panel_1, panel_2)
7158 });
7159
7160 // Move panel_1 to the right
7161 panel_1.update(cx, |panel_1, cx| {
7162 panel_1.set_position(DockPosition::Right, cx)
7163 });
7164
7165 workspace.update(cx, |workspace, cx| {
7166 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7167 // Since it was the only panel on the left, the left dock should now be closed.
7168 assert!(!workspace.left_dock().read(cx).is_open());
7169 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7170 let right_dock = workspace.right_dock();
7171 assert_eq!(
7172 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7173 panel_1.panel_id()
7174 );
7175 assert_eq!(
7176 right_dock.read(cx).active_panel_size(cx).unwrap(),
7177 px(1337.)
7178 );
7179
7180 // Now we move panel_2 to the left
7181 panel_2.set_position(DockPosition::Left, cx);
7182 });
7183
7184 workspace.update(cx, |workspace, cx| {
7185 // Since panel_2 was not visible on the right, we don't open the left dock.
7186 assert!(!workspace.left_dock().read(cx).is_open());
7187 // And the right dock is unaffected in its displaying of panel_1
7188 assert!(workspace.right_dock().read(cx).is_open());
7189 assert_eq!(
7190 workspace
7191 .right_dock()
7192 .read(cx)
7193 .visible_panel()
7194 .unwrap()
7195 .panel_id(),
7196 panel_1.panel_id(),
7197 );
7198 });
7199
7200 // Move panel_1 back to the left
7201 panel_1.update(cx, |panel_1, cx| {
7202 panel_1.set_position(DockPosition::Left, cx)
7203 });
7204
7205 workspace.update(cx, |workspace, cx| {
7206 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7207 let left_dock = workspace.left_dock();
7208 assert!(left_dock.read(cx).is_open());
7209 assert_eq!(
7210 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7211 panel_1.panel_id()
7212 );
7213 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7214 // And the right dock should be closed as it no longer has any panels.
7215 assert!(!workspace.right_dock().read(cx).is_open());
7216
7217 // Now we move panel_1 to the bottom
7218 panel_1.set_position(DockPosition::Bottom, cx);
7219 });
7220
7221 workspace.update(cx, |workspace, cx| {
7222 // Since panel_1 was visible on the left, we close the left dock.
7223 assert!(!workspace.left_dock().read(cx).is_open());
7224 // The bottom dock is sized based on the panel's default size,
7225 // since the panel orientation changed from vertical to horizontal.
7226 let bottom_dock = workspace.bottom_dock();
7227 assert_eq!(
7228 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7229 panel_1.size(cx),
7230 );
7231 // Close bottom dock and move panel_1 back to the left.
7232 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7233 panel_1.set_position(DockPosition::Left, cx);
7234 });
7235
7236 // Emit activated event on panel 1
7237 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7238
7239 // Now the left dock is open and panel_1 is active and focused.
7240 workspace.update(cx, |workspace, cx| {
7241 let left_dock = workspace.left_dock();
7242 assert!(left_dock.read(cx).is_open());
7243 assert_eq!(
7244 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7245 panel_1.panel_id(),
7246 );
7247 assert!(panel_1.focus_handle(cx).is_focused(cx));
7248 });
7249
7250 // Emit closed event on panel 2, which is not active
7251 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7252
7253 // Wo don't close the left dock, because panel_2 wasn't the active panel
7254 workspace.update(cx, |workspace, cx| {
7255 let left_dock = workspace.left_dock();
7256 assert!(left_dock.read(cx).is_open());
7257 assert_eq!(
7258 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7259 panel_1.panel_id(),
7260 );
7261 });
7262
7263 // Emitting a ZoomIn event shows the panel as zoomed.
7264 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7265 workspace.update(cx, |workspace, _| {
7266 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7267 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7268 });
7269
7270 // Move panel to another dock while it is zoomed
7271 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7272 workspace.update(cx, |workspace, _| {
7273 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7274
7275 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7276 });
7277
7278 // This is a helper for getting a:
7279 // - valid focus on an element,
7280 // - that isn't a part of the panes and panels system of the Workspace,
7281 // - and doesn't trigger the 'on_focus_lost' API.
7282 let focus_other_view = {
7283 let workspace = workspace.clone();
7284 move |cx: &mut VisualTestContext| {
7285 workspace.update(cx, |workspace, cx| {
7286 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7287 workspace.toggle_modal(cx, TestModal::new);
7288 workspace.toggle_modal(cx, TestModal::new);
7289 } else {
7290 workspace.toggle_modal(cx, TestModal::new);
7291 }
7292 })
7293 }
7294 };
7295
7296 // If focus is transferred to another view that's not a panel or another pane, we still show
7297 // the panel as zoomed.
7298 focus_other_view(cx);
7299 workspace.update(cx, |workspace, _| {
7300 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7302 });
7303
7304 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7305 workspace.update(cx, |_, cx| cx.focus_self());
7306 workspace.update(cx, |workspace, _| {
7307 assert_eq!(workspace.zoomed, None);
7308 assert_eq!(workspace.zoomed_position, None);
7309 });
7310
7311 // If focus is transferred again to another view that's not a panel or a pane, we won't
7312 // show the panel as zoomed because it wasn't zoomed before.
7313 focus_other_view(cx);
7314 workspace.update(cx, |workspace, _| {
7315 assert_eq!(workspace.zoomed, None);
7316 assert_eq!(workspace.zoomed_position, None);
7317 });
7318
7319 // When the panel is activated, it is zoomed again.
7320 cx.dispatch_action(ToggleRightDock);
7321 workspace.update(cx, |workspace, _| {
7322 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7323 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7324 });
7325
7326 // Emitting a ZoomOut event unzooms the panel.
7327 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7328 workspace.update(cx, |workspace, _| {
7329 assert_eq!(workspace.zoomed, None);
7330 assert_eq!(workspace.zoomed_position, None);
7331 });
7332
7333 // Emit closed event on panel 1, which is active
7334 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7335
7336 // Now the left dock is closed, because panel_1 was the active panel
7337 workspace.update(cx, |workspace, cx| {
7338 let right_dock = workspace.right_dock();
7339 assert!(!right_dock.read(cx).is_open());
7340 });
7341 }
7342
7343 mod register_project_item_tests {
7344 use ui::Context as _;
7345
7346 use super::*;
7347
7348 // View
7349 struct TestPngItemView {
7350 focus_handle: FocusHandle,
7351 }
7352 // Model
7353 struct TestPngItem {}
7354
7355 impl project::Item for TestPngItem {
7356 fn try_open(
7357 _project: &Model<Project>,
7358 path: &ProjectPath,
7359 cx: &mut AppContext,
7360 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7361 if path.path.extension().unwrap() == "png" {
7362 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7363 } else {
7364 None
7365 }
7366 }
7367
7368 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7369 None
7370 }
7371
7372 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7373 None
7374 }
7375 }
7376
7377 impl Item for TestPngItemView {
7378 type Event = ();
7379 }
7380 impl EventEmitter<()> for TestPngItemView {}
7381 impl FocusableView for TestPngItemView {
7382 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7383 self.focus_handle.clone()
7384 }
7385 }
7386
7387 impl Render for TestPngItemView {
7388 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7389 Empty
7390 }
7391 }
7392
7393 impl ProjectItem for TestPngItemView {
7394 type Item = TestPngItem;
7395
7396 fn for_project_item(
7397 _project: Model<Project>,
7398 _item: Model<Self::Item>,
7399 cx: &mut ViewContext<Self>,
7400 ) -> Self
7401 where
7402 Self: Sized,
7403 {
7404 Self {
7405 focus_handle: cx.focus_handle(),
7406 }
7407 }
7408 }
7409
7410 // View
7411 struct TestIpynbItemView {
7412 focus_handle: FocusHandle,
7413 }
7414 // Model
7415 struct TestIpynbItem {}
7416
7417 impl project::Item for TestIpynbItem {
7418 fn try_open(
7419 _project: &Model<Project>,
7420 path: &ProjectPath,
7421 cx: &mut AppContext,
7422 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7423 if path.path.extension().unwrap() == "ipynb" {
7424 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7425 } else {
7426 None
7427 }
7428 }
7429
7430 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7431 None
7432 }
7433
7434 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7435 None
7436 }
7437 }
7438
7439 impl Item for TestIpynbItemView {
7440 type Event = ();
7441 }
7442 impl EventEmitter<()> for TestIpynbItemView {}
7443 impl FocusableView for TestIpynbItemView {
7444 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7445 self.focus_handle.clone()
7446 }
7447 }
7448
7449 impl Render for TestIpynbItemView {
7450 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7451 Empty
7452 }
7453 }
7454
7455 impl ProjectItem for TestIpynbItemView {
7456 type Item = TestIpynbItem;
7457
7458 fn for_project_item(
7459 _project: Model<Project>,
7460 _item: Model<Self::Item>,
7461 cx: &mut ViewContext<Self>,
7462 ) -> Self
7463 where
7464 Self: Sized,
7465 {
7466 Self {
7467 focus_handle: cx.focus_handle(),
7468 }
7469 }
7470 }
7471
7472 struct TestAlternatePngItemView {
7473 focus_handle: FocusHandle,
7474 }
7475
7476 impl Item for TestAlternatePngItemView {
7477 type Event = ();
7478 }
7479
7480 impl EventEmitter<()> for TestAlternatePngItemView {}
7481 impl FocusableView for TestAlternatePngItemView {
7482 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7483 self.focus_handle.clone()
7484 }
7485 }
7486
7487 impl Render for TestAlternatePngItemView {
7488 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7489 Empty
7490 }
7491 }
7492
7493 impl ProjectItem for TestAlternatePngItemView {
7494 type Item = TestPngItem;
7495
7496 fn for_project_item(
7497 _project: Model<Project>,
7498 _item: Model<Self::Item>,
7499 cx: &mut ViewContext<Self>,
7500 ) -> Self
7501 where
7502 Self: Sized,
7503 {
7504 Self {
7505 focus_handle: cx.focus_handle(),
7506 }
7507 }
7508 }
7509
7510 #[gpui::test]
7511 async fn test_register_project_item(cx: &mut TestAppContext) {
7512 init_test(cx);
7513
7514 cx.update(|cx| {
7515 register_project_item::<TestPngItemView>(cx);
7516 register_project_item::<TestIpynbItemView>(cx);
7517 });
7518
7519 let fs = FakeFs::new(cx.executor());
7520 fs.insert_tree(
7521 "/root1",
7522 json!({
7523 "one.png": "BINARYDATAHERE",
7524 "two.ipynb": "{ totally a notebook }",
7525 "three.txt": "editing text, sure why not?"
7526 }),
7527 )
7528 .await;
7529
7530 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7531 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7532
7533 let worktree_id = project.update(cx, |project, cx| {
7534 project.worktrees(cx).next().unwrap().read(cx).id()
7535 });
7536
7537 let handle = workspace
7538 .update(cx, |workspace, cx| {
7539 let project_path = (worktree_id, "one.png");
7540 workspace.open_path(project_path, None, true, cx)
7541 })
7542 .await
7543 .unwrap();
7544
7545 // Now we can check if the handle we got back errored or not
7546 assert_eq!(
7547 handle.to_any().entity_type(),
7548 TypeId::of::<TestPngItemView>()
7549 );
7550
7551 let handle = workspace
7552 .update(cx, |workspace, cx| {
7553 let project_path = (worktree_id, "two.ipynb");
7554 workspace.open_path(project_path, None, true, cx)
7555 })
7556 .await
7557 .unwrap();
7558
7559 assert_eq!(
7560 handle.to_any().entity_type(),
7561 TypeId::of::<TestIpynbItemView>()
7562 );
7563
7564 let handle = workspace
7565 .update(cx, |workspace, cx| {
7566 let project_path = (worktree_id, "three.txt");
7567 workspace.open_path(project_path, None, true, cx)
7568 })
7569 .await;
7570 assert!(handle.is_err());
7571 }
7572
7573 #[gpui::test]
7574 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7575 init_test(cx);
7576
7577 cx.update(|cx| {
7578 register_project_item::<TestPngItemView>(cx);
7579 register_project_item::<TestAlternatePngItemView>(cx);
7580 });
7581
7582 let fs = FakeFs::new(cx.executor());
7583 fs.insert_tree(
7584 "/root1",
7585 json!({
7586 "one.png": "BINARYDATAHERE",
7587 "two.ipynb": "{ totally a notebook }",
7588 "three.txt": "editing text, sure why not?"
7589 }),
7590 )
7591 .await;
7592
7593 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7594 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7595
7596 let worktree_id = project.update(cx, |project, cx| {
7597 project.worktrees(cx).next().unwrap().read(cx).id()
7598 });
7599
7600 let handle = workspace
7601 .update(cx, |workspace, cx| {
7602 let project_path = (worktree_id, "one.png");
7603 workspace.open_path(project_path, None, true, cx)
7604 })
7605 .await
7606 .unwrap();
7607
7608 // This _must_ be the second item registered
7609 assert_eq!(
7610 handle.to_any().entity_type(),
7611 TypeId::of::<TestAlternatePngItemView>()
7612 );
7613
7614 let handle = workspace
7615 .update(cx, |workspace, cx| {
7616 let project_path = (worktree_id, "three.txt");
7617 workspace.open_path(project_path, None, true, cx)
7618 })
7619 .await;
7620 assert!(handle.is_err());
7621 }
7622 }
7623
7624 pub fn init_test(cx: &mut TestAppContext) {
7625 cx.update(|cx| {
7626 let settings_store = SettingsStore::test(cx);
7627 cx.set_global(settings_store);
7628 theme::init(theme::LoadThemes::JustBase, cx);
7629 language::init(cx);
7630 crate::init_settings(cx);
7631 Project::init_settings(cx);
7632 });
7633 }
7634}