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