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