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