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() {
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::JoinAll => self.join_all_panes(cx),
2969 pane::Event::Remove { focus_on_pane } => {
2970 self.remove_pane(pane, focus_on_pane.clone(), cx)
2971 }
2972 pane::Event::ActivateItem { local } => {
2973 cx.on_next_frame(|_, cx| {
2974 cx.invalidate_character_coordinates();
2975 });
2976
2977 pane.model.update(cx, |pane, _| {
2978 pane.track_alternate_file_items();
2979 });
2980 if *local {
2981 self.unfollow_in_pane(&pane, cx);
2982 }
2983 if &pane == self.active_pane() {
2984 self.active_item_path_changed(cx);
2985 self.update_active_view_for_followers(cx);
2986 }
2987 }
2988 pane::Event::UserSavedItem { item, save_intent } => cx.emit(Event::UserSavedItem {
2989 pane: pane.downgrade(),
2990 item: item.boxed_clone(),
2991 save_intent: *save_intent,
2992 }),
2993 pane::Event::ChangeItemTitle => {
2994 if pane == self.active_pane {
2995 self.active_item_path_changed(cx);
2996 }
2997 self.update_window_edited(cx);
2998 }
2999 pane::Event::RemoveItem { .. } => {}
3000 pane::Event::RemovedItem { item_id } => {
3001 cx.emit(Event::ActiveItemChanged);
3002 self.update_window_edited(cx);
3003 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3004 if entry.get().entity_id() == pane.entity_id() {
3005 entry.remove();
3006 }
3007 }
3008 }
3009 pane::Event::Focus => {
3010 cx.on_next_frame(|_, cx| {
3011 cx.invalidate_character_coordinates();
3012 });
3013 self.handle_pane_focused(pane.clone(), cx);
3014 }
3015 pane::Event::ZoomIn => {
3016 if pane == self.active_pane {
3017 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3018 if pane.read(cx).has_focus(cx) {
3019 self.zoomed = Some(pane.downgrade().into());
3020 self.zoomed_position = None;
3021 cx.emit(Event::ZoomChanged);
3022 }
3023 cx.notify();
3024 }
3025 }
3026 pane::Event::ZoomOut => {
3027 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3028 if self.zoomed_position.is_none() {
3029 self.zoomed = None;
3030 cx.emit(Event::ZoomChanged);
3031 }
3032 cx.notify();
3033 }
3034 }
3035
3036 self.serialize_workspace(cx);
3037 }
3038
3039 pub fn unfollow_in_pane(
3040 &mut self,
3041 pane: &View<Pane>,
3042 cx: &mut ViewContext<Workspace>,
3043 ) -> Option<PeerId> {
3044 let leader_id = self.leader_for_pane(pane)?;
3045 self.unfollow(leader_id, cx);
3046 Some(leader_id)
3047 }
3048
3049 pub fn split_pane(
3050 &mut self,
3051 pane_to_split: View<Pane>,
3052 split_direction: SplitDirection,
3053 cx: &mut ViewContext<Self>,
3054 ) -> View<Pane> {
3055 let new_pane = self.add_pane(cx);
3056 self.center
3057 .split(&pane_to_split, &new_pane, split_direction)
3058 .unwrap();
3059 cx.notify();
3060 new_pane
3061 }
3062
3063 pub fn split_and_clone(
3064 &mut self,
3065 pane: View<Pane>,
3066 direction: SplitDirection,
3067 cx: &mut ViewContext<Self>,
3068 ) -> Option<View<Pane>> {
3069 let item = pane.read(cx).active_item()?;
3070 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3071 let new_pane = self.add_pane(cx);
3072 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3073 self.center.split(&pane, &new_pane, direction).unwrap();
3074 Some(new_pane)
3075 } else {
3076 None
3077 };
3078 cx.notify();
3079 maybe_pane_handle
3080 }
3081
3082 pub fn split_pane_with_item(
3083 &mut self,
3084 pane_to_split: WeakView<Pane>,
3085 split_direction: SplitDirection,
3086 from: WeakView<Pane>,
3087 item_id_to_move: EntityId,
3088 cx: &mut ViewContext<Self>,
3089 ) {
3090 let Some(pane_to_split) = pane_to_split.upgrade() else {
3091 return;
3092 };
3093 let Some(from) = from.upgrade() else {
3094 return;
3095 };
3096
3097 let new_pane = self.add_pane(cx);
3098 move_item(&from, &new_pane, item_id_to_move, 0, cx);
3099 self.center
3100 .split(&pane_to_split, &new_pane, split_direction)
3101 .unwrap();
3102 cx.notify();
3103 }
3104
3105 pub fn split_pane_with_project_entry(
3106 &mut self,
3107 pane_to_split: WeakView<Pane>,
3108 split_direction: SplitDirection,
3109 project_entry: ProjectEntryId,
3110 cx: &mut ViewContext<Self>,
3111 ) -> Option<Task<Result<()>>> {
3112 let pane_to_split = pane_to_split.upgrade()?;
3113 let new_pane = self.add_pane(cx);
3114 self.center
3115 .split(&pane_to_split, &new_pane, split_direction)
3116 .unwrap();
3117
3118 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3119 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3120 Some(cx.foreground_executor().spawn(async move {
3121 task.await?;
3122 Ok(())
3123 }))
3124 }
3125
3126 pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3127 let active_item = self.active_pane.read(cx).active_item();
3128 for pane in &self.panes {
3129 join_pane_into_active(&self.active_pane, pane, cx);
3130 }
3131 if let Some(active_item) = active_item {
3132 self.activate_item(active_item.as_ref(), true, true, cx);
3133 }
3134 cx.notify();
3135 }
3136
3137 pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3138 let next_pane = self
3139 .find_pane_in_direction(SplitDirection::Right, cx)
3140 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3141 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3142 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3143 let Some(next_pane) = next_pane else {
3144 return;
3145 };
3146 move_all_items(&pane, &next_pane, cx);
3147 cx.notify();
3148 }
3149
3150 fn remove_pane(
3151 &mut self,
3152 pane: View<Pane>,
3153 focus_on: Option<View<Pane>>,
3154 cx: &mut ViewContext<Self>,
3155 ) {
3156 if self.center.remove(&pane).unwrap() {
3157 self.force_remove_pane(&pane, &focus_on, cx);
3158 self.unfollow_in_pane(&pane, cx);
3159 self.last_leaders_by_pane.remove(&pane.downgrade());
3160 for removed_item in pane.read(cx).items() {
3161 self.panes_by_item.remove(&removed_item.item_id());
3162 }
3163
3164 cx.notify();
3165 } else {
3166 self.active_item_path_changed(cx);
3167 }
3168 cx.emit(Event::PaneRemoved);
3169 }
3170
3171 pub fn panes(&self) -> &[View<Pane>] {
3172 &self.panes
3173 }
3174
3175 pub fn active_pane(&self) -> &View<Pane> {
3176 &self.active_pane
3177 }
3178
3179 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3180 self.find_pane_in_direction(SplitDirection::Right, cx)
3181 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3182 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3183 .clone()
3184 }
3185
3186 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3187 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3188 weak_pane.upgrade()
3189 }
3190
3191 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3192 self.follower_states.retain(|leader_id, state| {
3193 if *leader_id == peer_id {
3194 for item in state.items_by_leader_view_id.values() {
3195 item.view.set_leader_peer_id(None, cx);
3196 }
3197 false
3198 } else {
3199 true
3200 }
3201 });
3202 cx.notify();
3203 }
3204
3205 pub fn start_following(
3206 &mut self,
3207 leader_id: PeerId,
3208 cx: &mut ViewContext<Self>,
3209 ) -> Option<Task<Result<()>>> {
3210 let pane = self.active_pane().clone();
3211
3212 self.last_leaders_by_pane
3213 .insert(pane.downgrade(), leader_id);
3214 self.unfollow(leader_id, cx);
3215 self.unfollow_in_pane(&pane, cx);
3216 self.follower_states.insert(
3217 leader_id,
3218 FollowerState {
3219 center_pane: pane.clone(),
3220 dock_pane: None,
3221 active_view_id: None,
3222 items_by_leader_view_id: Default::default(),
3223 },
3224 );
3225 cx.notify();
3226
3227 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3228 let project_id = self.project.read(cx).remote_id();
3229 let request = self.app_state.client.request(proto::Follow {
3230 room_id,
3231 project_id,
3232 leader_id: Some(leader_id),
3233 });
3234
3235 Some(cx.spawn(|this, mut cx| async move {
3236 let response = request.await?;
3237 this.update(&mut cx, |this, _| {
3238 let state = this
3239 .follower_states
3240 .get_mut(&leader_id)
3241 .ok_or_else(|| anyhow!("following interrupted"))?;
3242 state.active_view_id = response
3243 .active_view
3244 .as_ref()
3245 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3246 Ok::<_, anyhow::Error>(())
3247 })??;
3248 if let Some(view) = response.active_view {
3249 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3250 }
3251 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3252 Ok(())
3253 }))
3254 }
3255
3256 pub fn follow_next_collaborator(
3257 &mut self,
3258 _: &FollowNextCollaborator,
3259 cx: &mut ViewContext<Self>,
3260 ) {
3261 let collaborators = self.project.read(cx).collaborators();
3262 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3263 let mut collaborators = collaborators.keys().copied();
3264 for peer_id in collaborators.by_ref() {
3265 if peer_id == leader_id {
3266 break;
3267 }
3268 }
3269 collaborators.next()
3270 } else if let Some(last_leader_id) =
3271 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3272 {
3273 if collaborators.contains_key(last_leader_id) {
3274 Some(*last_leader_id)
3275 } else {
3276 None
3277 }
3278 } else {
3279 None
3280 };
3281
3282 let pane = self.active_pane.clone();
3283 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3284 else {
3285 return;
3286 };
3287 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3288 return;
3289 }
3290 if let Some(task) = self.start_following(leader_id, cx) {
3291 task.detach_and_log_err(cx)
3292 }
3293 }
3294
3295 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3296 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3297 return;
3298 };
3299 let room = room.read(cx);
3300 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3301 return;
3302 };
3303
3304 let project = self.project.read(cx);
3305
3306 let other_project_id = match remote_participant.location {
3307 call::ParticipantLocation::External => None,
3308 call::ParticipantLocation::UnsharedProject => None,
3309 call::ParticipantLocation::SharedProject { project_id } => {
3310 if Some(project_id) == project.remote_id() {
3311 None
3312 } else {
3313 Some(project_id)
3314 }
3315 }
3316 };
3317
3318 // if they are active in another project, follow there.
3319 if let Some(project_id) = other_project_id {
3320 let app_state = self.app_state.clone();
3321 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3322 .detach_and_log_err(cx);
3323 }
3324
3325 // if you're already following, find the right pane and focus it.
3326 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3327 cx.focus_view(follower_state.pane());
3328 return;
3329 }
3330
3331 // Otherwise, follow.
3332 if let Some(task) = self.start_following(leader_id, cx) {
3333 task.detach_and_log_err(cx)
3334 }
3335 }
3336
3337 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3338 cx.notify();
3339 let state = self.follower_states.remove(&leader_id)?;
3340 for (_, item) in state.items_by_leader_view_id {
3341 item.view.set_leader_peer_id(None, cx);
3342 }
3343
3344 let project_id = self.project.read(cx).remote_id();
3345 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3346 self.app_state
3347 .client
3348 .send(proto::Unfollow {
3349 room_id,
3350 project_id,
3351 leader_id: Some(leader_id),
3352 })
3353 .log_err();
3354
3355 Some(())
3356 }
3357
3358 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3359 self.follower_states.contains_key(&peer_id)
3360 }
3361
3362 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3363 cx.emit(Event::ActiveItemChanged);
3364 let active_entry = self.active_project_path(cx);
3365 self.project
3366 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3367
3368 self.update_window_title(cx);
3369 }
3370
3371 fn update_window_title(&mut self, cx: &mut WindowContext) {
3372 let project = self.project().read(cx);
3373 let mut title = String::new();
3374
3375 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3376 let filename = path
3377 .path
3378 .file_name()
3379 .map(|s| s.to_string_lossy())
3380 .or_else(|| {
3381 Some(Cow::Borrowed(
3382 project
3383 .worktree_for_id(path.worktree_id, cx)?
3384 .read(cx)
3385 .root_name(),
3386 ))
3387 });
3388
3389 if let Some(filename) = filename {
3390 title.push_str(filename.as_ref());
3391 title.push_str(" — ");
3392 }
3393 }
3394
3395 for (i, name) in project.worktree_root_names(cx).enumerate() {
3396 if i > 0 {
3397 title.push_str(", ");
3398 }
3399 title.push_str(name);
3400 }
3401
3402 if title.is_empty() {
3403 title = "empty project".to_string();
3404 }
3405
3406 if project.is_via_collab() {
3407 title.push_str(" ↙");
3408 } else if project.is_shared() {
3409 title.push_str(" ↗");
3410 }
3411
3412 cx.set_window_title(&title);
3413 }
3414
3415 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3416 let is_edited = !self.project.read(cx).is_disconnected()
3417 && self
3418 .items(cx)
3419 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3420 if is_edited != self.window_edited {
3421 self.window_edited = is_edited;
3422 cx.set_window_edited(self.window_edited)
3423 }
3424 }
3425
3426 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3427 if self.notifications.is_empty() {
3428 None
3429 } else {
3430 Some(
3431 div()
3432 .absolute()
3433 .right_3()
3434 .bottom_3()
3435 .w_112()
3436 .h_full()
3437 .flex()
3438 .flex_col()
3439 .justify_end()
3440 .gap_2()
3441 .children(
3442 self.notifications
3443 .iter()
3444 .map(|(_, notification)| notification.to_any()),
3445 ),
3446 )
3447 }
3448 }
3449
3450 // RPC handlers
3451
3452 fn active_view_for_follower(
3453 &self,
3454 follower_project_id: Option<u64>,
3455 cx: &mut ViewContext<Self>,
3456 ) -> Option<proto::View> {
3457 let (item, panel_id) = self.active_item_for_followers(cx);
3458 let item = item?;
3459 let leader_id = self
3460 .pane_for(&*item)
3461 .and_then(|pane| self.leader_for_pane(&pane));
3462
3463 let item_handle = item.to_followable_item_handle(cx)?;
3464 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3465 let variant = item_handle.to_state_proto(cx)?;
3466
3467 if item_handle.is_project_item(cx)
3468 && (follower_project_id.is_none()
3469 || follower_project_id != self.project.read(cx).remote_id())
3470 {
3471 return None;
3472 }
3473
3474 Some(proto::View {
3475 id: Some(id.to_proto()),
3476 leader_id,
3477 variant: Some(variant),
3478 panel_id: panel_id.map(|id| id as i32),
3479 })
3480 }
3481
3482 fn handle_follow(
3483 &mut self,
3484 follower_project_id: Option<u64>,
3485 cx: &mut ViewContext<Self>,
3486 ) -> proto::FollowResponse {
3487 let active_view = self.active_view_for_follower(follower_project_id, cx);
3488
3489 cx.notify();
3490 proto::FollowResponse {
3491 // TODO: Remove after version 0.145.x stabilizes.
3492 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3493 views: active_view.iter().cloned().collect(),
3494 active_view,
3495 }
3496 }
3497
3498 fn handle_update_followers(
3499 &mut self,
3500 leader_id: PeerId,
3501 message: proto::UpdateFollowers,
3502 _cx: &mut ViewContext<Self>,
3503 ) {
3504 self.leader_updates_tx
3505 .unbounded_send((leader_id, message))
3506 .ok();
3507 }
3508
3509 async fn process_leader_update(
3510 this: &WeakView<Self>,
3511 leader_id: PeerId,
3512 update: proto::UpdateFollowers,
3513 cx: &mut AsyncWindowContext,
3514 ) -> Result<()> {
3515 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3516 proto::update_followers::Variant::CreateView(view) => {
3517 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3518 let should_add_view = this.update(cx, |this, _| {
3519 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3520 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3521 } else {
3522 anyhow::Ok(false)
3523 }
3524 })??;
3525
3526 if should_add_view {
3527 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3528 }
3529 }
3530 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3531 let should_add_view = this.update(cx, |this, _| {
3532 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3533 state.active_view_id = update_active_view
3534 .view
3535 .as_ref()
3536 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3537
3538 if state.active_view_id.is_some_and(|view_id| {
3539 !state.items_by_leader_view_id.contains_key(&view_id)
3540 }) {
3541 anyhow::Ok(true)
3542 } else {
3543 anyhow::Ok(false)
3544 }
3545 } else {
3546 anyhow::Ok(false)
3547 }
3548 })??;
3549
3550 if should_add_view {
3551 if let Some(view) = update_active_view.view {
3552 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3553 }
3554 }
3555 }
3556 proto::update_followers::Variant::UpdateView(update_view) => {
3557 let variant = update_view
3558 .variant
3559 .ok_or_else(|| anyhow!("missing update view variant"))?;
3560 let id = update_view
3561 .id
3562 .ok_or_else(|| anyhow!("missing update view id"))?;
3563 let mut tasks = Vec::new();
3564 this.update(cx, |this, cx| {
3565 let project = this.project.clone();
3566 if let Some(state) = this.follower_states.get(&leader_id) {
3567 let view_id = ViewId::from_proto(id.clone())?;
3568 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3569 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3570 }
3571 }
3572 anyhow::Ok(())
3573 })??;
3574 try_join_all(tasks).await.log_err();
3575 }
3576 }
3577 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3578 Ok(())
3579 }
3580
3581 async fn add_view_from_leader(
3582 this: WeakView<Self>,
3583 leader_id: PeerId,
3584 view: &proto::View,
3585 cx: &mut AsyncWindowContext,
3586 ) -> Result<()> {
3587 let this = this.upgrade().context("workspace dropped")?;
3588
3589 let Some(id) = view.id.clone() else {
3590 return Err(anyhow!("no id for view"));
3591 };
3592 let id = ViewId::from_proto(id)?;
3593 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3594
3595 let pane = this.update(cx, |this, _cx| {
3596 let state = this
3597 .follower_states
3598 .get(&leader_id)
3599 .context("stopped following")?;
3600 anyhow::Ok(state.pane().clone())
3601 })??;
3602 let existing_item = pane.update(cx, |pane, cx| {
3603 let client = this.read(cx).client().clone();
3604 pane.items().find_map(|item| {
3605 let item = item.to_followable_item_handle(cx)?;
3606 if item.remote_id(&client, cx) == Some(id) {
3607 Some(item)
3608 } else {
3609 None
3610 }
3611 })
3612 })?;
3613 let item = if let Some(existing_item) = existing_item {
3614 existing_item
3615 } else {
3616 let variant = view.variant.clone();
3617 if variant.is_none() {
3618 Err(anyhow!("missing view variant"))?;
3619 }
3620
3621 let task = cx.update(|cx| {
3622 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3623 })?;
3624
3625 let Some(task) = task else {
3626 return Err(anyhow!(
3627 "failed to construct view from leader (maybe from a different version of zed?)"
3628 ));
3629 };
3630
3631 let mut new_item = task.await?;
3632 pane.update(cx, |pane, cx| {
3633 let mut item_ix_to_remove = None;
3634 for (ix, item) in pane.items().enumerate() {
3635 if let Some(item) = item.to_followable_item_handle(cx) {
3636 match new_item.dedup(item.as_ref(), cx) {
3637 Some(item::Dedup::KeepExisting) => {
3638 new_item =
3639 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3640 break;
3641 }
3642 Some(item::Dedup::ReplaceExisting) => {
3643 item_ix_to_remove = Some(ix);
3644 break;
3645 }
3646 None => {}
3647 }
3648 }
3649 }
3650
3651 if let Some(ix) = item_ix_to_remove {
3652 pane.remove_item(ix, false, false, cx);
3653 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3654 }
3655 })?;
3656
3657 new_item
3658 };
3659
3660 this.update(cx, |this, cx| {
3661 let state = this.follower_states.get_mut(&leader_id)?;
3662 item.set_leader_peer_id(Some(leader_id), cx);
3663 state.items_by_leader_view_id.insert(
3664 id,
3665 FollowerView {
3666 view: item,
3667 location: panel_id,
3668 },
3669 );
3670
3671 Some(())
3672 })?;
3673
3674 Ok(())
3675 }
3676
3677 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3678 let mut is_project_item = true;
3679 let mut update = proto::UpdateActiveView::default();
3680 if cx.is_window_active() {
3681 let (active_item, panel_id) = self.active_item_for_followers(cx);
3682
3683 if let Some(item) = active_item {
3684 if item.focus_handle(cx).contains_focused(cx) {
3685 let leader_id = self
3686 .pane_for(&*item)
3687 .and_then(|pane| self.leader_for_pane(&pane));
3688
3689 if let Some(item) = item.to_followable_item_handle(cx) {
3690 let id = item
3691 .remote_id(&self.app_state.client, cx)
3692 .map(|id| id.to_proto());
3693
3694 if let Some(id) = id.clone() {
3695 if let Some(variant) = item.to_state_proto(cx) {
3696 let view = Some(proto::View {
3697 id: Some(id.clone()),
3698 leader_id,
3699 variant: Some(variant),
3700 panel_id: panel_id.map(|id| id as i32),
3701 });
3702
3703 is_project_item = item.is_project_item(cx);
3704 update = proto::UpdateActiveView {
3705 view,
3706 // TODO: Remove after version 0.145.x stabilizes.
3707 id: Some(id.clone()),
3708 leader_id,
3709 };
3710 }
3711 };
3712 }
3713 }
3714 }
3715 }
3716
3717 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3718 if active_view_id != self.last_active_view_id.as_ref() {
3719 self.last_active_view_id = active_view_id.cloned();
3720 self.update_followers(
3721 is_project_item,
3722 proto::update_followers::Variant::UpdateActiveView(update),
3723 cx,
3724 );
3725 }
3726 }
3727
3728 fn active_item_for_followers(
3729 &self,
3730 cx: &mut WindowContext,
3731 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3732 let mut active_item = None;
3733 let mut panel_id = None;
3734 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3735 if dock.focus_handle(cx).contains_focused(cx) {
3736 if let Some(panel) = dock.read(cx).active_panel() {
3737 if let Some(pane) = panel.pane(cx) {
3738 if let Some(item) = pane.read(cx).active_item() {
3739 active_item = Some(item);
3740 panel_id = panel.remote_id();
3741 break;
3742 }
3743 }
3744 }
3745 }
3746 }
3747
3748 if active_item.is_none() {
3749 active_item = self.active_pane().read(cx).active_item();
3750 }
3751 (active_item, panel_id)
3752 }
3753
3754 fn update_followers(
3755 &self,
3756 project_only: bool,
3757 update: proto::update_followers::Variant,
3758 cx: &mut WindowContext,
3759 ) -> Option<()> {
3760 // If this update only applies to for followers in the current project,
3761 // then skip it unless this project is shared. If it applies to all
3762 // followers, regardless of project, then set `project_id` to none,
3763 // indicating that it goes to all followers.
3764 let project_id = if project_only {
3765 Some(self.project.read(cx).remote_id()?)
3766 } else {
3767 None
3768 };
3769 self.app_state().workspace_store.update(cx, |store, cx| {
3770 store.update_followers(project_id, update, cx)
3771 })
3772 }
3773
3774 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3775 self.follower_states.iter().find_map(|(leader_id, state)| {
3776 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3777 Some(*leader_id)
3778 } else {
3779 None
3780 }
3781 })
3782 }
3783
3784 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3785 cx.notify();
3786
3787 let call = self.active_call()?;
3788 let room = call.read(cx).room()?.read(cx);
3789 let participant = room.remote_participant_for_peer_id(leader_id)?;
3790
3791 let leader_in_this_app;
3792 let leader_in_this_project;
3793 match participant.location {
3794 call::ParticipantLocation::SharedProject { project_id } => {
3795 leader_in_this_app = true;
3796 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3797 }
3798 call::ParticipantLocation::UnsharedProject => {
3799 leader_in_this_app = true;
3800 leader_in_this_project = false;
3801 }
3802 call::ParticipantLocation::External => {
3803 leader_in_this_app = false;
3804 leader_in_this_project = false;
3805 }
3806 };
3807
3808 let state = self.follower_states.get(&leader_id)?;
3809 let mut item_to_activate = None;
3810 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3811 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3812 if leader_in_this_project || !item.view.is_project_item(cx) {
3813 item_to_activate = Some((item.location, item.view.boxed_clone()));
3814 }
3815 }
3816 } else if let Some(shared_screen) =
3817 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3818 {
3819 item_to_activate = Some((None, Box::new(shared_screen)));
3820 }
3821
3822 let (panel_id, item) = item_to_activate?;
3823
3824 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3825 let pane;
3826 if let Some(panel_id) = panel_id {
3827 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3828 let state = self.follower_states.get_mut(&leader_id)?;
3829 state.dock_pane = Some(pane.clone());
3830 } else {
3831 pane = state.center_pane.clone();
3832 let state = self.follower_states.get_mut(&leader_id)?;
3833 if let Some(dock_pane) = state.dock_pane.take() {
3834 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3835 }
3836 }
3837
3838 pane.update(cx, |pane, cx| {
3839 let focus_active_item = pane.has_focus(cx) || transfer_focus;
3840 if let Some(index) = pane.index_for_item(item.as_ref()) {
3841 pane.activate_item(index, false, false, cx);
3842 } else {
3843 pane.add_item(item.boxed_clone(), false, false, None, cx)
3844 }
3845
3846 if focus_active_item {
3847 pane.focus_active_item(cx)
3848 }
3849 });
3850
3851 None
3852 }
3853
3854 fn shared_screen_for_peer(
3855 &self,
3856 peer_id: PeerId,
3857 pane: &View<Pane>,
3858 cx: &mut WindowContext,
3859 ) -> Option<View<SharedScreen>> {
3860 let call = self.active_call()?;
3861 let room = call.read(cx).room()?.read(cx);
3862 let participant = room.remote_participant_for_peer_id(peer_id)?;
3863 let track = participant.video_tracks.values().next()?.clone();
3864 let user = participant.user.clone();
3865
3866 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3867 if item.read(cx).peer_id == peer_id {
3868 return Some(item);
3869 }
3870 }
3871
3872 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3873 }
3874
3875 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3876 if cx.is_window_active() {
3877 self.update_active_view_for_followers(cx);
3878
3879 if let Some(database_id) = self.database_id {
3880 cx.background_executor()
3881 .spawn(persistence::DB.update_timestamp(database_id))
3882 .detach();
3883 }
3884 } else {
3885 for pane in &self.panes {
3886 pane.update(cx, |pane, cx| {
3887 if let Some(item) = pane.active_item() {
3888 item.workspace_deactivated(cx);
3889 }
3890 for item in pane.items() {
3891 if matches!(
3892 item.workspace_settings(cx).autosave,
3893 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3894 ) {
3895 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3896 .detach_and_log_err(cx);
3897 }
3898 }
3899 });
3900 }
3901 }
3902 }
3903
3904 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3905 self.active_call.as_ref().map(|(call, _)| call)
3906 }
3907
3908 fn on_active_call_event(
3909 &mut self,
3910 _: Model<ActiveCall>,
3911 event: &call::room::Event,
3912 cx: &mut ViewContext<Self>,
3913 ) {
3914 match event {
3915 call::room::Event::ParticipantLocationChanged { participant_id }
3916 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3917 self.leader_updated(*participant_id, cx);
3918 }
3919 _ => {}
3920 }
3921 }
3922
3923 pub fn database_id(&self) -> Option<WorkspaceId> {
3924 self.database_id
3925 }
3926
3927 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
3928 let project = self.project().read(cx);
3929
3930 if project.is_local_or_ssh() {
3931 Some(
3932 project
3933 .visible_worktrees(cx)
3934 .map(|worktree| worktree.read(cx).abs_path())
3935 .collect::<Vec<_>>(),
3936 )
3937 } else {
3938 None
3939 }
3940 }
3941
3942 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3943 match member {
3944 Member::Axis(PaneAxis { members, .. }) => {
3945 for child in members.iter() {
3946 self.remove_panes(child.clone(), cx)
3947 }
3948 }
3949 Member::Pane(pane) => {
3950 self.force_remove_pane(&pane, &None, cx);
3951 }
3952 }
3953 }
3954
3955 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
3956 self.session_id.take();
3957 self.serialize_workspace_internal(cx)
3958 }
3959
3960 fn force_remove_pane(
3961 &mut self,
3962 pane: &View<Pane>,
3963 focus_on: &Option<View<Pane>>,
3964 cx: &mut ViewContext<Workspace>,
3965 ) {
3966 self.panes.retain(|p| p != pane);
3967 if let Some(focus_on) = focus_on {
3968 focus_on.update(cx, |pane, cx| pane.focus(cx));
3969 } else {
3970 self.panes
3971 .last()
3972 .unwrap()
3973 .update(cx, |pane, cx| pane.focus(cx));
3974 }
3975 if self.last_active_center_pane == Some(pane.downgrade()) {
3976 self.last_active_center_pane = None;
3977 }
3978 cx.notify();
3979 }
3980
3981 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
3982 if self._schedule_serialize.is_none() {
3983 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3984 cx.background_executor()
3985 .timer(Duration::from_millis(100))
3986 .await;
3987 this.update(&mut cx, |this, cx| {
3988 this.serialize_workspace_internal(cx).detach();
3989 this._schedule_serialize.take();
3990 })
3991 .log_err();
3992 }));
3993 }
3994 }
3995
3996 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
3997 let Some(database_id) = self.database_id() else {
3998 return Task::ready(());
3999 };
4000
4001 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4002 let (items, active, pinned_count) = {
4003 let pane = pane_handle.read(cx);
4004 let active_item_id = pane.active_item().map(|item| item.item_id());
4005 (
4006 pane.items()
4007 .filter_map(|handle| {
4008 let handle = handle.to_serializable_item_handle(cx)?;
4009
4010 Some(SerializedItem {
4011 kind: Arc::from(handle.serialized_item_kind()),
4012 item_id: handle.item_id().as_u64(),
4013 active: Some(handle.item_id()) == active_item_id,
4014 preview: pane.is_active_preview_item(handle.item_id()),
4015 })
4016 })
4017 .collect::<Vec<_>>(),
4018 pane.has_focus(cx),
4019 pane.pinned_count(),
4020 )
4021 };
4022
4023 SerializedPane::new(items, active, pinned_count)
4024 }
4025
4026 fn build_serialized_pane_group(
4027 pane_group: &Member,
4028 cx: &WindowContext,
4029 ) -> SerializedPaneGroup {
4030 match pane_group {
4031 Member::Axis(PaneAxis {
4032 axis,
4033 members,
4034 flexes,
4035 bounding_boxes: _,
4036 }) => SerializedPaneGroup::Group {
4037 axis: SerializedAxis(*axis),
4038 children: members
4039 .iter()
4040 .map(|member| build_serialized_pane_group(member, cx))
4041 .collect::<Vec<_>>(),
4042 flexes: Some(flexes.lock().clone()),
4043 },
4044 Member::Pane(pane_handle) => {
4045 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4046 }
4047 }
4048 }
4049
4050 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4051 let left_dock = this.left_dock.read(cx);
4052 let left_visible = left_dock.is_open();
4053 let left_active_panel = left_dock
4054 .visible_panel()
4055 .map(|panel| panel.persistent_name().to_string());
4056 let left_dock_zoom = left_dock
4057 .visible_panel()
4058 .map(|panel| panel.is_zoomed(cx))
4059 .unwrap_or(false);
4060
4061 let right_dock = this.right_dock.read(cx);
4062 let right_visible = right_dock.is_open();
4063 let right_active_panel = right_dock
4064 .visible_panel()
4065 .map(|panel| panel.persistent_name().to_string());
4066 let right_dock_zoom = right_dock
4067 .visible_panel()
4068 .map(|panel| panel.is_zoomed(cx))
4069 .unwrap_or(false);
4070
4071 let bottom_dock = this.bottom_dock.read(cx);
4072 let bottom_visible = bottom_dock.is_open();
4073 let bottom_active_panel = bottom_dock
4074 .visible_panel()
4075 .map(|panel| panel.persistent_name().to_string());
4076 let bottom_dock_zoom = bottom_dock
4077 .visible_panel()
4078 .map(|panel| panel.is_zoomed(cx))
4079 .unwrap_or(false);
4080
4081 DockStructure {
4082 left: DockData {
4083 visible: left_visible,
4084 active_panel: left_active_panel,
4085 zoom: left_dock_zoom,
4086 },
4087 right: DockData {
4088 visible: right_visible,
4089 active_panel: right_active_panel,
4090 zoom: right_dock_zoom,
4091 },
4092 bottom: DockData {
4093 visible: bottom_visible,
4094 active_panel: bottom_active_panel,
4095 zoom: bottom_dock_zoom,
4096 },
4097 }
4098 }
4099
4100 let location = if let Some(local_paths) = self.local_paths(cx) {
4101 if !local_paths.is_empty() {
4102 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4103 } else {
4104 None
4105 }
4106 } else if let Some(dev_server_project_id) = self.project().read(cx).dev_server_project_id()
4107 {
4108 let store = dev_server_projects::Store::global(cx).read(cx);
4109 maybe!({
4110 let project = store.dev_server_project(dev_server_project_id)?;
4111 let dev_server = store.dev_server(project.dev_server_id)?;
4112
4113 let dev_server_project = SerializedDevServerProject {
4114 id: dev_server_project_id,
4115 dev_server_name: dev_server.name.to_string(),
4116 paths: project.paths.to_vec(),
4117 };
4118 Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
4119 })
4120 } else {
4121 None
4122 };
4123
4124 if let Some(location) = location {
4125 let center_group = build_serialized_pane_group(&self.center.root, cx);
4126 let docks = build_serialized_docks(self, cx);
4127 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4128 let serialized_workspace = SerializedWorkspace {
4129 id: database_id,
4130 location,
4131 center_group,
4132 window_bounds,
4133 display: Default::default(),
4134 docks,
4135 centered_layout: self.centered_layout,
4136 session_id: self.session_id.clone(),
4137 window_id: Some(cx.window_handle().window_id().as_u64()),
4138 };
4139 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4140 }
4141 Task::ready(())
4142 }
4143
4144 async fn serialize_items(
4145 this: &WeakView<Self>,
4146 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4147 cx: &mut AsyncWindowContext,
4148 ) -> Result<()> {
4149 const CHUNK_SIZE: usize = 200;
4150 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4151
4152 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4153
4154 while let Some(items_received) = serializable_items.next().await {
4155 let unique_items =
4156 items_received
4157 .into_iter()
4158 .fold(HashMap::default(), |mut acc, item| {
4159 acc.entry(item.item_id()).or_insert(item);
4160 acc
4161 });
4162
4163 // We use into_iter() here so that the references to the items are moved into
4164 // the tasks and not kept alive while we're sleeping.
4165 for (_, item) in unique_items.into_iter() {
4166 if let Ok(Some(task)) =
4167 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4168 {
4169 cx.background_executor()
4170 .spawn(async move { task.await.log_err() })
4171 .detach();
4172 }
4173 }
4174
4175 cx.background_executor().timer(THROTTLE_TIME).await;
4176 }
4177
4178 Ok(())
4179 }
4180
4181 pub(crate) fn enqueue_item_serialization(
4182 &mut self,
4183 item: Box<dyn SerializableItemHandle>,
4184 ) -> Result<()> {
4185 self.serializable_items_tx
4186 .unbounded_send(item)
4187 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4188 }
4189
4190 pub(crate) fn load_workspace(
4191 serialized_workspace: SerializedWorkspace,
4192 paths_to_open: Vec<Option<ProjectPath>>,
4193 cx: &mut ViewContext<Workspace>,
4194 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4195 cx.spawn(|workspace, mut cx| async move {
4196 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4197
4198 let mut center_group = None;
4199 let mut center_items = None;
4200
4201 // Traverse the splits tree and add to things
4202 if let Some((group, active_pane, items)) = serialized_workspace
4203 .center_group
4204 .deserialize(
4205 &project,
4206 serialized_workspace.id,
4207 workspace.clone(),
4208 &mut cx,
4209 )
4210 .await
4211 {
4212 center_items = Some(items);
4213 center_group = Some((group, active_pane))
4214 }
4215
4216 let mut items_by_project_path = HashMap::default();
4217 let mut item_ids_by_kind = HashMap::default();
4218 let mut all_deserialized_items = Vec::default();
4219 cx.update(|cx| {
4220 for item in center_items.unwrap_or_default().into_iter().flatten() {
4221 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4222 item_ids_by_kind
4223 .entry(serializable_item_handle.serialized_item_kind())
4224 .or_insert(Vec::new())
4225 .push(item.item_id().as_u64() as ItemId);
4226 }
4227
4228 if let Some(project_path) = item.project_path(cx) {
4229 items_by_project_path.insert(project_path, item.clone());
4230 }
4231 all_deserialized_items.push(item);
4232 }
4233 })?;
4234
4235 let opened_items = paths_to_open
4236 .into_iter()
4237 .map(|path_to_open| {
4238 path_to_open
4239 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4240 })
4241 .collect::<Vec<_>>();
4242
4243 // Remove old panes from workspace panes list
4244 workspace.update(&mut cx, |workspace, cx| {
4245 if let Some((center_group, active_pane)) = center_group {
4246 workspace.remove_panes(workspace.center.root.clone(), cx);
4247
4248 // Swap workspace center group
4249 workspace.center = PaneGroup::with_root(center_group);
4250 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
4251 if let Some(active_pane) = active_pane {
4252 workspace.active_pane = active_pane;
4253 cx.focus_self();
4254 } else {
4255 workspace.active_pane = workspace.center.first_pane().clone();
4256 }
4257 }
4258
4259 let docks = serialized_workspace.docks;
4260
4261 for (dock, serialized_dock) in [
4262 (&mut workspace.right_dock, docks.right),
4263 (&mut workspace.left_dock, docks.left),
4264 (&mut workspace.bottom_dock, docks.bottom),
4265 ]
4266 .iter_mut()
4267 {
4268 dock.update(cx, |dock, cx| {
4269 dock.serialized_dock = Some(serialized_dock.clone());
4270 dock.restore_state(cx);
4271 });
4272 }
4273
4274 cx.notify();
4275 })?;
4276
4277 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4278 // after loading the items, we might have different items and in order to avoid
4279 // the database filling up, we delete items that haven't been loaded now.
4280 //
4281 // The items that have been loaded, have been saved after they've been added to the workspace.
4282 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4283 item_ids_by_kind
4284 .into_iter()
4285 .map(|(item_kind, loaded_items)| {
4286 SerializableItemRegistry::cleanup(
4287 item_kind,
4288 serialized_workspace.id,
4289 loaded_items,
4290 cx,
4291 )
4292 .log_err()
4293 })
4294 .collect::<Vec<_>>()
4295 })?;
4296
4297 futures::future::join_all(clean_up_tasks).await;
4298
4299 workspace
4300 .update(&mut cx, |workspace, cx| {
4301 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4302 workspace.serialize_workspace_internal(cx).detach();
4303
4304 // Ensure that we mark the window as edited if we did load dirty items
4305 workspace.update_window_edited(cx);
4306 })
4307 .ok();
4308
4309 Ok(opened_items)
4310 })
4311 }
4312
4313 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4314 self.add_workspace_actions_listeners(div, cx)
4315 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4316 .on_action(cx.listener(Self::close_all_items_and_panes))
4317 .on_action(cx.listener(Self::save_all))
4318 .on_action(cx.listener(Self::send_keystrokes))
4319 .on_action(cx.listener(Self::add_folder_to_project))
4320 .on_action(cx.listener(Self::follow_next_collaborator))
4321 .on_action(cx.listener(Self::open))
4322 .on_action(cx.listener(Self::close_window))
4323 .on_action(cx.listener(Self::activate_pane_at_index))
4324 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4325 let pane = workspace.active_pane().clone();
4326 workspace.unfollow_in_pane(&pane, cx);
4327 }))
4328 .on_action(cx.listener(|workspace, action: &Save, cx| {
4329 workspace
4330 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4331 .detach_and_log_err(cx);
4332 }))
4333 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4334 workspace
4335 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4336 .detach_and_log_err(cx);
4337 }))
4338 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4339 workspace
4340 .save_active_item(SaveIntent::SaveAs, cx)
4341 .detach_and_log_err(cx);
4342 }))
4343 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4344 workspace.activate_previous_pane(cx)
4345 }))
4346 .on_action(
4347 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4348 )
4349 .on_action(
4350 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4351 workspace.activate_pane_in_direction(action.0, cx)
4352 }),
4353 )
4354 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4355 workspace.swap_pane_in_direction(action.0, cx)
4356 }))
4357 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4358 this.toggle_dock(DockPosition::Left, cx);
4359 }))
4360 .on_action(
4361 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4362 workspace.toggle_dock(DockPosition::Right, cx);
4363 }),
4364 )
4365 .on_action(
4366 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4367 workspace.toggle_dock(DockPosition::Bottom, cx);
4368 }),
4369 )
4370 .on_action(
4371 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4372 workspace.close_all_docks(cx);
4373 }),
4374 )
4375 .on_action(
4376 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4377 workspace.clear_all_notifications(cx);
4378 }),
4379 )
4380 .on_action(
4381 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4382 workspace.reopen_closed_item(cx).detach();
4383 }),
4384 )
4385 .on_action(cx.listener(Workspace::toggle_centered_layout))
4386 }
4387
4388 #[cfg(any(test, feature = "test-support"))]
4389 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4390 use node_runtime::FakeNodeRuntime;
4391 use session::Session;
4392
4393 let client = project.read(cx).client();
4394 let user_store = project.read(cx).user_store();
4395
4396 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4397 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4398 cx.activate_window();
4399 let app_state = Arc::new(AppState {
4400 languages: project.read(cx).languages().clone(),
4401 workspace_store,
4402 client,
4403 user_store,
4404 fs: project.read(cx).fs().clone(),
4405 build_window_options: |_, _| Default::default(),
4406 node_runtime: FakeNodeRuntime::new(),
4407 session,
4408 });
4409 let workspace = Self::new(Default::default(), project, app_state, cx);
4410 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4411 workspace
4412 }
4413
4414 pub fn register_action<A: Action>(
4415 &mut self,
4416 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4417 ) -> &mut Self {
4418 let callback = Arc::new(callback);
4419
4420 self.workspace_actions.push(Box::new(move |div, cx| {
4421 let callback = callback.clone();
4422 div.on_action(
4423 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4424 )
4425 }));
4426 self
4427 }
4428
4429 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4430 for action in self.workspace_actions.iter() {
4431 div = (action)(div, cx)
4432 }
4433 div
4434 }
4435
4436 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4437 self.modal_layer.read(cx).has_active_modal()
4438 }
4439
4440 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
4441 self.modal_layer.read(cx).active_modal()
4442 }
4443
4444 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4445 where
4446 B: FnOnce(&mut ViewContext<V>) -> V,
4447 {
4448 self.modal_layer
4449 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4450 }
4451
4452 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4453 self.centered_layout = !self.centered_layout;
4454 if let Some(database_id) = self.database_id() {
4455 cx.background_executor()
4456 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4457 .detach_and_log_err(cx);
4458 }
4459 cx.notify();
4460 }
4461
4462 fn adjust_padding(padding: Option<f32>) -> f32 {
4463 padding
4464 .unwrap_or(Self::DEFAULT_PADDING)
4465 .clamp(0.0, Self::MAX_PADDING)
4466 }
4467
4468 fn render_dock(
4469 &self,
4470 position: DockPosition,
4471 dock: &View<Dock>,
4472 cx: &WindowContext,
4473 ) -> Option<Div> {
4474 if self.zoomed_position == Some(position) {
4475 return None;
4476 }
4477
4478 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4479 let pane = panel.pane(cx)?;
4480 let follower_states = &self.follower_states;
4481 leader_border_for_pane(follower_states, &pane, cx)
4482 });
4483
4484 Some(
4485 div()
4486 .flex()
4487 .flex_none()
4488 .overflow_hidden()
4489 .child(dock.clone())
4490 .children(leader_border),
4491 )
4492 }
4493}
4494
4495fn leader_border_for_pane(
4496 follower_states: &HashMap<PeerId, FollowerState>,
4497 pane: &View<Pane>,
4498 cx: &WindowContext,
4499) -> Option<Div> {
4500 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4501 if state.pane() == pane {
4502 Some((*leader_id, state))
4503 } else {
4504 None
4505 }
4506 })?;
4507
4508 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4509 let leader = room.remote_participant_for_peer_id(leader_id)?;
4510
4511 let mut leader_color = cx
4512 .theme()
4513 .players()
4514 .color_for_participant(leader.participant_index.0)
4515 .cursor;
4516 leader_color.fade_out(0.3);
4517 Some(
4518 div()
4519 .absolute()
4520 .size_full()
4521 .left_0()
4522 .top_0()
4523 .border_2()
4524 .border_color(leader_color),
4525 )
4526}
4527
4528fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4529 ZED_WINDOW_POSITION
4530 .zip(*ZED_WINDOW_SIZE)
4531 .map(|(position, size)| Bounds {
4532 origin: position,
4533 size,
4534 })
4535}
4536
4537fn open_items(
4538 serialized_workspace: Option<SerializedWorkspace>,
4539 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4540 app_state: Arc<AppState>,
4541 cx: &mut ViewContext<Workspace>,
4542) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4543 let restored_items = serialized_workspace.map(|serialized_workspace| {
4544 Workspace::load_workspace(
4545 serialized_workspace,
4546 project_paths_to_open
4547 .iter()
4548 .map(|(_, project_path)| project_path)
4549 .cloned()
4550 .collect(),
4551 cx,
4552 )
4553 });
4554
4555 cx.spawn(|workspace, mut cx| async move {
4556 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4557
4558 if let Some(restored_items) = restored_items {
4559 let restored_items = restored_items.await?;
4560
4561 let restored_project_paths = restored_items
4562 .iter()
4563 .filter_map(|item| {
4564 cx.update(|cx| item.as_ref()?.project_path(cx))
4565 .ok()
4566 .flatten()
4567 })
4568 .collect::<HashSet<_>>();
4569
4570 for restored_item in restored_items {
4571 opened_items.push(restored_item.map(Ok));
4572 }
4573
4574 project_paths_to_open
4575 .iter_mut()
4576 .for_each(|(_, project_path)| {
4577 if let Some(project_path_to_open) = project_path {
4578 if restored_project_paths.contains(project_path_to_open) {
4579 *project_path = None;
4580 }
4581 }
4582 });
4583 } else {
4584 for _ in 0..project_paths_to_open.len() {
4585 opened_items.push(None);
4586 }
4587 }
4588 assert!(opened_items.len() == project_paths_to_open.len());
4589
4590 let tasks =
4591 project_paths_to_open
4592 .into_iter()
4593 .enumerate()
4594 .map(|(ix, (abs_path, project_path))| {
4595 let workspace = workspace.clone();
4596 cx.spawn(|mut cx| {
4597 let fs = app_state.fs.clone();
4598 async move {
4599 let file_project_path = project_path?;
4600 if fs.is_dir(&abs_path).await {
4601 None
4602 } else {
4603 Some((
4604 ix,
4605 workspace
4606 .update(&mut cx, |workspace, cx| {
4607 workspace.open_path(file_project_path, None, true, cx)
4608 })
4609 .log_err()?
4610 .await,
4611 ))
4612 }
4613 }
4614 })
4615 });
4616
4617 let tasks = tasks.collect::<Vec<_>>();
4618
4619 let tasks = futures::future::join_all(tasks);
4620 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4621 opened_items[ix] = Some(path_open_result);
4622 }
4623
4624 Ok(opened_items)
4625 })
4626}
4627
4628enum ActivateInDirectionTarget {
4629 Pane(View<Pane>),
4630 Dock(View<Dock>),
4631}
4632
4633fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4634 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4635
4636 workspace
4637 .update(cx, |workspace, cx| {
4638 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4639 struct DatabaseFailedNotification;
4640
4641 workspace.show_notification_once(
4642 NotificationId::unique::<DatabaseFailedNotification>(),
4643 cx,
4644 |cx| {
4645 cx.new_view(|_| {
4646 MessageNotification::new("Failed to load the database file.")
4647 .with_click_message("File an issue")
4648 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4649 })
4650 },
4651 );
4652 }
4653 })
4654 .log_err();
4655}
4656
4657impl FocusableView for Workspace {
4658 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4659 self.active_pane.focus_handle(cx)
4660 }
4661}
4662
4663#[derive(Clone, Render)]
4664struct DraggedDock(DockPosition);
4665
4666impl Render for Workspace {
4667 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4668 let mut context = KeyContext::new_with_defaults();
4669 context.add("Workspace");
4670 let centered_layout = self.centered_layout
4671 && self.center.panes().len() == 1
4672 && self.active_item(cx).is_some();
4673 let render_padding = |size| {
4674 (size > 0.0).then(|| {
4675 div()
4676 .h_full()
4677 .w(relative(size))
4678 .bg(cx.theme().colors().editor_background)
4679 .border_color(cx.theme().colors().pane_group_border)
4680 })
4681 };
4682 let paddings = if centered_layout {
4683 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4684 (
4685 render_padding(Self::adjust_padding(settings.left_padding)),
4686 render_padding(Self::adjust_padding(settings.right_padding)),
4687 )
4688 } else {
4689 (None, None)
4690 };
4691 let ui_font = theme::setup_ui_font(cx);
4692
4693 let theme = cx.theme().clone();
4694 let colors = theme.colors();
4695
4696 client_side_decorations(
4697 self.actions(div(), cx)
4698 .key_context(context)
4699 .relative()
4700 .size_full()
4701 .flex()
4702 .flex_col()
4703 .font(ui_font)
4704 .gap_0()
4705 .justify_start()
4706 .items_start()
4707 .text_color(colors.text)
4708 .overflow_hidden()
4709 .children(self.titlebar_item.clone())
4710 .child(
4711 div()
4712 .id("workspace")
4713 .bg(colors.background)
4714 .relative()
4715 .flex_1()
4716 .w_full()
4717 .flex()
4718 .flex_col()
4719 .overflow_hidden()
4720 .border_t_1()
4721 .border_b_1()
4722 .border_color(colors.border)
4723 .child({
4724 let this = cx.view().clone();
4725 canvas(
4726 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4727 |_, _, _| {},
4728 )
4729 .absolute()
4730 .size_full()
4731 })
4732 .when(self.zoomed.is_none(), |this| {
4733 this.on_drag_move(cx.listener(
4734 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4735 DockPosition::Left => {
4736 let size = e.event.position.x - workspace.bounds.left();
4737 workspace.left_dock.update(cx, |left_dock, cx| {
4738 left_dock.resize_active_panel(Some(size), cx);
4739 });
4740 }
4741 DockPosition::Right => {
4742 let size = workspace.bounds.right() - e.event.position.x;
4743 workspace.right_dock.update(cx, |right_dock, cx| {
4744 right_dock.resize_active_panel(Some(size), cx);
4745 });
4746 }
4747 DockPosition::Bottom => {
4748 let size = workspace.bounds.bottom() - e.event.position.y;
4749 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4750 bottom_dock.resize_active_panel(Some(size), cx);
4751 });
4752 }
4753 },
4754 ))
4755 })
4756 .child(
4757 div()
4758 .flex()
4759 .flex_row()
4760 .h_full()
4761 // Left Dock
4762 .children(self.render_dock(DockPosition::Left, &self.left_dock, cx))
4763 // Panes
4764 .child(
4765 div()
4766 .flex()
4767 .flex_col()
4768 .flex_1()
4769 .overflow_hidden()
4770 .child(
4771 h_flex()
4772 .flex_1()
4773 .when_some(paddings.0, |this, p| {
4774 this.child(p.border_r_1())
4775 })
4776 .child(self.center.render(
4777 &self.project,
4778 &self.follower_states,
4779 self.active_call(),
4780 &self.active_pane,
4781 self.zoomed.as_ref(),
4782 &self.app_state,
4783 cx,
4784 ))
4785 .when_some(paddings.1, |this, p| {
4786 this.child(p.border_l_1())
4787 }),
4788 )
4789 .children(self.render_dock(
4790 DockPosition::Bottom,
4791 &self.bottom_dock,
4792 cx,
4793 )),
4794 )
4795 // Right Dock
4796 .children(self.render_dock(
4797 DockPosition::Right,
4798 &self.right_dock,
4799 cx,
4800 )),
4801 )
4802 .children(self.zoomed.as_ref().and_then(|view| {
4803 let zoomed_view = view.upgrade()?;
4804 let div = div()
4805 .occlude()
4806 .absolute()
4807 .overflow_hidden()
4808 .border_color(colors.border)
4809 .bg(colors.background)
4810 .child(zoomed_view)
4811 .inset_0()
4812 .shadow_lg();
4813
4814 Some(match self.zoomed_position {
4815 Some(DockPosition::Left) => div.right_2().border_r_1(),
4816 Some(DockPosition::Right) => div.left_2().border_l_1(),
4817 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4818 None => div.top_2().bottom_2().left_2().right_2().border_1(),
4819 })
4820 }))
4821 .child(self.modal_layer.clone())
4822 .children(self.render_notifications(cx)),
4823 )
4824 .child(self.status_bar.clone())
4825 .children(if self.project.read(cx).is_disconnected() {
4826 if let Some(render) = self.render_disconnected_overlay.take() {
4827 let result = render(self, cx);
4828 self.render_disconnected_overlay = Some(render);
4829 Some(result)
4830 } else {
4831 None
4832 }
4833 } else {
4834 None
4835 }),
4836 cx,
4837 )
4838 }
4839}
4840
4841impl WorkspaceStore {
4842 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4843 Self {
4844 workspaces: Default::default(),
4845 _subscriptions: vec![
4846 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4847 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4848 ],
4849 client,
4850 }
4851 }
4852
4853 pub fn update_followers(
4854 &self,
4855 project_id: Option<u64>,
4856 update: proto::update_followers::Variant,
4857 cx: &AppContext,
4858 ) -> Option<()> {
4859 let active_call = ActiveCall::try_global(cx)?;
4860 let room_id = active_call.read(cx).room()?.read(cx).id();
4861 self.client
4862 .send(proto::UpdateFollowers {
4863 room_id,
4864 project_id,
4865 variant: Some(update),
4866 })
4867 .log_err()
4868 }
4869
4870 pub async fn handle_follow(
4871 this: Model<Self>,
4872 envelope: TypedEnvelope<proto::Follow>,
4873 mut cx: AsyncAppContext,
4874 ) -> Result<proto::FollowResponse> {
4875 this.update(&mut cx, |this, cx| {
4876 let follower = Follower {
4877 project_id: envelope.payload.project_id,
4878 peer_id: envelope.original_sender_id()?,
4879 };
4880
4881 let mut response = proto::FollowResponse::default();
4882 this.workspaces.retain(|workspace| {
4883 workspace
4884 .update(cx, |workspace, cx| {
4885 let handler_response = workspace.handle_follow(follower.project_id, cx);
4886 if let Some(active_view) = handler_response.active_view.clone() {
4887 if workspace.project.read(cx).remote_id() == follower.project_id {
4888 response.active_view = Some(active_view)
4889 }
4890 }
4891 })
4892 .is_ok()
4893 });
4894
4895 Ok(response)
4896 })?
4897 }
4898
4899 async fn handle_update_followers(
4900 this: Model<Self>,
4901 envelope: TypedEnvelope<proto::UpdateFollowers>,
4902 mut cx: AsyncAppContext,
4903 ) -> Result<()> {
4904 let leader_id = envelope.original_sender_id()?;
4905 let update = envelope.payload;
4906
4907 this.update(&mut cx, |this, cx| {
4908 this.workspaces.retain(|workspace| {
4909 workspace
4910 .update(cx, |workspace, cx| {
4911 let project_id = workspace.project.read(cx).remote_id();
4912 if update.project_id != project_id && update.project_id.is_some() {
4913 return;
4914 }
4915 workspace.handle_update_followers(leader_id, update.clone(), cx);
4916 })
4917 .is_ok()
4918 });
4919 Ok(())
4920 })?
4921 }
4922}
4923
4924impl ViewId {
4925 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4926 Ok(Self {
4927 creator: message
4928 .creator
4929 .ok_or_else(|| anyhow!("creator is missing"))?,
4930 id: message.id,
4931 })
4932 }
4933
4934 pub(crate) fn to_proto(self) -> proto::ViewId {
4935 proto::ViewId {
4936 creator: Some(self.creator),
4937 id: self.id,
4938 }
4939 }
4940}
4941
4942impl FollowerState {
4943 fn pane(&self) -> &View<Pane> {
4944 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
4945 }
4946}
4947
4948pub trait WorkspaceHandle {
4949 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4950}
4951
4952impl WorkspaceHandle for View<Workspace> {
4953 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4954 self.read(cx)
4955 .worktrees(cx)
4956 .flat_map(|worktree| {
4957 let worktree_id = worktree.read(cx).id();
4958 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4959 worktree_id,
4960 path: f.path.clone(),
4961 })
4962 })
4963 .collect::<Vec<_>>()
4964 }
4965}
4966
4967impl std::fmt::Debug for OpenPaths {
4968 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4969 f.debug_struct("OpenPaths")
4970 .field("paths", &self.paths)
4971 .finish()
4972 }
4973}
4974
4975pub fn activate_workspace_for_project(
4976 cx: &mut AppContext,
4977 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4978) -> Option<WindowHandle<Workspace>> {
4979 for window in cx.windows() {
4980 let Some(workspace) = window.downcast::<Workspace>() else {
4981 continue;
4982 };
4983
4984 let predicate = workspace
4985 .update(cx, |workspace, cx| {
4986 let project = workspace.project.read(cx);
4987 if predicate(project, cx) {
4988 cx.activate_window();
4989 true
4990 } else {
4991 false
4992 }
4993 })
4994 .log_err()
4995 .unwrap_or(false);
4996
4997 if predicate {
4998 return Some(workspace);
4999 }
5000 }
5001
5002 None
5003}
5004
5005pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
5006 DB.last_workspace().await.log_err().flatten()
5007}
5008
5009pub fn last_session_workspace_locations(
5010 last_session_id: &str,
5011 last_session_window_stack: Option<Vec<WindowId>>,
5012) -> Option<Vec<LocalPaths>> {
5013 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5014 .log_err()
5015}
5016
5017actions!(collab, [OpenChannelNotes]);
5018actions!(zed, [OpenLog]);
5019
5020async fn join_channel_internal(
5021 channel_id: ChannelId,
5022 app_state: &Arc<AppState>,
5023 requesting_window: Option<WindowHandle<Workspace>>,
5024 active_call: &Model<ActiveCall>,
5025 cx: &mut AsyncAppContext,
5026) -> Result<bool> {
5027 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5028 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5029 return (false, None);
5030 };
5031
5032 let already_in_channel = room.channel_id() == Some(channel_id);
5033 let should_prompt = room.is_sharing_project()
5034 && !room.remote_participants().is_empty()
5035 && !already_in_channel;
5036 let open_room = if already_in_channel {
5037 active_call.room().cloned()
5038 } else {
5039 None
5040 };
5041 (should_prompt, open_room)
5042 })?;
5043
5044 if let Some(room) = open_room {
5045 let task = room.update(cx, |room, cx| {
5046 if let Some((project, host)) = room.most_active_project(cx) {
5047 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5048 }
5049
5050 None
5051 })?;
5052 if let Some(task) = task {
5053 task.await?;
5054 }
5055 return anyhow::Ok(true);
5056 }
5057
5058 if should_prompt {
5059 if let Some(workspace) = requesting_window {
5060 let answer = workspace
5061 .update(cx, |_, cx| {
5062 cx.prompt(
5063 PromptLevel::Warning,
5064 "Do you want to switch channels?",
5065 Some("Leaving this call will unshare your current project."),
5066 &["Yes, Join Channel", "Cancel"],
5067 )
5068 })?
5069 .await;
5070
5071 if answer == Ok(1) {
5072 return Ok(false);
5073 }
5074 } else {
5075 return Ok(false); // unreachable!() hopefully
5076 }
5077 }
5078
5079 let client = cx.update(|cx| active_call.read(cx).client())?;
5080
5081 let mut client_status = client.status();
5082
5083 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5084 'outer: loop {
5085 let Some(status) = client_status.recv().await else {
5086 return Err(anyhow!("error connecting"));
5087 };
5088
5089 match status {
5090 Status::Connecting
5091 | Status::Authenticating
5092 | Status::Reconnecting
5093 | Status::Reauthenticating => continue,
5094 Status::Connected { .. } => break 'outer,
5095 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5096 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5097 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5098 return Err(ErrorCode::Disconnected.into());
5099 }
5100 }
5101 }
5102
5103 let room = active_call
5104 .update(cx, |active_call, cx| {
5105 active_call.join_channel(channel_id, cx)
5106 })?
5107 .await?;
5108
5109 let Some(room) = room else {
5110 return anyhow::Ok(true);
5111 };
5112
5113 room.update(cx, |room, _| room.room_update_completed())?
5114 .await;
5115
5116 let task = room.update(cx, |room, cx| {
5117 if let Some((project, host)) = room.most_active_project(cx) {
5118 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5119 }
5120
5121 // If you are the first to join a channel, see if you should share your project.
5122 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5123 if let Some(workspace) = requesting_window {
5124 let project = workspace.update(cx, |workspace, cx| {
5125 let project = workspace.project.read(cx);
5126 let is_dev_server = project.dev_server_project_id().is_some();
5127
5128 if !is_dev_server && !CallSettings::get_global(cx).share_on_join {
5129 return None;
5130 }
5131
5132 if (project.is_local_or_ssh() || is_dev_server)
5133 && project.visible_worktrees(cx).any(|tree| {
5134 tree.read(cx)
5135 .root_entry()
5136 .map_or(false, |entry| entry.is_dir())
5137 })
5138 {
5139 Some(workspace.project.clone())
5140 } else {
5141 None
5142 }
5143 });
5144 if let Ok(Some(project)) = project {
5145 return Some(cx.spawn(|room, mut cx| async move {
5146 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5147 .await?;
5148 Ok(())
5149 }));
5150 }
5151 }
5152 }
5153
5154 None
5155 })?;
5156 if let Some(task) = task {
5157 task.await?;
5158 return anyhow::Ok(true);
5159 }
5160 anyhow::Ok(false)
5161}
5162
5163pub fn join_channel(
5164 channel_id: ChannelId,
5165 app_state: Arc<AppState>,
5166 requesting_window: Option<WindowHandle<Workspace>>,
5167 cx: &mut AppContext,
5168) -> Task<Result<()>> {
5169 let active_call = ActiveCall::global(cx);
5170 cx.spawn(|mut cx| async move {
5171 let result = join_channel_internal(
5172 channel_id,
5173 &app_state,
5174 requesting_window,
5175 &active_call,
5176 &mut cx,
5177 )
5178 .await;
5179
5180 // join channel succeeded, and opened a window
5181 if matches!(result, Ok(true)) {
5182 return anyhow::Ok(());
5183 }
5184
5185 // find an existing workspace to focus and show call controls
5186 let mut active_window =
5187 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5188 if active_window.is_none() {
5189 // no open workspaces, make one to show the error in (blergh)
5190 let (window_handle, _) = cx
5191 .update(|cx| {
5192 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5193 })?
5194 .await?;
5195
5196 if result.is_ok() {
5197 cx.update(|cx| {
5198 cx.dispatch_action(&OpenChannelNotes);
5199 }).log_err();
5200 }
5201
5202 active_window = Some(window_handle);
5203 }
5204
5205 if let Err(err) = result {
5206 log::error!("failed to join channel: {}", err);
5207 if let Some(active_window) = active_window {
5208 active_window
5209 .update(&mut cx, |_, cx| {
5210 let detail: SharedString = match err.error_code() {
5211 ErrorCode::SignedOut => {
5212 "Please sign in to continue.".into()
5213 }
5214 ErrorCode::UpgradeRequired => {
5215 "Your are running an unsupported version of Zed. Please update to continue.".into()
5216 }
5217 ErrorCode::NoSuchChannel => {
5218 "No matching channel was found. Please check the link and try again.".into()
5219 }
5220 ErrorCode::Forbidden => {
5221 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5222 }
5223 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5224 _ => format!("{}\n\nPlease try again.", err).into(),
5225 };
5226 cx.prompt(
5227 PromptLevel::Critical,
5228 "Failed to join channel",
5229 Some(&detail),
5230 &["Ok"],
5231 )
5232 })?
5233 .await
5234 .ok();
5235 }
5236 }
5237
5238 // return ok, we showed the error to the user.
5239 anyhow::Ok(())
5240 })
5241}
5242
5243pub async fn get_any_active_workspace(
5244 app_state: Arc<AppState>,
5245 mut cx: AsyncAppContext,
5246) -> anyhow::Result<WindowHandle<Workspace>> {
5247 // find an existing workspace to focus and show call controls
5248 let active_window = activate_any_workspace_window(&mut cx);
5249 if active_window.is_none() {
5250 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5251 .await?;
5252 }
5253 activate_any_workspace_window(&mut cx).context("could not open zed")
5254}
5255
5256fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5257 cx.update(|cx| {
5258 if let Some(workspace_window) = cx
5259 .active_window()
5260 .and_then(|window| window.downcast::<Workspace>())
5261 {
5262 return Some(workspace_window);
5263 }
5264
5265 for window in cx.windows() {
5266 if let Some(workspace_window) = window.downcast::<Workspace>() {
5267 workspace_window
5268 .update(cx, |_, cx| cx.activate_window())
5269 .ok();
5270 return Some(workspace_window);
5271 }
5272 }
5273 None
5274 })
5275 .ok()
5276 .flatten()
5277}
5278
5279pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5280 cx.windows()
5281 .into_iter()
5282 .filter_map(|window| window.downcast::<Workspace>())
5283 .filter(|workspace| {
5284 workspace
5285 .read(cx)
5286 .is_ok_and(|workspace| workspace.project.read(cx).is_local_or_ssh())
5287 })
5288 .collect()
5289}
5290
5291#[derive(Default)]
5292pub struct OpenOptions {
5293 pub open_new_workspace: Option<bool>,
5294 pub replace_window: Option<WindowHandle<Workspace>>,
5295 pub env: Option<HashMap<String, String>>,
5296}
5297
5298#[allow(clippy::type_complexity)]
5299pub fn open_paths(
5300 abs_paths: &[PathBuf],
5301 app_state: Arc<AppState>,
5302 open_options: OpenOptions,
5303 cx: &mut AppContext,
5304) -> Task<
5305 anyhow::Result<(
5306 WindowHandle<Workspace>,
5307 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5308 )>,
5309> {
5310 let abs_paths = abs_paths.to_vec();
5311 let mut existing = None;
5312 let mut best_match = None;
5313 let mut open_visible = OpenVisible::All;
5314
5315 if open_options.open_new_workspace != Some(true) {
5316 for window in local_workspace_windows(cx) {
5317 if let Ok(workspace) = window.read(cx) {
5318 let m = workspace
5319 .project
5320 .read(cx)
5321 .visibility_for_paths(&abs_paths, cx);
5322 if m > best_match {
5323 existing = Some(window);
5324 best_match = m;
5325 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5326 existing = Some(window)
5327 }
5328 }
5329 }
5330 }
5331
5332 cx.spawn(move |mut cx| async move {
5333 if open_options.open_new_workspace.is_none() && existing.is_none() {
5334 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5335 if futures::future::join_all(all_files)
5336 .await
5337 .into_iter()
5338 .filter_map(|result| result.ok().flatten())
5339 .all(|file| !file.is_dir)
5340 {
5341 cx.update(|cx| {
5342 for window in local_workspace_windows(cx) {
5343 if let Ok(workspace) = window.read(cx) {
5344 let project = workspace.project().read(cx);
5345 if project.is_via_collab() {
5346 continue;
5347 }
5348 existing = Some(window);
5349 open_visible = OpenVisible::None;
5350 break;
5351 }
5352 }
5353 })?;
5354 }
5355 }
5356
5357 if let Some(existing) = existing {
5358 Ok((
5359 existing,
5360 existing
5361 .update(&mut cx, |workspace, cx| {
5362 cx.activate_window();
5363 workspace.open_paths(abs_paths, open_visible, None, cx)
5364 })?
5365 .await,
5366 ))
5367 } else {
5368 cx.update(move |cx| {
5369 Workspace::new_local(
5370 abs_paths,
5371 app_state.clone(),
5372 open_options.replace_window,
5373 open_options.env,
5374 cx,
5375 )
5376 })?
5377 .await
5378 }
5379 })
5380}
5381
5382pub fn open_new(
5383 open_options: OpenOptions,
5384 app_state: Arc<AppState>,
5385 cx: &mut AppContext,
5386 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5387) -> Task<anyhow::Result<()>> {
5388 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5389 cx.spawn(|mut cx| async move {
5390 let (workspace, opened_paths) = task.await?;
5391 workspace.update(&mut cx, |workspace, cx| {
5392 if opened_paths.is_empty() {
5393 init(workspace, cx)
5394 }
5395 })?;
5396 Ok(())
5397 })
5398}
5399
5400pub fn create_and_open_local_file(
5401 path: &'static Path,
5402 cx: &mut ViewContext<Workspace>,
5403 default_content: impl 'static + Send + FnOnce() -> Rope,
5404) -> Task<Result<Box<dyn ItemHandle>>> {
5405 cx.spawn(|workspace, mut cx| async move {
5406 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5407 if !fs.is_file(path).await {
5408 fs.create_file(path, Default::default()).await?;
5409 fs.save(path, &default_content(), Default::default())
5410 .await?;
5411 }
5412
5413 let mut items = workspace
5414 .update(&mut cx, |workspace, cx| {
5415 workspace.with_local_workspace(cx, |workspace, cx| {
5416 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5417 })
5418 })?
5419 .await?
5420 .await;
5421
5422 let item = items.pop().flatten();
5423 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5424 })
5425}
5426
5427pub fn join_hosted_project(
5428 hosted_project_id: ProjectId,
5429 app_state: Arc<AppState>,
5430 cx: &mut AppContext,
5431) -> Task<Result<()>> {
5432 cx.spawn(|mut cx| async move {
5433 let existing_window = cx.update(|cx| {
5434 cx.windows().into_iter().find_map(|window| {
5435 let workspace = window.downcast::<Workspace>()?;
5436 workspace
5437 .read(cx)
5438 .is_ok_and(|workspace| {
5439 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
5440 })
5441 .then_some(workspace)
5442 })
5443 })?;
5444
5445 let workspace = if let Some(existing_window) = existing_window {
5446 existing_window
5447 } else {
5448 let project = Project::hosted(
5449 hosted_project_id,
5450 app_state.user_store.clone(),
5451 app_state.client.clone(),
5452 app_state.languages.clone(),
5453 app_state.fs.clone(),
5454 cx.clone(),
5455 )
5456 .await?;
5457
5458 let window_bounds_override = window_bounds_env_override();
5459 cx.update(|cx| {
5460 let mut options = (app_state.build_window_options)(None, cx);
5461 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5462 cx.open_window(options, |cx| {
5463 cx.new_view(|cx| {
5464 Workspace::new(Default::default(), project, app_state.clone(), cx)
5465 })
5466 })
5467 })??
5468 };
5469
5470 workspace.update(&mut cx, |_, cx| {
5471 cx.activate(true);
5472 cx.activate_window();
5473 })?;
5474
5475 Ok(())
5476 })
5477}
5478
5479pub fn join_dev_server_project(
5480 dev_server_project_id: DevServerProjectId,
5481 project_id: ProjectId,
5482 app_state: Arc<AppState>,
5483 window_to_replace: Option<WindowHandle<Workspace>>,
5484 cx: &mut AppContext,
5485) -> Task<Result<WindowHandle<Workspace>>> {
5486 let windows = cx.windows();
5487 cx.spawn(|mut cx| async move {
5488 let existing_workspace = windows.into_iter().find_map(|window| {
5489 window.downcast::<Workspace>().and_then(|window| {
5490 window
5491 .update(&mut cx, |workspace, cx| {
5492 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
5493 Some(window)
5494 } else {
5495 None
5496 }
5497 })
5498 .unwrap_or(None)
5499 })
5500 });
5501
5502 let workspace = if let Some(existing_workspace) = existing_workspace {
5503 existing_workspace
5504 } else {
5505 let project = Project::remote(
5506 project_id.0,
5507 app_state.client.clone(),
5508 app_state.user_store.clone(),
5509 app_state.languages.clone(),
5510 app_state.fs.clone(),
5511 cx.clone(),
5512 )
5513 .await?;
5514
5515 let serialized_workspace: Option<SerializedWorkspace> =
5516 persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
5517
5518 let workspace_id = if let Some(serialized_workspace) = serialized_workspace {
5519 serialized_workspace.id
5520 } else {
5521 persistence::DB.next_id().await?
5522 };
5523
5524 if let Some(window_to_replace) = window_to_replace {
5525 cx.update_window(window_to_replace.into(), |_, cx| {
5526 cx.replace_root_view(|cx| {
5527 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5528 });
5529 })?;
5530 window_to_replace
5531 } else {
5532 let window_bounds_override = window_bounds_env_override();
5533 cx.update(|cx| {
5534 let mut options = (app_state.build_window_options)(None, cx);
5535 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5536 cx.open_window(options, |cx| {
5537 cx.new_view(|cx| {
5538 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5539 })
5540 })
5541 })??
5542 }
5543 };
5544
5545 workspace.update(&mut cx, |_, cx| {
5546 cx.activate(true);
5547 cx.activate_window();
5548 })?;
5549
5550 anyhow::Ok(workspace)
5551 })
5552}
5553
5554pub fn join_in_room_project(
5555 project_id: u64,
5556 follow_user_id: u64,
5557 app_state: Arc<AppState>,
5558 cx: &mut AppContext,
5559) -> Task<Result<()>> {
5560 let windows = cx.windows();
5561 cx.spawn(|mut cx| async move {
5562 let existing_workspace = windows.into_iter().find_map(|window| {
5563 window.downcast::<Workspace>().and_then(|window| {
5564 window
5565 .update(&mut cx, |workspace, cx| {
5566 if workspace.project().read(cx).remote_id() == Some(project_id) {
5567 Some(window)
5568 } else {
5569 None
5570 }
5571 })
5572 .unwrap_or(None)
5573 })
5574 });
5575
5576 let workspace = if let Some(existing_workspace) = existing_workspace {
5577 existing_workspace
5578 } else {
5579 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5580 let room = active_call
5581 .read_with(&cx, |call, _| call.room().cloned())?
5582 .ok_or_else(|| anyhow!("not in a call"))?;
5583 let project = room
5584 .update(&mut cx, |room, cx| {
5585 room.join_project(
5586 project_id,
5587 app_state.languages.clone(),
5588 app_state.fs.clone(),
5589 cx,
5590 )
5591 })?
5592 .await?;
5593
5594 let window_bounds_override = window_bounds_env_override();
5595 cx.update(|cx| {
5596 let mut options = (app_state.build_window_options)(None, cx);
5597 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5598 cx.open_window(options, |cx| {
5599 cx.new_view(|cx| {
5600 Workspace::new(Default::default(), project, app_state.clone(), cx)
5601 })
5602 })
5603 })??
5604 };
5605
5606 workspace.update(&mut cx, |workspace, cx| {
5607 cx.activate(true);
5608 cx.activate_window();
5609
5610 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5611 let follow_peer_id = room
5612 .read(cx)
5613 .remote_participants()
5614 .iter()
5615 .find(|(_, participant)| participant.user.id == follow_user_id)
5616 .map(|(_, p)| p.peer_id)
5617 .or_else(|| {
5618 // If we couldn't follow the given user, follow the host instead.
5619 let collaborator = workspace
5620 .project()
5621 .read(cx)
5622 .collaborators()
5623 .values()
5624 .find(|collaborator| collaborator.replica_id == 0)?;
5625 Some(collaborator.peer_id)
5626 });
5627
5628 if let Some(follow_peer_id) = follow_peer_id {
5629 workspace.follow(follow_peer_id, cx);
5630 }
5631 }
5632 })?;
5633
5634 anyhow::Ok(())
5635 })
5636}
5637
5638pub fn reload(reload: &Reload, cx: &mut AppContext) {
5639 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5640 let mut workspace_windows = cx
5641 .windows()
5642 .into_iter()
5643 .filter_map(|window| window.downcast::<Workspace>())
5644 .collect::<Vec<_>>();
5645
5646 // If multiple windows have unsaved changes, and need a save prompt,
5647 // prompt in the active window before switching to a different window.
5648 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5649
5650 let mut prompt = None;
5651 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5652 prompt = window
5653 .update(cx, |_, cx| {
5654 cx.prompt(
5655 PromptLevel::Info,
5656 "Are you sure you want to restart?",
5657 None,
5658 &["Restart", "Cancel"],
5659 )
5660 })
5661 .ok();
5662 }
5663
5664 let binary_path = reload.binary_path.clone();
5665 cx.spawn(|mut cx| async move {
5666 if let Some(prompt) = prompt {
5667 let answer = prompt.await?;
5668 if answer != 0 {
5669 return Ok(());
5670 }
5671 }
5672
5673 // If the user cancels any save prompt, then keep the app open.
5674 for window in workspace_windows {
5675 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5676 workspace.prepare_to_close(CloseIntent::Quit, cx)
5677 }) {
5678 if !should_close.await? {
5679 return Ok(());
5680 }
5681 }
5682 }
5683
5684 cx.update(|cx| cx.restart(binary_path))
5685 })
5686 .detach_and_log_err(cx);
5687}
5688
5689fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5690 let mut parts = value.split(',');
5691 let x: usize = parts.next()?.parse().ok()?;
5692 let y: usize = parts.next()?.parse().ok()?;
5693 Some(point(px(x as f32), px(y as f32)))
5694}
5695
5696fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5697 let mut parts = value.split(',');
5698 let width: usize = parts.next()?.parse().ok()?;
5699 let height: usize = parts.next()?.parse().ok()?;
5700 Some(size(px(width as f32), px(height as f32)))
5701}
5702
5703pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5704 const BORDER_SIZE: Pixels = px(1.0);
5705 let decorations = cx.window_decorations();
5706
5707 if matches!(decorations, Decorations::Client { .. }) {
5708 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5709 }
5710
5711 struct GlobalResizeEdge(ResizeEdge);
5712 impl Global for GlobalResizeEdge {}
5713
5714 div()
5715 .id("window-backdrop")
5716 .bg(transparent_black())
5717 .map(|div| match decorations {
5718 Decorations::Server => div,
5719 Decorations::Client { tiling, .. } => div
5720 .when(!(tiling.top || tiling.right), |div| {
5721 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5722 })
5723 .when(!(tiling.top || tiling.left), |div| {
5724 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5725 })
5726 .when(!(tiling.bottom || tiling.right), |div| {
5727 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5728 })
5729 .when(!(tiling.bottom || tiling.left), |div| {
5730 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5731 })
5732 .when(!tiling.top, |div| {
5733 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5734 })
5735 .when(!tiling.bottom, |div| {
5736 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5737 })
5738 .when(!tiling.left, |div| {
5739 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5740 })
5741 .when(!tiling.right, |div| {
5742 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5743 })
5744 .on_mouse_move(move |e, cx| {
5745 let size = cx.window_bounds().get_bounds().size;
5746 let pos = e.position;
5747
5748 let new_edge =
5749 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5750
5751 let edge = cx.try_global::<GlobalResizeEdge>();
5752 if new_edge != edge.map(|edge| edge.0) {
5753 cx.window_handle()
5754 .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5755 .ok();
5756 }
5757 })
5758 .on_mouse_down(MouseButton::Left, move |e, cx| {
5759 let size = cx.window_bounds().get_bounds().size;
5760 let pos = e.position;
5761
5762 let edge = match resize_edge(
5763 pos,
5764 theme::CLIENT_SIDE_DECORATION_SHADOW,
5765 size,
5766 tiling,
5767 ) {
5768 Some(value) => value,
5769 None => return,
5770 };
5771
5772 cx.start_window_resize(edge);
5773 }),
5774 })
5775 .size_full()
5776 .child(
5777 div()
5778 .cursor(CursorStyle::Arrow)
5779 .map(|div| match decorations {
5780 Decorations::Server => div,
5781 Decorations::Client { tiling } => div
5782 .border_color(cx.theme().colors().border)
5783 .when(!(tiling.top || tiling.right), |div| {
5784 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5785 })
5786 .when(!(tiling.top || tiling.left), |div| {
5787 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5788 })
5789 .when(!(tiling.bottom || tiling.right), |div| {
5790 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5791 })
5792 .when(!(tiling.bottom || tiling.left), |div| {
5793 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5794 })
5795 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5796 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5797 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5798 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5799 .when(!tiling.is_tiled(), |div| {
5800 div.shadow(smallvec::smallvec![gpui::BoxShadow {
5801 color: Hsla {
5802 h: 0.,
5803 s: 0.,
5804 l: 0.,
5805 a: 0.4,
5806 },
5807 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5808 spread_radius: px(0.),
5809 offset: point(px(0.0), px(0.0)),
5810 }])
5811 }),
5812 })
5813 .on_mouse_move(|_e, cx| {
5814 cx.stop_propagation();
5815 })
5816 .size_full()
5817 .child(element),
5818 )
5819 .map(|div| match decorations {
5820 Decorations::Server => div,
5821 Decorations::Client { tiling, .. } => div.child(
5822 canvas(
5823 |_bounds, cx| {
5824 cx.insert_hitbox(
5825 Bounds::new(
5826 point(px(0.0), px(0.0)),
5827 cx.window_bounds().get_bounds().size,
5828 ),
5829 false,
5830 )
5831 },
5832 move |_bounds, hitbox, cx| {
5833 let mouse = cx.mouse_position();
5834 let size = cx.window_bounds().get_bounds().size;
5835 let Some(edge) =
5836 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5837 else {
5838 return;
5839 };
5840 cx.set_global(GlobalResizeEdge(edge));
5841 cx.set_cursor_style(
5842 match edge {
5843 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5844 ResizeEdge::Left | ResizeEdge::Right => {
5845 CursorStyle::ResizeLeftRight
5846 }
5847 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5848 CursorStyle::ResizeUpLeftDownRight
5849 }
5850 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5851 CursorStyle::ResizeUpRightDownLeft
5852 }
5853 },
5854 &hitbox,
5855 );
5856 },
5857 )
5858 .size_full()
5859 .absolute(),
5860 ),
5861 })
5862}
5863
5864fn resize_edge(
5865 pos: Point<Pixels>,
5866 shadow_size: Pixels,
5867 window_size: Size<Pixels>,
5868 tiling: Tiling,
5869) -> Option<ResizeEdge> {
5870 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
5871 if bounds.contains(&pos) {
5872 return None;
5873 }
5874
5875 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
5876 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
5877 if !tiling.top && top_left_bounds.contains(&pos) {
5878 return Some(ResizeEdge::TopLeft);
5879 }
5880
5881 let top_right_bounds = Bounds::new(
5882 Point::new(window_size.width - corner_size.width, px(0.)),
5883 corner_size,
5884 );
5885 if !tiling.top && top_right_bounds.contains(&pos) {
5886 return Some(ResizeEdge::TopRight);
5887 }
5888
5889 let bottom_left_bounds = Bounds::new(
5890 Point::new(px(0.), window_size.height - corner_size.height),
5891 corner_size,
5892 );
5893 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
5894 return Some(ResizeEdge::BottomLeft);
5895 }
5896
5897 let bottom_right_bounds = Bounds::new(
5898 Point::new(
5899 window_size.width - corner_size.width,
5900 window_size.height - corner_size.height,
5901 ),
5902 corner_size,
5903 );
5904 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
5905 return Some(ResizeEdge::BottomRight);
5906 }
5907
5908 if !tiling.top && pos.y < shadow_size {
5909 Some(ResizeEdge::Top)
5910 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
5911 Some(ResizeEdge::Bottom)
5912 } else if !tiling.left && pos.x < shadow_size {
5913 Some(ResizeEdge::Left)
5914 } else if !tiling.right && pos.x > window_size.width - shadow_size {
5915 Some(ResizeEdge::Right)
5916 } else {
5917 None
5918 }
5919}
5920
5921fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
5922 if pane == active_pane {
5923 return;
5924 } else if pane.read(cx).items_len() == 0 {
5925 pane.update(cx, |_, cx| {
5926 cx.emit(pane::Event::Remove {
5927 focus_on_pane: None,
5928 });
5929 })
5930 } else {
5931 move_all_items(pane, active_pane, cx);
5932 }
5933}
5934
5935fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
5936 let destination_is_different = from_pane != to_pane;
5937 let mut moved_items = 0;
5938 for (item_ix, item_handle) in from_pane
5939 .read(cx)
5940 .items()
5941 .enumerate()
5942 .map(|(ix, item)| (ix, item.clone()))
5943 .collect::<Vec<_>>()
5944 {
5945 let ix = item_ix - moved_items;
5946 if destination_is_different {
5947 // Close item from previous pane
5948 from_pane.update(cx, |source, cx| {
5949 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
5950 });
5951 moved_items += 1;
5952 }
5953
5954 // This automatically removes duplicate items in the pane
5955 to_pane.update(cx, |destination, cx| {
5956 destination.add_item(item_handle, true, true, None, cx);
5957 destination.focus(cx)
5958 });
5959 }
5960}
5961
5962pub fn move_item(
5963 source: &View<Pane>,
5964 destination: &View<Pane>,
5965 item_id_to_move: EntityId,
5966 destination_index: usize,
5967 cx: &mut WindowContext<'_>,
5968) {
5969 let Some((item_ix, item_handle)) = source
5970 .read(cx)
5971 .items()
5972 .enumerate()
5973 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
5974 .map(|(ix, item)| (ix, item.clone()))
5975 else {
5976 // Tab was closed during drag
5977 return;
5978 };
5979
5980 if source != destination {
5981 // Close item from previous pane
5982 source.update(cx, |source, cx| {
5983 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
5984 });
5985 }
5986
5987 // This automatically removes duplicate items in the pane
5988 destination.update(cx, |destination, cx| {
5989 destination.add_item(item_handle, true, true, Some(destination_index), cx);
5990 destination.focus(cx)
5991 });
5992}
5993
5994#[cfg(test)]
5995mod tests {
5996 use std::{cell::RefCell, rc::Rc};
5997
5998 use super::*;
5999 use crate::{
6000 dock::{test::TestPanel, PanelEvent},
6001 item::{
6002 test::{TestItem, TestProjectItem},
6003 ItemEvent,
6004 },
6005 };
6006 use fs::FakeFs;
6007 use gpui::{
6008 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6009 UpdateGlobal, VisualTestContext,
6010 };
6011 use project::{Project, ProjectEntryId};
6012 use serde_json::json;
6013 use settings::SettingsStore;
6014
6015 #[gpui::test]
6016 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6017 init_test(cx);
6018
6019 let fs = FakeFs::new(cx.executor());
6020 let project = Project::test(fs, [], cx).await;
6021 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6022
6023 // Adding an item with no ambiguity renders the tab without detail.
6024 let item1 = cx.new_view(|cx| {
6025 let mut item = TestItem::new(cx);
6026 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6027 item
6028 });
6029 workspace.update(cx, |workspace, cx| {
6030 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6031 });
6032 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6033
6034 // Adding an item that creates ambiguity increases the level of detail on
6035 // both tabs.
6036 let item2 = cx.new_view(|cx| {
6037 let mut item = TestItem::new(cx);
6038 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6039 item
6040 });
6041 workspace.update(cx, |workspace, cx| {
6042 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6043 });
6044 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6045 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6046
6047 // Adding an item that creates ambiguity increases the level of detail only
6048 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6049 // we stop at the highest detail available.
6050 let item3 = cx.new_view(|cx| {
6051 let mut item = TestItem::new(cx);
6052 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6053 item
6054 });
6055 workspace.update(cx, |workspace, cx| {
6056 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6057 });
6058 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6059 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6060 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6061 }
6062
6063 #[gpui::test]
6064 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6065 init_test(cx);
6066
6067 let fs = FakeFs::new(cx.executor());
6068 fs.insert_tree(
6069 "/root1",
6070 json!({
6071 "one.txt": "",
6072 "two.txt": "",
6073 }),
6074 )
6075 .await;
6076 fs.insert_tree(
6077 "/root2",
6078 json!({
6079 "three.txt": "",
6080 }),
6081 )
6082 .await;
6083
6084 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6085 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6086 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6087 let worktree_id = project.update(cx, |project, cx| {
6088 project.worktrees(cx).next().unwrap().read(cx).id()
6089 });
6090
6091 let item1 = cx.new_view(|cx| {
6092 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6093 });
6094 let item2 = cx.new_view(|cx| {
6095 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6096 });
6097
6098 // Add an item to an empty pane
6099 workspace.update(cx, |workspace, cx| {
6100 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6101 });
6102 project.update(cx, |project, cx| {
6103 assert_eq!(
6104 project.active_entry(),
6105 project
6106 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6107 .map(|e| e.id)
6108 );
6109 });
6110 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6111
6112 // Add a second item to a non-empty pane
6113 workspace.update(cx, |workspace, cx| {
6114 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6115 });
6116 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
6117 project.update(cx, |project, cx| {
6118 assert_eq!(
6119 project.active_entry(),
6120 project
6121 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6122 .map(|e| e.id)
6123 );
6124 });
6125
6126 // Close the active item
6127 pane.update(cx, |pane, cx| {
6128 pane.close_active_item(&Default::default(), cx).unwrap()
6129 })
6130 .await
6131 .unwrap();
6132 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6133 project.update(cx, |project, cx| {
6134 assert_eq!(
6135 project.active_entry(),
6136 project
6137 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6138 .map(|e| e.id)
6139 );
6140 });
6141
6142 // Add a project folder
6143 project
6144 .update(cx, |project, cx| {
6145 project.find_or_create_worktree("root2", true, cx)
6146 })
6147 .await
6148 .unwrap();
6149 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
6150
6151 // Remove a project folder
6152 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6153 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
6154 }
6155
6156 #[gpui::test]
6157 async fn test_close_window(cx: &mut TestAppContext) {
6158 init_test(cx);
6159
6160 let fs = FakeFs::new(cx.executor());
6161 fs.insert_tree("/root", json!({ "one": "" })).await;
6162
6163 let project = Project::test(fs, ["root".as_ref()], cx).await;
6164 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6165
6166 // When there are no dirty items, there's nothing to do.
6167 let item1 = cx.new_view(TestItem::new);
6168 workspace.update(cx, |w, cx| {
6169 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6170 });
6171 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6172 assert!(task.await.unwrap());
6173
6174 // When there are dirty untitled items, prompt to save each one. If the user
6175 // cancels any prompt, then abort.
6176 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6177 let item3 = cx.new_view(|cx| {
6178 TestItem::new(cx)
6179 .with_dirty(true)
6180 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6181 });
6182 workspace.update(cx, |w, cx| {
6183 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6184 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6185 });
6186 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6187 cx.executor().run_until_parked();
6188 cx.simulate_prompt_answer(2); // cancel save all
6189 cx.executor().run_until_parked();
6190 cx.simulate_prompt_answer(2); // cancel save all
6191 cx.executor().run_until_parked();
6192 assert!(!cx.has_pending_prompt());
6193 assert!(!task.await.unwrap());
6194 }
6195
6196 #[gpui::test]
6197 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6198 init_test(cx);
6199
6200 // Register TestItem as a serializable item
6201 cx.update(|cx| {
6202 register_serializable_item::<TestItem>(cx);
6203 });
6204
6205 let fs = FakeFs::new(cx.executor());
6206 fs.insert_tree("/root", json!({ "one": "" })).await;
6207
6208 let project = Project::test(fs, ["root".as_ref()], cx).await;
6209 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6210
6211 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6212 let item1 = cx.new_view(|cx| {
6213 TestItem::new(cx)
6214 .with_dirty(true)
6215 .with_serialize(|| Some(Task::ready(Ok(()))))
6216 });
6217 let item2 = cx.new_view(|cx| {
6218 TestItem::new(cx)
6219 .with_dirty(true)
6220 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6221 .with_serialize(|| Some(Task::ready(Ok(()))))
6222 });
6223 workspace.update(cx, |w, cx| {
6224 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6225 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6226 });
6227 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6228 assert!(task.await.unwrap());
6229 }
6230
6231 #[gpui::test]
6232 async fn test_close_pane_items(cx: &mut TestAppContext) {
6233 init_test(cx);
6234
6235 let fs = FakeFs::new(cx.executor());
6236
6237 let project = Project::test(fs, None, cx).await;
6238 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6239
6240 let item1 = cx.new_view(|cx| {
6241 TestItem::new(cx)
6242 .with_dirty(true)
6243 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6244 });
6245 let item2 = cx.new_view(|cx| {
6246 TestItem::new(cx)
6247 .with_dirty(true)
6248 .with_conflict(true)
6249 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6250 });
6251 let item3 = cx.new_view(|cx| {
6252 TestItem::new(cx)
6253 .with_dirty(true)
6254 .with_conflict(true)
6255 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6256 });
6257 let item4 = cx.new_view(|cx| {
6258 TestItem::new(cx)
6259 .with_dirty(true)
6260 .with_project_items(&[TestProjectItem::new_untitled(cx)])
6261 });
6262 let pane = workspace.update(cx, |workspace, cx| {
6263 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6264 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6265 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6266 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6267 workspace.active_pane().clone()
6268 });
6269
6270 let close_items = pane.update(cx, |pane, cx| {
6271 pane.activate_item(1, true, true, cx);
6272 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6273 let item1_id = item1.item_id();
6274 let item3_id = item3.item_id();
6275 let item4_id = item4.item_id();
6276 pane.close_items(cx, SaveIntent::Close, move |id| {
6277 [item1_id, item3_id, item4_id].contains(&id)
6278 })
6279 });
6280 cx.executor().run_until_parked();
6281
6282 assert!(cx.has_pending_prompt());
6283 // Ignore "Save all" prompt
6284 cx.simulate_prompt_answer(2);
6285 cx.executor().run_until_parked();
6286 // There's a prompt to save item 1.
6287 pane.update(cx, |pane, _| {
6288 assert_eq!(pane.items_len(), 4);
6289 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6290 });
6291 // Confirm saving item 1.
6292 cx.simulate_prompt_answer(0);
6293 cx.executor().run_until_parked();
6294
6295 // Item 1 is saved. There's a prompt to save item 3.
6296 pane.update(cx, |pane, cx| {
6297 assert_eq!(item1.read(cx).save_count, 1);
6298 assert_eq!(item1.read(cx).save_as_count, 0);
6299 assert_eq!(item1.read(cx).reload_count, 0);
6300 assert_eq!(pane.items_len(), 3);
6301 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6302 });
6303 assert!(cx.has_pending_prompt());
6304
6305 // Cancel saving item 3.
6306 cx.simulate_prompt_answer(1);
6307 cx.executor().run_until_parked();
6308
6309 // Item 3 is reloaded. There's a prompt to save item 4.
6310 pane.update(cx, |pane, cx| {
6311 assert_eq!(item3.read(cx).save_count, 0);
6312 assert_eq!(item3.read(cx).save_as_count, 0);
6313 assert_eq!(item3.read(cx).reload_count, 1);
6314 assert_eq!(pane.items_len(), 2);
6315 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6316 });
6317 assert!(cx.has_pending_prompt());
6318
6319 // Confirm saving item 4.
6320 cx.simulate_prompt_answer(0);
6321 cx.executor().run_until_parked();
6322
6323 // There's a prompt for a path for item 4.
6324 cx.simulate_new_path_selection(|_| Some(Default::default()));
6325 close_items.await.unwrap();
6326
6327 // The requested items are closed.
6328 pane.update(cx, |pane, cx| {
6329 assert_eq!(item4.read(cx).save_count, 0);
6330 assert_eq!(item4.read(cx).save_as_count, 1);
6331 assert_eq!(item4.read(cx).reload_count, 0);
6332 assert_eq!(pane.items_len(), 1);
6333 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6334 });
6335 }
6336
6337 #[gpui::test]
6338 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6339 init_test(cx);
6340
6341 let fs = FakeFs::new(cx.executor());
6342 let project = Project::test(fs, [], cx).await;
6343 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6344
6345 // Create several workspace items with single project entries, and two
6346 // workspace items with multiple project entries.
6347 let single_entry_items = (0..=4)
6348 .map(|project_entry_id| {
6349 cx.new_view(|cx| {
6350 TestItem::new(cx)
6351 .with_dirty(true)
6352 .with_project_items(&[TestProjectItem::new(
6353 project_entry_id,
6354 &format!("{project_entry_id}.txt"),
6355 cx,
6356 )])
6357 })
6358 })
6359 .collect::<Vec<_>>();
6360 let item_2_3 = cx.new_view(|cx| {
6361 TestItem::new(cx)
6362 .with_dirty(true)
6363 .with_singleton(false)
6364 .with_project_items(&[
6365 single_entry_items[2].read(cx).project_items[0].clone(),
6366 single_entry_items[3].read(cx).project_items[0].clone(),
6367 ])
6368 });
6369 let item_3_4 = cx.new_view(|cx| {
6370 TestItem::new(cx)
6371 .with_dirty(true)
6372 .with_singleton(false)
6373 .with_project_items(&[
6374 single_entry_items[3].read(cx).project_items[0].clone(),
6375 single_entry_items[4].read(cx).project_items[0].clone(),
6376 ])
6377 });
6378
6379 // Create two panes that contain the following project entries:
6380 // left pane:
6381 // multi-entry items: (2, 3)
6382 // single-entry items: 0, 1, 2, 3, 4
6383 // right pane:
6384 // single-entry items: 1
6385 // multi-entry items: (3, 4)
6386 let left_pane = workspace.update(cx, |workspace, cx| {
6387 let left_pane = workspace.active_pane().clone();
6388 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6389 for item in single_entry_items {
6390 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6391 }
6392 left_pane.update(cx, |pane, cx| {
6393 pane.activate_item(2, true, true, cx);
6394 });
6395
6396 let right_pane = workspace
6397 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6398 .unwrap();
6399
6400 right_pane.update(cx, |pane, cx| {
6401 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6402 });
6403
6404 left_pane
6405 });
6406
6407 cx.focus_view(&left_pane);
6408
6409 // When closing all of the items in the left pane, we should be prompted twice:
6410 // once for project entry 0, and once for project entry 2. Project entries 1,
6411 // 3, and 4 are all still open in the other paten. After those two
6412 // prompts, the task should complete.
6413
6414 let close = left_pane.update(cx, |pane, cx| {
6415 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6416 });
6417 cx.executor().run_until_parked();
6418
6419 // Discard "Save all" prompt
6420 cx.simulate_prompt_answer(2);
6421
6422 cx.executor().run_until_parked();
6423 left_pane.update(cx, |pane, cx| {
6424 assert_eq!(
6425 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6426 &[ProjectEntryId::from_proto(0)]
6427 );
6428 });
6429 cx.simulate_prompt_answer(0);
6430
6431 cx.executor().run_until_parked();
6432 left_pane.update(cx, |pane, cx| {
6433 assert_eq!(
6434 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6435 &[ProjectEntryId::from_proto(2)]
6436 );
6437 });
6438 cx.simulate_prompt_answer(0);
6439
6440 cx.executor().run_until_parked();
6441 close.await.unwrap();
6442 left_pane.update(cx, |pane, _| {
6443 assert_eq!(pane.items_len(), 0);
6444 });
6445 }
6446
6447 #[gpui::test]
6448 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6449 init_test(cx);
6450
6451 let fs = FakeFs::new(cx.executor());
6452 let project = Project::test(fs, [], cx).await;
6453 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6454 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6455
6456 let item = cx.new_view(|cx| {
6457 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6458 });
6459 let item_id = item.entity_id();
6460 workspace.update(cx, |workspace, cx| {
6461 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6462 });
6463
6464 // Autosave on window change.
6465 item.update(cx, |item, cx| {
6466 SettingsStore::update_global(cx, |settings, cx| {
6467 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6468 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6469 })
6470 });
6471 item.is_dirty = true;
6472 });
6473
6474 // Deactivating the window saves the file.
6475 cx.deactivate_window();
6476 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6477
6478 // Re-activating the window doesn't save the file.
6479 cx.update(|cx| cx.activate_window());
6480 cx.executor().run_until_parked();
6481 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6482
6483 // Autosave on focus change.
6484 item.update(cx, |item, cx| {
6485 cx.focus_self();
6486 SettingsStore::update_global(cx, |settings, cx| {
6487 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6488 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6489 })
6490 });
6491 item.is_dirty = true;
6492 });
6493
6494 // Blurring the item saves the file.
6495 item.update(cx, |_, cx| cx.blur());
6496 cx.executor().run_until_parked();
6497 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6498
6499 // Deactivating the window still saves the file.
6500 item.update(cx, |item, cx| {
6501 cx.focus_self();
6502 item.is_dirty = true;
6503 });
6504 cx.deactivate_window();
6505 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6506
6507 // Autosave after delay.
6508 item.update(cx, |item, cx| {
6509 SettingsStore::update_global(cx, |settings, cx| {
6510 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6511 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6512 })
6513 });
6514 item.is_dirty = true;
6515 cx.emit(ItemEvent::Edit);
6516 });
6517
6518 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6519 cx.executor().advance_clock(Duration::from_millis(250));
6520 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6521
6522 // After delay expires, the file is saved.
6523 cx.executor().advance_clock(Duration::from_millis(250));
6524 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6525
6526 // Autosave on focus change, ensuring closing the tab counts as such.
6527 item.update(cx, |item, cx| {
6528 SettingsStore::update_global(cx, |settings, cx| {
6529 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6530 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6531 })
6532 });
6533 item.is_dirty = true;
6534 });
6535
6536 pane.update(cx, |pane, cx| {
6537 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6538 })
6539 .await
6540 .unwrap();
6541 assert!(!cx.has_pending_prompt());
6542 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6543
6544 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6545 workspace.update(cx, |workspace, cx| {
6546 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6547 });
6548 item.update(cx, |item, cx| {
6549 item.project_items[0].update(cx, |item, _| {
6550 item.entry_id = None;
6551 });
6552 item.is_dirty = true;
6553 cx.blur();
6554 });
6555 cx.run_until_parked();
6556 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6557
6558 // Ensure autosave is prevented for deleted files also when closing the buffer.
6559 let _close_items = pane.update(cx, |pane, cx| {
6560 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6561 });
6562 cx.run_until_parked();
6563 assert!(cx.has_pending_prompt());
6564 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6565 }
6566
6567 #[gpui::test]
6568 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6569 init_test(cx);
6570
6571 let fs = FakeFs::new(cx.executor());
6572
6573 let project = Project::test(fs, [], cx).await;
6574 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6575
6576 let item = cx.new_view(|cx| {
6577 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6578 });
6579 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6580 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6581 let toolbar_notify_count = Rc::new(RefCell::new(0));
6582
6583 workspace.update(cx, |workspace, cx| {
6584 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6585 let toolbar_notification_count = toolbar_notify_count.clone();
6586 cx.observe(&toolbar, move |_, _, _| {
6587 *toolbar_notification_count.borrow_mut() += 1
6588 })
6589 .detach();
6590 });
6591
6592 pane.update(cx, |pane, _| {
6593 assert!(!pane.can_navigate_backward());
6594 assert!(!pane.can_navigate_forward());
6595 });
6596
6597 item.update(cx, |item, cx| {
6598 item.set_state("one".to_string(), cx);
6599 });
6600
6601 // Toolbar must be notified to re-render the navigation buttons
6602 assert_eq!(*toolbar_notify_count.borrow(), 1);
6603
6604 pane.update(cx, |pane, _| {
6605 assert!(pane.can_navigate_backward());
6606 assert!(!pane.can_navigate_forward());
6607 });
6608
6609 workspace
6610 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6611 .await
6612 .unwrap();
6613
6614 assert_eq!(*toolbar_notify_count.borrow(), 2);
6615 pane.update(cx, |pane, _| {
6616 assert!(!pane.can_navigate_backward());
6617 assert!(pane.can_navigate_forward());
6618 });
6619 }
6620
6621 #[gpui::test]
6622 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6623 init_test(cx);
6624 let fs = FakeFs::new(cx.executor());
6625
6626 let project = Project::test(fs, [], cx).await;
6627 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6628
6629 let panel = workspace.update(cx, |workspace, cx| {
6630 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6631 workspace.add_panel(panel.clone(), cx);
6632
6633 workspace
6634 .right_dock()
6635 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6636
6637 panel
6638 });
6639
6640 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6641 pane.update(cx, |pane, cx| {
6642 let item = cx.new_view(TestItem::new);
6643 pane.add_item(Box::new(item), true, true, None, cx);
6644 });
6645
6646 // Transfer focus from center to panel
6647 workspace.update(cx, |workspace, cx| {
6648 workspace.toggle_panel_focus::<TestPanel>(cx);
6649 });
6650
6651 workspace.update(cx, |workspace, cx| {
6652 assert!(workspace.right_dock().read(cx).is_open());
6653 assert!(!panel.is_zoomed(cx));
6654 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6655 });
6656
6657 // Transfer focus from panel to center
6658 workspace.update(cx, |workspace, cx| {
6659 workspace.toggle_panel_focus::<TestPanel>(cx);
6660 });
6661
6662 workspace.update(cx, |workspace, cx| {
6663 assert!(workspace.right_dock().read(cx).is_open());
6664 assert!(!panel.is_zoomed(cx));
6665 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6666 });
6667
6668 // Close the dock
6669 workspace.update(cx, |workspace, cx| {
6670 workspace.toggle_dock(DockPosition::Right, cx);
6671 });
6672
6673 workspace.update(cx, |workspace, cx| {
6674 assert!(!workspace.right_dock().read(cx).is_open());
6675 assert!(!panel.is_zoomed(cx));
6676 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6677 });
6678
6679 // Open the dock
6680 workspace.update(cx, |workspace, cx| {
6681 workspace.toggle_dock(DockPosition::Right, cx);
6682 });
6683
6684 workspace.update(cx, |workspace, cx| {
6685 assert!(workspace.right_dock().read(cx).is_open());
6686 assert!(!panel.is_zoomed(cx));
6687 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6688 });
6689
6690 // Focus and zoom panel
6691 panel.update(cx, |panel, cx| {
6692 cx.focus_self();
6693 panel.set_zoomed(true, cx)
6694 });
6695
6696 workspace.update(cx, |workspace, cx| {
6697 assert!(workspace.right_dock().read(cx).is_open());
6698 assert!(panel.is_zoomed(cx));
6699 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6700 });
6701
6702 // Transfer focus to the center closes the dock
6703 workspace.update(cx, |workspace, cx| {
6704 workspace.toggle_panel_focus::<TestPanel>(cx);
6705 });
6706
6707 workspace.update(cx, |workspace, cx| {
6708 assert!(!workspace.right_dock().read(cx).is_open());
6709 assert!(panel.is_zoomed(cx));
6710 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6711 });
6712
6713 // Transferring focus back to the panel keeps it zoomed
6714 workspace.update(cx, |workspace, cx| {
6715 workspace.toggle_panel_focus::<TestPanel>(cx);
6716 });
6717
6718 workspace.update(cx, |workspace, cx| {
6719 assert!(workspace.right_dock().read(cx).is_open());
6720 assert!(panel.is_zoomed(cx));
6721 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6722 });
6723
6724 // Close the dock while it is zoomed
6725 workspace.update(cx, |workspace, cx| {
6726 workspace.toggle_dock(DockPosition::Right, cx)
6727 });
6728
6729 workspace.update(cx, |workspace, cx| {
6730 assert!(!workspace.right_dock().read(cx).is_open());
6731 assert!(panel.is_zoomed(cx));
6732 assert!(workspace.zoomed.is_none());
6733 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6734 });
6735
6736 // Opening the dock, when it's zoomed, retains focus
6737 workspace.update(cx, |workspace, cx| {
6738 workspace.toggle_dock(DockPosition::Right, cx)
6739 });
6740
6741 workspace.update(cx, |workspace, cx| {
6742 assert!(workspace.right_dock().read(cx).is_open());
6743 assert!(panel.is_zoomed(cx));
6744 assert!(workspace.zoomed.is_some());
6745 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6746 });
6747
6748 // Unzoom and close the panel, zoom the active pane.
6749 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6750 workspace.update(cx, |workspace, cx| {
6751 workspace.toggle_dock(DockPosition::Right, cx)
6752 });
6753 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6754
6755 // Opening a dock unzooms the pane.
6756 workspace.update(cx, |workspace, cx| {
6757 workspace.toggle_dock(DockPosition::Right, cx)
6758 });
6759 workspace.update(cx, |workspace, cx| {
6760 let pane = pane.read(cx);
6761 assert!(!pane.is_zoomed());
6762 assert!(!pane.focus_handle(cx).is_focused(cx));
6763 assert!(workspace.right_dock().read(cx).is_open());
6764 assert!(workspace.zoomed.is_none());
6765 });
6766 }
6767
6768 #[gpui::test]
6769 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6770 init_test(cx);
6771
6772 let fs = FakeFs::new(cx.executor());
6773
6774 let project = Project::test(fs, None, cx).await;
6775 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6776
6777 // Let's arrange the panes like this:
6778 //
6779 // +-----------------------+
6780 // | top |
6781 // +------+--------+-------+
6782 // | left | center | right |
6783 // +------+--------+-------+
6784 // | bottom |
6785 // +-----------------------+
6786
6787 let top_item = cx.new_view(|cx| {
6788 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6789 });
6790 let bottom_item = cx.new_view(|cx| {
6791 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6792 });
6793 let left_item = cx.new_view(|cx| {
6794 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6795 });
6796 let right_item = cx.new_view(|cx| {
6797 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6798 });
6799 let center_item = cx.new_view(|cx| {
6800 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6801 });
6802
6803 let top_pane_id = workspace.update(cx, |workspace, cx| {
6804 let top_pane_id = workspace.active_pane().entity_id();
6805 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6806 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6807 top_pane_id
6808 });
6809 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6810 let bottom_pane_id = workspace.active_pane().entity_id();
6811 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6812 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6813 bottom_pane_id
6814 });
6815 let left_pane_id = workspace.update(cx, |workspace, cx| {
6816 let left_pane_id = workspace.active_pane().entity_id();
6817 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6818 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6819 left_pane_id
6820 });
6821 let right_pane_id = workspace.update(cx, |workspace, cx| {
6822 let right_pane_id = workspace.active_pane().entity_id();
6823 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6824 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6825 right_pane_id
6826 });
6827 let center_pane_id = workspace.update(cx, |workspace, cx| {
6828 let center_pane_id = workspace.active_pane().entity_id();
6829 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6830 center_pane_id
6831 });
6832 cx.executor().run_until_parked();
6833
6834 workspace.update(cx, |workspace, cx| {
6835 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6836
6837 // Join into next from center pane into right
6838 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6839 });
6840
6841 workspace.update(cx, |workspace, cx| {
6842 let active_pane = workspace.active_pane();
6843 assert_eq!(right_pane_id, active_pane.entity_id());
6844 assert_eq!(2, active_pane.read(cx).items_len());
6845 let item_ids_in_pane =
6846 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6847 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6848 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6849
6850 // Join into next from right pane into bottom
6851 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6852 });
6853
6854 workspace.update(cx, |workspace, cx| {
6855 let active_pane = workspace.active_pane();
6856 assert_eq!(bottom_pane_id, active_pane.entity_id());
6857 assert_eq!(3, active_pane.read(cx).items_len());
6858 let item_ids_in_pane =
6859 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6860 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6861 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6862 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6863
6864 // Join into next from bottom pane into left
6865 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6866 });
6867
6868 workspace.update(cx, |workspace, cx| {
6869 let active_pane = workspace.active_pane();
6870 assert_eq!(left_pane_id, active_pane.entity_id());
6871 assert_eq!(4, active_pane.read(cx).items_len());
6872 let item_ids_in_pane =
6873 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6874 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6875 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6876 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6877 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6878
6879 // Join into next from left pane into top
6880 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6881 });
6882
6883 workspace.update(cx, |workspace, cx| {
6884 let active_pane = workspace.active_pane();
6885 assert_eq!(top_pane_id, active_pane.entity_id());
6886 assert_eq!(5, active_pane.read(cx).items_len());
6887 let item_ids_in_pane =
6888 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6889 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6890 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6891 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6892 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6893 assert!(item_ids_in_pane.contains(&top_item.item_id()));
6894
6895 // Single pane left: no-op
6896 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
6897 });
6898
6899 workspace.update(cx, |workspace, _cx| {
6900 let active_pane = workspace.active_pane();
6901 assert_eq!(top_pane_id, active_pane.entity_id());
6902 });
6903 }
6904
6905 fn add_an_item_to_active_pane(
6906 cx: &mut VisualTestContext,
6907 workspace: &View<Workspace>,
6908 item_id: u64,
6909 ) -> View<TestItem> {
6910 let item = cx.new_view(|cx| {
6911 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
6912 item_id,
6913 "item{item_id}.txt",
6914 cx,
6915 )])
6916 });
6917 workspace.update(cx, |workspace, cx| {
6918 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
6919 });
6920 return item;
6921 }
6922
6923 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
6924 return workspace.update(cx, |workspace, cx| {
6925 let new_pane =
6926 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6927 new_pane
6928 });
6929 }
6930
6931 #[gpui::test]
6932 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
6933 init_test(cx);
6934 let fs = FakeFs::new(cx.executor());
6935 let project = Project::test(fs, None, cx).await;
6936 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6937
6938 add_an_item_to_active_pane(cx, &workspace, 1);
6939 split_pane(cx, &workspace);
6940 add_an_item_to_active_pane(cx, &workspace, 2);
6941 split_pane(cx, &workspace); // empty pane
6942 split_pane(cx, &workspace);
6943 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
6944
6945 cx.executor().run_until_parked();
6946
6947 workspace.update(cx, |workspace, cx| {
6948 let num_panes = workspace.panes().len();
6949 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
6950 let active_item = workspace
6951 .active_pane()
6952 .read(cx)
6953 .active_item()
6954 .expect("item is in focus");
6955
6956 assert_eq!(num_panes, 4);
6957 assert_eq!(num_items_in_current_pane, 1);
6958 assert_eq!(active_item.item_id(), last_item.item_id());
6959 });
6960
6961 workspace.update(cx, |workspace, cx| {
6962 workspace.join_all_panes(cx);
6963 });
6964
6965 workspace.update(cx, |workspace, cx| {
6966 let num_panes = workspace.panes().len();
6967 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
6968 let active_item = workspace
6969 .active_pane()
6970 .read(cx)
6971 .active_item()
6972 .expect("item is in focus");
6973
6974 assert_eq!(num_panes, 1);
6975 assert_eq!(num_items_in_current_pane, 3);
6976 assert_eq!(active_item.item_id(), last_item.item_id());
6977 });
6978 }
6979 struct TestModal(FocusHandle);
6980
6981 impl TestModal {
6982 fn new(cx: &mut ViewContext<Self>) -> Self {
6983 Self(cx.focus_handle())
6984 }
6985 }
6986
6987 impl EventEmitter<DismissEvent> for TestModal {}
6988
6989 impl FocusableView for TestModal {
6990 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6991 self.0.clone()
6992 }
6993 }
6994
6995 impl ModalView for TestModal {}
6996
6997 impl Render for TestModal {
6998 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
6999 div().track_focus(&self.0)
7000 }
7001 }
7002
7003 #[gpui::test]
7004 async fn test_panels(cx: &mut gpui::TestAppContext) {
7005 init_test(cx);
7006 let fs = FakeFs::new(cx.executor());
7007
7008 let project = Project::test(fs, [], cx).await;
7009 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7010
7011 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7012 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7013 workspace.add_panel(panel_1.clone(), cx);
7014 workspace
7015 .left_dock()
7016 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7017 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7018 workspace.add_panel(panel_2.clone(), cx);
7019 workspace
7020 .right_dock()
7021 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7022
7023 let left_dock = workspace.left_dock();
7024 assert_eq!(
7025 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7026 panel_1.panel_id()
7027 );
7028 assert_eq!(
7029 left_dock.read(cx).active_panel_size(cx).unwrap(),
7030 panel_1.size(cx)
7031 );
7032
7033 left_dock.update(cx, |left_dock, cx| {
7034 left_dock.resize_active_panel(Some(px(1337.)), cx)
7035 });
7036 assert_eq!(
7037 workspace
7038 .right_dock()
7039 .read(cx)
7040 .visible_panel()
7041 .unwrap()
7042 .panel_id(),
7043 panel_2.panel_id(),
7044 );
7045
7046 (panel_1, panel_2)
7047 });
7048
7049 // Move panel_1 to the right
7050 panel_1.update(cx, |panel_1, cx| {
7051 panel_1.set_position(DockPosition::Right, cx)
7052 });
7053
7054 workspace.update(cx, |workspace, cx| {
7055 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7056 // Since it was the only panel on the left, the left dock should now be closed.
7057 assert!(!workspace.left_dock().read(cx).is_open());
7058 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7059 let right_dock = workspace.right_dock();
7060 assert_eq!(
7061 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7062 panel_1.panel_id()
7063 );
7064 assert_eq!(
7065 right_dock.read(cx).active_panel_size(cx).unwrap(),
7066 px(1337.)
7067 );
7068
7069 // Now we move panel_2 to the left
7070 panel_2.set_position(DockPosition::Left, cx);
7071 });
7072
7073 workspace.update(cx, |workspace, cx| {
7074 // Since panel_2 was not visible on the right, we don't open the left dock.
7075 assert!(!workspace.left_dock().read(cx).is_open());
7076 // And the right dock is unaffected in its displaying of panel_1
7077 assert!(workspace.right_dock().read(cx).is_open());
7078 assert_eq!(
7079 workspace
7080 .right_dock()
7081 .read(cx)
7082 .visible_panel()
7083 .unwrap()
7084 .panel_id(),
7085 panel_1.panel_id(),
7086 );
7087 });
7088
7089 // Move panel_1 back to the left
7090 panel_1.update(cx, |panel_1, cx| {
7091 panel_1.set_position(DockPosition::Left, cx)
7092 });
7093
7094 workspace.update(cx, |workspace, cx| {
7095 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7096 let left_dock = workspace.left_dock();
7097 assert!(left_dock.read(cx).is_open());
7098 assert_eq!(
7099 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7100 panel_1.panel_id()
7101 );
7102 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7103 // And the right dock should be closed as it no longer has any panels.
7104 assert!(!workspace.right_dock().read(cx).is_open());
7105
7106 // Now we move panel_1 to the bottom
7107 panel_1.set_position(DockPosition::Bottom, cx);
7108 });
7109
7110 workspace.update(cx, |workspace, cx| {
7111 // Since panel_1 was visible on the left, we close the left dock.
7112 assert!(!workspace.left_dock().read(cx).is_open());
7113 // The bottom dock is sized based on the panel's default size,
7114 // since the panel orientation changed from vertical to horizontal.
7115 let bottom_dock = workspace.bottom_dock();
7116 assert_eq!(
7117 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7118 panel_1.size(cx),
7119 );
7120 // Close bottom dock and move panel_1 back to the left.
7121 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7122 panel_1.set_position(DockPosition::Left, cx);
7123 });
7124
7125 // Emit activated event on panel 1
7126 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7127
7128 // Now the left dock is open and panel_1 is active and focused.
7129 workspace.update(cx, |workspace, cx| {
7130 let left_dock = workspace.left_dock();
7131 assert!(left_dock.read(cx).is_open());
7132 assert_eq!(
7133 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7134 panel_1.panel_id(),
7135 );
7136 assert!(panel_1.focus_handle(cx).is_focused(cx));
7137 });
7138
7139 // Emit closed event on panel 2, which is not active
7140 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7141
7142 // Wo don't close the left dock, because panel_2 wasn't the active panel
7143 workspace.update(cx, |workspace, cx| {
7144 let left_dock = workspace.left_dock();
7145 assert!(left_dock.read(cx).is_open());
7146 assert_eq!(
7147 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7148 panel_1.panel_id(),
7149 );
7150 });
7151
7152 // Emitting a ZoomIn event shows the panel as zoomed.
7153 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7154 workspace.update(cx, |workspace, _| {
7155 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7156 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7157 });
7158
7159 // Move panel to another dock while it is zoomed
7160 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7161 workspace.update(cx, |workspace, _| {
7162 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7163
7164 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7165 });
7166
7167 // This is a helper for getting a:
7168 // - valid focus on an element,
7169 // - that isn't a part of the panes and panels system of the Workspace,
7170 // - and doesn't trigger the 'on_focus_lost' API.
7171 let focus_other_view = {
7172 let workspace = workspace.clone();
7173 move |cx: &mut VisualTestContext| {
7174 workspace.update(cx, |workspace, cx| {
7175 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7176 workspace.toggle_modal(cx, TestModal::new);
7177 workspace.toggle_modal(cx, TestModal::new);
7178 } else {
7179 workspace.toggle_modal(cx, TestModal::new);
7180 }
7181 })
7182 }
7183 };
7184
7185 // If focus is transferred to another view that's not a panel or another pane, we still show
7186 // the panel as zoomed.
7187 focus_other_view(cx);
7188 workspace.update(cx, |workspace, _| {
7189 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7190 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7191 });
7192
7193 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7194 workspace.update(cx, |_, cx| cx.focus_self());
7195 workspace.update(cx, |workspace, _| {
7196 assert_eq!(workspace.zoomed, None);
7197 assert_eq!(workspace.zoomed_position, None);
7198 });
7199
7200 // If focus is transferred again to another view that's not a panel or a pane, we won't
7201 // show the panel as zoomed because it wasn't zoomed before.
7202 focus_other_view(cx);
7203 workspace.update(cx, |workspace, _| {
7204 assert_eq!(workspace.zoomed, None);
7205 assert_eq!(workspace.zoomed_position, None);
7206 });
7207
7208 // When the panel is activated, it is zoomed again.
7209 cx.dispatch_action(ToggleRightDock);
7210 workspace.update(cx, |workspace, _| {
7211 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7212 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7213 });
7214
7215 // Emitting a ZoomOut event unzooms the panel.
7216 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7217 workspace.update(cx, |workspace, _| {
7218 assert_eq!(workspace.zoomed, None);
7219 assert_eq!(workspace.zoomed_position, None);
7220 });
7221
7222 // Emit closed event on panel 1, which is active
7223 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7224
7225 // Now the left dock is closed, because panel_1 was the active panel
7226 workspace.update(cx, |workspace, cx| {
7227 let right_dock = workspace.right_dock();
7228 assert!(!right_dock.read(cx).is_open());
7229 });
7230 }
7231
7232 mod register_project_item_tests {
7233 use ui::Context as _;
7234
7235 use super::*;
7236
7237 // View
7238 struct TestPngItemView {
7239 focus_handle: FocusHandle,
7240 }
7241 // Model
7242 struct TestPngItem {}
7243
7244 impl project::Item for TestPngItem {
7245 fn try_open(
7246 _project: &Model<Project>,
7247 path: &ProjectPath,
7248 cx: &mut AppContext,
7249 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7250 if path.path.extension().unwrap() == "png" {
7251 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7252 } else {
7253 None
7254 }
7255 }
7256
7257 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7258 None
7259 }
7260
7261 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7262 None
7263 }
7264 }
7265
7266 impl Item for TestPngItemView {
7267 type Event = ();
7268 }
7269 impl EventEmitter<()> for TestPngItemView {}
7270 impl FocusableView for TestPngItemView {
7271 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7272 self.focus_handle.clone()
7273 }
7274 }
7275
7276 impl Render for TestPngItemView {
7277 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7278 Empty
7279 }
7280 }
7281
7282 impl ProjectItem for TestPngItemView {
7283 type Item = TestPngItem;
7284
7285 fn for_project_item(
7286 _project: Model<Project>,
7287 _item: Model<Self::Item>,
7288 cx: &mut ViewContext<Self>,
7289 ) -> Self
7290 where
7291 Self: Sized,
7292 {
7293 Self {
7294 focus_handle: cx.focus_handle(),
7295 }
7296 }
7297 }
7298
7299 // View
7300 struct TestIpynbItemView {
7301 focus_handle: FocusHandle,
7302 }
7303 // Model
7304 struct TestIpynbItem {}
7305
7306 impl project::Item for TestIpynbItem {
7307 fn try_open(
7308 _project: &Model<Project>,
7309 path: &ProjectPath,
7310 cx: &mut AppContext,
7311 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7312 if path.path.extension().unwrap() == "ipynb" {
7313 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7314 } else {
7315 None
7316 }
7317 }
7318
7319 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7320 None
7321 }
7322
7323 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7324 None
7325 }
7326 }
7327
7328 impl Item for TestIpynbItemView {
7329 type Event = ();
7330 }
7331 impl EventEmitter<()> for TestIpynbItemView {}
7332 impl FocusableView for TestIpynbItemView {
7333 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7334 self.focus_handle.clone()
7335 }
7336 }
7337
7338 impl Render for TestIpynbItemView {
7339 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7340 Empty
7341 }
7342 }
7343
7344 impl ProjectItem for TestIpynbItemView {
7345 type Item = TestIpynbItem;
7346
7347 fn for_project_item(
7348 _project: Model<Project>,
7349 _item: Model<Self::Item>,
7350 cx: &mut ViewContext<Self>,
7351 ) -> Self
7352 where
7353 Self: Sized,
7354 {
7355 Self {
7356 focus_handle: cx.focus_handle(),
7357 }
7358 }
7359 }
7360
7361 struct TestAlternatePngItemView {
7362 focus_handle: FocusHandle,
7363 }
7364
7365 impl Item for TestAlternatePngItemView {
7366 type Event = ();
7367 }
7368
7369 impl EventEmitter<()> for TestAlternatePngItemView {}
7370 impl FocusableView for TestAlternatePngItemView {
7371 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7372 self.focus_handle.clone()
7373 }
7374 }
7375
7376 impl Render for TestAlternatePngItemView {
7377 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7378 Empty
7379 }
7380 }
7381
7382 impl ProjectItem for TestAlternatePngItemView {
7383 type Item = TestPngItem;
7384
7385 fn for_project_item(
7386 _project: Model<Project>,
7387 _item: Model<Self::Item>,
7388 cx: &mut ViewContext<Self>,
7389 ) -> Self
7390 where
7391 Self: Sized,
7392 {
7393 Self {
7394 focus_handle: cx.focus_handle(),
7395 }
7396 }
7397 }
7398
7399 #[gpui::test]
7400 async fn test_register_project_item(cx: &mut TestAppContext) {
7401 init_test(cx);
7402
7403 cx.update(|cx| {
7404 register_project_item::<TestPngItemView>(cx);
7405 register_project_item::<TestIpynbItemView>(cx);
7406 });
7407
7408 let fs = FakeFs::new(cx.executor());
7409 fs.insert_tree(
7410 "/root1",
7411 json!({
7412 "one.png": "BINARYDATAHERE",
7413 "two.ipynb": "{ totally a notebook }",
7414 "three.txt": "editing text, sure why not?"
7415 }),
7416 )
7417 .await;
7418
7419 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7420 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7421
7422 let worktree_id = project.update(cx, |project, cx| {
7423 project.worktrees(cx).next().unwrap().read(cx).id()
7424 });
7425
7426 let handle = workspace
7427 .update(cx, |workspace, cx| {
7428 let project_path = (worktree_id, "one.png");
7429 workspace.open_path(project_path, None, true, cx)
7430 })
7431 .await
7432 .unwrap();
7433
7434 // Now we can check if the handle we got back errored or not
7435 assert_eq!(
7436 handle.to_any().entity_type(),
7437 TypeId::of::<TestPngItemView>()
7438 );
7439
7440 let handle = workspace
7441 .update(cx, |workspace, cx| {
7442 let project_path = (worktree_id, "two.ipynb");
7443 workspace.open_path(project_path, None, true, cx)
7444 })
7445 .await
7446 .unwrap();
7447
7448 assert_eq!(
7449 handle.to_any().entity_type(),
7450 TypeId::of::<TestIpynbItemView>()
7451 );
7452
7453 let handle = workspace
7454 .update(cx, |workspace, cx| {
7455 let project_path = (worktree_id, "three.txt");
7456 workspace.open_path(project_path, None, true, cx)
7457 })
7458 .await;
7459 assert!(handle.is_err());
7460 }
7461
7462 #[gpui::test]
7463 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7464 init_test(cx);
7465
7466 cx.update(|cx| {
7467 register_project_item::<TestPngItemView>(cx);
7468 register_project_item::<TestAlternatePngItemView>(cx);
7469 });
7470
7471 let fs = FakeFs::new(cx.executor());
7472 fs.insert_tree(
7473 "/root1",
7474 json!({
7475 "one.png": "BINARYDATAHERE",
7476 "two.ipynb": "{ totally a notebook }",
7477 "three.txt": "editing text, sure why not?"
7478 }),
7479 )
7480 .await;
7481
7482 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7483 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7484
7485 let worktree_id = project.update(cx, |project, cx| {
7486 project.worktrees(cx).next().unwrap().read(cx).id()
7487 });
7488
7489 let handle = workspace
7490 .update(cx, |workspace, cx| {
7491 let project_path = (worktree_id, "one.png");
7492 workspace.open_path(project_path, None, true, cx)
7493 })
7494 .await
7495 .unwrap();
7496
7497 // This _must_ be the second item registered
7498 assert_eq!(
7499 handle.to_any().entity_type(),
7500 TypeId::of::<TestAlternatePngItemView>()
7501 );
7502
7503 let handle = workspace
7504 .update(cx, |workspace, cx| {
7505 let project_path = (worktree_id, "three.txt");
7506 workspace.open_path(project_path, None, true, cx)
7507 })
7508 .await;
7509 assert!(handle.is_err());
7510 }
7511 }
7512
7513 pub fn init_test(cx: &mut TestAppContext) {
7514 cx.update(|cx| {
7515 let settings_store = SettingsStore::test(cx);
7516 cx.set_global(settings_store);
7517 theme::init(theme::LoadThemes::JustBase, cx);
7518 language::init(cx);
7519 crate::init_settings(cx);
7520 Project::init_settings(cx);
7521 });
7522 }
7523}