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