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