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