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