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