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::{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(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
609>;
610
611/// Collects everything project-related for a certain window opened.
612/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
613///
614/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
615/// The `Workspace` owns everybody's state and serves as a default, "global context",
616/// that can be used to register a global action to be triggered from any place in the window.
617pub struct Workspace {
618 weak_self: WeakView<Self>,
619 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
620 zoomed: Option<AnyWeakView>,
621 zoomed_position: Option<DockPosition>,
622 center: PaneGroup,
623 left_dock: View<Dock>,
624 bottom_dock: View<Dock>,
625 right_dock: View<Dock>,
626 panes: Vec<View<Pane>>,
627 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
628 active_pane: View<Pane>,
629 last_active_center_pane: Option<WeakView<Pane>>,
630 last_active_view_id: Option<proto::ViewId>,
631 status_bar: View<StatusBar>,
632 modal_layer: View<ModalLayer>,
633 titlebar_item: Option<AnyView>,
634 notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
635 project: Model<Project>,
636 follower_states: HashMap<PeerId, FollowerState>,
637 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
638 window_edited: bool,
639 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
640 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
641 database_id: Option<WorkspaceId>,
642 app_state: Arc<AppState>,
643 dispatching_keystrokes: Rc<RefCell<Vec<Keystroke>>>,
644 _subscriptions: Vec<Subscription>,
645 _apply_leader_updates: Task<Result<()>>,
646 _observe_current_user: Task<Result<()>>,
647 _schedule_serialize: Option<Task<()>>,
648 pane_history_timestamp: Arc<AtomicUsize>,
649 bounds: Bounds<Pixels>,
650 centered_layout: bool,
651 bounds_save_task_queued: Option<Task<()>>,
652 on_prompt_for_new_path: Option<PromptForNewPath>,
653 on_prompt_for_open_path: Option<PromptForOpenPath>,
654 render_disconnected_overlay:
655 Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
656}
657
658impl EventEmitter<Event> for Workspace {}
659
660#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
661pub struct ViewId {
662 pub creator: PeerId,
663 pub id: u64,
664}
665
666struct FollowerState {
667 center_pane: View<Pane>,
668 dock_pane: Option<View<Pane>>,
669 active_view_id: Option<ViewId>,
670 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
671}
672
673struct FollowerView {
674 view: Box<dyn FollowableItemHandle>,
675 location: Option<proto::PanelId>,
676}
677
678impl Workspace {
679 const DEFAULT_PADDING: f32 = 0.2;
680 const MAX_PADDING: f32 = 0.4;
681
682 pub fn new(
683 workspace_id: Option<WorkspaceId>,
684 project: Model<Project>,
685 app_state: Arc<AppState>,
686 cx: &mut ViewContext<Self>,
687 ) -> Self {
688 cx.observe(&project, |_, _, cx| cx.notify()).detach();
689 cx.subscribe(&project, move |this, _, event, cx| {
690 match event {
691 project::Event::RemoteIdChanged(_) => {
692 this.update_window_title(cx);
693 }
694
695 project::Event::CollaboratorLeft(peer_id) => {
696 this.collaborator_left(*peer_id, cx);
697 }
698
699 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
700 this.update_window_title(cx);
701 this.serialize_workspace(cx);
702 }
703
704 project::Event::DisconnectedFromHost => {
705 this.update_window_edited(cx);
706 let leaders_to_unfollow =
707 this.follower_states.keys().copied().collect::<Vec<_>>();
708 for leader_id in leaders_to_unfollow {
709 this.unfollow(leader_id, cx);
710 }
711 }
712
713 project::Event::Closed => {
714 cx.remove_window();
715 }
716
717 project::Event::DeletedEntry(entry_id) => {
718 for pane in this.panes.iter() {
719 pane.update(cx, |pane, cx| {
720 pane.handle_deleted_project_item(*entry_id, cx)
721 });
722 }
723 }
724
725 project::Event::Notification(message) => {
726 struct ProjectNotification;
727
728 this.show_notification(
729 NotificationId::unique::<ProjectNotification>(),
730 cx,
731 |cx| cx.new_view(|_| MessageNotification::new(message.clone())),
732 )
733 }
734
735 project::Event::LanguageServerPrompt(request) => {
736 struct LanguageServerPrompt;
737
738 let mut hasher = DefaultHasher::new();
739 request.lsp_name.as_str().hash(&mut hasher);
740 let id = hasher.finish();
741
742 this.show_notification(
743 NotificationId::identified::<LanguageServerPrompt>(id as usize),
744 cx,
745 |cx| {
746 cx.new_view(|_| {
747 notifications::LanguageServerPrompt::new(request.clone())
748 })
749 },
750 );
751 }
752
753 _ => {}
754 }
755 cx.notify()
756 })
757 .detach();
758
759 cx.on_focus_lost(|this, cx| {
760 let focus_handle = this.focus_handle(cx);
761 cx.focus(&focus_handle);
762 })
763 .detach();
764
765 let weak_handle = cx.view().downgrade();
766 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
767
768 let center_pane = cx.new_view(|cx| {
769 Pane::new(
770 weak_handle.clone(),
771 project.clone(),
772 pane_history_timestamp.clone(),
773 None,
774 NewFile.boxed_clone(),
775 cx,
776 )
777 });
778 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
779
780 cx.focus_view(¢er_pane);
781 cx.emit(Event::PaneAdded(center_pane.clone()));
782
783 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
784 app_state.workspace_store.update(cx, |store, _| {
785 store.workspaces.insert(window_handle);
786 });
787
788 let mut current_user = app_state.user_store.read(cx).watch_current_user();
789 let mut connection_status = app_state.client.status();
790 let _observe_current_user = cx.spawn(|this, mut cx| async move {
791 current_user.next().await;
792 connection_status.next().await;
793 let mut stream =
794 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
795
796 while stream.recv().await.is_some() {
797 this.update(&mut cx, |_, cx| cx.notify())?;
798 }
799 anyhow::Ok(())
800 });
801
802 // All leader updates are enqueued and then processed in a single task, so
803 // that each asynchronous operation can be run in order.
804 let (leader_updates_tx, mut leader_updates_rx) =
805 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
806 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
807 while let Some((leader_id, update)) = leader_updates_rx.next().await {
808 Self::process_leader_update(&this, leader_id, update, &mut cx)
809 .await
810 .log_err();
811 }
812
813 Ok(())
814 });
815
816 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
817
818 let left_dock = Dock::new(DockPosition::Left, cx);
819 let bottom_dock = Dock::new(DockPosition::Bottom, cx);
820 let right_dock = Dock::new(DockPosition::Right, cx);
821 let left_dock_buttons = cx.new_view(|cx| PanelButtons::new(left_dock.clone(), cx));
822 let bottom_dock_buttons = cx.new_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
823 let right_dock_buttons = cx.new_view(|cx| PanelButtons::new(right_dock.clone(), cx));
824 let status_bar = cx.new_view(|cx| {
825 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
826 status_bar.add_left_item(left_dock_buttons, cx);
827 status_bar.add_right_item(right_dock_buttons, cx);
828 status_bar.add_right_item(bottom_dock_buttons, cx);
829 status_bar
830 });
831
832 let modal_layer = cx.new_view(|_| ModalLayer::new());
833
834 let mut active_call = None;
835 if let Some(call) = ActiveCall::try_global(cx) {
836 let call = call.clone();
837 let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
838 active_call = Some((call, subscriptions));
839 }
840
841 let subscriptions = vec![
842 cx.observe_window_activation(Self::on_window_activation_changed),
843 cx.observe_window_bounds(move |this, cx| {
844 if this.bounds_save_task_queued.is_some() {
845 return;
846 }
847 this.bounds_save_task_queued = Some(cx.spawn(|this, mut cx| async move {
848 cx.background_executor()
849 .timer(Duration::from_millis(100))
850 .await;
851 this.update(&mut cx, |this, cx| {
852 if let Some(display) = cx.display() {
853 if let Some(display_uuid) = display.uuid().ok() {
854 let window_bounds = cx.window_bounds();
855 if let Some(database_id) = workspace_id {
856 cx.background_executor()
857 .spawn(DB.set_window_open_status(
858 database_id,
859 SerializedWindowBounds(window_bounds),
860 display_uuid,
861 ))
862 .detach_and_log_err(cx);
863 }
864 }
865 }
866 this.bounds_save_task_queued.take();
867 })
868 .ok();
869 }));
870 cx.notify();
871 }),
872 cx.observe_window_appearance(|_, cx| {
873 let window_appearance = cx.appearance();
874
875 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
876
877 ThemeSettings::reload_current_theme(cx);
878 }),
879 cx.observe(&left_dock, |this, _, cx| {
880 this.serialize_workspace(cx);
881 cx.notify();
882 }),
883 cx.observe(&bottom_dock, |this, _, cx| {
884 this.serialize_workspace(cx);
885 cx.notify();
886 }),
887 cx.observe(&right_dock, |this, _, cx| {
888 this.serialize_workspace(cx);
889 cx.notify();
890 }),
891 cx.on_release(|this, window, cx| {
892 this.app_state.workspace_store.update(cx, |store, _| {
893 let window = window.downcast::<Self>().unwrap();
894 store.workspaces.remove(&window);
895 })
896 }),
897 ];
898
899 cx.defer(|this, cx| {
900 this.update_window_title(cx);
901 });
902 Workspace {
903 weak_self: weak_handle.clone(),
904 zoomed: None,
905 zoomed_position: None,
906 center: PaneGroup::new(center_pane.clone()),
907 panes: vec![center_pane.clone()],
908 panes_by_item: Default::default(),
909 active_pane: center_pane.clone(),
910 last_active_center_pane: Some(center_pane.downgrade()),
911 last_active_view_id: None,
912 status_bar,
913 modal_layer,
914 titlebar_item: None,
915 notifications: Default::default(),
916 left_dock,
917 bottom_dock,
918 right_dock,
919 project: project.clone(),
920 follower_states: Default::default(),
921 last_leaders_by_pane: Default::default(),
922 dispatching_keystrokes: Default::default(),
923 window_edited: false,
924 active_call,
925 database_id: workspace_id,
926 app_state,
927 _observe_current_user,
928 _apply_leader_updates,
929 _schedule_serialize: None,
930 leader_updates_tx,
931 _subscriptions: subscriptions,
932 pane_history_timestamp,
933 workspace_actions: Default::default(),
934 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
935 bounds: Default::default(),
936 centered_layout: false,
937 bounds_save_task_queued: None,
938 on_prompt_for_new_path: None,
939 on_prompt_for_open_path: None,
940 render_disconnected_overlay: None,
941 }
942 }
943
944 pub fn new_local(
945 abs_paths: Vec<PathBuf>,
946 app_state: Arc<AppState>,
947 requesting_window: Option<WindowHandle<Workspace>>,
948 cx: &mut AppContext,
949 ) -> Task<
950 anyhow::Result<(
951 WindowHandle<Workspace>,
952 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
953 )>,
954 > {
955 let project_handle = Project::local(
956 app_state.client.clone(),
957 app_state.node_runtime.clone(),
958 app_state.user_store.clone(),
959 app_state.languages.clone(),
960 app_state.fs.clone(),
961 cx,
962 );
963
964 cx.spawn(|mut cx| async move {
965 let serialized_workspace: Option<SerializedWorkspace> =
966 persistence::DB.workspace_for_roots(abs_paths.as_slice());
967
968 let mut paths_to_open = abs_paths;
969
970 let paths_order = serialized_workspace
971 .as_ref()
972 .map(|ws| &ws.location)
973 .and_then(|loc| match loc {
974 SerializedWorkspaceLocation::Local(_, order) => Some(order.order()),
975 _ => None,
976 });
977
978 if let Some(paths_order) = paths_order {
979 paths_to_open = paths_order
980 .iter()
981 .filter_map(|i| paths_to_open.get(*i).cloned())
982 .collect::<Vec<_>>();
983 if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
984 project_handle
985 .update(&mut cx, |project, _| {
986 project.set_worktrees_reordered(true);
987 })
988 .log_err();
989 }
990 }
991
992 // Get project paths for all of the abs_paths
993 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
994 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
995 Vec::with_capacity(paths_to_open.len());
996 for path in paths_to_open.into_iter() {
997 if let Some((worktree, project_entry)) = cx
998 .update(|cx| {
999 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1000 })?
1001 .await
1002 .log_err()
1003 {
1004 worktree_roots.extend(worktree.update(&mut cx, |tree, _| tree.abs_path()).ok());
1005 project_paths.push((path, Some(project_entry)));
1006 } else {
1007 project_paths.push((path, None));
1008 }
1009 }
1010
1011 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1012 serialized_workspace.id
1013 } else {
1014 DB.next_id().await.unwrap_or_else(|_| Default::default())
1015 };
1016
1017 let window = if let Some(window) = requesting_window {
1018 cx.update_window(window.into(), |_, cx| {
1019 cx.replace_root_view(|cx| {
1020 Workspace::new(
1021 Some(workspace_id),
1022 project_handle.clone(),
1023 app_state.clone(),
1024 cx,
1025 )
1026 });
1027 })?;
1028 window
1029 } else {
1030 let window_bounds_override = window_bounds_env_override();
1031
1032 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1033 (Some(WindowBounds::Windowed(bounds)), None)
1034 } else {
1035 let restorable_bounds = serialized_workspace
1036 .as_ref()
1037 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1038 .or_else(|| {
1039 let (display, window_bounds) = DB.last_window().log_err()?;
1040 Some((display?, window_bounds?))
1041 });
1042
1043 if let Some((serialized_display, serialized_status)) = restorable_bounds {
1044 (Some(serialized_status.0), Some(serialized_display))
1045 } else {
1046 (None, None)
1047 }
1048 };
1049
1050 // Use the serialized workspace to construct the new window
1051 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1052 options.window_bounds = window_bounds;
1053 let centered_layout = serialized_workspace
1054 .as_ref()
1055 .map(|w| w.centered_layout)
1056 .unwrap_or(false);
1057 cx.open_window(options, {
1058 let app_state = app_state.clone();
1059 let project_handle = project_handle.clone();
1060 move |cx| {
1061 cx.new_view(|cx| {
1062 let mut workspace =
1063 Workspace::new(Some(workspace_id), project_handle, app_state, cx);
1064 workspace.centered_layout = centered_layout;
1065 workspace
1066 })
1067 }
1068 })?
1069 };
1070
1071 notify_if_database_failed(window, &mut cx);
1072 let opened_items = window
1073 .update(&mut cx, |_workspace, cx| {
1074 open_items(serialized_workspace, project_paths, app_state, cx)
1075 })?
1076 .await
1077 .unwrap_or_default();
1078
1079 window
1080 .update(&mut cx, |_, cx| cx.activate_window())
1081 .log_err();
1082 Ok((window, opened_items))
1083 })
1084 }
1085
1086 pub fn weak_handle(&self) -> WeakView<Self> {
1087 self.weak_self.clone()
1088 }
1089
1090 pub fn left_dock(&self) -> &View<Dock> {
1091 &self.left_dock
1092 }
1093
1094 pub fn bottom_dock(&self) -> &View<Dock> {
1095 &self.bottom_dock
1096 }
1097
1098 pub fn right_dock(&self) -> &View<Dock> {
1099 &self.right_dock
1100 }
1101
1102 pub fn is_edited(&self) -> bool {
1103 self.window_edited
1104 }
1105
1106 pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
1107 let focus_handle = panel.focus_handle(cx);
1108 cx.on_focus_in(&focus_handle, Self::handle_panel_focused)
1109 .detach();
1110
1111 let dock = match panel.position(cx) {
1112 DockPosition::Left => &self.left_dock,
1113 DockPosition::Bottom => &self.bottom_dock,
1114 DockPosition::Right => &self.right_dock,
1115 };
1116
1117 dock.update(cx, |dock, cx| {
1118 dock.add_panel(panel, self.weak_self.clone(), cx)
1119 });
1120 }
1121
1122 pub fn status_bar(&self) -> &View<StatusBar> {
1123 &self.status_bar
1124 }
1125
1126 pub fn app_state(&self) -> &Arc<AppState> {
1127 &self.app_state
1128 }
1129
1130 pub fn user_store(&self) -> &Model<UserStore> {
1131 &self.app_state.user_store
1132 }
1133
1134 pub fn project(&self) -> &Model<Project> {
1135 &self.project
1136 }
1137
1138 pub fn recent_navigation_history(
1139 &self,
1140 limit: Option<usize>,
1141 cx: &AppContext,
1142 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1143 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1144 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1145 for pane in &self.panes {
1146 let pane = pane.read(cx);
1147 pane.nav_history()
1148 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1149 if let Some(fs_path) = &fs_path {
1150 abs_paths_opened
1151 .entry(fs_path.clone())
1152 .or_default()
1153 .insert(project_path.clone());
1154 }
1155 let timestamp = entry.timestamp;
1156 match history.entry(project_path) {
1157 hash_map::Entry::Occupied(mut entry) => {
1158 let (_, old_timestamp) = entry.get();
1159 if ×tamp > old_timestamp {
1160 entry.insert((fs_path, timestamp));
1161 }
1162 }
1163 hash_map::Entry::Vacant(entry) => {
1164 entry.insert((fs_path, timestamp));
1165 }
1166 }
1167 });
1168 }
1169
1170 history
1171 .into_iter()
1172 .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1173 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1174 .rev()
1175 .filter(|(history_path, abs_path)| {
1176 let latest_project_path_opened = abs_path
1177 .as_ref()
1178 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1179 .and_then(|project_paths| {
1180 project_paths
1181 .iter()
1182 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1183 });
1184
1185 match latest_project_path_opened {
1186 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1187 None => true,
1188 }
1189 })
1190 .take(limit.unwrap_or(usize::MAX))
1191 .collect()
1192 }
1193
1194 fn navigate_history(
1195 &mut self,
1196 pane: WeakView<Pane>,
1197 mode: NavigationMode,
1198 cx: &mut ViewContext<Workspace>,
1199 ) -> Task<Result<()>> {
1200 let to_load = if let Some(pane) = pane.upgrade() {
1201 pane.update(cx, |pane, cx| {
1202 pane.focus(cx);
1203 loop {
1204 // Retrieve the weak item handle from the history.
1205 let entry = pane.nav_history_mut().pop(mode, cx)?;
1206
1207 // If the item is still present in this pane, then activate it.
1208 if let Some(index) = entry
1209 .item
1210 .upgrade()
1211 .and_then(|v| pane.index_for_item(v.as_ref()))
1212 {
1213 let prev_active_item_index = pane.active_item_index();
1214 pane.nav_history_mut().set_mode(mode);
1215 pane.activate_item(index, true, true, cx);
1216 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1217
1218 let mut navigated = prev_active_item_index != pane.active_item_index();
1219 if let Some(data) = entry.data {
1220 navigated |= pane.active_item()?.navigate(data, cx);
1221 }
1222
1223 if navigated {
1224 break None;
1225 }
1226 }
1227 // If the item is no longer present in this pane, then retrieve its
1228 // project path in order to reopen it.
1229 else {
1230 break pane
1231 .nav_history()
1232 .path_for_item(entry.item.id())
1233 .map(|(project_path, _)| (project_path, entry));
1234 }
1235 }
1236 })
1237 } else {
1238 None
1239 };
1240
1241 if let Some((project_path, entry)) = to_load {
1242 // If the item was no longer present, then load it again from its previous path.
1243 let task = self.load_path(project_path, cx);
1244 cx.spawn(|workspace, mut cx| async move {
1245 let task = task.await;
1246 let mut navigated = false;
1247 if let Some((project_entry_id, build_item)) = task.log_err() {
1248 let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1249 pane.nav_history_mut().set_mode(mode);
1250 pane.active_item().map(|p| p.item_id())
1251 })?;
1252
1253 pane.update(&mut cx, |pane, cx| {
1254 let item = pane.open_item(
1255 project_entry_id,
1256 true,
1257 entry.is_preview,
1258 cx,
1259 build_item,
1260 );
1261 navigated |= Some(item.item_id()) != prev_active_item_id;
1262 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1263 if let Some(data) = entry.data {
1264 navigated |= item.navigate(data, cx);
1265 }
1266 })?;
1267 }
1268
1269 if !navigated {
1270 workspace
1271 .update(&mut cx, |workspace, cx| {
1272 Self::navigate_history(workspace, pane, mode, cx)
1273 })?
1274 .await?;
1275 }
1276
1277 Ok(())
1278 })
1279 } else {
1280 Task::ready(Ok(()))
1281 }
1282 }
1283
1284 pub fn go_back(
1285 &mut self,
1286 pane: WeakView<Pane>,
1287 cx: &mut ViewContext<Workspace>,
1288 ) -> Task<Result<()>> {
1289 self.navigate_history(pane, NavigationMode::GoingBack, cx)
1290 }
1291
1292 pub fn go_forward(
1293 &mut self,
1294 pane: WeakView<Pane>,
1295 cx: &mut ViewContext<Workspace>,
1296 ) -> Task<Result<()>> {
1297 self.navigate_history(pane, NavigationMode::GoingForward, cx)
1298 }
1299
1300 pub fn reopen_closed_item(&mut self, cx: &mut ViewContext<Workspace>) -> Task<Result<()>> {
1301 self.navigate_history(
1302 self.active_pane().downgrade(),
1303 NavigationMode::ReopeningClosedItem,
1304 cx,
1305 )
1306 }
1307
1308 pub fn client(&self) -> &Arc<Client> {
1309 &self.app_state.client
1310 }
1311
1312 pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
1313 self.titlebar_item = Some(item);
1314 cx.notify();
1315 }
1316
1317 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1318 self.on_prompt_for_new_path = Some(prompt)
1319 }
1320
1321 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1322 self.on_prompt_for_open_path = Some(prompt)
1323 }
1324
1325 pub fn set_render_disconnected_overlay(
1326 &mut self,
1327 render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
1328 ) {
1329 self.render_disconnected_overlay = Some(Box::new(render))
1330 }
1331
1332 pub fn prompt_for_open_path(
1333 &mut self,
1334 path_prompt_options: PathPromptOptions,
1335 cx: &mut ViewContext<Self>,
1336 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1337 if self.project.read(cx).is_remote()
1338 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1339 {
1340 let prompt = self.on_prompt_for_open_path.take().unwrap();
1341 let rx = prompt(self, cx);
1342 self.on_prompt_for_open_path = Some(prompt);
1343 rx
1344 } else {
1345 let (tx, rx) = oneshot::channel();
1346 let abs_path = cx.prompt_for_paths(path_prompt_options);
1347
1348 cx.spawn(|this, mut cx| async move {
1349 let Ok(result) = abs_path.await else {
1350 return Ok(());
1351 };
1352
1353 match result {
1354 Ok(result) => {
1355 tx.send(result).log_err();
1356 }
1357 Err(err) => {
1358 let rx = this.update(&mut cx, |this, cx| {
1359 this.show_portal_error(err.to_string(), cx);
1360 let prompt = this.on_prompt_for_open_path.take().unwrap();
1361 let rx = prompt(this, cx);
1362 this.on_prompt_for_open_path = Some(prompt);
1363 rx
1364 })?;
1365 if let Ok(path) = rx.await {
1366 tx.send(path).log_err();
1367 }
1368 }
1369 };
1370 anyhow::Ok(())
1371 })
1372 .detach();
1373
1374 rx
1375 }
1376 }
1377
1378 pub fn prompt_for_new_path(
1379 &mut self,
1380 cx: &mut ViewContext<Self>,
1381 ) -> oneshot::Receiver<Option<ProjectPath>> {
1382 if self.project.read(cx).is_remote()
1383 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1384 {
1385 let prompt = self.on_prompt_for_new_path.take().unwrap();
1386 let rx = prompt(self, cx);
1387 self.on_prompt_for_new_path = Some(prompt);
1388 rx
1389 } else {
1390 let start_abs_path = self
1391 .project
1392 .update(cx, |project, cx| {
1393 let worktree = project.visible_worktrees(cx).next()?;
1394 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1395 })
1396 .unwrap_or_else(|| Path::new("").into());
1397
1398 let (tx, rx) = oneshot::channel();
1399 let abs_path = cx.prompt_for_new_path(&start_abs_path);
1400 cx.spawn(|this, mut cx| async move {
1401 let abs_path = match abs_path.await? {
1402 Ok(path) => path,
1403 Err(err) => {
1404 let rx = this.update(&mut cx, |this, cx| {
1405 this.show_portal_error(err.to_string(), cx);
1406
1407 let prompt = this.on_prompt_for_new_path.take().unwrap();
1408 let rx = prompt(this, cx);
1409 this.on_prompt_for_new_path = Some(prompt);
1410 rx
1411 })?;
1412 if let Ok(path) = rx.await {
1413 tx.send(path).log_err();
1414 }
1415 return anyhow::Ok(());
1416 }
1417 };
1418
1419 let project_path = abs_path.and_then(|abs_path| {
1420 this.update(&mut cx, |this, cx| {
1421 this.project.update(cx, |project, cx| {
1422 project.find_or_create_local_worktree(abs_path, true, cx)
1423 })
1424 })
1425 .ok()
1426 });
1427
1428 if let Some(project_path) = project_path {
1429 let (worktree, path) = project_path.await?;
1430 let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1431 tx.send(Some(ProjectPath {
1432 worktree_id,
1433 path: path.into(),
1434 }))
1435 .ok();
1436 } else {
1437 tx.send(None).ok();
1438 }
1439 anyhow::Ok(())
1440 })
1441 .detach_and_log_err(cx);
1442
1443 rx
1444 }
1445 }
1446
1447 pub fn titlebar_item(&self) -> Option<AnyView> {
1448 self.titlebar_item.clone()
1449 }
1450
1451 /// Call the given callback with a workspace whose project is local.
1452 ///
1453 /// If the given workspace has a local project, then it will be passed
1454 /// to the callback. Otherwise, a new empty window will be created.
1455 pub fn with_local_workspace<T, F>(
1456 &mut self,
1457 cx: &mut ViewContext<Self>,
1458 callback: F,
1459 ) -> Task<Result<T>>
1460 where
1461 T: 'static,
1462 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
1463 {
1464 if self.project.read(cx).is_local() {
1465 Task::Ready(Some(Ok(callback(self, cx))))
1466 } else {
1467 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
1468 cx.spawn(|_vh, mut cx| async move {
1469 let (workspace, _) = task.await?;
1470 workspace.update(&mut cx, callback)
1471 })
1472 }
1473 }
1474
1475 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
1476 self.project.read(cx).worktrees()
1477 }
1478
1479 pub fn visible_worktrees<'a>(
1480 &self,
1481 cx: &'a AppContext,
1482 ) -> impl 'a + Iterator<Item = Model<Worktree>> {
1483 self.project.read(cx).visible_worktrees(cx)
1484 }
1485
1486 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
1487 let futures = self
1488 .worktrees(cx)
1489 .filter_map(|worktree| worktree.read(cx).as_local())
1490 .map(|worktree| worktree.scan_complete())
1491 .collect::<Vec<_>>();
1492 async move {
1493 for future in futures {
1494 future.await;
1495 }
1496 }
1497 }
1498
1499 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1500 cx.defer(|cx| {
1501 cx.windows().iter().find(|window| {
1502 window
1503 .update(cx, |_, window| {
1504 if window.is_window_active() {
1505 //This can only get called when the window's project connection has been lost
1506 //so we don't need to prompt the user for anything and instead just close the window
1507 window.remove_window();
1508 true
1509 } else {
1510 false
1511 }
1512 })
1513 .unwrap_or(false)
1514 });
1515 });
1516 }
1517
1518 pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
1519 let window = cx.window_handle();
1520 let prepare = self.prepare_to_close(false, cx);
1521 cx.spawn(|_, mut cx| async move {
1522 if prepare.await? {
1523 window.update(&mut cx, |_, cx| {
1524 cx.remove_window();
1525 })?;
1526 }
1527 anyhow::Ok(())
1528 })
1529 .detach_and_log_err(cx)
1530 }
1531
1532 pub fn prepare_to_close(
1533 &mut self,
1534 quitting: bool,
1535 cx: &mut ViewContext<Self>,
1536 ) -> Task<Result<bool>> {
1537 let active_call = self.active_call().cloned();
1538 let window = cx.window_handle();
1539
1540 cx.spawn(|this, mut cx| async move {
1541 let workspace_count = (*cx).update(|cx| {
1542 cx.windows()
1543 .iter()
1544 .filter(|window| window.downcast::<Workspace>().is_some())
1545 .count()
1546 })?;
1547
1548 if let Some(active_call) = active_call {
1549 if !quitting
1550 && workspace_count == 1
1551 && active_call.read_with(&cx, |call, _| call.room().is_some())?
1552 {
1553 let answer = window.update(&mut cx, |_, cx| {
1554 cx.prompt(
1555 PromptLevel::Warning,
1556 "Do you want to leave the current call?",
1557 None,
1558 &["Close window and hang up", "Cancel"],
1559 )
1560 })?;
1561
1562 if answer.await.log_err() == Some(1) {
1563 return anyhow::Ok(false);
1564 } else {
1565 active_call
1566 .update(&mut cx, |call, cx| call.hang_up(cx))?
1567 .await
1568 .log_err();
1569 }
1570 }
1571 }
1572
1573 this.update(&mut cx, |this, cx| {
1574 this.save_all_internal(SaveIntent::Close, cx)
1575 })?
1576 .await
1577 })
1578 }
1579
1580 fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
1581 self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
1582 .detach_and_log_err(cx);
1583 }
1584
1585 fn send_keystrokes(&mut self, action: &SendKeystrokes, cx: &mut ViewContext<Self>) {
1586 let mut keystrokes: Vec<Keystroke> = action
1587 .0
1588 .split(' ')
1589 .flat_map(|k| Keystroke::parse(k).log_err())
1590 .collect();
1591 keystrokes.reverse();
1592
1593 self.dispatching_keystrokes
1594 .borrow_mut()
1595 .append(&mut keystrokes);
1596
1597 let keystrokes = self.dispatching_keystrokes.clone();
1598 cx.window_context()
1599 .spawn(|mut cx| async move {
1600 // limit to 100 keystrokes to avoid infinite recursion.
1601 for _ in 0..100 {
1602 let Some(keystroke) = keystrokes.borrow_mut().pop() else {
1603 return Ok(());
1604 };
1605 cx.update(|cx| {
1606 let focused = cx.focused();
1607 cx.dispatch_keystroke(keystroke.clone());
1608 if cx.focused() != focused {
1609 // dispatch_keystroke may cause the focus to change.
1610 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1611 // And we need that to happen before the next keystroke to keep vim mode happy...
1612 // (Note that the tests always do this implicitly, so you must manually test with something like:
1613 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1614 // )
1615 cx.draw();
1616 }
1617 })?;
1618 }
1619 keystrokes.borrow_mut().clear();
1620 Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1621 })
1622 .detach_and_log_err(cx);
1623 }
1624
1625 fn save_all_internal(
1626 &mut self,
1627 mut save_intent: SaveIntent,
1628 cx: &mut ViewContext<Self>,
1629 ) -> Task<Result<bool>> {
1630 if self.project.read(cx).is_disconnected() {
1631 return Task::ready(Ok(true));
1632 }
1633 let dirty_items = self
1634 .panes
1635 .iter()
1636 .flat_map(|pane| {
1637 pane.read(cx).items().filter_map(|item| {
1638 if item.is_dirty(cx) {
1639 Some((pane.downgrade(), item.boxed_clone()))
1640 } else {
1641 None
1642 }
1643 })
1644 })
1645 .collect::<Vec<_>>();
1646
1647 let project = self.project.clone();
1648 cx.spawn(|workspace, mut cx| async move {
1649 // Override save mode and display "Save all files" prompt
1650 if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
1651 let answer = workspace.update(&mut cx, |_, cx| {
1652 let (prompt, detail) = Pane::file_names_for_prompt(
1653 &mut dirty_items.iter().map(|(_, handle)| handle),
1654 dirty_items.len(),
1655 cx,
1656 );
1657 cx.prompt(
1658 PromptLevel::Warning,
1659 &prompt,
1660 Some(&detail),
1661 &["Save all", "Discard all", "Cancel"],
1662 )
1663 })?;
1664 match answer.await.log_err() {
1665 Some(0) => save_intent = SaveIntent::SaveAll,
1666 Some(1) => save_intent = SaveIntent::Skip,
1667 _ => {}
1668 }
1669 }
1670 for (pane, item) in dirty_items {
1671 let (singleton, project_entry_ids) =
1672 cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
1673 if singleton || !project_entry_ids.is_empty() {
1674 if let Some(ix) =
1675 pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
1676 {
1677 if !Pane::save_item(
1678 project.clone(),
1679 &pane,
1680 ix,
1681 &*item,
1682 save_intent,
1683 &mut cx,
1684 )
1685 .await?
1686 {
1687 return Ok(false);
1688 }
1689 }
1690 }
1691 }
1692 Ok(true)
1693 })
1694 }
1695
1696 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
1697 self.client()
1698 .telemetry()
1699 .report_app_event("open project".to_string());
1700 let paths = self.prompt_for_open_path(
1701 PathPromptOptions {
1702 files: true,
1703 directories: true,
1704 multiple: true,
1705 },
1706 cx,
1707 );
1708
1709 cx.spawn(|this, mut cx| async move {
1710 let Some(paths) = paths.await.log_err().flatten() else {
1711 return;
1712 };
1713
1714 if let Some(task) = this
1715 .update(&mut cx, |this, cx| {
1716 this.open_workspace_for_paths(false, paths, cx)
1717 })
1718 .log_err()
1719 {
1720 task.await.log_err();
1721 }
1722 })
1723 .detach()
1724 }
1725
1726 pub fn open_workspace_for_paths(
1727 &mut self,
1728 replace_current_window: bool,
1729 paths: Vec<PathBuf>,
1730 cx: &mut ViewContext<Self>,
1731 ) -> Task<Result<()>> {
1732 let window = cx.window_handle().downcast::<Self>();
1733 let is_remote = self.project.read(cx).is_remote();
1734 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1735 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1736
1737 let window_to_replace = if replace_current_window {
1738 window
1739 } else if is_remote || has_worktree || has_dirty_items {
1740 None
1741 } else {
1742 window
1743 };
1744 let app_state = self.app_state.clone();
1745
1746 cx.spawn(|_, mut cx| async move {
1747 cx.update(|cx| {
1748 open_paths(
1749 &paths,
1750 app_state,
1751 OpenOptions {
1752 replace_window: window_to_replace,
1753 ..Default::default()
1754 },
1755 cx,
1756 )
1757 })?
1758 .await?;
1759 Ok(())
1760 })
1761 }
1762
1763 #[allow(clippy::type_complexity)]
1764 pub fn open_paths(
1765 &mut self,
1766 mut abs_paths: Vec<PathBuf>,
1767 visible: OpenVisible,
1768 pane: Option<WeakView<Pane>>,
1769 cx: &mut ViewContext<Self>,
1770 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1771 log::info!("open paths {abs_paths:?}");
1772
1773 let fs = self.app_state.fs.clone();
1774
1775 // Sort the paths to ensure we add worktrees for parents before their children.
1776 abs_paths.sort_unstable();
1777 cx.spawn(move |this, mut cx| async move {
1778 let mut tasks = Vec::with_capacity(abs_paths.len());
1779
1780 for abs_path in &abs_paths {
1781 let visible = match visible {
1782 OpenVisible::All => Some(true),
1783 OpenVisible::None => Some(false),
1784 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
1785 Some(Some(metadata)) => Some(!metadata.is_dir),
1786 Some(None) => Some(true),
1787 None => None,
1788 },
1789 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
1790 Some(Some(metadata)) => Some(metadata.is_dir),
1791 Some(None) => Some(false),
1792 None => None,
1793 },
1794 };
1795 let project_path = match visible {
1796 Some(visible) => match this
1797 .update(&mut cx, |this, cx| {
1798 Workspace::project_path_for_path(
1799 this.project.clone(),
1800 abs_path,
1801 visible,
1802 cx,
1803 )
1804 })
1805 .log_err()
1806 {
1807 Some(project_path) => project_path.await.log_err(),
1808 None => None,
1809 },
1810 None => None,
1811 };
1812
1813 let this = this.clone();
1814 let abs_path = abs_path.clone();
1815 let fs = fs.clone();
1816 let pane = pane.clone();
1817 let task = cx.spawn(move |mut cx| async move {
1818 let (worktree, project_path) = project_path?;
1819 if fs.is_dir(&abs_path).await {
1820 this.update(&mut cx, |workspace, cx| {
1821 let worktree = worktree.read(cx);
1822 let worktree_abs_path = worktree.abs_path();
1823 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1824 worktree.root_entry()
1825 } else {
1826 abs_path
1827 .strip_prefix(worktree_abs_path.as_ref())
1828 .ok()
1829 .and_then(|relative_path| {
1830 worktree.entry_for_path(relative_path)
1831 })
1832 }
1833 .map(|entry| entry.id);
1834 if let Some(entry_id) = entry_id {
1835 workspace.project.update(cx, |_, cx| {
1836 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1837 })
1838 }
1839 })
1840 .log_err()?;
1841 None
1842 } else {
1843 Some(
1844 this.update(&mut cx, |this, cx| {
1845 this.open_path(project_path, pane, true, cx)
1846 })
1847 .log_err()?
1848 .await,
1849 )
1850 }
1851 });
1852 tasks.push(task);
1853 }
1854
1855 futures::future::join_all(tasks).await
1856 })
1857 }
1858
1859 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1860 if self.project.read(cx).is_remote() {
1861 self.show_error(
1862 &anyhow!("Folders cannot yet be added to remote projects"),
1863 cx,
1864 );
1865 return;
1866 }
1867 let paths = self.prompt_for_open_path(
1868 PathPromptOptions {
1869 files: false,
1870 directories: true,
1871 multiple: true,
1872 },
1873 cx,
1874 );
1875 cx.spawn(|this, mut cx| async move {
1876 if let Some(paths) = paths.await.log_err().flatten() {
1877 let results = this
1878 .update(&mut cx, |this, cx| {
1879 this.open_paths(paths, OpenVisible::All, None, cx)
1880 })?
1881 .await;
1882 for result in results.into_iter().flatten() {
1883 result.log_err();
1884 }
1885 }
1886 anyhow::Ok(())
1887 })
1888 .detach_and_log_err(cx);
1889 }
1890
1891 fn project_path_for_path(
1892 project: Model<Project>,
1893 abs_path: &Path,
1894 visible: bool,
1895 cx: &mut AppContext,
1896 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1897 let entry = project.update(cx, |project, cx| {
1898 project.find_or_create_local_worktree(abs_path, visible, cx)
1899 });
1900 cx.spawn(|mut cx| async move {
1901 let (worktree, path) = entry.await?;
1902 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1903 Ok((
1904 worktree,
1905 ProjectPath {
1906 worktree_id,
1907 path: path.into(),
1908 },
1909 ))
1910 })
1911 }
1912
1913 pub fn items<'a>(
1914 &'a self,
1915 cx: &'a AppContext,
1916 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1917 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1918 }
1919
1920 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1921 self.items_of_type(cx).max_by_key(|item| item.item_id())
1922 }
1923
1924 pub fn items_of_type<'a, T: Item>(
1925 &'a self,
1926 cx: &'a AppContext,
1927 ) -> impl 'a + Iterator<Item = View<T>> {
1928 self.panes
1929 .iter()
1930 .flat_map(|pane| pane.read(cx).items_of_type())
1931 }
1932
1933 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1934 self.active_pane().read(cx).active_item()
1935 }
1936
1937 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1938 let item = self.active_item(cx)?;
1939 item.to_any().downcast::<I>().ok()
1940 }
1941
1942 fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
1943 self.active_item(cx).and_then(|item| item.project_path(cx))
1944 }
1945
1946 pub fn save_active_item(
1947 &mut self,
1948 save_intent: SaveIntent,
1949 cx: &mut WindowContext,
1950 ) -> Task<Result<()>> {
1951 let project = self.project.clone();
1952 let pane = self.active_pane();
1953 let item_ix = pane.read(cx).active_item_index();
1954 let item = pane.read(cx).active_item();
1955 let pane = pane.downgrade();
1956
1957 cx.spawn(|mut cx| async move {
1958 if let Some(item) = item {
1959 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1960 .await
1961 .map(|_| ())
1962 } else {
1963 Ok(())
1964 }
1965 })
1966 }
1967
1968 pub fn close_inactive_items_and_panes(
1969 &mut self,
1970 action: &CloseInactiveTabsAndPanes,
1971 cx: &mut ViewContext<Self>,
1972 ) {
1973 if let Some(task) =
1974 self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1975 {
1976 task.detach_and_log_err(cx)
1977 }
1978 }
1979
1980 pub fn close_all_items_and_panes(
1981 &mut self,
1982 action: &CloseAllItemsAndPanes,
1983 cx: &mut ViewContext<Self>,
1984 ) {
1985 if let Some(task) =
1986 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1987 {
1988 task.detach_and_log_err(cx)
1989 }
1990 }
1991
1992 fn close_all_internal(
1993 &mut self,
1994 retain_active_pane: bool,
1995 save_intent: SaveIntent,
1996 cx: &mut ViewContext<Self>,
1997 ) -> Option<Task<Result<()>>> {
1998 let current_pane = self.active_pane();
1999
2000 let mut tasks = Vec::new();
2001
2002 if retain_active_pane {
2003 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2004 pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2005 }) {
2006 tasks.push(current_pane_close);
2007 };
2008 }
2009
2010 for pane in self.panes() {
2011 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2012 continue;
2013 }
2014
2015 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2016 pane.close_all_items(
2017 &CloseAllItems {
2018 save_intent: Some(save_intent),
2019 },
2020 cx,
2021 )
2022 }) {
2023 tasks.push(close_pane_items)
2024 }
2025 }
2026
2027 if tasks.is_empty() {
2028 None
2029 } else {
2030 Some(cx.spawn(|_, _| async move {
2031 for task in tasks {
2032 task.await?
2033 }
2034 Ok(())
2035 }))
2036 }
2037 }
2038
2039 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
2040 let dock = match dock_side {
2041 DockPosition::Left => &self.left_dock,
2042 DockPosition::Bottom => &self.bottom_dock,
2043 DockPosition::Right => &self.right_dock,
2044 };
2045 let mut focus_center = false;
2046 let mut reveal_dock = false;
2047 dock.update(cx, |dock, cx| {
2048 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2049 let was_visible = dock.is_open() && !other_is_zoomed;
2050 dock.set_open(!was_visible, cx);
2051
2052 if let Some(active_panel) = dock.active_panel() {
2053 if was_visible {
2054 if active_panel.focus_handle(cx).contains_focused(cx) {
2055 focus_center = true;
2056 }
2057 } else {
2058 let focus_handle = &active_panel.focus_handle(cx);
2059 cx.focus(focus_handle);
2060 reveal_dock = true;
2061 }
2062 }
2063 });
2064
2065 if reveal_dock {
2066 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2067 }
2068
2069 if focus_center {
2070 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2071 }
2072
2073 cx.notify();
2074 self.serialize_workspace(cx);
2075 }
2076
2077 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2078 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2079
2080 for dock in docks {
2081 dock.update(cx, |dock, cx| {
2082 dock.set_open(false, cx);
2083 });
2084 }
2085
2086 cx.focus_self();
2087 cx.notify();
2088 self.serialize_workspace(cx);
2089 }
2090
2091 /// Transfer focus to the panel of the given type.
2092 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2093 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2094 panel.to_any().downcast().ok()
2095 }
2096
2097 /// Focus the panel of the given type if it isn't already focused. If it is
2098 /// already focused, then transfer focus back to the workspace center.
2099 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2100 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2101 !panel.focus_handle(cx).contains_focused(cx)
2102 });
2103 }
2104
2105 pub fn activate_panel_for_proto_id(
2106 &mut self,
2107 panel_id: PanelId,
2108 cx: &mut ViewContext<Self>,
2109 ) -> Option<Arc<dyn PanelHandle>> {
2110 let mut panel = None;
2111 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2112 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2113 panel = dock.update(cx, |dock, cx| {
2114 dock.activate_panel(panel_index, cx);
2115 dock.set_open(true, cx);
2116 dock.active_panel().cloned()
2117 });
2118 break;
2119 }
2120 }
2121
2122 if panel.is_some() {
2123 cx.notify();
2124 self.serialize_workspace(cx);
2125 }
2126
2127 panel
2128 }
2129
2130 /// Focus or unfocus the given panel type, depending on the given callback.
2131 fn focus_or_unfocus_panel<T: Panel>(
2132 &mut self,
2133 cx: &mut ViewContext<Self>,
2134 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2135 ) -> Option<Arc<dyn PanelHandle>> {
2136 let mut result_panel = None;
2137 let mut serialize = false;
2138 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2139 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2140 let mut focus_center = false;
2141 let panel = dock.update(cx, |dock, cx| {
2142 dock.activate_panel(panel_index, cx);
2143
2144 let panel = dock.active_panel().cloned();
2145 if let Some(panel) = panel.as_ref() {
2146 if should_focus(&**panel, cx) {
2147 dock.set_open(true, cx);
2148 panel.focus_handle(cx).focus(cx);
2149 } else {
2150 focus_center = true;
2151 }
2152 }
2153 panel
2154 });
2155
2156 if focus_center {
2157 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2158 }
2159
2160 result_panel = panel;
2161 serialize = true;
2162 break;
2163 }
2164 }
2165
2166 if serialize {
2167 self.serialize_workspace(cx);
2168 }
2169
2170 cx.notify();
2171 result_panel
2172 }
2173
2174 /// Open the panel of the given type
2175 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2176 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2177 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2178 dock.update(cx, |dock, cx| {
2179 dock.activate_panel(panel_index, cx);
2180 dock.set_open(true, cx);
2181 });
2182 }
2183 }
2184 }
2185
2186 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2187 [&self.left_dock, &self.bottom_dock, &self.right_dock]
2188 .iter()
2189 .find_map(|dock| dock.read(cx).panel::<T>())
2190 }
2191
2192 fn dismiss_zoomed_items_to_reveal(
2193 &mut self,
2194 dock_to_reveal: Option<DockPosition>,
2195 cx: &mut ViewContext<Self>,
2196 ) {
2197 // If a center pane is zoomed, unzoom it.
2198 for pane in &self.panes {
2199 if pane != &self.active_pane || dock_to_reveal.is_some() {
2200 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2201 }
2202 }
2203
2204 // If another dock is zoomed, hide it.
2205 let mut focus_center = false;
2206 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2207 dock.update(cx, |dock, cx| {
2208 if Some(dock.position()) != dock_to_reveal {
2209 if let Some(panel) = dock.active_panel() {
2210 if panel.is_zoomed(cx) {
2211 focus_center |= panel.focus_handle(cx).contains_focused(cx);
2212 dock.set_open(false, cx);
2213 }
2214 }
2215 }
2216 });
2217 }
2218
2219 if focus_center {
2220 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2221 }
2222
2223 if self.zoomed_position != dock_to_reveal {
2224 self.zoomed = None;
2225 self.zoomed_position = None;
2226 cx.emit(Event::ZoomChanged);
2227 }
2228
2229 cx.notify();
2230 }
2231
2232 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2233 let pane = cx.new_view(|cx| {
2234 Pane::new(
2235 self.weak_handle(),
2236 self.project.clone(),
2237 self.pane_history_timestamp.clone(),
2238 None,
2239 NewFile.boxed_clone(),
2240 cx,
2241 )
2242 });
2243 cx.subscribe(&pane, Self::handle_pane_event).detach();
2244 self.panes.push(pane.clone());
2245 cx.focus_view(&pane);
2246 cx.emit(Event::PaneAdded(pane.clone()));
2247 pane
2248 }
2249
2250 pub fn add_item_to_center(
2251 &mut self,
2252 item: Box<dyn ItemHandle>,
2253 cx: &mut ViewContext<Self>,
2254 ) -> bool {
2255 if let Some(center_pane) = self.last_active_center_pane.clone() {
2256 if let Some(center_pane) = center_pane.upgrade() {
2257 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2258 true
2259 } else {
2260 false
2261 }
2262 } else {
2263 false
2264 }
2265 }
2266
2267 pub fn add_item_to_active_pane(
2268 &mut self,
2269 item: Box<dyn ItemHandle>,
2270 destination_index: Option<usize>,
2271 cx: &mut WindowContext,
2272 ) {
2273 self.add_item(self.active_pane.clone(), item, destination_index, cx)
2274 }
2275
2276 pub fn add_item(
2277 &mut self,
2278 pane: View<Pane>,
2279 item: Box<dyn ItemHandle>,
2280 destination_index: Option<usize>,
2281 cx: &mut WindowContext,
2282 ) {
2283 if let Some(text) = item.telemetry_event_text(cx) {
2284 self.client()
2285 .telemetry()
2286 .report_app_event(format!("{}: open", text));
2287 }
2288
2289 pane.update(cx, |pane, cx| {
2290 pane.add_item(item, true, true, destination_index, cx)
2291 });
2292 }
2293
2294 pub fn split_item(
2295 &mut self,
2296 split_direction: SplitDirection,
2297 item: Box<dyn ItemHandle>,
2298 cx: &mut ViewContext<Self>,
2299 ) {
2300 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2301 self.add_item(new_pane, item, None, cx);
2302 }
2303
2304 pub fn open_abs_path(
2305 &mut self,
2306 abs_path: PathBuf,
2307 visible: bool,
2308 cx: &mut ViewContext<Self>,
2309 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2310 cx.spawn(|workspace, mut cx| async move {
2311 let open_paths_task_result = workspace
2312 .update(&mut cx, |workspace, cx| {
2313 workspace.open_paths(
2314 vec![abs_path.clone()],
2315 if visible {
2316 OpenVisible::All
2317 } else {
2318 OpenVisible::None
2319 },
2320 None,
2321 cx,
2322 )
2323 })
2324 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2325 .await;
2326 anyhow::ensure!(
2327 open_paths_task_result.len() == 1,
2328 "open abs path {abs_path:?} task returned incorrect number of results"
2329 );
2330 match open_paths_task_result
2331 .into_iter()
2332 .next()
2333 .expect("ensured single task result")
2334 {
2335 Some(open_result) => {
2336 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2337 }
2338 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2339 }
2340 })
2341 }
2342
2343 pub fn split_abs_path(
2344 &mut self,
2345 abs_path: PathBuf,
2346 visible: bool,
2347 cx: &mut ViewContext<Self>,
2348 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2349 let project_path_task =
2350 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2351 cx.spawn(|this, mut cx| async move {
2352 let (_, path) = project_path_task.await?;
2353 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2354 .await
2355 })
2356 }
2357
2358 pub fn open_path(
2359 &mut self,
2360 path: impl Into<ProjectPath>,
2361 pane: Option<WeakView<Pane>>,
2362 focus_item: bool,
2363 cx: &mut WindowContext,
2364 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2365 self.open_path_preview(path, pane, focus_item, false, cx)
2366 }
2367
2368 pub fn open_path_preview(
2369 &mut self,
2370 path: impl Into<ProjectPath>,
2371 pane: Option<WeakView<Pane>>,
2372 focus_item: bool,
2373 allow_preview: bool,
2374 cx: &mut WindowContext,
2375 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2376 let pane = pane.unwrap_or_else(|| {
2377 self.last_active_center_pane.clone().unwrap_or_else(|| {
2378 self.panes
2379 .first()
2380 .expect("There must be an active pane")
2381 .downgrade()
2382 })
2383 });
2384
2385 let task = self.load_path(path.into(), cx);
2386 cx.spawn(move |mut cx| async move {
2387 let (project_entry_id, build_item) = task.await?;
2388 pane.update(&mut cx, |pane, cx| {
2389 pane.open_item(project_entry_id, focus_item, allow_preview, cx, build_item)
2390 })
2391 })
2392 }
2393
2394 pub fn split_path(
2395 &mut self,
2396 path: impl Into<ProjectPath>,
2397 cx: &mut ViewContext<Self>,
2398 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2399 self.split_path_preview(path, false, cx)
2400 }
2401
2402 pub fn split_path_preview(
2403 &mut self,
2404 path: impl Into<ProjectPath>,
2405 allow_preview: bool,
2406 cx: &mut ViewContext<Self>,
2407 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2408 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2409 self.panes
2410 .first()
2411 .expect("There must be an active pane")
2412 .downgrade()
2413 });
2414
2415 if let Member::Pane(center_pane) = &self.center.root {
2416 if center_pane.read(cx).items_len() == 0 {
2417 return self.open_path(path, Some(pane), true, cx);
2418 }
2419 }
2420
2421 let task = self.load_path(path.into(), cx);
2422 cx.spawn(|this, mut cx| async move {
2423 let (project_entry_id, build_item) = task.await?;
2424 this.update(&mut cx, move |this, cx| -> Option<_> {
2425 let pane = pane.upgrade()?;
2426 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2427 new_pane.update(cx, |new_pane, cx| {
2428 Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item))
2429 })
2430 })
2431 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2432 })
2433 }
2434
2435 fn load_path(
2436 &mut self,
2437 path: ProjectPath,
2438 cx: &mut WindowContext,
2439 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2440 let project = self.project().clone();
2441 let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2442 let Some(open_project_item) = project_item_builders
2443 .iter()
2444 .rev()
2445 .find_map(|open_project_item| open_project_item(&project, &path, cx))
2446 else {
2447 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2448 };
2449 open_project_item
2450 }
2451
2452 pub fn open_project_item<T>(
2453 &mut self,
2454 pane: View<Pane>,
2455 project_item: Model<T::Item>,
2456 cx: &mut ViewContext<Self>,
2457 ) -> View<T>
2458 where
2459 T: ProjectItem,
2460 {
2461 use project::Item as _;
2462
2463 let entry_id = project_item.read(cx).entry_id(cx);
2464 if let Some(item) = entry_id
2465 .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
2466 .and_then(|item| item.downcast())
2467 {
2468 self.activate_item(&item, cx);
2469 return item;
2470 }
2471
2472 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2473
2474 let item_id = item.item_id();
2475 let mut destination_index = None;
2476 pane.update(cx, |pane, cx| {
2477 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2478 if let Some(preview_item_id) = pane.preview_item_id() {
2479 if preview_item_id != item_id {
2480 destination_index = pane.close_current_preview_item(cx);
2481 }
2482 }
2483 }
2484 pane.set_preview_item_id(Some(item.item_id()), cx)
2485 });
2486
2487 self.add_item(pane, Box::new(item.clone()), destination_index, cx);
2488 item
2489 }
2490
2491 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2492 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2493 self.active_pane.update(cx, |pane, cx| {
2494 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2495 });
2496 }
2497 }
2498
2499 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2500 let result = self.panes.iter().find_map(|pane| {
2501 pane.read(cx)
2502 .index_for_item(item)
2503 .map(|ix| (pane.clone(), ix))
2504 });
2505 if let Some((pane, ix)) = result {
2506 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2507 true
2508 } else {
2509 false
2510 }
2511 }
2512
2513 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2514 let panes = self.center.panes();
2515 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2516 cx.focus_view(&pane);
2517 } else {
2518 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2519 }
2520 }
2521
2522 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2523 let panes = self.center.panes();
2524 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2525 let next_ix = (ix + 1) % panes.len();
2526 let next_pane = panes[next_ix].clone();
2527 cx.focus_view(&next_pane);
2528 }
2529 }
2530
2531 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2532 let panes = self.center.panes();
2533 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2534 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2535 let prev_pane = panes[prev_ix].clone();
2536 cx.focus_view(&prev_pane);
2537 }
2538 }
2539
2540 pub fn activate_pane_in_direction(
2541 &mut self,
2542 direction: SplitDirection,
2543 cx: &mut WindowContext,
2544 ) {
2545 use ActivateInDirectionTarget as Target;
2546 enum Origin {
2547 LeftDock,
2548 RightDock,
2549 BottomDock,
2550 Center,
2551 }
2552
2553 let origin: Origin = [
2554 (&self.left_dock, Origin::LeftDock),
2555 (&self.right_dock, Origin::RightDock),
2556 (&self.bottom_dock, Origin::BottomDock),
2557 ]
2558 .into_iter()
2559 .find_map(|(dock, origin)| {
2560 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2561 Some(origin)
2562 } else {
2563 None
2564 }
2565 })
2566 .unwrap_or(Origin::Center);
2567
2568 let get_last_active_pane = || {
2569 self.last_active_center_pane.as_ref().and_then(|p| {
2570 let p = p.upgrade()?;
2571 (p.read(cx).items_len() != 0).then_some(p)
2572 })
2573 };
2574
2575 let try_dock =
2576 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2577
2578 let target = match (origin, direction) {
2579 // We're in the center, so we first try to go to a different pane,
2580 // otherwise try to go to a dock.
2581 (Origin::Center, direction) => {
2582 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2583 Some(Target::Pane(pane))
2584 } else {
2585 match direction {
2586 SplitDirection::Up => None,
2587 SplitDirection::Down => try_dock(&self.bottom_dock),
2588 SplitDirection::Left => try_dock(&self.left_dock),
2589 SplitDirection::Right => try_dock(&self.right_dock),
2590 }
2591 }
2592 }
2593
2594 (Origin::LeftDock, SplitDirection::Right) => {
2595 if let Some(last_active_pane) = get_last_active_pane() {
2596 Some(Target::Pane(last_active_pane))
2597 } else {
2598 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2599 }
2600 }
2601
2602 (Origin::LeftDock, SplitDirection::Down)
2603 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2604
2605 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2606 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2607 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2608
2609 (Origin::RightDock, SplitDirection::Left) => {
2610 if let Some(last_active_pane) = get_last_active_pane() {
2611 Some(Target::Pane(last_active_pane))
2612 } else {
2613 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2614 }
2615 }
2616
2617 _ => None,
2618 };
2619
2620 match target {
2621 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2622 Some(ActivateInDirectionTarget::Dock(dock)) => {
2623 if let Some(panel) = dock.read(cx).active_panel() {
2624 panel.focus_handle(cx).focus(cx);
2625 } else {
2626 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2627 }
2628 }
2629 None => {}
2630 }
2631 }
2632
2633 pub fn find_pane_in_direction(
2634 &mut self,
2635 direction: SplitDirection,
2636 cx: &WindowContext,
2637 ) -> Option<View<Pane>> {
2638 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2639 return None;
2640 };
2641 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2642 let center = match cursor {
2643 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2644 _ => bounding_box.center(),
2645 };
2646
2647 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2648
2649 let target = match direction {
2650 SplitDirection::Left => {
2651 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2652 }
2653 SplitDirection::Right => {
2654 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2655 }
2656 SplitDirection::Up => {
2657 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2658 }
2659 SplitDirection::Down => {
2660 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2661 }
2662 };
2663 self.center.pane_at_pixel_position(target).cloned()
2664 }
2665
2666 pub fn swap_pane_in_direction(
2667 &mut self,
2668 direction: SplitDirection,
2669 cx: &mut ViewContext<Self>,
2670 ) {
2671 if let Some(to) = self
2672 .find_pane_in_direction(direction, cx)
2673 .map(|pane| pane.clone())
2674 {
2675 self.center.swap(&self.active_pane.clone(), &to);
2676 cx.notify();
2677 }
2678 }
2679
2680 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2681 // This is explicitly hoisted out of the following check for pane identity as
2682 // terminal panel panes are not registered as a center panes.
2683 self.status_bar.update(cx, |status_bar, cx| {
2684 status_bar.set_active_pane(&pane, cx);
2685 });
2686 if self.active_pane != pane {
2687 self.active_pane = pane.clone();
2688 self.active_item_path_changed(cx);
2689 self.last_active_center_pane = Some(pane.downgrade());
2690 }
2691
2692 self.dismiss_zoomed_items_to_reveal(None, cx);
2693 if pane.read(cx).is_zoomed() {
2694 self.zoomed = Some(pane.downgrade().into());
2695 } else {
2696 self.zoomed = None;
2697 }
2698 self.zoomed_position = None;
2699 cx.emit(Event::ZoomChanged);
2700 self.update_active_view_for_followers(cx);
2701 pane.model.update(cx, |pane, _| {
2702 pane.track_alternate_file_items();
2703 });
2704
2705 cx.notify();
2706 }
2707
2708 fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
2709 self.update_active_view_for_followers(cx);
2710 }
2711
2712 fn handle_pane_event(
2713 &mut self,
2714 pane: View<Pane>,
2715 event: &pane::Event,
2716 cx: &mut ViewContext<Self>,
2717 ) {
2718 match event {
2719 pane::Event::AddItem { item } => {
2720 item.added_to_pane(self, pane, cx);
2721 cx.emit(Event::ItemAdded);
2722 }
2723 pane::Event::Split(direction) => {
2724 self.split_and_clone(pane, *direction, cx);
2725 }
2726 pane::Event::Remove => self.remove_pane(pane, cx),
2727 pane::Event::ActivateItem { local } => {
2728 pane.model.update(cx, |pane, _| {
2729 pane.track_alternate_file_items();
2730 });
2731 if *local {
2732 self.unfollow_in_pane(&pane, cx);
2733 }
2734 if &pane == self.active_pane() {
2735 self.active_item_path_changed(cx);
2736 self.update_active_view_for_followers(cx);
2737 }
2738 }
2739 pane::Event::ChangeItemTitle => {
2740 if pane == self.active_pane {
2741 self.active_item_path_changed(cx);
2742 }
2743 self.update_window_edited(cx);
2744 }
2745 pane::Event::RemoveItem { item_id } => {
2746 cx.emit(Event::ActiveItemChanged);
2747 self.update_window_edited(cx);
2748 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2749 if entry.get().entity_id() == pane.entity_id() {
2750 entry.remove();
2751 }
2752 }
2753 }
2754 pane::Event::Focus => {
2755 self.handle_pane_focused(pane.clone(), cx);
2756 }
2757 pane::Event::ZoomIn => {
2758 if pane == self.active_pane {
2759 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2760 if pane.read(cx).has_focus(cx) {
2761 self.zoomed = Some(pane.downgrade().into());
2762 self.zoomed_position = None;
2763 cx.emit(Event::ZoomChanged);
2764 }
2765 cx.notify();
2766 }
2767 }
2768 pane::Event::ZoomOut => {
2769 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2770 if self.zoomed_position.is_none() {
2771 self.zoomed = None;
2772 cx.emit(Event::ZoomChanged);
2773 }
2774 cx.notify();
2775 }
2776 }
2777
2778 self.serialize_workspace(cx);
2779 }
2780
2781 pub fn unfollow_in_pane(
2782 &mut self,
2783 pane: &View<Pane>,
2784 cx: &mut ViewContext<Workspace>,
2785 ) -> Option<PeerId> {
2786 let leader_id = self.leader_for_pane(pane)?;
2787 self.unfollow(leader_id, cx);
2788 Some(leader_id)
2789 }
2790
2791 pub fn split_pane(
2792 &mut self,
2793 pane_to_split: View<Pane>,
2794 split_direction: SplitDirection,
2795 cx: &mut ViewContext<Self>,
2796 ) -> View<Pane> {
2797 let new_pane = self.add_pane(cx);
2798 self.center
2799 .split(&pane_to_split, &new_pane, split_direction)
2800 .unwrap();
2801 cx.notify();
2802 new_pane
2803 }
2804
2805 pub fn split_and_clone(
2806 &mut self,
2807 pane: View<Pane>,
2808 direction: SplitDirection,
2809 cx: &mut ViewContext<Self>,
2810 ) -> Option<View<Pane>> {
2811 let item = pane.read(cx).active_item()?;
2812 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2813 let new_pane = self.add_pane(cx);
2814 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2815 self.center.split(&pane, &new_pane, direction).unwrap();
2816 Some(new_pane)
2817 } else {
2818 None
2819 };
2820 cx.notify();
2821 maybe_pane_handle
2822 }
2823
2824 pub fn split_pane_with_item(
2825 &mut self,
2826 pane_to_split: WeakView<Pane>,
2827 split_direction: SplitDirection,
2828 from: WeakView<Pane>,
2829 item_id_to_move: EntityId,
2830 cx: &mut ViewContext<Self>,
2831 ) {
2832 let Some(pane_to_split) = pane_to_split.upgrade() else {
2833 return;
2834 };
2835 let Some(from) = from.upgrade() else {
2836 return;
2837 };
2838
2839 let new_pane = self.add_pane(cx);
2840 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2841 self.center
2842 .split(&pane_to_split, &new_pane, split_direction)
2843 .unwrap();
2844 cx.notify();
2845 }
2846
2847 pub fn split_pane_with_project_entry(
2848 &mut self,
2849 pane_to_split: WeakView<Pane>,
2850 split_direction: SplitDirection,
2851 project_entry: ProjectEntryId,
2852 cx: &mut ViewContext<Self>,
2853 ) -> Option<Task<Result<()>>> {
2854 let pane_to_split = pane_to_split.upgrade()?;
2855 let new_pane = self.add_pane(cx);
2856 self.center
2857 .split(&pane_to_split, &new_pane, split_direction)
2858 .unwrap();
2859
2860 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2861 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2862 Some(cx.foreground_executor().spawn(async move {
2863 task.await?;
2864 Ok(())
2865 }))
2866 }
2867
2868 pub fn move_item(
2869 &mut self,
2870 source: View<Pane>,
2871 destination: View<Pane>,
2872 item_id_to_move: EntityId,
2873 destination_index: usize,
2874 cx: &mut ViewContext<Self>,
2875 ) {
2876 let Some((item_ix, item_handle)) = source
2877 .read(cx)
2878 .items()
2879 .enumerate()
2880 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2881 else {
2882 // Tab was closed during drag
2883 return;
2884 };
2885
2886 let item_handle = item_handle.clone();
2887
2888 if source != destination {
2889 // Close item from previous pane
2890 source.update(cx, |source, cx| {
2891 source.remove_item(item_ix, false, true, cx);
2892 });
2893 }
2894
2895 // This automatically removes duplicate items in the pane
2896 destination.update(cx, |destination, cx| {
2897 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2898 destination.focus(cx)
2899 });
2900 }
2901
2902 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2903 if self.center.remove(&pane).unwrap() {
2904 self.force_remove_pane(&pane, cx);
2905 self.unfollow_in_pane(&pane, cx);
2906 self.last_leaders_by_pane.remove(&pane.downgrade());
2907 for removed_item in pane.read(cx).items() {
2908 self.panes_by_item.remove(&removed_item.item_id());
2909 }
2910
2911 cx.notify();
2912 } else {
2913 self.active_item_path_changed(cx);
2914 }
2915 cx.emit(Event::PaneRemoved);
2916 }
2917
2918 pub fn panes(&self) -> &[View<Pane>] {
2919 &self.panes
2920 }
2921
2922 pub fn active_pane(&self) -> &View<Pane> {
2923 &self.active_pane
2924 }
2925
2926 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2927 self.find_pane_in_direction(SplitDirection::Right, cx)
2928 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
2929 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
2930 .clone()
2931 }
2932
2933 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2934 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2935 weak_pane.upgrade()
2936 }
2937
2938 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2939 self.follower_states.retain(|leader_id, state| {
2940 if *leader_id == peer_id {
2941 for item in state.items_by_leader_view_id.values() {
2942 item.view.set_leader_peer_id(None, cx);
2943 }
2944 false
2945 } else {
2946 true
2947 }
2948 });
2949 cx.notify();
2950 }
2951
2952 pub fn start_following(
2953 &mut self,
2954 leader_id: PeerId,
2955 cx: &mut ViewContext<Self>,
2956 ) -> Option<Task<Result<()>>> {
2957 let pane = self.active_pane().clone();
2958
2959 self.last_leaders_by_pane
2960 .insert(pane.downgrade(), leader_id);
2961 self.unfollow(leader_id, cx);
2962 self.unfollow_in_pane(&pane, cx);
2963 self.follower_states.insert(
2964 leader_id,
2965 FollowerState {
2966 center_pane: pane.clone(),
2967 dock_pane: None,
2968 active_view_id: None,
2969 items_by_leader_view_id: Default::default(),
2970 },
2971 );
2972 cx.notify();
2973
2974 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2975 let project_id = self.project.read(cx).remote_id();
2976 let request = self.app_state.client.request(proto::Follow {
2977 room_id,
2978 project_id,
2979 leader_id: Some(leader_id),
2980 });
2981
2982 Some(cx.spawn(|this, mut cx| async move {
2983 let response = request.await?;
2984 this.update(&mut cx, |this, _| {
2985 let state = this
2986 .follower_states
2987 .get_mut(&leader_id)
2988 .ok_or_else(|| anyhow!("following interrupted"))?;
2989 state.active_view_id = response
2990 .active_view
2991 .as_ref()
2992 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
2993 Ok::<_, anyhow::Error>(())
2994 })??;
2995 if let Some(view) = response.active_view {
2996 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
2997 }
2998 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2999 Ok(())
3000 }))
3001 }
3002
3003 pub fn follow_next_collaborator(
3004 &mut self,
3005 _: &FollowNextCollaborator,
3006 cx: &mut ViewContext<Self>,
3007 ) {
3008 let collaborators = self.project.read(cx).collaborators();
3009 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3010 let mut collaborators = collaborators.keys().copied();
3011 for peer_id in collaborators.by_ref() {
3012 if peer_id == leader_id {
3013 break;
3014 }
3015 }
3016 collaborators.next()
3017 } else if let Some(last_leader_id) =
3018 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3019 {
3020 if collaborators.contains_key(last_leader_id) {
3021 Some(*last_leader_id)
3022 } else {
3023 None
3024 }
3025 } else {
3026 None
3027 };
3028
3029 let pane = self.active_pane.clone();
3030 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3031 else {
3032 return;
3033 };
3034 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3035 return;
3036 }
3037 if let Some(task) = self.start_following(leader_id, cx) {
3038 task.detach_and_log_err(cx)
3039 }
3040 }
3041
3042 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3043 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3044 return;
3045 };
3046 let room = room.read(cx);
3047 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3048 return;
3049 };
3050
3051 let project = self.project.read(cx);
3052
3053 let other_project_id = match remote_participant.location {
3054 call::ParticipantLocation::External => None,
3055 call::ParticipantLocation::UnsharedProject => None,
3056 call::ParticipantLocation::SharedProject { project_id } => {
3057 if Some(project_id) == project.remote_id() {
3058 None
3059 } else {
3060 Some(project_id)
3061 }
3062 }
3063 };
3064
3065 // if they are active in another project, follow there.
3066 if let Some(project_id) = other_project_id {
3067 let app_state = self.app_state.clone();
3068 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3069 .detach_and_log_err(cx);
3070 }
3071
3072 // if you're already following, find the right pane and focus it.
3073 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3074 cx.focus_view(&follower_state.pane());
3075 return;
3076 }
3077
3078 // Otherwise, follow.
3079 if let Some(task) = self.start_following(leader_id, cx) {
3080 task.detach_and_log_err(cx)
3081 }
3082 }
3083
3084 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3085 cx.notify();
3086 let state = self.follower_states.remove(&leader_id)?;
3087 for (_, item) in state.items_by_leader_view_id {
3088 item.view.set_leader_peer_id(None, cx);
3089 }
3090
3091 let project_id = self.project.read(cx).remote_id();
3092 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3093 self.app_state
3094 .client
3095 .send(proto::Unfollow {
3096 room_id,
3097 project_id,
3098 leader_id: Some(leader_id),
3099 })
3100 .log_err();
3101
3102 Some(())
3103 }
3104
3105 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3106 self.follower_states.contains_key(&peer_id)
3107 }
3108
3109 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3110 cx.emit(Event::ActiveItemChanged);
3111 let active_entry = self.active_project_path(cx);
3112 self.project
3113 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3114
3115 self.update_window_title(cx);
3116 }
3117
3118 fn update_window_title(&mut self, cx: &mut WindowContext) {
3119 let project = self.project().read(cx);
3120 let mut title = String::new();
3121
3122 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3123 let filename = path
3124 .path
3125 .file_name()
3126 .map(|s| s.to_string_lossy())
3127 .or_else(|| {
3128 Some(Cow::Borrowed(
3129 project
3130 .worktree_for_id(path.worktree_id, cx)?
3131 .read(cx)
3132 .root_name(),
3133 ))
3134 });
3135
3136 if let Some(filename) = filename {
3137 title.push_str(filename.as_ref());
3138 title.push_str(" — ");
3139 }
3140 }
3141
3142 for (i, name) in project.worktree_root_names(cx).enumerate() {
3143 if i > 0 {
3144 title.push_str(", ");
3145 }
3146 title.push_str(name);
3147 }
3148
3149 if title.is_empty() {
3150 title = "empty project".to_string();
3151 }
3152
3153 if project.is_remote() {
3154 title.push_str(" ↙");
3155 } else if project.is_shared() {
3156 title.push_str(" ↗");
3157 }
3158
3159 cx.set_window_title(&title);
3160 }
3161
3162 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3163 let is_edited = !self.project.read(cx).is_disconnected()
3164 && self
3165 .items(cx)
3166 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3167 if is_edited != self.window_edited {
3168 self.window_edited = is_edited;
3169 cx.set_window_edited(self.window_edited)
3170 }
3171 }
3172
3173 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3174 if self.notifications.is_empty() {
3175 None
3176 } else {
3177 Some(
3178 div()
3179 .absolute()
3180 .right_3()
3181 .bottom_3()
3182 .w_112()
3183 .h_full()
3184 .flex()
3185 .flex_col()
3186 .justify_end()
3187 .gap_2()
3188 .children(
3189 self.notifications
3190 .iter()
3191 .map(|(_, notification)| notification.to_any()),
3192 ),
3193 )
3194 }
3195 }
3196
3197 // RPC handlers
3198
3199 fn active_view_for_follower(
3200 &self,
3201 follower_project_id: Option<u64>,
3202 cx: &mut ViewContext<Self>,
3203 ) -> Option<proto::View> {
3204 let (item, panel_id) = self.active_item_for_followers(cx);
3205 let item = item?;
3206 let leader_id = self
3207 .pane_for(&*item)
3208 .and_then(|pane| self.leader_for_pane(&pane));
3209
3210 let item_handle = item.to_followable_item_handle(cx)?;
3211 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3212 let variant = item_handle.to_state_proto(cx)?;
3213
3214 if item_handle.is_project_item(cx)
3215 && (follower_project_id.is_none()
3216 || follower_project_id != self.project.read(cx).remote_id())
3217 {
3218 return None;
3219 }
3220
3221 Some(proto::View {
3222 id: Some(id.to_proto()),
3223 leader_id,
3224 variant: Some(variant),
3225 panel_id: panel_id.map(|id| id as i32),
3226 })
3227 }
3228
3229 fn handle_follow(
3230 &mut self,
3231 follower_project_id: Option<u64>,
3232 cx: &mut ViewContext<Self>,
3233 ) -> proto::FollowResponse {
3234 let active_view = self.active_view_for_follower(follower_project_id, cx);
3235
3236 cx.notify();
3237 proto::FollowResponse {
3238 // TODO: Remove after version 0.145.x stabilizes.
3239 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3240 views: active_view.iter().cloned().collect(),
3241 active_view,
3242 }
3243 }
3244
3245 fn handle_update_followers(
3246 &mut self,
3247 leader_id: PeerId,
3248 message: proto::UpdateFollowers,
3249 _cx: &mut ViewContext<Self>,
3250 ) {
3251 self.leader_updates_tx
3252 .unbounded_send((leader_id, message))
3253 .ok();
3254 }
3255
3256 async fn process_leader_update(
3257 this: &WeakView<Self>,
3258 leader_id: PeerId,
3259 update: proto::UpdateFollowers,
3260 cx: &mut AsyncWindowContext,
3261 ) -> Result<()> {
3262 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3263 proto::update_followers::Variant::CreateView(view) => {
3264 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3265 let should_add_view = this.update(cx, |this, _| {
3266 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3267 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3268 } else {
3269 anyhow::Ok(false)
3270 }
3271 })??;
3272
3273 if should_add_view {
3274 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3275 }
3276 }
3277 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3278 let should_add_view = this.update(cx, |this, _| {
3279 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3280 state.active_view_id = update_active_view
3281 .view
3282 .as_ref()
3283 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3284
3285 if state.active_view_id.is_some_and(|view_id| {
3286 !state.items_by_leader_view_id.contains_key(&view_id)
3287 }) {
3288 anyhow::Ok(true)
3289 } else {
3290 anyhow::Ok(false)
3291 }
3292 } else {
3293 anyhow::Ok(false)
3294 }
3295 })??;
3296
3297 if should_add_view {
3298 if let Some(view) = update_active_view.view {
3299 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3300 }
3301 }
3302 }
3303 proto::update_followers::Variant::UpdateView(update_view) => {
3304 let variant = update_view
3305 .variant
3306 .ok_or_else(|| anyhow!("missing update view variant"))?;
3307 let id = update_view
3308 .id
3309 .ok_or_else(|| anyhow!("missing update view id"))?;
3310 let mut tasks = Vec::new();
3311 this.update(cx, |this, cx| {
3312 let project = this.project.clone();
3313 if let Some(state) = this.follower_states.get(&leader_id) {
3314 let view_id = ViewId::from_proto(id.clone())?;
3315 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3316 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3317 }
3318 }
3319 anyhow::Ok(())
3320 })??;
3321 try_join_all(tasks).await.log_err();
3322 }
3323 }
3324 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3325 Ok(())
3326 }
3327
3328 async fn add_view_from_leader(
3329 this: WeakView<Self>,
3330 leader_id: PeerId,
3331 view: &proto::View,
3332 cx: &mut AsyncWindowContext,
3333 ) -> Result<()> {
3334 let this = this.upgrade().context("workspace dropped")?;
3335
3336 let Some(id) = view.id.clone() else {
3337 return Err(anyhow!("no id for view"));
3338 };
3339 let id = ViewId::from_proto(id)?;
3340 let panel_id = view.panel_id.and_then(|id| proto::PanelId::from_i32(id));
3341
3342 let pane = this.update(cx, |this, _cx| {
3343 let state = this
3344 .follower_states
3345 .get(&leader_id)
3346 .context("stopped following")?;
3347 anyhow::Ok(state.pane().clone())
3348 })??;
3349 let existing_item = pane.update(cx, |pane, cx| {
3350 let client = this.read(cx).client().clone();
3351 pane.items().find_map(|item| {
3352 let item = item.to_followable_item_handle(cx)?;
3353 if item.remote_id(&client, cx) == Some(id) {
3354 Some(item)
3355 } else {
3356 None
3357 }
3358 })
3359 })?;
3360 let item = if let Some(existing_item) = existing_item {
3361 existing_item
3362 } else {
3363 let variant = view.variant.clone();
3364 if variant.is_none() {
3365 Err(anyhow!("missing view variant"))?;
3366 }
3367
3368 let task = cx.update(|cx| {
3369 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3370 })?;
3371
3372 let Some(task) = task else {
3373 return Err(anyhow!(
3374 "failed to construct view from leader (maybe from a different version of zed?)"
3375 ));
3376 };
3377
3378 let mut new_item = task.await?;
3379 pane.update(cx, |pane, cx| {
3380 let mut item_ix_to_remove = None;
3381 for (ix, item) in pane.items().enumerate() {
3382 if let Some(item) = item.to_followable_item_handle(cx) {
3383 match new_item.dedup(item.as_ref(), cx) {
3384 Some(item::Dedup::KeepExisting) => {
3385 new_item =
3386 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3387 break;
3388 }
3389 Some(item::Dedup::ReplaceExisting) => {
3390 item_ix_to_remove = Some(ix);
3391 break;
3392 }
3393 None => {}
3394 }
3395 }
3396 }
3397
3398 if let Some(ix) = item_ix_to_remove {
3399 pane.remove_item(ix, false, false, cx);
3400 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3401 }
3402 })?;
3403
3404 new_item
3405 };
3406
3407 this.update(cx, |this, cx| {
3408 let state = this.follower_states.get_mut(&leader_id)?;
3409 item.set_leader_peer_id(Some(leader_id), cx);
3410 state.items_by_leader_view_id.insert(
3411 id,
3412 FollowerView {
3413 view: item,
3414 location: panel_id,
3415 },
3416 );
3417
3418 Some(())
3419 })?;
3420
3421 Ok(())
3422 }
3423
3424 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3425 let mut is_project_item = true;
3426 let mut update = proto::UpdateActiveView::default();
3427 if cx.is_window_active() {
3428 let (active_item, panel_id) = self.active_item_for_followers(cx);
3429
3430 if let Some(item) = active_item {
3431 if item.focus_handle(cx).contains_focused(cx) {
3432 let leader_id = self
3433 .pane_for(&*item)
3434 .and_then(|pane| self.leader_for_pane(&pane));
3435
3436 if let Some(item) = item.to_followable_item_handle(cx) {
3437 let id = item
3438 .remote_id(&self.app_state.client, cx)
3439 .map(|id| id.to_proto());
3440
3441 if let Some(id) = id.clone() {
3442 if let Some(variant) = item.to_state_proto(cx) {
3443 let view = Some(proto::View {
3444 id: Some(id.clone()),
3445 leader_id,
3446 variant: Some(variant),
3447 panel_id: panel_id.map(|id| id as i32),
3448 });
3449
3450 is_project_item = item.is_project_item(cx);
3451 update = proto::UpdateActiveView {
3452 view,
3453 // TODO: Remove after version 0.145.x stabilizes.
3454 id: Some(id.clone()),
3455 leader_id,
3456 };
3457 }
3458 };
3459 }
3460 }
3461 }
3462 }
3463
3464 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3465 if active_view_id != self.last_active_view_id.as_ref() {
3466 self.last_active_view_id = active_view_id.cloned();
3467 self.update_followers(
3468 is_project_item,
3469 proto::update_followers::Variant::UpdateActiveView(update),
3470 cx,
3471 );
3472 }
3473 }
3474
3475 fn active_item_for_followers(
3476 &self,
3477 cx: &mut WindowContext,
3478 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3479 let mut active_item = None;
3480 let mut panel_id = None;
3481 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3482 if dock.focus_handle(cx).contains_focused(cx) {
3483 if let Some(panel) = dock.read(cx).active_panel() {
3484 if let Some(pane) = panel.pane(cx) {
3485 if let Some(item) = pane.read(cx).active_item() {
3486 active_item = Some(item);
3487 panel_id = panel.remote_id();
3488 break;
3489 }
3490 }
3491 }
3492 }
3493 }
3494
3495 if active_item.is_none() {
3496 active_item = self.active_pane().read(cx).active_item();
3497 }
3498 (active_item, panel_id)
3499 }
3500
3501 fn update_followers(
3502 &self,
3503 project_only: bool,
3504 update: proto::update_followers::Variant,
3505 cx: &mut WindowContext,
3506 ) -> Option<()> {
3507 // If this update only applies to for followers in the current project,
3508 // then skip it unless this project is shared. If it applies to all
3509 // followers, regardless of project, then set `project_id` to none,
3510 // indicating that it goes to all followers.
3511 let project_id = if project_only {
3512 Some(self.project.read(cx).remote_id()?)
3513 } else {
3514 None
3515 };
3516 self.app_state().workspace_store.update(cx, |store, cx| {
3517 store.update_followers(project_id, update, cx)
3518 })
3519 }
3520
3521 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3522 self.follower_states.iter().find_map(|(leader_id, state)| {
3523 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3524 Some(*leader_id)
3525 } else {
3526 None
3527 }
3528 })
3529 }
3530
3531 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3532 cx.notify();
3533
3534 let call = self.active_call()?;
3535 let room = call.read(cx).room()?.read(cx);
3536 let participant = room.remote_participant_for_peer_id(leader_id)?;
3537
3538 let leader_in_this_app;
3539 let leader_in_this_project;
3540 match participant.location {
3541 call::ParticipantLocation::SharedProject { project_id } => {
3542 leader_in_this_app = true;
3543 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3544 }
3545 call::ParticipantLocation::UnsharedProject => {
3546 leader_in_this_app = true;
3547 leader_in_this_project = false;
3548 }
3549 call::ParticipantLocation::External => {
3550 leader_in_this_app = false;
3551 leader_in_this_project = false;
3552 }
3553 };
3554
3555 let state = self.follower_states.get(&leader_id)?;
3556 let mut item_to_activate = None;
3557 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3558 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3559 if leader_in_this_project || !item.view.is_project_item(cx) {
3560 item_to_activate = Some((item.location, item.view.boxed_clone()));
3561 }
3562 }
3563 } else if let Some(shared_screen) =
3564 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3565 {
3566 item_to_activate = Some((None, Box::new(shared_screen)));
3567 }
3568
3569 let (panel_id, item) = item_to_activate?;
3570
3571 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3572 let pane;
3573 if let Some(panel_id) = panel_id {
3574 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3575 let state = self.follower_states.get_mut(&leader_id)?;
3576 state.dock_pane = Some(pane.clone());
3577 } else {
3578 pane = state.center_pane.clone();
3579 let state = self.follower_states.get_mut(&leader_id)?;
3580 if let Some(dock_pane) = state.dock_pane.take() {
3581 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3582 }
3583 }
3584
3585 pane.update(cx, |pane, cx| {
3586 let focus_active_item = pane.has_focus(cx) || transfer_focus;
3587 if let Some(index) = pane.index_for_item(item.as_ref()) {
3588 pane.activate_item(index, false, false, cx);
3589 } else {
3590 pane.add_item(item.boxed_clone(), false, false, None, cx)
3591 }
3592
3593 if focus_active_item {
3594 pane.focus_active_item(cx)
3595 }
3596 });
3597
3598 None
3599 }
3600
3601 fn shared_screen_for_peer(
3602 &self,
3603 peer_id: PeerId,
3604 pane: &View<Pane>,
3605 cx: &mut WindowContext,
3606 ) -> Option<View<SharedScreen>> {
3607 let call = self.active_call()?;
3608 let room = call.read(cx).room()?.read(cx);
3609 let participant = room.remote_participant_for_peer_id(peer_id)?;
3610 let track = participant.video_tracks.values().next()?.clone();
3611 let user = participant.user.clone();
3612
3613 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3614 if item.read(cx).peer_id == peer_id {
3615 return Some(item);
3616 }
3617 }
3618
3619 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3620 }
3621
3622 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3623 if cx.is_window_active() {
3624 self.update_active_view_for_followers(cx);
3625
3626 if let Some(database_id) = self.database_id {
3627 cx.background_executor()
3628 .spawn(persistence::DB.update_timestamp(database_id))
3629 .detach();
3630 }
3631 } else {
3632 for pane in &self.panes {
3633 pane.update(cx, |pane, cx| {
3634 if let Some(item) = pane.active_item() {
3635 item.workspace_deactivated(cx);
3636 }
3637 for item in pane.items() {
3638 if matches!(
3639 item.workspace_settings(cx).autosave,
3640 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3641 ) {
3642 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3643 .detach_and_log_err(cx);
3644 }
3645 }
3646 });
3647 }
3648 }
3649 }
3650
3651 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3652 self.active_call.as_ref().map(|(call, _)| call)
3653 }
3654
3655 fn on_active_call_event(
3656 &mut self,
3657 _: Model<ActiveCall>,
3658 event: &call::room::Event,
3659 cx: &mut ViewContext<Self>,
3660 ) {
3661 match event {
3662 call::room::Event::ParticipantLocationChanged { participant_id }
3663 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3664 self.leader_updated(*participant_id, cx);
3665 }
3666 _ => {}
3667 }
3668 }
3669
3670 pub fn database_id(&self) -> Option<WorkspaceId> {
3671 self.database_id
3672 }
3673
3674 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
3675 let project = self.project().read(cx);
3676
3677 if project.is_local() {
3678 Some(
3679 project
3680 .visible_worktrees(cx)
3681 .map(|worktree| worktree.read(cx).abs_path())
3682 .collect::<Vec<_>>(),
3683 )
3684 } else {
3685 None
3686 }
3687 }
3688
3689 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3690 match member {
3691 Member::Axis(PaneAxis { members, .. }) => {
3692 for child in members.iter() {
3693 self.remove_panes(child.clone(), cx)
3694 }
3695 }
3696 Member::Pane(pane) => {
3697 self.force_remove_pane(&pane, cx);
3698 }
3699 }
3700 }
3701
3702 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3703 self.panes.retain(|p| p != pane);
3704 self.panes
3705 .last()
3706 .unwrap()
3707 .update(cx, |pane, cx| pane.focus(cx));
3708 if self.last_active_center_pane == Some(pane.downgrade()) {
3709 self.last_active_center_pane = None;
3710 }
3711 cx.notify();
3712 }
3713
3714 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
3715 if self._schedule_serialize.is_none() {
3716 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3717 cx.background_executor()
3718 .timer(Duration::from_millis(100))
3719 .await;
3720 this.update(&mut cx, |this, cx| {
3721 this.serialize_workspace_internal(cx).detach();
3722 this._schedule_serialize.take();
3723 })
3724 .log_err();
3725 }));
3726 }
3727 }
3728
3729 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
3730 let Some(database_id) = self.database_id() else {
3731 return Task::ready(());
3732 };
3733
3734 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3735 let (items, active) = {
3736 let pane = pane_handle.read(cx);
3737 let active_item_id = pane.active_item().map(|item| item.item_id());
3738 (
3739 pane.items()
3740 .filter_map(|item_handle| {
3741 Some(SerializedItem {
3742 kind: Arc::from(item_handle.serialized_item_kind()?),
3743 item_id: item_handle.item_id().as_u64(),
3744 active: Some(item_handle.item_id()) == active_item_id,
3745 preview: pane.is_active_preview_item(item_handle.item_id()),
3746 })
3747 })
3748 .collect::<Vec<_>>(),
3749 pane.has_focus(cx),
3750 )
3751 };
3752
3753 SerializedPane::new(items, active)
3754 }
3755
3756 fn build_serialized_pane_group(
3757 pane_group: &Member,
3758 cx: &WindowContext,
3759 ) -> SerializedPaneGroup {
3760 match pane_group {
3761 Member::Axis(PaneAxis {
3762 axis,
3763 members,
3764 flexes,
3765 bounding_boxes: _,
3766 }) => SerializedPaneGroup::Group {
3767 axis: SerializedAxis(*axis),
3768 children: members
3769 .iter()
3770 .map(|member| build_serialized_pane_group(member, cx))
3771 .collect::<Vec<_>>(),
3772 flexes: Some(flexes.lock().clone()),
3773 },
3774 Member::Pane(pane_handle) => {
3775 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3776 }
3777 }
3778 }
3779
3780 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3781 let left_dock = this.left_dock.read(cx);
3782 let left_visible = left_dock.is_open();
3783 let left_active_panel = left_dock
3784 .visible_panel()
3785 .map(|panel| panel.persistent_name().to_string());
3786 let left_dock_zoom = left_dock
3787 .visible_panel()
3788 .map(|panel| panel.is_zoomed(cx))
3789 .unwrap_or(false);
3790
3791 let right_dock = this.right_dock.read(cx);
3792 let right_visible = right_dock.is_open();
3793 let right_active_panel = right_dock
3794 .visible_panel()
3795 .map(|panel| panel.persistent_name().to_string());
3796 let right_dock_zoom = right_dock
3797 .visible_panel()
3798 .map(|panel| panel.is_zoomed(cx))
3799 .unwrap_or(false);
3800
3801 let bottom_dock = this.bottom_dock.read(cx);
3802 let bottom_visible = bottom_dock.is_open();
3803 let bottom_active_panel = bottom_dock
3804 .visible_panel()
3805 .map(|panel| panel.persistent_name().to_string());
3806 let bottom_dock_zoom = bottom_dock
3807 .visible_panel()
3808 .map(|panel| panel.is_zoomed(cx))
3809 .unwrap_or(false);
3810
3811 DockStructure {
3812 left: DockData {
3813 visible: left_visible,
3814 active_panel: left_active_panel,
3815 zoom: left_dock_zoom,
3816 },
3817 right: DockData {
3818 visible: right_visible,
3819 active_panel: right_active_panel,
3820 zoom: right_dock_zoom,
3821 },
3822 bottom: DockData {
3823 visible: bottom_visible,
3824 active_panel: bottom_active_panel,
3825 zoom: bottom_dock_zoom,
3826 },
3827 }
3828 }
3829
3830 let location = if let Some(local_paths) = self.local_paths(cx) {
3831 if !local_paths.is_empty() {
3832 let (order, paths): (Vec<_>, Vec<_>) = local_paths
3833 .iter()
3834 .enumerate()
3835 .sorted_by(|a, b| a.1.cmp(b.1))
3836 .unzip();
3837
3838 Some(SerializedWorkspaceLocation::Local(
3839 LocalPaths::new(paths),
3840 LocalPathsOrder::new(order),
3841 ))
3842 } else {
3843 None
3844 }
3845 } else if let Some(dev_server_project_id) = self.project().read(cx).dev_server_project_id()
3846 {
3847 let store = dev_server_projects::Store::global(cx).read(cx);
3848 maybe!({
3849 let project = store.dev_server_project(dev_server_project_id)?;
3850 let dev_server = store.dev_server(project.dev_server_id)?;
3851
3852 let dev_server_project = SerializedDevServerProject {
3853 id: dev_server_project_id,
3854 dev_server_name: dev_server.name.to_string(),
3855 path: project.path.to_string(),
3856 };
3857 Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
3858 })
3859 } else {
3860 None
3861 };
3862
3863 // don't save workspace state for the empty workspace.
3864 if let Some(location) = location {
3865 let center_group = build_serialized_pane_group(&self.center.root, cx);
3866 let docks = build_serialized_docks(self, cx);
3867 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
3868 let serialized_workspace = SerializedWorkspace {
3869 id: database_id,
3870 location,
3871 center_group,
3872 window_bounds,
3873 display: Default::default(),
3874 docks,
3875 centered_layout: self.centered_layout,
3876 };
3877 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3878 }
3879 Task::ready(())
3880 }
3881
3882 pub(crate) fn load_workspace(
3883 serialized_workspace: SerializedWorkspace,
3884 paths_to_open: Vec<Option<ProjectPath>>,
3885 cx: &mut ViewContext<Workspace>,
3886 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3887 cx.spawn(|workspace, mut cx| async move {
3888 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3889
3890 let mut center_group = None;
3891 let mut center_items = None;
3892
3893 // Traverse the splits tree and add to things
3894 if let Some((group, active_pane, items)) = serialized_workspace
3895 .center_group
3896 .deserialize(
3897 &project,
3898 serialized_workspace.id,
3899 workspace.clone(),
3900 &mut cx,
3901 )
3902 .await
3903 {
3904 center_items = Some(items);
3905 center_group = Some((group, active_pane))
3906 }
3907
3908 let mut items_by_project_path = cx.update(|cx| {
3909 center_items
3910 .unwrap_or_default()
3911 .into_iter()
3912 .filter_map(|item| {
3913 let item = item?;
3914 let project_path = item.project_path(cx)?;
3915 Some((project_path, item))
3916 })
3917 .collect::<HashMap<_, _>>()
3918 })?;
3919
3920 let opened_items = paths_to_open
3921 .into_iter()
3922 .map(|path_to_open| {
3923 path_to_open
3924 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3925 })
3926 .collect::<Vec<_>>();
3927
3928 // Remove old panes from workspace panes list
3929 workspace.update(&mut cx, |workspace, cx| {
3930 if let Some((center_group, active_pane)) = center_group {
3931 workspace.remove_panes(workspace.center.root.clone(), cx);
3932
3933 // Swap workspace center group
3934 workspace.center = PaneGroup::with_root(center_group);
3935 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3936 if let Some(active_pane) = active_pane {
3937 workspace.active_pane = active_pane;
3938 cx.focus_self();
3939 } else {
3940 workspace.active_pane = workspace.center.first_pane().clone();
3941 }
3942 }
3943
3944 let docks = serialized_workspace.docks;
3945
3946 for (dock, serialized_dock) in [
3947 (&mut workspace.right_dock, docks.right),
3948 (&mut workspace.left_dock, docks.left),
3949 (&mut workspace.bottom_dock, docks.bottom),
3950 ]
3951 .iter_mut()
3952 {
3953 dock.update(cx, |dock, cx| {
3954 dock.serialized_dock = Some(serialized_dock.clone());
3955 dock.restore_state(cx);
3956 });
3957 }
3958
3959 cx.notify();
3960 })?;
3961
3962 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3963 workspace
3964 .update(&mut cx, |workspace, cx| {
3965 workspace.serialize_workspace_internal(cx).detach();
3966 })
3967 .ok();
3968
3969 Ok(opened_items)
3970 })
3971 }
3972
3973 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3974 self.add_workspace_actions_listeners(div, cx)
3975 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3976 .on_action(cx.listener(Self::close_all_items_and_panes))
3977 .on_action(cx.listener(Self::save_all))
3978 .on_action(cx.listener(Self::send_keystrokes))
3979 .on_action(cx.listener(Self::add_folder_to_project))
3980 .on_action(cx.listener(Self::follow_next_collaborator))
3981 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3982 let pane = workspace.active_pane().clone();
3983 workspace.unfollow_in_pane(&pane, cx);
3984 }))
3985 .on_action(cx.listener(|workspace, action: &Save, cx| {
3986 workspace
3987 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3988 .detach_and_log_err(cx);
3989 }))
3990 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
3991 workspace
3992 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
3993 .detach_and_log_err(cx);
3994 }))
3995 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3996 workspace
3997 .save_active_item(SaveIntent::SaveAs, cx)
3998 .detach_and_log_err(cx);
3999 }))
4000 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4001 workspace.activate_previous_pane(cx)
4002 }))
4003 .on_action(
4004 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4005 )
4006 .on_action(
4007 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4008 workspace.activate_pane_in_direction(action.0, cx)
4009 }),
4010 )
4011 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4012 workspace.swap_pane_in_direction(action.0, cx)
4013 }))
4014 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4015 this.toggle_dock(DockPosition::Left, cx);
4016 }))
4017 .on_action(
4018 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4019 workspace.toggle_dock(DockPosition::Right, cx);
4020 }),
4021 )
4022 .on_action(
4023 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4024 workspace.toggle_dock(DockPosition::Bottom, cx);
4025 }),
4026 )
4027 .on_action(
4028 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4029 workspace.close_all_docks(cx);
4030 }),
4031 )
4032 .on_action(
4033 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4034 workspace.clear_all_notifications(cx);
4035 }),
4036 )
4037 .on_action(cx.listener(Workspace::open))
4038 .on_action(cx.listener(Workspace::close_window))
4039 .on_action(cx.listener(Workspace::activate_pane_at_index))
4040 .on_action(
4041 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4042 workspace.reopen_closed_item(cx).detach();
4043 }),
4044 )
4045 .on_action(cx.listener(Workspace::toggle_centered_layout))
4046 }
4047
4048 #[cfg(any(test, feature = "test-support"))]
4049 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4050 use node_runtime::FakeNodeRuntime;
4051
4052 let client = project.read(cx).client();
4053 let user_store = project.read(cx).user_store();
4054
4055 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4056 cx.activate_window();
4057 let app_state = Arc::new(AppState {
4058 languages: project.read(cx).languages().clone(),
4059 workspace_store,
4060 client,
4061 user_store,
4062 fs: project.read(cx).fs().clone(),
4063 build_window_options: |_, _| Default::default(),
4064 node_runtime: FakeNodeRuntime::new(),
4065 });
4066 let workspace = Self::new(Default::default(), project, app_state, cx);
4067 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4068 workspace
4069 }
4070
4071 pub fn register_action<A: Action>(
4072 &mut self,
4073 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4074 ) -> &mut Self {
4075 let callback = Arc::new(callback);
4076
4077 self.workspace_actions.push(Box::new(move |div, cx| {
4078 let callback = callback.clone();
4079 div.on_action(
4080 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4081 )
4082 }));
4083 self
4084 }
4085
4086 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4087 let mut div = div
4088 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4089 .on_action(cx.listener(Self::close_all_items_and_panes))
4090 .on_action(cx.listener(Self::add_folder_to_project))
4091 .on_action(cx.listener(Self::save_all))
4092 .on_action(cx.listener(Self::open));
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_local_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}