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