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