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