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