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