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