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