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