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