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