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