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