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