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