1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11pub mod tasks;
12mod theme_preview;
13mod toolbar;
14mod workspace_settings;
15
16use anyhow::{anyhow, Context as _, Result};
17use call::{call_settings::CallSettings, ActiveCall};
18use client::{
19 proto::{self, ErrorCode, PanelId, PeerId},
20 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
21};
22use collections::{hash_map, HashMap, HashSet};
23use derive_more::{Deref, DerefMut};
24use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
25use futures::{
26 channel::{
27 mpsc::{self, UnboundedReceiver, UnboundedSender},
28 oneshot,
29 },
30 future::try_join_all,
31 Future, FutureExt, StreamExt,
32};
33use gpui::{
34 action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
35 transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
36 AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
37 EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
38 ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
39 ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
40 WindowHandle, WindowId, WindowOptions,
41};
42pub use item::{
43 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
44 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
45};
46use itertools::Itertools;
47use language::{LanguageRegistry, Rope};
48pub use modal_layer::*;
49use node_runtime::NodeRuntime;
50use notifications::{
51 simple_message_notification::MessageNotification, DetachAndPromptErr, NotificationHandle,
52};
53pub use pane::*;
54pub use pane_group::*;
55pub use persistence::{
56 model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
57 WorkspaceDb, DB as WORKSPACE_DB,
58};
59use persistence::{
60 model::{SerializedSshProject, SerializedWorkspace},
61 SerializedWindowBounds, DB,
62};
63use postage::stream::Stream;
64use project::{
65 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
66};
67use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
68use serde::Deserialize;
69use session::AppSession;
70use settings::Settings;
71use shared_screen::SharedScreen;
72use sqlez::{
73 bindable::{Bind, Column, StaticColumnCount},
74 statement::Statement,
75};
76use status_bar::StatusBar;
77pub use status_bar::StatusItemView;
78use std::{
79 any::TypeId,
80 borrow::Cow,
81 cell::RefCell,
82 cmp,
83 collections::hash_map::DefaultHasher,
84 env,
85 hash::{Hash, Hasher},
86 path::{Path, PathBuf},
87 rc::Rc,
88 sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
89 time::Duration,
90};
91use task::SpawnInTerminal;
92use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
93pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
94pub use ui;
95use ui::{
96 div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _,
97 IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
98 VisualContext as _, WindowContext,
99};
100use util::{ResultExt, TryFutureExt};
101use uuid::Uuid;
102pub use workspace_settings::{
103 AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
104};
105
106use crate::notifications::NotificationId;
107use crate::persistence::{
108 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
109 SerializedAxis,
110};
111
112static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
113 env::var("ZED_WINDOW_SIZE")
114 .ok()
115 .as_deref()
116 .and_then(parse_pixel_size_env_var)
117});
118
119static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
120 env::var("ZED_WINDOW_POSITION")
121 .ok()
122 .as_deref()
123 .and_then(parse_pixel_position_env_var)
124});
125
126#[derive(Clone, PartialEq)]
127pub struct RemoveWorktreeFromProject(pub WorktreeId);
128
129actions!(assistant, [ShowConfiguration]);
130
131actions!(
132 workspace,
133 [
134 ActivateNextPane,
135 ActivatePreviousPane,
136 AddFolderToProject,
137 ClearAllNotifications,
138 CloseAllDocks,
139 CloseWindow,
140 CopyPath,
141 CopyRelativePath,
142 Feedback,
143 FollowNextCollaborator,
144 NewCenterTerminal,
145 NewFile,
146 NewFileSplitVertical,
147 NewFileSplitHorizontal,
148 NewSearch,
149 NewTerminal,
150 NewWindow,
151 Open,
152 OpenInTerminal,
153 ReloadActiveItem,
154 SaveAs,
155 SaveWithoutFormat,
156 ToggleBottomDock,
157 ToggleCenteredLayout,
158 ToggleLeftDock,
159 ToggleRightDock,
160 ToggleZoom,
161 Unfollow,
162 Welcome,
163 ]
164);
165
166#[derive(Clone, PartialEq)]
167pub struct OpenPaths {
168 pub paths: Vec<PathBuf>,
169}
170
171#[derive(Clone, Deserialize, PartialEq)]
172pub struct ActivatePane(pub usize);
173
174#[derive(Clone, Deserialize, PartialEq)]
175pub struct ActivatePaneInDirection(pub SplitDirection);
176
177#[derive(Clone, Deserialize, PartialEq)]
178pub struct SwapPaneInDirection(pub SplitDirection);
179
180#[derive(Clone, PartialEq, Debug, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct SaveAll {
183 pub save_intent: Option<SaveIntent>,
184}
185
186#[derive(Clone, PartialEq, Debug, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct Save {
189 pub save_intent: Option<SaveIntent>,
190}
191
192#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
193#[serde(rename_all = "camelCase")]
194pub struct CloseAllItemsAndPanes {
195 pub save_intent: Option<SaveIntent>,
196}
197
198#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
199#[serde(rename_all = "camelCase")]
200pub struct CloseInactiveTabsAndPanes {
201 pub save_intent: Option<SaveIntent>,
202}
203
204#[derive(Clone, Deserialize, PartialEq)]
205pub struct SendKeystrokes(pub String);
206
207#[derive(Clone, Deserialize, PartialEq, Default)]
208pub struct Reload {
209 pub binary_path: Option<PathBuf>,
210}
211
212action_as!(project_symbols, ToggleProjectSymbols as Toggle);
213
214#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)]
215pub struct ToggleFileFinder {
216 #[serde(default)]
217 pub separate_history: bool,
218}
219
220impl_action_as!(file_finder, ToggleFileFinder as Toggle);
221
222impl_actions!(
223 workspace,
224 [
225 ActivatePane,
226 ActivatePaneInDirection,
227 CloseAllItemsAndPanes,
228 CloseInactiveTabsAndPanes,
229 OpenTerminal,
230 Reload,
231 Save,
232 SaveAll,
233 SwapPaneInDirection,
234 SendKeystrokes,
235 ]
236);
237
238#[derive(PartialEq, Eq, Debug)]
239pub enum CloseIntent {
240 /// Quit the program entirely.
241 Quit,
242 /// Close a window.
243 CloseWindow,
244 /// Replace the workspace in an existing window.
245 ReplaceWindow,
246}
247
248#[derive(Clone)]
249pub struct Toast {
250 id: NotificationId,
251 msg: Cow<'static, str>,
252 autohide: bool,
253 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
254}
255
256impl Toast {
257 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
258 Toast {
259 id,
260 msg: msg.into(),
261 on_click: None,
262 autohide: false,
263 }
264 }
265
266 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
267 where
268 M: Into<Cow<'static, str>>,
269 F: Fn(&mut WindowContext) + 'static,
270 {
271 self.on_click = Some((message.into(), Arc::new(on_click)));
272 self
273 }
274
275 pub fn autohide(mut self) -> Self {
276 self.autohide = true;
277 self
278 }
279}
280
281impl PartialEq for Toast {
282 fn eq(&self, other: &Self) -> bool {
283 self.id == other.id
284 && self.msg == other.msg
285 && self.on_click.is_some() == other.on_click.is_some()
286 }
287}
288
289#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
290pub struct OpenTerminal {
291 pub working_directory: PathBuf,
292}
293
294#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
295pub struct WorkspaceId(i64);
296
297impl StaticColumnCount for WorkspaceId {}
298impl Bind for WorkspaceId {
299 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
300 self.0.bind(statement, start_index)
301 }
302}
303impl Column for WorkspaceId {
304 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
305 i64::column(statement, start_index)
306 .map(|(i, next_index)| (Self(i), next_index))
307 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
308 }
309}
310impl From<WorkspaceId> for i64 {
311 fn from(val: WorkspaceId) -> Self {
312 val.0
313 }
314}
315
316pub fn init_settings(cx: &mut AppContext) {
317 WorkspaceSettings::register(cx);
318 ItemSettings::register(cx);
319 PreviewTabsSettings::register(cx);
320 TabBarSettings::register(cx);
321}
322
323pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
324 init_settings(cx);
325 notifications::init(cx);
326 theme_preview::init(cx);
327
328 cx.on_action(Workspace::close_global);
329 cx.on_action(reload);
330
331 cx.on_action({
332 let app_state = Arc::downgrade(&app_state);
333 move |_: &Open, cx: &mut AppContext| {
334 let paths = cx.prompt_for_paths(PathPromptOptions {
335 files: true,
336 directories: true,
337 multiple: true,
338 });
339
340 if let Some(app_state) = app_state.upgrade() {
341 cx.spawn(move |cx| async move {
342 match Flatten::flatten(paths.await.map_err(|e| e.into())) {
343 Ok(Some(paths)) => {
344 cx.update(|cx| {
345 open_paths(&paths, app_state, OpenOptions::default(), cx)
346 .detach_and_log_err(cx)
347 })
348 .ok();
349 }
350 Ok(None) => {}
351 Err(err) => {
352 cx.update(|cx| {
353 if let Some(workspace_window) = cx
354 .active_window()
355 .and_then(|window| window.downcast::<Workspace>())
356 {
357 workspace_window
358 .update(cx, |workspace, cx| {
359 workspace.show_portal_error(err.to_string(), cx);
360 })
361 .ok();
362 }
363 })
364 .ok();
365 }
366 };
367 })
368 .detach();
369 }
370 }
371 });
372}
373
374#[derive(Clone, Default, Deref, DerefMut)]
375struct ProjectItemOpeners(Vec<ProjectItemOpener>);
376
377type ProjectItemOpener = fn(
378 &Model<Project>,
379 &ProjectPath,
380 &mut WindowContext,
381)
382 -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
383
384type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
385
386impl Global for ProjectItemOpeners {}
387
388/// Registers a [ProjectItem] for the app. When opening a file, all the registered
389/// items will get a chance to open the file, starting from the project item that
390/// was added last.
391pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
392 let builders = cx.default_global::<ProjectItemOpeners>();
393 builders.push(|project, project_path, cx| {
394 let project_item = <I::Item as project::Item>::try_open(project, project_path, cx)?;
395 let project = project.clone();
396 Some(cx.spawn(|cx| async move {
397 let project_item = project_item.await?;
398 let project_entry_id: Option<ProjectEntryId> =
399 project_item.read_with(&cx, project::Item::entry_id)?;
400 let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
401 Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
402 as Box<dyn ItemHandle>
403 }) as Box<_>;
404 Ok((project_entry_id, build_workspace_item))
405 }))
406 });
407}
408
409#[derive(Default)]
410pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
411
412struct FollowableViewDescriptor {
413 from_state_proto: fn(
414 View<Workspace>,
415 ViewId,
416 &mut Option<proto::view::Variant>,
417 &mut WindowContext,
418 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
419 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
420}
421
422impl Global for FollowableViewRegistry {}
423
424impl FollowableViewRegistry {
425 pub fn register<I: FollowableItem>(cx: &mut AppContext) {
426 cx.default_global::<Self>().0.insert(
427 TypeId::of::<I>(),
428 FollowableViewDescriptor {
429 from_state_proto: |workspace, id, state, cx| {
430 I::from_state_proto(workspace, id, state, cx).map(|task| {
431 cx.foreground_executor()
432 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
433 })
434 },
435 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
436 },
437 );
438 }
439
440 pub fn from_state_proto(
441 workspace: View<Workspace>,
442 view_id: ViewId,
443 mut state: Option<proto::view::Variant>,
444 cx: &mut WindowContext,
445 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
446 cx.update_default_global(|this: &mut Self, cx| {
447 this.0.values().find_map(|descriptor| {
448 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
449 })
450 })
451 }
452
453 pub fn to_followable_view(
454 view: impl Into<AnyView>,
455 cx: &AppContext,
456 ) -> Option<Box<dyn FollowableItemHandle>> {
457 let this = cx.try_global::<Self>()?;
458 let view = view.into();
459 let descriptor = this.0.get(&view.entity_type())?;
460 Some((descriptor.to_followable_view)(&view))
461 }
462}
463
464#[derive(Copy, Clone)]
465struct SerializableItemDescriptor {
466 deserialize: fn(
467 Model<Project>,
468 WeakView<Workspace>,
469 WorkspaceId,
470 ItemId,
471 &mut ViewContext<Pane>,
472 ) -> Task<Result<Box<dyn ItemHandle>>>,
473 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
474 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
475}
476
477#[derive(Default)]
478struct SerializableItemRegistry {
479 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
480 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
481}
482
483impl Global for SerializableItemRegistry {}
484
485impl SerializableItemRegistry {
486 fn deserialize(
487 item_kind: &str,
488 project: Model<Project>,
489 workspace: WeakView<Workspace>,
490 workspace_id: WorkspaceId,
491 item_item: ItemId,
492 cx: &mut ViewContext<Pane>,
493 ) -> Task<Result<Box<dyn ItemHandle>>> {
494 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
495 return Task::ready(Err(anyhow!(
496 "cannot deserialize {}, descriptor not found",
497 item_kind
498 )));
499 };
500
501 (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
502 }
503
504 fn cleanup(
505 item_kind: &str,
506 workspace_id: WorkspaceId,
507 loaded_items: Vec<ItemId>,
508 cx: &mut WindowContext,
509 ) -> Task<Result<()>> {
510 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
511 return Task::ready(Err(anyhow!(
512 "cannot cleanup {}, descriptor not found",
513 item_kind
514 )));
515 };
516
517 (descriptor.cleanup)(workspace_id, loaded_items, cx)
518 }
519
520 fn view_to_serializable_item_handle(
521 view: AnyView,
522 cx: &AppContext,
523 ) -> Option<Box<dyn SerializableItemHandle>> {
524 let this = cx.try_global::<Self>()?;
525 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
526 Some((descriptor.view_to_serializable_item)(view))
527 }
528
529 fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
530 let this = cx.try_global::<Self>()?;
531 this.descriptors_by_kind.get(item_kind).copied()
532 }
533}
534
535pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
536 let serialized_item_kind = I::serialized_item_kind();
537
538 let registry = cx.default_global::<SerializableItemRegistry>();
539 let descriptor = SerializableItemDescriptor {
540 deserialize: |project, workspace, workspace_id, item_id, cx| {
541 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
542 cx.foreground_executor()
543 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
544 },
545 cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
546 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
547 };
548 registry
549 .descriptors_by_kind
550 .insert(Arc::from(serialized_item_kind), descriptor);
551 registry
552 .descriptors_by_type
553 .insert(TypeId::of::<I>(), descriptor);
554}
555
556pub struct AppState {
557 pub languages: Arc<LanguageRegistry>,
558 pub client: Arc<Client>,
559 pub user_store: Model<UserStore>,
560 pub workspace_store: Model<WorkspaceStore>,
561 pub fs: Arc<dyn fs::Fs>,
562 pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
563 pub node_runtime: NodeRuntime,
564 pub session: Model<AppSession>,
565}
566
567struct GlobalAppState(Weak<AppState>);
568
569impl Global for GlobalAppState {}
570
571pub struct WorkspaceStore {
572 workspaces: HashSet<WindowHandle<Workspace>>,
573 client: Arc<Client>,
574 _subscriptions: Vec<client::Subscription>,
575}
576
577#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
578struct Follower {
579 project_id: Option<u64>,
580 peer_id: PeerId,
581}
582
583impl AppState {
584 pub fn global(cx: &AppContext) -> Weak<Self> {
585 cx.global::<GlobalAppState>().0.clone()
586 }
587 pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
588 cx.try_global::<GlobalAppState>()
589 .map(|state| state.0.clone())
590 }
591 pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
592 cx.set_global(GlobalAppState(state));
593 }
594
595 #[cfg(any(test, feature = "test-support"))]
596 pub fn test(cx: &mut AppContext) -> Arc<Self> {
597 use node_runtime::NodeRuntime;
598 use session::Session;
599 use settings::SettingsStore;
600 use ui::Context as _;
601
602 if !cx.has_global::<SettingsStore>() {
603 let settings_store = SettingsStore::test(cx);
604 cx.set_global(settings_store);
605 }
606
607 let fs = fs::FakeFs::new(cx.background_executor().clone());
608 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
609 let clock = Arc::new(clock::FakeSystemClock::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| this.bounds = bounds)
4828 },
4829 |_, _, _| {},
4830 )
4831 .absolute()
4832 .size_full()
4833 })
4834 .when(self.zoomed.is_none(), |this| {
4835 this.on_drag_move(cx.listener(
4836 |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4837 match e.drag(cx).0 {
4838 DockPosition::Left => {
4839 let size = e.event.position.x
4840 - workspace.bounds.left();
4841 workspace.left_dock.update(
4842 cx,
4843 |left_dock, cx| {
4844 left_dock.resize_active_panel(
4845 Some(size),
4846 cx,
4847 );
4848 },
4849 );
4850 }
4851 DockPosition::Right => {
4852 let size = workspace.bounds.right()
4853 - e.event.position.x;
4854 workspace.right_dock.update(
4855 cx,
4856 |right_dock, cx| {
4857 right_dock.resize_active_panel(
4858 Some(size),
4859 cx,
4860 );
4861 },
4862 );
4863 }
4864 DockPosition::Bottom => {
4865 let size = workspace.bounds.bottom()
4866 - e.event.position.y;
4867 workspace.bottom_dock.update(
4868 cx,
4869 |bottom_dock, cx| {
4870 bottom_dock.resize_active_panel(
4871 Some(size),
4872 cx,
4873 );
4874 },
4875 );
4876 }
4877 }
4878 },
4879 ))
4880 })
4881 .child(
4882 div()
4883 .flex()
4884 .flex_row()
4885 .h_full()
4886 // Left Dock
4887 .children(self.render_dock(
4888 DockPosition::Left,
4889 &self.left_dock,
4890 cx,
4891 ))
4892 // Panes
4893 .child(
4894 div()
4895 .flex()
4896 .flex_col()
4897 .flex_1()
4898 .overflow_hidden()
4899 .child(
4900 h_flex()
4901 .flex_1()
4902 .when_some(paddings.0, |this, p| {
4903 this.child(p.border_r_1())
4904 })
4905 .child(self.center.render(
4906 &self.project,
4907 &self.follower_states,
4908 self.active_call(),
4909 &self.active_pane,
4910 self.zoomed.as_ref(),
4911 &self.app_state,
4912 cx,
4913 ))
4914 .when_some(paddings.1, |this, p| {
4915 this.child(p.border_l_1())
4916 }),
4917 )
4918 .children(self.render_dock(
4919 DockPosition::Bottom,
4920 &self.bottom_dock,
4921 cx,
4922 )),
4923 )
4924 // Right Dock
4925 .children(self.render_dock(
4926 DockPosition::Right,
4927 &self.right_dock,
4928 cx,
4929 )),
4930 )
4931 .children(self.zoomed.as_ref().and_then(|view| {
4932 let zoomed_view = view.upgrade()?;
4933 let div = div()
4934 .occlude()
4935 .absolute()
4936 .overflow_hidden()
4937 .border_color(colors.border)
4938 .bg(colors.background)
4939 .child(zoomed_view)
4940 .inset_0()
4941 .shadow_lg();
4942
4943 Some(match self.zoomed_position {
4944 Some(DockPosition::Left) => div.right_2().border_r_1(),
4945 Some(DockPosition::Right) => div.left_2().border_l_1(),
4946 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4947 None => {
4948 div.top_2().bottom_2().left_2().right_2().border_1()
4949 }
4950 })
4951 }))
4952 .children(self.render_notifications(cx)),
4953 )
4954 .child(self.status_bar.clone())
4955 .child(self.modal_layer.clone()),
4956 ),
4957 cx,
4958 )
4959 }
4960}
4961
4962impl WorkspaceStore {
4963 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4964 Self {
4965 workspaces: Default::default(),
4966 _subscriptions: vec![
4967 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4968 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4969 ],
4970 client,
4971 }
4972 }
4973
4974 pub fn update_followers(
4975 &self,
4976 project_id: Option<u64>,
4977 update: proto::update_followers::Variant,
4978 cx: &AppContext,
4979 ) -> Option<()> {
4980 let active_call = ActiveCall::try_global(cx)?;
4981 let room_id = active_call.read(cx).room()?.read(cx).id();
4982 self.client
4983 .send(proto::UpdateFollowers {
4984 room_id,
4985 project_id,
4986 variant: Some(update),
4987 })
4988 .log_err()
4989 }
4990
4991 pub async fn handle_follow(
4992 this: Model<Self>,
4993 envelope: TypedEnvelope<proto::Follow>,
4994 mut cx: AsyncAppContext,
4995 ) -> Result<proto::FollowResponse> {
4996 this.update(&mut cx, |this, cx| {
4997 let follower = Follower {
4998 project_id: envelope.payload.project_id,
4999 peer_id: envelope.original_sender_id()?,
5000 };
5001
5002 let mut response = proto::FollowResponse::default();
5003 this.workspaces.retain(|workspace| {
5004 workspace
5005 .update(cx, |workspace, cx| {
5006 let handler_response = workspace.handle_follow(follower.project_id, cx);
5007 if let Some(active_view) = handler_response.active_view.clone() {
5008 if workspace.project.read(cx).remote_id() == follower.project_id {
5009 response.active_view = Some(active_view)
5010 }
5011 }
5012 })
5013 .is_ok()
5014 });
5015
5016 Ok(response)
5017 })?
5018 }
5019
5020 async fn handle_update_followers(
5021 this: Model<Self>,
5022 envelope: TypedEnvelope<proto::UpdateFollowers>,
5023 mut cx: AsyncAppContext,
5024 ) -> Result<()> {
5025 let leader_id = envelope.original_sender_id()?;
5026 let update = envelope.payload;
5027
5028 this.update(&mut cx, |this, cx| {
5029 this.workspaces.retain(|workspace| {
5030 workspace
5031 .update(cx, |workspace, cx| {
5032 let project_id = workspace.project.read(cx).remote_id();
5033 if update.project_id != project_id && update.project_id.is_some() {
5034 return;
5035 }
5036 workspace.handle_update_followers(leader_id, update.clone(), cx);
5037 })
5038 .is_ok()
5039 });
5040 Ok(())
5041 })?
5042 }
5043}
5044
5045impl ViewId {
5046 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5047 Ok(Self {
5048 creator: message
5049 .creator
5050 .ok_or_else(|| anyhow!("creator is missing"))?,
5051 id: message.id,
5052 })
5053 }
5054
5055 pub(crate) fn to_proto(self) -> proto::ViewId {
5056 proto::ViewId {
5057 creator: Some(self.creator),
5058 id: self.id,
5059 }
5060 }
5061}
5062
5063impl FollowerState {
5064 fn pane(&self) -> &View<Pane> {
5065 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5066 }
5067}
5068
5069pub trait WorkspaceHandle {
5070 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5071}
5072
5073impl WorkspaceHandle for View<Workspace> {
5074 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5075 self.read(cx)
5076 .worktrees(cx)
5077 .flat_map(|worktree| {
5078 let worktree_id = worktree.read(cx).id();
5079 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5080 worktree_id,
5081 path: f.path.clone(),
5082 })
5083 })
5084 .collect::<Vec<_>>()
5085 }
5086}
5087
5088impl std::fmt::Debug for OpenPaths {
5089 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5090 f.debug_struct("OpenPaths")
5091 .field("paths", &self.paths)
5092 .finish()
5093 }
5094}
5095
5096pub fn activate_workspace_for_project(
5097 cx: &mut AppContext,
5098 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5099) -> Option<WindowHandle<Workspace>> {
5100 for window in cx.windows() {
5101 let Some(workspace) = window.downcast::<Workspace>() else {
5102 continue;
5103 };
5104
5105 let predicate = workspace
5106 .update(cx, |workspace, cx| {
5107 let project = workspace.project.read(cx);
5108 if predicate(project, cx) {
5109 cx.activate_window();
5110 true
5111 } else {
5112 false
5113 }
5114 })
5115 .log_err()
5116 .unwrap_or(false);
5117
5118 if predicate {
5119 return Some(workspace);
5120 }
5121 }
5122
5123 None
5124}
5125
5126pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5127 DB.last_workspace().await.log_err().flatten()
5128}
5129
5130pub fn last_session_workspace_locations(
5131 last_session_id: &str,
5132 last_session_window_stack: Option<Vec<WindowId>>,
5133) -> Option<Vec<SerializedWorkspaceLocation>> {
5134 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5135 .log_err()
5136}
5137
5138actions!(collab, [OpenChannelNotes]);
5139actions!(zed, [OpenLog]);
5140
5141async fn join_channel_internal(
5142 channel_id: ChannelId,
5143 app_state: &Arc<AppState>,
5144 requesting_window: Option<WindowHandle<Workspace>>,
5145 active_call: &Model<ActiveCall>,
5146 cx: &mut AsyncAppContext,
5147) -> Result<bool> {
5148 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5149 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5150 return (false, None);
5151 };
5152
5153 let already_in_channel = room.channel_id() == Some(channel_id);
5154 let should_prompt = room.is_sharing_project()
5155 && !room.remote_participants().is_empty()
5156 && !already_in_channel;
5157 let open_room = if already_in_channel {
5158 active_call.room().cloned()
5159 } else {
5160 None
5161 };
5162 (should_prompt, open_room)
5163 })?;
5164
5165 if let Some(room) = open_room {
5166 let task = room.update(cx, |room, cx| {
5167 if let Some((project, host)) = room.most_active_project(cx) {
5168 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5169 }
5170
5171 None
5172 })?;
5173 if let Some(task) = task {
5174 task.await?;
5175 }
5176 return anyhow::Ok(true);
5177 }
5178
5179 if should_prompt {
5180 if let Some(workspace) = requesting_window {
5181 let answer = workspace
5182 .update(cx, |_, cx| {
5183 cx.prompt(
5184 PromptLevel::Warning,
5185 "Do you want to switch channels?",
5186 Some("Leaving this call will unshare your current project."),
5187 &["Yes, Join Channel", "Cancel"],
5188 )
5189 })?
5190 .await;
5191
5192 if answer == Ok(1) {
5193 return Ok(false);
5194 }
5195 } else {
5196 return Ok(false); // unreachable!() hopefully
5197 }
5198 }
5199
5200 let client = cx.update(|cx| active_call.read(cx).client())?;
5201
5202 let mut client_status = client.status();
5203
5204 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5205 'outer: loop {
5206 let Some(status) = client_status.recv().await else {
5207 return Err(anyhow!("error connecting"));
5208 };
5209
5210 match status {
5211 Status::Connecting
5212 | Status::Authenticating
5213 | Status::Reconnecting
5214 | Status::Reauthenticating => continue,
5215 Status::Connected { .. } => break 'outer,
5216 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5217 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5218 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5219 return Err(ErrorCode::Disconnected.into());
5220 }
5221 }
5222 }
5223
5224 let room = active_call
5225 .update(cx, |active_call, cx| {
5226 active_call.join_channel(channel_id, cx)
5227 })?
5228 .await?;
5229
5230 let Some(room) = room else {
5231 return anyhow::Ok(true);
5232 };
5233
5234 room.update(cx, |room, _| room.room_update_completed())?
5235 .await;
5236
5237 let task = room.update(cx, |room, cx| {
5238 if let Some((project, host)) = room.most_active_project(cx) {
5239 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5240 }
5241
5242 // If you are the first to join a channel, see if you should share your project.
5243 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5244 if let Some(workspace) = requesting_window {
5245 let project = workspace.update(cx, |workspace, cx| {
5246 let project = workspace.project.read(cx);
5247
5248 if !CallSettings::get_global(cx).share_on_join {
5249 return None;
5250 }
5251
5252 if (project.is_local() || project.is_via_ssh())
5253 && project.visible_worktrees(cx).any(|tree| {
5254 tree.read(cx)
5255 .root_entry()
5256 .map_or(false, |entry| entry.is_dir())
5257 })
5258 {
5259 Some(workspace.project.clone())
5260 } else {
5261 None
5262 }
5263 });
5264 if let Ok(Some(project)) = project {
5265 return Some(cx.spawn(|room, mut cx| async move {
5266 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5267 .await?;
5268 Ok(())
5269 }));
5270 }
5271 }
5272 }
5273
5274 None
5275 })?;
5276 if let Some(task) = task {
5277 task.await?;
5278 return anyhow::Ok(true);
5279 }
5280 anyhow::Ok(false)
5281}
5282
5283pub fn join_channel(
5284 channel_id: ChannelId,
5285 app_state: Arc<AppState>,
5286 requesting_window: Option<WindowHandle<Workspace>>,
5287 cx: &mut AppContext,
5288) -> Task<Result<()>> {
5289 let active_call = ActiveCall::global(cx);
5290 cx.spawn(|mut cx| async move {
5291 let result = join_channel_internal(
5292 channel_id,
5293 &app_state,
5294 requesting_window,
5295 &active_call,
5296 &mut cx,
5297 )
5298 .await;
5299
5300 // join channel succeeded, and opened a window
5301 if matches!(result, Ok(true)) {
5302 return anyhow::Ok(());
5303 }
5304
5305 // find an existing workspace to focus and show call controls
5306 let mut active_window =
5307 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5308 if active_window.is_none() {
5309 // no open workspaces, make one to show the error in (blergh)
5310 let (window_handle, _) = cx
5311 .update(|cx| {
5312 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5313 })?
5314 .await?;
5315
5316 if result.is_ok() {
5317 cx.update(|cx| {
5318 cx.dispatch_action(&OpenChannelNotes);
5319 }).log_err();
5320 }
5321
5322 active_window = Some(window_handle);
5323 }
5324
5325 if let Err(err) = result {
5326 log::error!("failed to join channel: {}", err);
5327 if let Some(active_window) = active_window {
5328 active_window
5329 .update(&mut cx, |_, cx| {
5330 let detail: SharedString = match err.error_code() {
5331 ErrorCode::SignedOut => {
5332 "Please sign in to continue.".into()
5333 }
5334 ErrorCode::UpgradeRequired => {
5335 "Your are running an unsupported version of Zed. Please update to continue.".into()
5336 }
5337 ErrorCode::NoSuchChannel => {
5338 "No matching channel was found. Please check the link and try again.".into()
5339 }
5340 ErrorCode::Forbidden => {
5341 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5342 }
5343 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5344 _ => format!("{}\n\nPlease try again.", err).into(),
5345 };
5346 cx.prompt(
5347 PromptLevel::Critical,
5348 "Failed to join channel",
5349 Some(&detail),
5350 &["Ok"],
5351 )
5352 })?
5353 .await
5354 .ok();
5355 }
5356 }
5357
5358 // return ok, we showed the error to the user.
5359 anyhow::Ok(())
5360 })
5361}
5362
5363pub async fn get_any_active_workspace(
5364 app_state: Arc<AppState>,
5365 mut cx: AsyncAppContext,
5366) -> anyhow::Result<WindowHandle<Workspace>> {
5367 // find an existing workspace to focus and show call controls
5368 let active_window = activate_any_workspace_window(&mut cx);
5369 if active_window.is_none() {
5370 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5371 .await?;
5372 }
5373 activate_any_workspace_window(&mut cx).context("could not open zed")
5374}
5375
5376fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5377 cx.update(|cx| {
5378 if let Some(workspace_window) = cx
5379 .active_window()
5380 .and_then(|window| window.downcast::<Workspace>())
5381 {
5382 return Some(workspace_window);
5383 }
5384
5385 for window in cx.windows() {
5386 if let Some(workspace_window) = window.downcast::<Workspace>() {
5387 workspace_window
5388 .update(cx, |_, cx| cx.activate_window())
5389 .ok();
5390 return Some(workspace_window);
5391 }
5392 }
5393 None
5394 })
5395 .ok()
5396 .flatten()
5397}
5398
5399pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5400 cx.windows()
5401 .into_iter()
5402 .filter_map(|window| window.downcast::<Workspace>())
5403 .filter(|workspace| {
5404 workspace
5405 .read(cx)
5406 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5407 })
5408 .collect()
5409}
5410
5411#[derive(Default)]
5412pub struct OpenOptions {
5413 pub open_new_workspace: Option<bool>,
5414 pub replace_window: Option<WindowHandle<Workspace>>,
5415 pub env: Option<HashMap<String, String>>,
5416}
5417
5418#[allow(clippy::type_complexity)]
5419pub fn open_paths(
5420 abs_paths: &[PathBuf],
5421 app_state: Arc<AppState>,
5422 open_options: OpenOptions,
5423 cx: &mut AppContext,
5424) -> Task<
5425 anyhow::Result<(
5426 WindowHandle<Workspace>,
5427 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5428 )>,
5429> {
5430 let abs_paths = abs_paths.to_vec();
5431 let mut existing = None;
5432 let mut best_match = None;
5433 let mut open_visible = OpenVisible::All;
5434
5435 if open_options.open_new_workspace != Some(true) {
5436 for window in local_workspace_windows(cx) {
5437 if let Ok(workspace) = window.read(cx) {
5438 let m = workspace
5439 .project
5440 .read(cx)
5441 .visibility_for_paths(&abs_paths, cx);
5442 if m > best_match {
5443 existing = Some(window);
5444 best_match = m;
5445 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5446 existing = Some(window)
5447 }
5448 }
5449 }
5450 }
5451
5452 cx.spawn(move |mut cx| async move {
5453 if open_options.open_new_workspace.is_none() && existing.is_none() {
5454 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5455 if futures::future::join_all(all_files)
5456 .await
5457 .into_iter()
5458 .filter_map(|result| result.ok().flatten())
5459 .all(|file| !file.is_dir)
5460 {
5461 cx.update(|cx| {
5462 for window in local_workspace_windows(cx) {
5463 if let Ok(workspace) = window.read(cx) {
5464 let project = workspace.project().read(cx);
5465 if project.is_via_collab() {
5466 continue;
5467 }
5468 existing = Some(window);
5469 open_visible = OpenVisible::None;
5470 break;
5471 }
5472 }
5473 })?;
5474 }
5475 }
5476
5477 if let Some(existing) = existing {
5478 Ok((
5479 existing,
5480 existing
5481 .update(&mut cx, |workspace, cx| {
5482 cx.activate_window();
5483 workspace.open_paths(abs_paths, open_visible, None, cx)
5484 })?
5485 .await,
5486 ))
5487 } else {
5488 cx.update(move |cx| {
5489 Workspace::new_local(
5490 abs_paths,
5491 app_state.clone(),
5492 open_options.replace_window,
5493 open_options.env,
5494 cx,
5495 )
5496 })?
5497 .await
5498 }
5499 })
5500}
5501
5502pub fn open_new(
5503 open_options: OpenOptions,
5504 app_state: Arc<AppState>,
5505 cx: &mut AppContext,
5506 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5507) -> Task<anyhow::Result<()>> {
5508 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5509 cx.spawn(|mut cx| async move {
5510 let (workspace, opened_paths) = task.await?;
5511 workspace.update(&mut cx, |workspace, cx| {
5512 if opened_paths.is_empty() {
5513 init(workspace, cx)
5514 }
5515 })?;
5516 Ok(())
5517 })
5518}
5519
5520pub fn create_and_open_local_file(
5521 path: &'static Path,
5522 cx: &mut ViewContext<Workspace>,
5523 default_content: impl 'static + Send + FnOnce() -> Rope,
5524) -> Task<Result<Box<dyn ItemHandle>>> {
5525 cx.spawn(|workspace, mut cx| async move {
5526 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5527 if !fs.is_file(path).await {
5528 fs.create_file(path, Default::default()).await?;
5529 fs.save(path, &default_content(), Default::default())
5530 .await?;
5531 }
5532
5533 let mut items = workspace
5534 .update(&mut cx, |workspace, cx| {
5535 workspace.with_local_workspace(cx, |workspace, cx| {
5536 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5537 })
5538 })?
5539 .await?
5540 .await;
5541
5542 let item = items.pop().flatten();
5543 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5544 })
5545}
5546
5547pub fn open_ssh_project(
5548 window: WindowHandle<Workspace>,
5549 connection_options: SshConnectionOptions,
5550 cancel_rx: oneshot::Receiver<()>,
5551 delegate: Arc<dyn SshClientDelegate>,
5552 app_state: Arc<AppState>,
5553 paths: Vec<PathBuf>,
5554 cx: &mut AppContext,
5555) -> Task<Result<()>> {
5556 cx.spawn(|mut cx| async move {
5557 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5558 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5559
5560 let session = match cx
5561 .update(|cx| {
5562 remote::SshRemoteClient::new(
5563 ConnectionIdentifier::Workspace(workspace_id.0),
5564 connection_options,
5565 cancel_rx,
5566 delegate,
5567 cx,
5568 )
5569 })?
5570 .await?
5571 {
5572 Some(result) => result,
5573 None => return Ok(()),
5574 };
5575
5576 let project = cx.update(|cx| {
5577 project::Project::ssh(
5578 session,
5579 app_state.client.clone(),
5580 app_state.node_runtime.clone(),
5581 app_state.user_store.clone(),
5582 app_state.languages.clone(),
5583 app_state.fs.clone(),
5584 cx,
5585 )
5586 })?;
5587
5588 let toolchains = DB.toolchains(workspace_id).await?;
5589 for (toolchain, worktree_id) in toolchains {
5590 project
5591 .update(&mut cx, |this, cx| {
5592 this.activate_toolchain(worktree_id, toolchain, cx)
5593 })?
5594 .await;
5595 }
5596 let mut project_paths_to_open = vec![];
5597 let mut project_path_errors = vec![];
5598
5599 for path in paths {
5600 let result = cx
5601 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5602 .await;
5603 match result {
5604 Ok((_, project_path)) => {
5605 project_paths_to_open.push((path.clone(), Some(project_path)));
5606 }
5607 Err(error) => {
5608 project_path_errors.push(error);
5609 }
5610 };
5611 }
5612
5613 if project_paths_to_open.is_empty() {
5614 return Err(project_path_errors
5615 .pop()
5616 .unwrap_or_else(|| anyhow!("no paths given")));
5617 }
5618
5619 cx.update_window(window.into(), |_, cx| {
5620 cx.replace_root_view(|cx| {
5621 let mut workspace =
5622 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5623
5624 workspace
5625 .client()
5626 .telemetry()
5627 .report_app_event("open ssh project".to_string());
5628
5629 workspace.set_serialized_ssh_project(serialized_ssh_project);
5630 workspace
5631 });
5632 })?;
5633
5634 window
5635 .update(&mut cx, |_, cx| {
5636 cx.activate_window();
5637
5638 open_items(serialized_workspace, project_paths_to_open, cx)
5639 })?
5640 .await?;
5641
5642 window.update(&mut cx, |workspace, cx| {
5643 for error in project_path_errors {
5644 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5645 if let Some(path) = error.error_tag("path") {
5646 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5647 }
5648 } else {
5649 workspace.show_error(&error, cx)
5650 }
5651 }
5652 })
5653 })
5654}
5655
5656fn serialize_ssh_project(
5657 connection_options: SshConnectionOptions,
5658 paths: Vec<PathBuf>,
5659 cx: &AsyncAppContext,
5660) -> Task<
5661 Result<(
5662 SerializedSshProject,
5663 WorkspaceId,
5664 Option<SerializedWorkspace>,
5665 )>,
5666> {
5667 cx.background_executor().spawn(async move {
5668 let serialized_ssh_project = persistence::DB
5669 .get_or_create_ssh_project(
5670 connection_options.host.clone(),
5671 connection_options.port,
5672 paths
5673 .iter()
5674 .map(|path| path.to_string_lossy().to_string())
5675 .collect::<Vec<_>>(),
5676 connection_options.username.clone(),
5677 )
5678 .await?;
5679
5680 let serialized_workspace =
5681 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5682
5683 let workspace_id = if let Some(workspace_id) =
5684 serialized_workspace.as_ref().map(|workspace| workspace.id)
5685 {
5686 workspace_id
5687 } else {
5688 persistence::DB.next_id().await?
5689 };
5690
5691 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5692 })
5693}
5694
5695pub fn join_in_room_project(
5696 project_id: u64,
5697 follow_user_id: u64,
5698 app_state: Arc<AppState>,
5699 cx: &mut AppContext,
5700) -> Task<Result<()>> {
5701 let windows = cx.windows();
5702 cx.spawn(|mut cx| async move {
5703 let existing_workspace = windows.into_iter().find_map(|window| {
5704 window.downcast::<Workspace>().and_then(|window| {
5705 window
5706 .update(&mut cx, |workspace, cx| {
5707 if workspace.project().read(cx).remote_id() == Some(project_id) {
5708 Some(window)
5709 } else {
5710 None
5711 }
5712 })
5713 .unwrap_or(None)
5714 })
5715 });
5716
5717 let workspace = if let Some(existing_workspace) = existing_workspace {
5718 existing_workspace
5719 } else {
5720 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5721 let room = active_call
5722 .read_with(&cx, |call, _| call.room().cloned())?
5723 .ok_or_else(|| anyhow!("not in a call"))?;
5724 let project = room
5725 .update(&mut cx, |room, cx| {
5726 room.join_project(
5727 project_id,
5728 app_state.languages.clone(),
5729 app_state.fs.clone(),
5730 cx,
5731 )
5732 })?
5733 .await?;
5734
5735 let window_bounds_override = window_bounds_env_override();
5736 cx.update(|cx| {
5737 let mut options = (app_state.build_window_options)(None, cx);
5738 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5739 cx.open_window(options, |cx| {
5740 cx.new_view(|cx| {
5741 Workspace::new(Default::default(), project, app_state.clone(), cx)
5742 })
5743 })
5744 })??
5745 };
5746
5747 workspace.update(&mut cx, |workspace, cx| {
5748 cx.activate(true);
5749 cx.activate_window();
5750
5751 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5752 let follow_peer_id = room
5753 .read(cx)
5754 .remote_participants()
5755 .iter()
5756 .find(|(_, participant)| participant.user.id == follow_user_id)
5757 .map(|(_, p)| p.peer_id)
5758 .or_else(|| {
5759 // If we couldn't follow the given user, follow the host instead.
5760 let collaborator = workspace
5761 .project()
5762 .read(cx)
5763 .collaborators()
5764 .values()
5765 .find(|collaborator| collaborator.is_host)?;
5766 Some(collaborator.peer_id)
5767 });
5768
5769 if let Some(follow_peer_id) = follow_peer_id {
5770 workspace.follow(follow_peer_id, cx);
5771 }
5772 }
5773 })?;
5774
5775 anyhow::Ok(())
5776 })
5777}
5778
5779pub fn reload(reload: &Reload, cx: &mut AppContext) {
5780 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5781 let mut workspace_windows = cx
5782 .windows()
5783 .into_iter()
5784 .filter_map(|window| window.downcast::<Workspace>())
5785 .collect::<Vec<_>>();
5786
5787 // If multiple windows have unsaved changes, and need a save prompt,
5788 // prompt in the active window before switching to a different window.
5789 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5790
5791 let mut prompt = None;
5792 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5793 prompt = window
5794 .update(cx, |_, cx| {
5795 cx.prompt(
5796 PromptLevel::Info,
5797 "Are you sure you want to restart?",
5798 None,
5799 &["Restart", "Cancel"],
5800 )
5801 })
5802 .ok();
5803 }
5804
5805 let binary_path = reload.binary_path.clone();
5806 cx.spawn(|mut cx| async move {
5807 if let Some(prompt) = prompt {
5808 let answer = prompt.await?;
5809 if answer != 0 {
5810 return Ok(());
5811 }
5812 }
5813
5814 // If the user cancels any save prompt, then keep the app open.
5815 for window in workspace_windows {
5816 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5817 workspace.prepare_to_close(CloseIntent::Quit, cx)
5818 }) {
5819 if !should_close.await? {
5820 return Ok(());
5821 }
5822 }
5823 }
5824
5825 cx.update(|cx| cx.restart(binary_path))
5826 })
5827 .detach_and_log_err(cx);
5828}
5829
5830fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5831 let mut parts = value.split(',');
5832 let x: usize = parts.next()?.parse().ok()?;
5833 let y: usize = parts.next()?.parse().ok()?;
5834 Some(point(px(x as f32), px(y as f32)))
5835}
5836
5837fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5838 let mut parts = value.split(',');
5839 let width: usize = parts.next()?.parse().ok()?;
5840 let height: usize = parts.next()?.parse().ok()?;
5841 Some(size(px(width as f32), px(height as f32)))
5842}
5843
5844pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5845 const BORDER_SIZE: Pixels = px(1.0);
5846 let decorations = cx.window_decorations();
5847
5848 if matches!(decorations, Decorations::Client { .. }) {
5849 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5850 }
5851
5852 struct GlobalResizeEdge(ResizeEdge);
5853 impl Global for GlobalResizeEdge {}
5854
5855 div()
5856 .id("window-backdrop")
5857 .bg(transparent_black())
5858 .map(|div| match decorations {
5859 Decorations::Server => div,
5860 Decorations::Client { tiling, .. } => div
5861 .when(!(tiling.top || tiling.right), |div| {
5862 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5863 })
5864 .when(!(tiling.top || tiling.left), |div| {
5865 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5866 })
5867 .when(!(tiling.bottom || tiling.right), |div| {
5868 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5869 })
5870 .when(!(tiling.bottom || tiling.left), |div| {
5871 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5872 })
5873 .when(!tiling.top, |div| {
5874 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5875 })
5876 .when(!tiling.bottom, |div| {
5877 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5878 })
5879 .when(!tiling.left, |div| {
5880 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5881 })
5882 .when(!tiling.right, |div| {
5883 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5884 })
5885 .on_mouse_move(move |e, cx| {
5886 let size = cx.window_bounds().get_bounds().size;
5887 let pos = e.position;
5888
5889 let new_edge =
5890 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5891
5892 let edge = cx.try_global::<GlobalResizeEdge>();
5893 if new_edge != edge.map(|edge| edge.0) {
5894 cx.window_handle()
5895 .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
5896 .ok();
5897 }
5898 })
5899 .on_mouse_down(MouseButton::Left, move |e, cx| {
5900 let size = cx.window_bounds().get_bounds().size;
5901 let pos = e.position;
5902
5903 let edge = match resize_edge(
5904 pos,
5905 theme::CLIENT_SIDE_DECORATION_SHADOW,
5906 size,
5907 tiling,
5908 ) {
5909 Some(value) => value,
5910 None => return,
5911 };
5912
5913 cx.start_window_resize(edge);
5914 }),
5915 })
5916 .size_full()
5917 .child(
5918 div()
5919 .cursor(CursorStyle::Arrow)
5920 .map(|div| match decorations {
5921 Decorations::Server => div,
5922 Decorations::Client { tiling } => div
5923 .border_color(cx.theme().colors().border)
5924 .when(!(tiling.top || tiling.right), |div| {
5925 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5926 })
5927 .when(!(tiling.top || tiling.left), |div| {
5928 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5929 })
5930 .when(!(tiling.bottom || tiling.right), |div| {
5931 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5932 })
5933 .when(!(tiling.bottom || tiling.left), |div| {
5934 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5935 })
5936 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5937 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5938 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5939 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5940 .when(!tiling.is_tiled(), |div| {
5941 div.shadow(smallvec::smallvec![gpui::BoxShadow {
5942 color: Hsla {
5943 h: 0.,
5944 s: 0.,
5945 l: 0.,
5946 a: 0.4,
5947 },
5948 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5949 spread_radius: px(0.),
5950 offset: point(px(0.0), px(0.0)),
5951 }])
5952 }),
5953 })
5954 .on_mouse_move(|_e, cx| {
5955 cx.stop_propagation();
5956 })
5957 .size_full()
5958 .child(element),
5959 )
5960 .map(|div| match decorations {
5961 Decorations::Server => div,
5962 Decorations::Client { tiling, .. } => div.child(
5963 canvas(
5964 |_bounds, cx| {
5965 cx.insert_hitbox(
5966 Bounds::new(
5967 point(px(0.0), px(0.0)),
5968 cx.window_bounds().get_bounds().size,
5969 ),
5970 false,
5971 )
5972 },
5973 move |_bounds, hitbox, cx| {
5974 let mouse = cx.mouse_position();
5975 let size = cx.window_bounds().get_bounds().size;
5976 let Some(edge) =
5977 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5978 else {
5979 return;
5980 };
5981 cx.set_global(GlobalResizeEdge(edge));
5982 cx.set_cursor_style(
5983 match edge {
5984 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5985 ResizeEdge::Left | ResizeEdge::Right => {
5986 CursorStyle::ResizeLeftRight
5987 }
5988 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5989 CursorStyle::ResizeUpLeftDownRight
5990 }
5991 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5992 CursorStyle::ResizeUpRightDownLeft
5993 }
5994 },
5995 &hitbox,
5996 );
5997 },
5998 )
5999 .size_full()
6000 .absolute(),
6001 ),
6002 })
6003}
6004
6005fn resize_edge(
6006 pos: Point<Pixels>,
6007 shadow_size: Pixels,
6008 window_size: Size<Pixels>,
6009 tiling: Tiling,
6010) -> Option<ResizeEdge> {
6011 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6012 if bounds.contains(&pos) {
6013 return None;
6014 }
6015
6016 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6017 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6018 if !tiling.top && top_left_bounds.contains(&pos) {
6019 return Some(ResizeEdge::TopLeft);
6020 }
6021
6022 let top_right_bounds = Bounds::new(
6023 Point::new(window_size.width - corner_size.width, px(0.)),
6024 corner_size,
6025 );
6026 if !tiling.top && top_right_bounds.contains(&pos) {
6027 return Some(ResizeEdge::TopRight);
6028 }
6029
6030 let bottom_left_bounds = Bounds::new(
6031 Point::new(px(0.), window_size.height - corner_size.height),
6032 corner_size,
6033 );
6034 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6035 return Some(ResizeEdge::BottomLeft);
6036 }
6037
6038 let bottom_right_bounds = Bounds::new(
6039 Point::new(
6040 window_size.width - corner_size.width,
6041 window_size.height - corner_size.height,
6042 ),
6043 corner_size,
6044 );
6045 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6046 return Some(ResizeEdge::BottomRight);
6047 }
6048
6049 if !tiling.top && pos.y < shadow_size {
6050 Some(ResizeEdge::Top)
6051 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6052 Some(ResizeEdge::Bottom)
6053 } else if !tiling.left && pos.x < shadow_size {
6054 Some(ResizeEdge::Left)
6055 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6056 Some(ResizeEdge::Right)
6057 } else {
6058 None
6059 }
6060}
6061
6062fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6063 if pane == active_pane {
6064 return;
6065 } else if pane.read(cx).items_len() == 0 {
6066 pane.update(cx, |_, cx| {
6067 cx.emit(pane::Event::Remove {
6068 focus_on_pane: None,
6069 });
6070 })
6071 } else {
6072 move_all_items(pane, active_pane, cx);
6073 }
6074}
6075
6076fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6077 let destination_is_different = from_pane != to_pane;
6078 let mut moved_items = 0;
6079 for (item_ix, item_handle) in from_pane
6080 .read(cx)
6081 .items()
6082 .enumerate()
6083 .map(|(ix, item)| (ix, item.clone()))
6084 .collect::<Vec<_>>()
6085 {
6086 let ix = item_ix - moved_items;
6087 if destination_is_different {
6088 // Close item from previous pane
6089 from_pane.update(cx, |source, cx| {
6090 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6091 });
6092 moved_items += 1;
6093 }
6094
6095 // This automatically removes duplicate items in the pane
6096 to_pane.update(cx, |destination, cx| {
6097 destination.add_item(item_handle, true, true, None, cx);
6098 destination.focus(cx)
6099 });
6100 }
6101}
6102
6103pub fn move_item(
6104 source: &View<Pane>,
6105 destination: &View<Pane>,
6106 item_id_to_move: EntityId,
6107 destination_index: usize,
6108 cx: &mut WindowContext<'_>,
6109) {
6110 let Some((item_ix, item_handle)) = source
6111 .read(cx)
6112 .items()
6113 .enumerate()
6114 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6115 .map(|(ix, item)| (ix, item.clone()))
6116 else {
6117 // Tab was closed during drag
6118 return;
6119 };
6120
6121 if source != destination {
6122 // Close item from previous pane
6123 source.update(cx, |source, cx| {
6124 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6125 });
6126 }
6127
6128 // This automatically removes duplicate items in the pane
6129 destination.update(cx, |destination, cx| {
6130 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6131 destination.focus(cx)
6132 });
6133}
6134
6135#[cfg(test)]
6136mod tests {
6137 use std::{cell::RefCell, rc::Rc};
6138
6139 use super::*;
6140 use crate::{
6141 dock::{test::TestPanel, PanelEvent},
6142 item::{
6143 test::{TestItem, TestProjectItem},
6144 ItemEvent,
6145 },
6146 };
6147 use fs::FakeFs;
6148 use gpui::{
6149 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6150 UpdateGlobal, VisualTestContext,
6151 };
6152 use project::{Project, ProjectEntryId};
6153 use serde_json::json;
6154 use settings::SettingsStore;
6155
6156 #[gpui::test]
6157 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6158 init_test(cx);
6159
6160 let fs = FakeFs::new(cx.executor());
6161 let project = Project::test(fs, [], cx).await;
6162 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6163
6164 // Adding an item with no ambiguity renders the tab without detail.
6165 let item1 = cx.new_view(|cx| {
6166 let mut item = TestItem::new(cx);
6167 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6168 item
6169 });
6170 workspace.update(cx, |workspace, cx| {
6171 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6172 });
6173 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6174
6175 // Adding an item that creates ambiguity increases the level of detail on
6176 // both tabs.
6177 let item2 = cx.new_view(|cx| {
6178 let mut item = TestItem::new(cx);
6179 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6180 item
6181 });
6182 workspace.update(cx, |workspace, cx| {
6183 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6184 });
6185 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6186 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6187
6188 // Adding an item that creates ambiguity increases the level of detail only
6189 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6190 // we stop at the highest detail available.
6191 let item3 = cx.new_view(|cx| {
6192 let mut item = TestItem::new(cx);
6193 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6194 item
6195 });
6196 workspace.update(cx, |workspace, cx| {
6197 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6198 });
6199 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6200 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6201 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6202 }
6203
6204 #[gpui::test]
6205 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6206 init_test(cx);
6207
6208 let fs = FakeFs::new(cx.executor());
6209 fs.insert_tree(
6210 "/root1",
6211 json!({
6212 "one.txt": "",
6213 "two.txt": "",
6214 }),
6215 )
6216 .await;
6217 fs.insert_tree(
6218 "/root2",
6219 json!({
6220 "three.txt": "",
6221 }),
6222 )
6223 .await;
6224
6225 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6226 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6227 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6228 let worktree_id = project.update(cx, |project, cx| {
6229 project.worktrees(cx).next().unwrap().read(cx).id()
6230 });
6231
6232 let item1 = cx.new_view(|cx| {
6233 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6234 });
6235 let item2 = cx.new_view(|cx| {
6236 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6237 });
6238
6239 // Add an item to an empty pane
6240 workspace.update(cx, |workspace, cx| {
6241 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6242 });
6243 project.update(cx, |project, cx| {
6244 assert_eq!(
6245 project.active_entry(),
6246 project
6247 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6248 .map(|e| e.id)
6249 );
6250 });
6251 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6252
6253 // Add a second item to a non-empty pane
6254 workspace.update(cx, |workspace, cx| {
6255 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6256 });
6257 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6258 project.update(cx, |project, cx| {
6259 assert_eq!(
6260 project.active_entry(),
6261 project
6262 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6263 .map(|e| e.id)
6264 );
6265 });
6266
6267 // Close the active item
6268 pane.update(cx, |pane, cx| {
6269 pane.close_active_item(&Default::default(), cx).unwrap()
6270 })
6271 .await
6272 .unwrap();
6273 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6274 project.update(cx, |project, cx| {
6275 assert_eq!(
6276 project.active_entry(),
6277 project
6278 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6279 .map(|e| e.id)
6280 );
6281 });
6282
6283 // Add a project folder
6284 project
6285 .update(cx, |project, cx| {
6286 project.find_or_create_worktree("root2", true, cx)
6287 })
6288 .await
6289 .unwrap();
6290 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6291
6292 // Remove a project folder
6293 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6294 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6295 }
6296
6297 #[gpui::test]
6298 async fn test_close_window(cx: &mut TestAppContext) {
6299 init_test(cx);
6300
6301 let fs = FakeFs::new(cx.executor());
6302 fs.insert_tree("/root", json!({ "one": "" })).await;
6303
6304 let project = Project::test(fs, ["root".as_ref()], cx).await;
6305 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6306
6307 // When there are no dirty items, there's nothing to do.
6308 let item1 = cx.new_view(TestItem::new);
6309 workspace.update(cx, |w, cx| {
6310 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6311 });
6312 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6313 assert!(task.await.unwrap());
6314
6315 // When there are dirty untitled items, prompt to save each one. If the user
6316 // cancels any prompt, then abort.
6317 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6318 let item3 = cx.new_view(|cx| {
6319 TestItem::new(cx)
6320 .with_dirty(true)
6321 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6322 });
6323 workspace.update(cx, |w, cx| {
6324 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6325 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6326 });
6327 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6328 cx.executor().run_until_parked();
6329 cx.simulate_prompt_answer(2); // cancel save all
6330 cx.executor().run_until_parked();
6331 cx.simulate_prompt_answer(2); // cancel save all
6332 cx.executor().run_until_parked();
6333 assert!(!cx.has_pending_prompt());
6334 assert!(!task.await.unwrap());
6335 }
6336
6337 #[gpui::test]
6338 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6339 init_test(cx);
6340
6341 // Register TestItem as a serializable item
6342 cx.update(|cx| {
6343 register_serializable_item::<TestItem>(cx);
6344 });
6345
6346 let fs = FakeFs::new(cx.executor());
6347 fs.insert_tree("/root", json!({ "one": "" })).await;
6348
6349 let project = Project::test(fs, ["root".as_ref()], cx).await;
6350 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6351
6352 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6353 let item1 = cx.new_view(|cx| {
6354 TestItem::new(cx)
6355 .with_dirty(true)
6356 .with_serialize(|| Some(Task::ready(Ok(()))))
6357 });
6358 let item2 = cx.new_view(|cx| {
6359 TestItem::new(cx)
6360 .with_dirty(true)
6361 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6362 .with_serialize(|| Some(Task::ready(Ok(()))))
6363 });
6364 workspace.update(cx, |w, cx| {
6365 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6366 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6367 });
6368 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6369 assert!(task.await.unwrap());
6370 }
6371
6372 #[gpui::test]
6373 async fn test_close_pane_items(cx: &mut TestAppContext) {
6374 init_test(cx);
6375
6376 let fs = FakeFs::new(cx.executor());
6377
6378 let project = Project::test(fs, None, cx).await;
6379 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6380
6381 let item1 = cx.new_view(|cx| {
6382 TestItem::new(cx)
6383 .with_dirty(true)
6384 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6385 });
6386 let item2 = cx.new_view(|cx| {
6387 TestItem::new(cx)
6388 .with_dirty(true)
6389 .with_conflict(true)
6390 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6391 });
6392 let item3 = cx.new_view(|cx| {
6393 TestItem::new(cx)
6394 .with_dirty(true)
6395 .with_conflict(true)
6396 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6397 });
6398 let item4 = cx.new_view(|cx| {
6399 TestItem::new(cx)
6400 .with_dirty(true)
6401 .with_project_items(&[TestProjectItem::new_untitled(cx)])
6402 });
6403 let pane = workspace.update(cx, |workspace, cx| {
6404 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6405 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6406 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6407 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6408 workspace.active_pane().clone()
6409 });
6410
6411 let close_items = pane.update(cx, |pane, cx| {
6412 pane.activate_item(1, true, true, cx);
6413 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6414 let item1_id = item1.item_id();
6415 let item3_id = item3.item_id();
6416 let item4_id = item4.item_id();
6417 pane.close_items(cx, SaveIntent::Close, move |id| {
6418 [item1_id, item3_id, item4_id].contains(&id)
6419 })
6420 });
6421 cx.executor().run_until_parked();
6422
6423 assert!(cx.has_pending_prompt());
6424 // Ignore "Save all" prompt
6425 cx.simulate_prompt_answer(2);
6426 cx.executor().run_until_parked();
6427 // There's a prompt to save item 1.
6428 pane.update(cx, |pane, _| {
6429 assert_eq!(pane.items_len(), 4);
6430 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6431 });
6432 // Confirm saving item 1.
6433 cx.simulate_prompt_answer(0);
6434 cx.executor().run_until_parked();
6435
6436 // Item 1 is saved. There's a prompt to save item 3.
6437 pane.update(cx, |pane, cx| {
6438 assert_eq!(item1.read(cx).save_count, 1);
6439 assert_eq!(item1.read(cx).save_as_count, 0);
6440 assert_eq!(item1.read(cx).reload_count, 0);
6441 assert_eq!(pane.items_len(), 3);
6442 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6443 });
6444 assert!(cx.has_pending_prompt());
6445
6446 // Cancel saving item 3.
6447 cx.simulate_prompt_answer(1);
6448 cx.executor().run_until_parked();
6449
6450 // Item 3 is reloaded. There's a prompt to save item 4.
6451 pane.update(cx, |pane, cx| {
6452 assert_eq!(item3.read(cx).save_count, 0);
6453 assert_eq!(item3.read(cx).save_as_count, 0);
6454 assert_eq!(item3.read(cx).reload_count, 1);
6455 assert_eq!(pane.items_len(), 2);
6456 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6457 });
6458 assert!(cx.has_pending_prompt());
6459
6460 // Confirm saving item 4.
6461 cx.simulate_prompt_answer(0);
6462 cx.executor().run_until_parked();
6463
6464 // There's a prompt for a path for item 4.
6465 cx.simulate_new_path_selection(|_| Some(Default::default()));
6466 close_items.await.unwrap();
6467
6468 // The requested items are closed.
6469 pane.update(cx, |pane, cx| {
6470 assert_eq!(item4.read(cx).save_count, 0);
6471 assert_eq!(item4.read(cx).save_as_count, 1);
6472 assert_eq!(item4.read(cx).reload_count, 0);
6473 assert_eq!(pane.items_len(), 1);
6474 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6475 });
6476 }
6477
6478 #[gpui::test]
6479 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6480 init_test(cx);
6481
6482 let fs = FakeFs::new(cx.executor());
6483 let project = Project::test(fs, [], cx).await;
6484 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6485
6486 // Create several workspace items with single project entries, and two
6487 // workspace items with multiple project entries.
6488 let single_entry_items = (0..=4)
6489 .map(|project_entry_id| {
6490 cx.new_view(|cx| {
6491 TestItem::new(cx)
6492 .with_dirty(true)
6493 .with_project_items(&[TestProjectItem::new(
6494 project_entry_id,
6495 &format!("{project_entry_id}.txt"),
6496 cx,
6497 )])
6498 })
6499 })
6500 .collect::<Vec<_>>();
6501 let item_2_3 = cx.new_view(|cx| {
6502 TestItem::new(cx)
6503 .with_dirty(true)
6504 .with_singleton(false)
6505 .with_project_items(&[
6506 single_entry_items[2].read(cx).project_items[0].clone(),
6507 single_entry_items[3].read(cx).project_items[0].clone(),
6508 ])
6509 });
6510 let item_3_4 = cx.new_view(|cx| {
6511 TestItem::new(cx)
6512 .with_dirty(true)
6513 .with_singleton(false)
6514 .with_project_items(&[
6515 single_entry_items[3].read(cx).project_items[0].clone(),
6516 single_entry_items[4].read(cx).project_items[0].clone(),
6517 ])
6518 });
6519
6520 // Create two panes that contain the following project entries:
6521 // left pane:
6522 // multi-entry items: (2, 3)
6523 // single-entry items: 0, 1, 2, 3, 4
6524 // right pane:
6525 // single-entry items: 1
6526 // multi-entry items: (3, 4)
6527 let left_pane = workspace.update(cx, |workspace, cx| {
6528 let left_pane = workspace.active_pane().clone();
6529 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6530 for item in single_entry_items {
6531 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6532 }
6533 left_pane.update(cx, |pane, cx| {
6534 pane.activate_item(2, true, true, cx);
6535 });
6536
6537 let right_pane = workspace
6538 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6539 .unwrap();
6540
6541 right_pane.update(cx, |pane, cx| {
6542 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6543 });
6544
6545 left_pane
6546 });
6547
6548 cx.focus_view(&left_pane);
6549
6550 // When closing all of the items in the left pane, we should be prompted twice:
6551 // once for project entry 0, and once for project entry 2. Project entries 1,
6552 // 3, and 4 are all still open in the other paten. After those two
6553 // prompts, the task should complete.
6554
6555 let close = left_pane.update(cx, |pane, cx| {
6556 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6557 });
6558 cx.executor().run_until_parked();
6559
6560 // Discard "Save all" prompt
6561 cx.simulate_prompt_answer(2);
6562
6563 cx.executor().run_until_parked();
6564 left_pane.update(cx, |pane, cx| {
6565 assert_eq!(
6566 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6567 &[ProjectEntryId::from_proto(0)]
6568 );
6569 });
6570 cx.simulate_prompt_answer(0);
6571
6572 cx.executor().run_until_parked();
6573 left_pane.update(cx, |pane, cx| {
6574 assert_eq!(
6575 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6576 &[ProjectEntryId::from_proto(2)]
6577 );
6578 });
6579 cx.simulate_prompt_answer(0);
6580
6581 cx.executor().run_until_parked();
6582 close.await.unwrap();
6583 left_pane.update(cx, |pane, _| {
6584 assert_eq!(pane.items_len(), 0);
6585 });
6586 }
6587
6588 #[gpui::test]
6589 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6590 init_test(cx);
6591
6592 let fs = FakeFs::new(cx.executor());
6593 let project = Project::test(fs, [], cx).await;
6594 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6595 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6596
6597 let item = cx.new_view(|cx| {
6598 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6599 });
6600 let item_id = item.entity_id();
6601 workspace.update(cx, |workspace, cx| {
6602 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6603 });
6604
6605 // Autosave on window change.
6606 item.update(cx, |item, cx| {
6607 SettingsStore::update_global(cx, |settings, cx| {
6608 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6609 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6610 })
6611 });
6612 item.is_dirty = true;
6613 });
6614
6615 // Deactivating the window saves the file.
6616 cx.deactivate_window();
6617 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6618
6619 // Re-activating the window doesn't save the file.
6620 cx.update(|cx| cx.activate_window());
6621 cx.executor().run_until_parked();
6622 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6623
6624 // Autosave on focus change.
6625 item.update(cx, |item, cx| {
6626 cx.focus_self();
6627 SettingsStore::update_global(cx, |settings, cx| {
6628 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6629 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6630 })
6631 });
6632 item.is_dirty = true;
6633 });
6634
6635 // Blurring the item saves the file.
6636 item.update(cx, |_, cx| cx.blur());
6637 cx.executor().run_until_parked();
6638 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6639
6640 // Deactivating the window still saves the file.
6641 item.update(cx, |item, cx| {
6642 cx.focus_self();
6643 item.is_dirty = true;
6644 });
6645 cx.deactivate_window();
6646 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6647
6648 // Autosave after delay.
6649 item.update(cx, |item, cx| {
6650 SettingsStore::update_global(cx, |settings, cx| {
6651 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6652 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6653 })
6654 });
6655 item.is_dirty = true;
6656 cx.emit(ItemEvent::Edit);
6657 });
6658
6659 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6660 cx.executor().advance_clock(Duration::from_millis(250));
6661 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6662
6663 // After delay expires, the file is saved.
6664 cx.executor().advance_clock(Duration::from_millis(250));
6665 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6666
6667 // Autosave on focus change, ensuring closing the tab counts as such.
6668 item.update(cx, |item, cx| {
6669 SettingsStore::update_global(cx, |settings, cx| {
6670 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6671 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6672 })
6673 });
6674 item.is_dirty = true;
6675 });
6676
6677 pane.update(cx, |pane, cx| {
6678 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6679 })
6680 .await
6681 .unwrap();
6682 assert!(!cx.has_pending_prompt());
6683 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6684
6685 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6686 workspace.update(cx, |workspace, cx| {
6687 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6688 });
6689 item.update(cx, |item, cx| {
6690 item.project_items[0].update(cx, |item, _| {
6691 item.entry_id = None;
6692 });
6693 item.is_dirty = true;
6694 cx.blur();
6695 });
6696 cx.run_until_parked();
6697 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6698
6699 // Ensure autosave is prevented for deleted files also when closing the buffer.
6700 let _close_items = pane.update(cx, |pane, cx| {
6701 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6702 });
6703 cx.run_until_parked();
6704 assert!(cx.has_pending_prompt());
6705 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6706 }
6707
6708 #[gpui::test]
6709 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6710 init_test(cx);
6711
6712 let fs = FakeFs::new(cx.executor());
6713
6714 let project = Project::test(fs, [], cx).await;
6715 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6716
6717 let item = cx.new_view(|cx| {
6718 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6719 });
6720 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6721 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6722 let toolbar_notify_count = Rc::new(RefCell::new(0));
6723
6724 workspace.update(cx, |workspace, cx| {
6725 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6726 let toolbar_notification_count = toolbar_notify_count.clone();
6727 cx.observe(&toolbar, move |_, _, _| {
6728 *toolbar_notification_count.borrow_mut() += 1
6729 })
6730 .detach();
6731 });
6732
6733 pane.update(cx, |pane, _| {
6734 assert!(!pane.can_navigate_backward());
6735 assert!(!pane.can_navigate_forward());
6736 });
6737
6738 item.update(cx, |item, cx| {
6739 item.set_state("one".to_string(), cx);
6740 });
6741
6742 // Toolbar must be notified to re-render the navigation buttons
6743 assert_eq!(*toolbar_notify_count.borrow(), 1);
6744
6745 pane.update(cx, |pane, _| {
6746 assert!(pane.can_navigate_backward());
6747 assert!(!pane.can_navigate_forward());
6748 });
6749
6750 workspace
6751 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6752 .await
6753 .unwrap();
6754
6755 assert_eq!(*toolbar_notify_count.borrow(), 2);
6756 pane.update(cx, |pane, _| {
6757 assert!(!pane.can_navigate_backward());
6758 assert!(pane.can_navigate_forward());
6759 });
6760 }
6761
6762 #[gpui::test]
6763 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6764 init_test(cx);
6765 let fs = FakeFs::new(cx.executor());
6766
6767 let project = Project::test(fs, [], cx).await;
6768 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6769
6770 let panel = workspace.update(cx, |workspace, cx| {
6771 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6772 workspace.add_panel(panel.clone(), cx);
6773
6774 workspace
6775 .right_dock()
6776 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6777
6778 panel
6779 });
6780
6781 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6782 pane.update(cx, |pane, cx| {
6783 let item = cx.new_view(TestItem::new);
6784 pane.add_item(Box::new(item), true, true, None, cx);
6785 });
6786
6787 // Transfer focus from center to panel
6788 workspace.update(cx, |workspace, cx| {
6789 workspace.toggle_panel_focus::<TestPanel>(cx);
6790 });
6791
6792 workspace.update(cx, |workspace, cx| {
6793 assert!(workspace.right_dock().read(cx).is_open());
6794 assert!(!panel.is_zoomed(cx));
6795 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6796 });
6797
6798 // Transfer focus from panel to center
6799 workspace.update(cx, |workspace, cx| {
6800 workspace.toggle_panel_focus::<TestPanel>(cx);
6801 });
6802
6803 workspace.update(cx, |workspace, cx| {
6804 assert!(workspace.right_dock().read(cx).is_open());
6805 assert!(!panel.is_zoomed(cx));
6806 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6807 });
6808
6809 // Close the dock
6810 workspace.update(cx, |workspace, cx| {
6811 workspace.toggle_dock(DockPosition::Right, cx);
6812 });
6813
6814 workspace.update(cx, |workspace, cx| {
6815 assert!(!workspace.right_dock().read(cx).is_open());
6816 assert!(!panel.is_zoomed(cx));
6817 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6818 });
6819
6820 // Open the dock
6821 workspace.update(cx, |workspace, cx| {
6822 workspace.toggle_dock(DockPosition::Right, cx);
6823 });
6824
6825 workspace.update(cx, |workspace, cx| {
6826 assert!(workspace.right_dock().read(cx).is_open());
6827 assert!(!panel.is_zoomed(cx));
6828 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6829 });
6830
6831 // Focus and zoom panel
6832 panel.update(cx, |panel, cx| {
6833 cx.focus_self();
6834 panel.set_zoomed(true, cx)
6835 });
6836
6837 workspace.update(cx, |workspace, cx| {
6838 assert!(workspace.right_dock().read(cx).is_open());
6839 assert!(panel.is_zoomed(cx));
6840 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6841 });
6842
6843 // Transfer focus to the center closes the dock
6844 workspace.update(cx, |workspace, cx| {
6845 workspace.toggle_panel_focus::<TestPanel>(cx);
6846 });
6847
6848 workspace.update(cx, |workspace, cx| {
6849 assert!(!workspace.right_dock().read(cx).is_open());
6850 assert!(panel.is_zoomed(cx));
6851 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6852 });
6853
6854 // Transferring focus back to the panel keeps it zoomed
6855 workspace.update(cx, |workspace, cx| {
6856 workspace.toggle_panel_focus::<TestPanel>(cx);
6857 });
6858
6859 workspace.update(cx, |workspace, cx| {
6860 assert!(workspace.right_dock().read(cx).is_open());
6861 assert!(panel.is_zoomed(cx));
6862 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6863 });
6864
6865 // Close the dock while it is zoomed
6866 workspace.update(cx, |workspace, cx| {
6867 workspace.toggle_dock(DockPosition::Right, cx)
6868 });
6869
6870 workspace.update(cx, |workspace, cx| {
6871 assert!(!workspace.right_dock().read(cx).is_open());
6872 assert!(panel.is_zoomed(cx));
6873 assert!(workspace.zoomed.is_none());
6874 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6875 });
6876
6877 // Opening the dock, when it's zoomed, retains focus
6878 workspace.update(cx, |workspace, cx| {
6879 workspace.toggle_dock(DockPosition::Right, cx)
6880 });
6881
6882 workspace.update(cx, |workspace, cx| {
6883 assert!(workspace.right_dock().read(cx).is_open());
6884 assert!(panel.is_zoomed(cx));
6885 assert!(workspace.zoomed.is_some());
6886 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6887 });
6888
6889 // Unzoom and close the panel, zoom the active pane.
6890 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6891 workspace.update(cx, |workspace, cx| {
6892 workspace.toggle_dock(DockPosition::Right, cx)
6893 });
6894 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6895
6896 // Opening a dock unzooms the pane.
6897 workspace.update(cx, |workspace, cx| {
6898 workspace.toggle_dock(DockPosition::Right, cx)
6899 });
6900 workspace.update(cx, |workspace, cx| {
6901 let pane = pane.read(cx);
6902 assert!(!pane.is_zoomed());
6903 assert!(!pane.focus_handle(cx).is_focused(cx));
6904 assert!(workspace.right_dock().read(cx).is_open());
6905 assert!(workspace.zoomed.is_none());
6906 });
6907 }
6908
6909 #[gpui::test]
6910 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6911 init_test(cx);
6912
6913 let fs = FakeFs::new(cx.executor());
6914
6915 let project = Project::test(fs, None, cx).await;
6916 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6917
6918 // Let's arrange the panes like this:
6919 //
6920 // +-----------------------+
6921 // | top |
6922 // +------+--------+-------+
6923 // | left | center | right |
6924 // +------+--------+-------+
6925 // | bottom |
6926 // +-----------------------+
6927
6928 let top_item = cx.new_view(|cx| {
6929 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6930 });
6931 let bottom_item = cx.new_view(|cx| {
6932 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6933 });
6934 let left_item = cx.new_view(|cx| {
6935 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6936 });
6937 let right_item = cx.new_view(|cx| {
6938 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6939 });
6940 let center_item = cx.new_view(|cx| {
6941 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6942 });
6943
6944 let top_pane_id = workspace.update(cx, |workspace, cx| {
6945 let top_pane_id = workspace.active_pane().entity_id();
6946 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6947 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6948 top_pane_id
6949 });
6950 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6951 let bottom_pane_id = workspace.active_pane().entity_id();
6952 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6953 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6954 bottom_pane_id
6955 });
6956 let left_pane_id = workspace.update(cx, |workspace, cx| {
6957 let left_pane_id = workspace.active_pane().entity_id();
6958 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6959 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6960 left_pane_id
6961 });
6962 let right_pane_id = workspace.update(cx, |workspace, cx| {
6963 let right_pane_id = workspace.active_pane().entity_id();
6964 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6965 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6966 right_pane_id
6967 });
6968 let center_pane_id = workspace.update(cx, |workspace, cx| {
6969 let center_pane_id = workspace.active_pane().entity_id();
6970 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6971 center_pane_id
6972 });
6973 cx.executor().run_until_parked();
6974
6975 workspace.update(cx, |workspace, cx| {
6976 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6977
6978 // Join into next from center pane into right
6979 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6980 });
6981
6982 workspace.update(cx, |workspace, cx| {
6983 let active_pane = workspace.active_pane();
6984 assert_eq!(right_pane_id, active_pane.entity_id());
6985 assert_eq!(2, active_pane.read(cx).items_len());
6986 let item_ids_in_pane =
6987 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6988 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6989 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6990
6991 // Join into next from right pane into bottom
6992 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6993 });
6994
6995 workspace.update(cx, |workspace, cx| {
6996 let active_pane = workspace.active_pane();
6997 assert_eq!(bottom_pane_id, active_pane.entity_id());
6998 assert_eq!(3, active_pane.read(cx).items_len());
6999 let item_ids_in_pane =
7000 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7001 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7002 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7003 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7004
7005 // Join into next from bottom pane into left
7006 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7007 });
7008
7009 workspace.update(cx, |workspace, cx| {
7010 let active_pane = workspace.active_pane();
7011 assert_eq!(left_pane_id, active_pane.entity_id());
7012 assert_eq!(4, active_pane.read(cx).items_len());
7013 let item_ids_in_pane =
7014 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7015 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7016 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7017 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7018 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7019
7020 // Join into next from left pane into top
7021 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7022 });
7023
7024 workspace.update(cx, |workspace, cx| {
7025 let active_pane = workspace.active_pane();
7026 assert_eq!(top_pane_id, active_pane.entity_id());
7027 assert_eq!(5, active_pane.read(cx).items_len());
7028 let item_ids_in_pane =
7029 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7030 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7031 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7032 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7033 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7034 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7035
7036 // Single pane left: no-op
7037 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7038 });
7039
7040 workspace.update(cx, |workspace, _cx| {
7041 let active_pane = workspace.active_pane();
7042 assert_eq!(top_pane_id, active_pane.entity_id());
7043 });
7044 }
7045
7046 fn add_an_item_to_active_pane(
7047 cx: &mut VisualTestContext,
7048 workspace: &View<Workspace>,
7049 item_id: u64,
7050 ) -> View<TestItem> {
7051 let item = cx.new_view(|cx| {
7052 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7053 item_id,
7054 "item{item_id}.txt",
7055 cx,
7056 )])
7057 });
7058 workspace.update(cx, |workspace, cx| {
7059 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7060 });
7061 return item;
7062 }
7063
7064 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7065 return workspace.update(cx, |workspace, cx| {
7066 let new_pane =
7067 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7068 new_pane
7069 });
7070 }
7071
7072 #[gpui::test]
7073 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7074 init_test(cx);
7075 let fs = FakeFs::new(cx.executor());
7076 let project = Project::test(fs, None, cx).await;
7077 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7078
7079 add_an_item_to_active_pane(cx, &workspace, 1);
7080 split_pane(cx, &workspace);
7081 add_an_item_to_active_pane(cx, &workspace, 2);
7082 split_pane(cx, &workspace); // empty pane
7083 split_pane(cx, &workspace);
7084 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7085
7086 cx.executor().run_until_parked();
7087
7088 workspace.update(cx, |workspace, cx| {
7089 let num_panes = workspace.panes().len();
7090 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7091 let active_item = workspace
7092 .active_pane()
7093 .read(cx)
7094 .active_item()
7095 .expect("item is in focus");
7096
7097 assert_eq!(num_panes, 4);
7098 assert_eq!(num_items_in_current_pane, 1);
7099 assert_eq!(active_item.item_id(), last_item.item_id());
7100 });
7101
7102 workspace.update(cx, |workspace, cx| {
7103 workspace.join_all_panes(cx);
7104 });
7105
7106 workspace.update(cx, |workspace, cx| {
7107 let num_panes = workspace.panes().len();
7108 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7109 let active_item = workspace
7110 .active_pane()
7111 .read(cx)
7112 .active_item()
7113 .expect("item is in focus");
7114
7115 assert_eq!(num_panes, 1);
7116 assert_eq!(num_items_in_current_pane, 3);
7117 assert_eq!(active_item.item_id(), last_item.item_id());
7118 });
7119 }
7120 struct TestModal(FocusHandle);
7121
7122 impl TestModal {
7123 fn new(cx: &mut ViewContext<Self>) -> Self {
7124 Self(cx.focus_handle())
7125 }
7126 }
7127
7128 impl EventEmitter<DismissEvent> for TestModal {}
7129
7130 impl FocusableView for TestModal {
7131 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7132 self.0.clone()
7133 }
7134 }
7135
7136 impl ModalView for TestModal {}
7137
7138 impl Render for TestModal {
7139 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7140 div().track_focus(&self.0)
7141 }
7142 }
7143
7144 #[gpui::test]
7145 async fn test_panels(cx: &mut gpui::TestAppContext) {
7146 init_test(cx);
7147 let fs = FakeFs::new(cx.executor());
7148
7149 let project = Project::test(fs, [], cx).await;
7150 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7151
7152 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7153 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7154 workspace.add_panel(panel_1.clone(), cx);
7155 workspace
7156 .left_dock()
7157 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7158 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7159 workspace.add_panel(panel_2.clone(), cx);
7160 workspace
7161 .right_dock()
7162 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7163
7164 let left_dock = workspace.left_dock();
7165 assert_eq!(
7166 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7167 panel_1.panel_id()
7168 );
7169 assert_eq!(
7170 left_dock.read(cx).active_panel_size(cx).unwrap(),
7171 panel_1.size(cx)
7172 );
7173
7174 left_dock.update(cx, |left_dock, cx| {
7175 left_dock.resize_active_panel(Some(px(1337.)), cx)
7176 });
7177 assert_eq!(
7178 workspace
7179 .right_dock()
7180 .read(cx)
7181 .visible_panel()
7182 .unwrap()
7183 .panel_id(),
7184 panel_2.panel_id(),
7185 );
7186
7187 (panel_1, panel_2)
7188 });
7189
7190 // Move panel_1 to the right
7191 panel_1.update(cx, |panel_1, cx| {
7192 panel_1.set_position(DockPosition::Right, cx)
7193 });
7194
7195 workspace.update(cx, |workspace, cx| {
7196 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7197 // Since it was the only panel on the left, the left dock should now be closed.
7198 assert!(!workspace.left_dock().read(cx).is_open());
7199 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7200 let right_dock = workspace.right_dock();
7201 assert_eq!(
7202 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7203 panel_1.panel_id()
7204 );
7205 assert_eq!(
7206 right_dock.read(cx).active_panel_size(cx).unwrap(),
7207 px(1337.)
7208 );
7209
7210 // Now we move panel_2 to the left
7211 panel_2.set_position(DockPosition::Left, cx);
7212 });
7213
7214 workspace.update(cx, |workspace, cx| {
7215 // Since panel_2 was not visible on the right, we don't open the left dock.
7216 assert!(!workspace.left_dock().read(cx).is_open());
7217 // And the right dock is unaffected in its displaying of panel_1
7218 assert!(workspace.right_dock().read(cx).is_open());
7219 assert_eq!(
7220 workspace
7221 .right_dock()
7222 .read(cx)
7223 .visible_panel()
7224 .unwrap()
7225 .panel_id(),
7226 panel_1.panel_id(),
7227 );
7228 });
7229
7230 // Move panel_1 back to the left
7231 panel_1.update(cx, |panel_1, cx| {
7232 panel_1.set_position(DockPosition::Left, cx)
7233 });
7234
7235 workspace.update(cx, |workspace, cx| {
7236 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7237 let left_dock = workspace.left_dock();
7238 assert!(left_dock.read(cx).is_open());
7239 assert_eq!(
7240 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7241 panel_1.panel_id()
7242 );
7243 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7244 // And the right dock should be closed as it no longer has any panels.
7245 assert!(!workspace.right_dock().read(cx).is_open());
7246
7247 // Now we move panel_1 to the bottom
7248 panel_1.set_position(DockPosition::Bottom, cx);
7249 });
7250
7251 workspace.update(cx, |workspace, cx| {
7252 // Since panel_1 was visible on the left, we close the left dock.
7253 assert!(!workspace.left_dock().read(cx).is_open());
7254 // The bottom dock is sized based on the panel's default size,
7255 // since the panel orientation changed from vertical to horizontal.
7256 let bottom_dock = workspace.bottom_dock();
7257 assert_eq!(
7258 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7259 panel_1.size(cx),
7260 );
7261 // Close bottom dock and move panel_1 back to the left.
7262 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7263 panel_1.set_position(DockPosition::Left, cx);
7264 });
7265
7266 // Emit activated event on panel 1
7267 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7268
7269 // Now the left dock is open and panel_1 is active and focused.
7270 workspace.update(cx, |workspace, cx| {
7271 let left_dock = workspace.left_dock();
7272 assert!(left_dock.read(cx).is_open());
7273 assert_eq!(
7274 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7275 panel_1.panel_id(),
7276 );
7277 assert!(panel_1.focus_handle(cx).is_focused(cx));
7278 });
7279
7280 // Emit closed event on panel 2, which is not active
7281 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7282
7283 // Wo don't close the left dock, because panel_2 wasn't the active panel
7284 workspace.update(cx, |workspace, cx| {
7285 let left_dock = workspace.left_dock();
7286 assert!(left_dock.read(cx).is_open());
7287 assert_eq!(
7288 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7289 panel_1.panel_id(),
7290 );
7291 });
7292
7293 // Emitting a ZoomIn event shows the panel as zoomed.
7294 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7295 workspace.update(cx, |workspace, _| {
7296 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7297 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7298 });
7299
7300 // Move panel to another dock while it is zoomed
7301 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7302 workspace.update(cx, |workspace, _| {
7303 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7304
7305 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7306 });
7307
7308 // This is a helper for getting a:
7309 // - valid focus on an element,
7310 // - that isn't a part of the panes and panels system of the Workspace,
7311 // - and doesn't trigger the 'on_focus_lost' API.
7312 let focus_other_view = {
7313 let workspace = workspace.clone();
7314 move |cx: &mut VisualTestContext| {
7315 workspace.update(cx, |workspace, cx| {
7316 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7317 workspace.toggle_modal(cx, TestModal::new);
7318 workspace.toggle_modal(cx, TestModal::new);
7319 } else {
7320 workspace.toggle_modal(cx, TestModal::new);
7321 }
7322 })
7323 }
7324 };
7325
7326 // If focus is transferred to another view that's not a panel or another pane, we still show
7327 // the panel as zoomed.
7328 focus_other_view(cx);
7329 workspace.update(cx, |workspace, _| {
7330 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7331 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7332 });
7333
7334 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7335 workspace.update(cx, |_, cx| cx.focus_self());
7336 workspace.update(cx, |workspace, _| {
7337 assert_eq!(workspace.zoomed, None);
7338 assert_eq!(workspace.zoomed_position, None);
7339 });
7340
7341 // If focus is transferred again to another view that's not a panel or a pane, we won't
7342 // show the panel as zoomed because it wasn't zoomed before.
7343 focus_other_view(cx);
7344 workspace.update(cx, |workspace, _| {
7345 assert_eq!(workspace.zoomed, None);
7346 assert_eq!(workspace.zoomed_position, None);
7347 });
7348
7349 // When the panel is activated, it is zoomed again.
7350 cx.dispatch_action(ToggleRightDock);
7351 workspace.update(cx, |workspace, _| {
7352 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7353 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7354 });
7355
7356 // Emitting a ZoomOut event unzooms the panel.
7357 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7358 workspace.update(cx, |workspace, _| {
7359 assert_eq!(workspace.zoomed, None);
7360 assert_eq!(workspace.zoomed_position, None);
7361 });
7362
7363 // Emit closed event on panel 1, which is active
7364 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7365
7366 // Now the left dock is closed, because panel_1 was the active panel
7367 workspace.update(cx, |workspace, cx| {
7368 let right_dock = workspace.right_dock();
7369 assert!(!right_dock.read(cx).is_open());
7370 });
7371 }
7372
7373 mod register_project_item_tests {
7374 use ui::Context as _;
7375
7376 use super::*;
7377
7378 // View
7379 struct TestPngItemView {
7380 focus_handle: FocusHandle,
7381 }
7382 // Model
7383 struct TestPngItem {}
7384
7385 impl project::Item for TestPngItem {
7386 fn try_open(
7387 _project: &Model<Project>,
7388 path: &ProjectPath,
7389 cx: &mut AppContext,
7390 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7391 if path.path.extension().unwrap() == "png" {
7392 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7393 } else {
7394 None
7395 }
7396 }
7397
7398 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7399 None
7400 }
7401
7402 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7403 None
7404 }
7405 }
7406
7407 impl Item for TestPngItemView {
7408 type Event = ();
7409 }
7410 impl EventEmitter<()> for TestPngItemView {}
7411 impl FocusableView for TestPngItemView {
7412 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7413 self.focus_handle.clone()
7414 }
7415 }
7416
7417 impl Render for TestPngItemView {
7418 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7419 Empty
7420 }
7421 }
7422
7423 impl ProjectItem for TestPngItemView {
7424 type Item = TestPngItem;
7425
7426 fn for_project_item(
7427 _project: Model<Project>,
7428 _item: Model<Self::Item>,
7429 cx: &mut ViewContext<Self>,
7430 ) -> Self
7431 where
7432 Self: Sized,
7433 {
7434 Self {
7435 focus_handle: cx.focus_handle(),
7436 }
7437 }
7438 }
7439
7440 // View
7441 struct TestIpynbItemView {
7442 focus_handle: FocusHandle,
7443 }
7444 // Model
7445 struct TestIpynbItem {}
7446
7447 impl project::Item for TestIpynbItem {
7448 fn try_open(
7449 _project: &Model<Project>,
7450 path: &ProjectPath,
7451 cx: &mut AppContext,
7452 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7453 if path.path.extension().unwrap() == "ipynb" {
7454 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7455 } else {
7456 None
7457 }
7458 }
7459
7460 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7461 None
7462 }
7463
7464 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7465 None
7466 }
7467 }
7468
7469 impl Item for TestIpynbItemView {
7470 type Event = ();
7471 }
7472 impl EventEmitter<()> for TestIpynbItemView {}
7473 impl FocusableView for TestIpynbItemView {
7474 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7475 self.focus_handle.clone()
7476 }
7477 }
7478
7479 impl Render for TestIpynbItemView {
7480 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7481 Empty
7482 }
7483 }
7484
7485 impl ProjectItem for TestIpynbItemView {
7486 type Item = TestIpynbItem;
7487
7488 fn for_project_item(
7489 _project: Model<Project>,
7490 _item: Model<Self::Item>,
7491 cx: &mut ViewContext<Self>,
7492 ) -> Self
7493 where
7494 Self: Sized,
7495 {
7496 Self {
7497 focus_handle: cx.focus_handle(),
7498 }
7499 }
7500 }
7501
7502 struct TestAlternatePngItemView {
7503 focus_handle: FocusHandle,
7504 }
7505
7506 impl Item for TestAlternatePngItemView {
7507 type Event = ();
7508 }
7509
7510 impl EventEmitter<()> for TestAlternatePngItemView {}
7511 impl FocusableView for TestAlternatePngItemView {
7512 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7513 self.focus_handle.clone()
7514 }
7515 }
7516
7517 impl Render for TestAlternatePngItemView {
7518 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7519 Empty
7520 }
7521 }
7522
7523 impl ProjectItem for TestAlternatePngItemView {
7524 type Item = TestPngItem;
7525
7526 fn for_project_item(
7527 _project: Model<Project>,
7528 _item: Model<Self::Item>,
7529 cx: &mut ViewContext<Self>,
7530 ) -> Self
7531 where
7532 Self: Sized,
7533 {
7534 Self {
7535 focus_handle: cx.focus_handle(),
7536 }
7537 }
7538 }
7539
7540 #[gpui::test]
7541 async fn test_register_project_item(cx: &mut TestAppContext) {
7542 init_test(cx);
7543
7544 cx.update(|cx| {
7545 register_project_item::<TestPngItemView>(cx);
7546 register_project_item::<TestIpynbItemView>(cx);
7547 });
7548
7549 let fs = FakeFs::new(cx.executor());
7550 fs.insert_tree(
7551 "/root1",
7552 json!({
7553 "one.png": "BINARYDATAHERE",
7554 "two.ipynb": "{ totally a notebook }",
7555 "three.txt": "editing text, sure why not?"
7556 }),
7557 )
7558 .await;
7559
7560 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7561 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7562
7563 let worktree_id = project.update(cx, |project, cx| {
7564 project.worktrees(cx).next().unwrap().read(cx).id()
7565 });
7566
7567 let handle = workspace
7568 .update(cx, |workspace, cx| {
7569 let project_path = (worktree_id, "one.png");
7570 workspace.open_path(project_path, None, true, cx)
7571 })
7572 .await
7573 .unwrap();
7574
7575 // Now we can check if the handle we got back errored or not
7576 assert_eq!(
7577 handle.to_any().entity_type(),
7578 TypeId::of::<TestPngItemView>()
7579 );
7580
7581 let handle = workspace
7582 .update(cx, |workspace, cx| {
7583 let project_path = (worktree_id, "two.ipynb");
7584 workspace.open_path(project_path, None, true, cx)
7585 })
7586 .await
7587 .unwrap();
7588
7589 assert_eq!(
7590 handle.to_any().entity_type(),
7591 TypeId::of::<TestIpynbItemView>()
7592 );
7593
7594 let handle = workspace
7595 .update(cx, |workspace, cx| {
7596 let project_path = (worktree_id, "three.txt");
7597 workspace.open_path(project_path, None, true, cx)
7598 })
7599 .await;
7600 assert!(handle.is_err());
7601 }
7602
7603 #[gpui::test]
7604 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7605 init_test(cx);
7606
7607 cx.update(|cx| {
7608 register_project_item::<TestPngItemView>(cx);
7609 register_project_item::<TestAlternatePngItemView>(cx);
7610 });
7611
7612 let fs = FakeFs::new(cx.executor());
7613 fs.insert_tree(
7614 "/root1",
7615 json!({
7616 "one.png": "BINARYDATAHERE",
7617 "two.ipynb": "{ totally a notebook }",
7618 "three.txt": "editing text, sure why not?"
7619 }),
7620 )
7621 .await;
7622
7623 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7624 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7625
7626 let worktree_id = project.update(cx, |project, cx| {
7627 project.worktrees(cx).next().unwrap().read(cx).id()
7628 });
7629
7630 let handle = workspace
7631 .update(cx, |workspace, cx| {
7632 let project_path = (worktree_id, "one.png");
7633 workspace.open_path(project_path, None, true, cx)
7634 })
7635 .await
7636 .unwrap();
7637
7638 // This _must_ be the second item registered
7639 assert_eq!(
7640 handle.to_any().entity_type(),
7641 TypeId::of::<TestAlternatePngItemView>()
7642 );
7643
7644 let handle = workspace
7645 .update(cx, |workspace, cx| {
7646 let project_path = (worktree_id, "three.txt");
7647 workspace.open_path(project_path, None, true, cx)
7648 })
7649 .await;
7650 assert!(handle.is_err());
7651 }
7652 }
7653
7654 pub fn init_test(cx: &mut TestAppContext) {
7655 cx.update(|cx| {
7656 let settings_store = SettingsStore::test(cx);
7657 cx.set_global(settings_store);
7658 theme::init(theme::LoadThemes::JustBase, cx);
7659 language::init(cx);
7660 crate::init_settings(cx);
7661 Project::init_settings(cx);
7662 });
7663 }
7664}