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