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