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