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