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