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