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