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