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