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