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