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 // Defer this to avoid a panic when the dock's active panel is already on the stack.
2979 cx.defer(move |cx| {
2980 let dock = dock.read(cx);
2981 if let Some(panel) = dock.active_panel() {
2982 panel.focus_handle(cx).focus(cx);
2983 } else {
2984 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
2985 }
2986 })
2987 }
2988 None => {}
2989 }
2990 }
2991
2992 pub fn move_item_to_pane_in_direction(
2993 &mut self,
2994 action: &MoveItemToPaneInDirection,
2995 cx: &mut WindowContext,
2996 ) {
2997 if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
2998 move_active_item(&self.active_pane, &destination, action.focus, true, cx);
2999 }
3000 }
3001
3002 pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
3003 self.center.bounding_box_for_pane(pane)
3004 }
3005
3006 pub fn find_pane_in_direction(
3007 &mut self,
3008 direction: SplitDirection,
3009 cx: &WindowContext,
3010 ) -> Option<View<Pane>> {
3011 self.center
3012 .find_pane_in_direction(&self.active_pane, direction, cx)
3013 .cloned()
3014 }
3015
3016 pub fn swap_pane_in_direction(
3017 &mut self,
3018 direction: SplitDirection,
3019 cx: &mut ViewContext<Self>,
3020 ) {
3021 if let Some(to) = self.find_pane_in_direction(direction, cx) {
3022 self.center.swap(&self.active_pane, &to);
3023 cx.notify();
3024 }
3025 }
3026
3027 pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3028 self.center
3029 .resize(&self.active_pane, axis, amount, &self.bounds);
3030 cx.notify();
3031 }
3032
3033 pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3034 self.center.reset_pane_sizes();
3035 cx.notify();
3036 }
3037
3038 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3039 // This is explicitly hoisted out of the following check for pane identity as
3040 // terminal panel panes are not registered as a center panes.
3041 self.status_bar.update(cx, |status_bar, cx| {
3042 status_bar.set_active_pane(&pane, cx);
3043 });
3044 if self.active_pane != pane {
3045 self.set_active_pane(&pane, cx);
3046 }
3047
3048 if self.last_active_center_pane.is_none() {
3049 self.last_active_center_pane = Some(pane.downgrade());
3050 }
3051
3052 self.dismiss_zoomed_items_to_reveal(None, cx);
3053 if pane.read(cx).is_zoomed() {
3054 self.zoomed = Some(pane.downgrade().into());
3055 } else {
3056 self.zoomed = None;
3057 }
3058 self.zoomed_position = None;
3059 cx.emit(Event::ZoomChanged);
3060 self.update_active_view_for_followers(cx);
3061 pane.model.update(cx, |pane, _| {
3062 pane.track_alternate_file_items();
3063 });
3064
3065 cx.notify();
3066 }
3067
3068 fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3069 self.active_pane = pane.clone();
3070 self.active_item_path_changed(cx);
3071 self.last_active_center_pane = Some(pane.downgrade());
3072 }
3073
3074 fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3075 self.update_active_view_for_followers(cx);
3076 }
3077
3078 fn handle_pane_event(
3079 &mut self,
3080 pane: View<Pane>,
3081 event: &pane::Event,
3082 cx: &mut ViewContext<Self>,
3083 ) {
3084 let mut serialize_workspace = true;
3085 match event {
3086 pane::Event::AddItem { item } => {
3087 item.added_to_pane(self, pane, cx);
3088 cx.emit(Event::ItemAdded {
3089 item: item.boxed_clone(),
3090 });
3091 }
3092 pane::Event::Split(direction) => {
3093 self.split_and_clone(pane, *direction, cx);
3094 }
3095 pane::Event::JoinIntoNext => {
3096 self.join_pane_into_next(pane, cx);
3097 }
3098 pane::Event::JoinAll => {
3099 self.join_all_panes(cx);
3100 }
3101 pane::Event::Remove { focus_on_pane } => {
3102 self.remove_pane(pane, focus_on_pane.clone(), cx);
3103 }
3104 pane::Event::ActivateItem {
3105 local,
3106 focus_changed,
3107 } => {
3108 cx.on_next_frame(|_, cx| {
3109 cx.invalidate_character_coordinates();
3110 });
3111
3112 pane.model.update(cx, |pane, _| {
3113 pane.track_alternate_file_items();
3114 });
3115 if *local {
3116 self.unfollow_in_pane(&pane, cx);
3117 }
3118 if &pane == self.active_pane() {
3119 self.active_item_path_changed(cx);
3120 self.update_active_view_for_followers(cx);
3121 }
3122 serialize_workspace = *focus_changed || &pane != self.active_pane();
3123 }
3124 pane::Event::UserSavedItem { item, save_intent } => {
3125 cx.emit(Event::UserSavedItem {
3126 pane: pane.downgrade(),
3127 item: item.boxed_clone(),
3128 save_intent: *save_intent,
3129 });
3130 serialize_workspace = false;
3131 }
3132 pane::Event::ChangeItemTitle => {
3133 if pane == self.active_pane {
3134 self.active_item_path_changed(cx);
3135 }
3136 self.update_window_edited(cx);
3137 serialize_workspace = false;
3138 }
3139 pane::Event::RemoveItem { .. } => {}
3140 pane::Event::RemovedItem { item_id } => {
3141 cx.emit(Event::ActiveItemChanged);
3142 self.update_window_edited(cx);
3143 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3144 if entry.get().entity_id() == pane.entity_id() {
3145 entry.remove();
3146 }
3147 }
3148 }
3149 pane::Event::Focus => {
3150 cx.on_next_frame(|_, cx| {
3151 cx.invalidate_character_coordinates();
3152 });
3153 self.handle_pane_focused(pane.clone(), cx);
3154 }
3155 pane::Event::ZoomIn => {
3156 if pane == self.active_pane {
3157 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3158 if pane.read(cx).has_focus(cx) {
3159 self.zoomed = Some(pane.downgrade().into());
3160 self.zoomed_position = None;
3161 cx.emit(Event::ZoomChanged);
3162 }
3163 cx.notify();
3164 }
3165 }
3166 pane::Event::ZoomOut => {
3167 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3168 if self.zoomed_position.is_none() {
3169 self.zoomed = None;
3170 cx.emit(Event::ZoomChanged);
3171 }
3172 cx.notify();
3173 }
3174 }
3175
3176 if serialize_workspace {
3177 self.serialize_workspace(cx);
3178 }
3179 }
3180
3181 pub fn unfollow_in_pane(
3182 &mut self,
3183 pane: &View<Pane>,
3184 cx: &mut ViewContext<Workspace>,
3185 ) -> Option<PeerId> {
3186 let leader_id = self.leader_for_pane(pane)?;
3187 self.unfollow(leader_id, cx);
3188 Some(leader_id)
3189 }
3190
3191 pub fn split_pane(
3192 &mut self,
3193 pane_to_split: View<Pane>,
3194 split_direction: SplitDirection,
3195 cx: &mut ViewContext<Self>,
3196 ) -> View<Pane> {
3197 let new_pane = self.add_pane(cx);
3198 self.center
3199 .split(&pane_to_split, &new_pane, split_direction)
3200 .unwrap();
3201 cx.notify();
3202 new_pane
3203 }
3204
3205 pub fn split_and_clone(
3206 &mut self,
3207 pane: View<Pane>,
3208 direction: SplitDirection,
3209 cx: &mut ViewContext<Self>,
3210 ) -> Option<View<Pane>> {
3211 let item = pane.read(cx).active_item()?;
3212 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3213 let new_pane = self.add_pane(cx);
3214 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3215 self.center.split(&pane, &new_pane, direction).unwrap();
3216 Some(new_pane)
3217 } else {
3218 None
3219 };
3220 cx.notify();
3221 maybe_pane_handle
3222 }
3223
3224 pub fn split_pane_with_item(
3225 &mut self,
3226 pane_to_split: WeakView<Pane>,
3227 split_direction: SplitDirection,
3228 from: WeakView<Pane>,
3229 item_id_to_move: EntityId,
3230 cx: &mut ViewContext<Self>,
3231 ) {
3232 let Some(pane_to_split) = pane_to_split.upgrade() else {
3233 return;
3234 };
3235 let Some(from) = from.upgrade() else {
3236 return;
3237 };
3238
3239 let new_pane = self.add_pane(cx);
3240 move_item(&from, &new_pane, item_id_to_move, 0, cx);
3241 self.center
3242 .split(&pane_to_split, &new_pane, split_direction)
3243 .unwrap();
3244 cx.notify();
3245 }
3246
3247 pub fn split_pane_with_project_entry(
3248 &mut self,
3249 pane_to_split: WeakView<Pane>,
3250 split_direction: SplitDirection,
3251 project_entry: ProjectEntryId,
3252 cx: &mut ViewContext<Self>,
3253 ) -> Option<Task<Result<()>>> {
3254 let pane_to_split = pane_to_split.upgrade()?;
3255 let new_pane = self.add_pane(cx);
3256 self.center
3257 .split(&pane_to_split, &new_pane, split_direction)
3258 .unwrap();
3259
3260 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3261 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3262 Some(cx.foreground_executor().spawn(async move {
3263 task.await?;
3264 Ok(())
3265 }))
3266 }
3267
3268 pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3269 let active_item = self.active_pane.read(cx).active_item();
3270 for pane in &self.panes {
3271 join_pane_into_active(&self.active_pane, pane, cx);
3272 }
3273 if let Some(active_item) = active_item {
3274 self.activate_item(active_item.as_ref(), true, true, cx);
3275 }
3276 cx.notify();
3277 }
3278
3279 pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3280 let next_pane = self
3281 .find_pane_in_direction(SplitDirection::Right, cx)
3282 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3283 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3284 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3285 let Some(next_pane) = next_pane else {
3286 return;
3287 };
3288 move_all_items(&pane, &next_pane, cx);
3289 cx.notify();
3290 }
3291
3292 fn remove_pane(
3293 &mut self,
3294 pane: View<Pane>,
3295 focus_on: Option<View<Pane>>,
3296 cx: &mut ViewContext<Self>,
3297 ) {
3298 if self.center.remove(&pane).unwrap() {
3299 self.force_remove_pane(&pane, &focus_on, cx);
3300 self.unfollow_in_pane(&pane, cx);
3301 self.last_leaders_by_pane.remove(&pane.downgrade());
3302 for removed_item in pane.read(cx).items() {
3303 self.panes_by_item.remove(&removed_item.item_id());
3304 }
3305
3306 cx.notify();
3307 } else {
3308 self.active_item_path_changed(cx);
3309 }
3310 cx.emit(Event::PaneRemoved);
3311 }
3312
3313 pub fn panes(&self) -> &[View<Pane>] {
3314 &self.panes
3315 }
3316
3317 pub fn active_pane(&self) -> &View<Pane> {
3318 &self.active_pane
3319 }
3320
3321 pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3322 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3323 if dock.focus_handle(cx).contains_focused(cx) {
3324 if let Some(pane) = dock
3325 .read(cx)
3326 .active_panel()
3327 .and_then(|panel| panel.pane(cx))
3328 {
3329 return pane;
3330 }
3331 }
3332 }
3333 self.active_pane().clone()
3334 }
3335
3336 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3337 self.find_pane_in_direction(SplitDirection::Right, cx)
3338 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3339 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3340 .clone()
3341 }
3342
3343 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3344 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3345 weak_pane.upgrade()
3346 }
3347
3348 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3349 self.follower_states.retain(|leader_id, state| {
3350 if *leader_id == peer_id {
3351 for item in state.items_by_leader_view_id.values() {
3352 item.view.set_leader_peer_id(None, cx);
3353 }
3354 false
3355 } else {
3356 true
3357 }
3358 });
3359 cx.notify();
3360 }
3361
3362 pub fn start_following(
3363 &mut self,
3364 leader_id: PeerId,
3365 cx: &mut ViewContext<Self>,
3366 ) -> Option<Task<Result<()>>> {
3367 let pane = self.active_pane().clone();
3368
3369 self.last_leaders_by_pane
3370 .insert(pane.downgrade(), leader_id);
3371 self.unfollow(leader_id, cx);
3372 self.unfollow_in_pane(&pane, cx);
3373 self.follower_states.insert(
3374 leader_id,
3375 FollowerState {
3376 center_pane: pane.clone(),
3377 dock_pane: None,
3378 active_view_id: None,
3379 items_by_leader_view_id: Default::default(),
3380 },
3381 );
3382 cx.notify();
3383
3384 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3385 let project_id = self.project.read(cx).remote_id();
3386 let request = self.app_state.client.request(proto::Follow {
3387 room_id,
3388 project_id,
3389 leader_id: Some(leader_id),
3390 });
3391
3392 Some(cx.spawn(|this, mut cx| async move {
3393 let response = request.await?;
3394 this.update(&mut cx, |this, _| {
3395 let state = this
3396 .follower_states
3397 .get_mut(&leader_id)
3398 .ok_or_else(|| anyhow!("following interrupted"))?;
3399 state.active_view_id = response
3400 .active_view
3401 .as_ref()
3402 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3403 Ok::<_, anyhow::Error>(())
3404 })??;
3405 if let Some(view) = response.active_view {
3406 Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3407 }
3408 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3409 Ok(())
3410 }))
3411 }
3412
3413 pub fn follow_next_collaborator(
3414 &mut self,
3415 _: &FollowNextCollaborator,
3416 cx: &mut ViewContext<Self>,
3417 ) {
3418 let collaborators = self.project.read(cx).collaborators();
3419 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3420 let mut collaborators = collaborators.keys().copied();
3421 for peer_id in collaborators.by_ref() {
3422 if peer_id == leader_id {
3423 break;
3424 }
3425 }
3426 collaborators.next()
3427 } else if let Some(last_leader_id) =
3428 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3429 {
3430 if collaborators.contains_key(last_leader_id) {
3431 Some(*last_leader_id)
3432 } else {
3433 None
3434 }
3435 } else {
3436 None
3437 };
3438
3439 let pane = self.active_pane.clone();
3440 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3441 else {
3442 return;
3443 };
3444 if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3445 return;
3446 }
3447 if let Some(task) = self.start_following(leader_id, cx) {
3448 task.detach_and_log_err(cx)
3449 }
3450 }
3451
3452 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3453 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3454 return;
3455 };
3456 let room = room.read(cx);
3457 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3458 return;
3459 };
3460
3461 let project = self.project.read(cx);
3462
3463 let other_project_id = match remote_participant.location {
3464 call::ParticipantLocation::External => None,
3465 call::ParticipantLocation::UnsharedProject => None,
3466 call::ParticipantLocation::SharedProject { project_id } => {
3467 if Some(project_id) == project.remote_id() {
3468 None
3469 } else {
3470 Some(project_id)
3471 }
3472 }
3473 };
3474
3475 // if they are active in another project, follow there.
3476 if let Some(project_id) = other_project_id {
3477 let app_state = self.app_state.clone();
3478 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3479 .detach_and_log_err(cx);
3480 }
3481
3482 // if you're already following, find the right pane and focus it.
3483 if let Some(follower_state) = self.follower_states.get(&leader_id) {
3484 cx.focus_view(follower_state.pane());
3485 return;
3486 }
3487
3488 // Otherwise, follow.
3489 if let Some(task) = self.start_following(leader_id, cx) {
3490 task.detach_and_log_err(cx)
3491 }
3492 }
3493
3494 pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3495 cx.notify();
3496 let state = self.follower_states.remove(&leader_id)?;
3497 for (_, item) in state.items_by_leader_view_id {
3498 item.view.set_leader_peer_id(None, cx);
3499 }
3500
3501 let project_id = self.project.read(cx).remote_id();
3502 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3503 self.app_state
3504 .client
3505 .send(proto::Unfollow {
3506 room_id,
3507 project_id,
3508 leader_id: Some(leader_id),
3509 })
3510 .log_err();
3511
3512 Some(())
3513 }
3514
3515 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3516 self.follower_states.contains_key(&peer_id)
3517 }
3518
3519 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3520 cx.emit(Event::ActiveItemChanged);
3521 let active_entry = self.active_project_path(cx);
3522 self.project
3523 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3524
3525 self.update_window_title(cx);
3526 }
3527
3528 fn update_window_title(&mut self, cx: &mut WindowContext) {
3529 let project = self.project().read(cx);
3530 let mut title = String::new();
3531
3532 for (i, name) in project.worktree_root_names(cx).enumerate() {
3533 if i > 0 {
3534 title.push_str(", ");
3535 }
3536 title.push_str(name);
3537 }
3538
3539 if title.is_empty() {
3540 title = "empty project".to_string();
3541 }
3542
3543 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3544 let filename = path
3545 .path
3546 .file_name()
3547 .map(|s| s.to_string_lossy())
3548 .or_else(|| {
3549 Some(Cow::Borrowed(
3550 project
3551 .worktree_for_id(path.worktree_id, cx)?
3552 .read(cx)
3553 .root_name(),
3554 ))
3555 });
3556
3557 if let Some(filename) = filename {
3558 title.push_str(" — ");
3559 title.push_str(filename.as_ref());
3560 }
3561 }
3562
3563 if project.is_via_collab() {
3564 title.push_str(" ↙");
3565 } else if project.is_shared() {
3566 title.push_str(" ↗");
3567 }
3568
3569 cx.set_window_title(&title);
3570 }
3571
3572 fn update_window_edited(&mut self, cx: &mut WindowContext) {
3573 let is_edited = !self.project.read(cx).is_disconnected(cx)
3574 && self
3575 .items(cx)
3576 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3577 if is_edited != self.window_edited {
3578 self.window_edited = is_edited;
3579 cx.set_window_edited(self.window_edited)
3580 }
3581 }
3582
3583 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3584 if self.notifications.is_empty() {
3585 None
3586 } else {
3587 Some(
3588 div()
3589 .absolute()
3590 .right_3()
3591 .bottom_3()
3592 .w_112()
3593 .h_full()
3594 .flex()
3595 .flex_col()
3596 .justify_end()
3597 .gap_2()
3598 .children(
3599 self.notifications
3600 .iter()
3601 .map(|(_, notification)| notification.to_any()),
3602 ),
3603 )
3604 }
3605 }
3606
3607 // RPC handlers
3608
3609 fn active_view_for_follower(
3610 &self,
3611 follower_project_id: Option<u64>,
3612 cx: &mut ViewContext<Self>,
3613 ) -> Option<proto::View> {
3614 let (item, panel_id) = self.active_item_for_followers(cx);
3615 let item = item?;
3616 let leader_id = self
3617 .pane_for(&*item)
3618 .and_then(|pane| self.leader_for_pane(&pane));
3619
3620 let item_handle = item.to_followable_item_handle(cx)?;
3621 let id = item_handle.remote_id(&self.app_state.client, cx)?;
3622 let variant = item_handle.to_state_proto(cx)?;
3623
3624 if item_handle.is_project_item(cx)
3625 && (follower_project_id.is_none()
3626 || follower_project_id != self.project.read(cx).remote_id())
3627 {
3628 return None;
3629 }
3630
3631 Some(proto::View {
3632 id: Some(id.to_proto()),
3633 leader_id,
3634 variant: Some(variant),
3635 panel_id: panel_id.map(|id| id as i32),
3636 })
3637 }
3638
3639 fn handle_follow(
3640 &mut self,
3641 follower_project_id: Option<u64>,
3642 cx: &mut ViewContext<Self>,
3643 ) -> proto::FollowResponse {
3644 let active_view = self.active_view_for_follower(follower_project_id, cx);
3645
3646 cx.notify();
3647 proto::FollowResponse {
3648 // TODO: Remove after version 0.145.x stabilizes.
3649 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3650 views: active_view.iter().cloned().collect(),
3651 active_view,
3652 }
3653 }
3654
3655 fn handle_update_followers(
3656 &mut self,
3657 leader_id: PeerId,
3658 message: proto::UpdateFollowers,
3659 _cx: &mut ViewContext<Self>,
3660 ) {
3661 self.leader_updates_tx
3662 .unbounded_send((leader_id, message))
3663 .ok();
3664 }
3665
3666 async fn process_leader_update(
3667 this: &WeakView<Self>,
3668 leader_id: PeerId,
3669 update: proto::UpdateFollowers,
3670 cx: &mut AsyncWindowContext,
3671 ) -> Result<()> {
3672 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3673 proto::update_followers::Variant::CreateView(view) => {
3674 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3675 let should_add_view = this.update(cx, |this, _| {
3676 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3677 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3678 } else {
3679 anyhow::Ok(false)
3680 }
3681 })??;
3682
3683 if should_add_view {
3684 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3685 }
3686 }
3687 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3688 let should_add_view = this.update(cx, |this, _| {
3689 if let Some(state) = this.follower_states.get_mut(&leader_id) {
3690 state.active_view_id = update_active_view
3691 .view
3692 .as_ref()
3693 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3694
3695 if state.active_view_id.is_some_and(|view_id| {
3696 !state.items_by_leader_view_id.contains_key(&view_id)
3697 }) {
3698 anyhow::Ok(true)
3699 } else {
3700 anyhow::Ok(false)
3701 }
3702 } else {
3703 anyhow::Ok(false)
3704 }
3705 })??;
3706
3707 if should_add_view {
3708 if let Some(view) = update_active_view.view {
3709 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3710 }
3711 }
3712 }
3713 proto::update_followers::Variant::UpdateView(update_view) => {
3714 let variant = update_view
3715 .variant
3716 .ok_or_else(|| anyhow!("missing update view variant"))?;
3717 let id = update_view
3718 .id
3719 .ok_or_else(|| anyhow!("missing update view id"))?;
3720 let mut tasks = Vec::new();
3721 this.update(cx, |this, cx| {
3722 let project = this.project.clone();
3723 if let Some(state) = this.follower_states.get(&leader_id) {
3724 let view_id = ViewId::from_proto(id.clone())?;
3725 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3726 tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3727 }
3728 }
3729 anyhow::Ok(())
3730 })??;
3731 try_join_all(tasks).await.log_err();
3732 }
3733 }
3734 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3735 Ok(())
3736 }
3737
3738 async fn add_view_from_leader(
3739 this: WeakView<Self>,
3740 leader_id: PeerId,
3741 view: &proto::View,
3742 cx: &mut AsyncWindowContext,
3743 ) -> Result<()> {
3744 let this = this.upgrade().context("workspace dropped")?;
3745
3746 let Some(id) = view.id.clone() else {
3747 return Err(anyhow!("no id for view"));
3748 };
3749 let id = ViewId::from_proto(id)?;
3750 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3751
3752 let pane = this.update(cx, |this, _cx| {
3753 let state = this
3754 .follower_states
3755 .get(&leader_id)
3756 .context("stopped following")?;
3757 anyhow::Ok(state.pane().clone())
3758 })??;
3759 let existing_item = pane.update(cx, |pane, cx| {
3760 let client = this.read(cx).client().clone();
3761 pane.items().find_map(|item| {
3762 let item = item.to_followable_item_handle(cx)?;
3763 if item.remote_id(&client, cx) == Some(id) {
3764 Some(item)
3765 } else {
3766 None
3767 }
3768 })
3769 })?;
3770 let item = if let Some(existing_item) = existing_item {
3771 existing_item
3772 } else {
3773 let variant = view.variant.clone();
3774 if variant.is_none() {
3775 Err(anyhow!("missing view variant"))?;
3776 }
3777
3778 let task = cx.update(|cx| {
3779 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3780 })?;
3781
3782 let Some(task) = task else {
3783 return Err(anyhow!(
3784 "failed to construct view from leader (maybe from a different version of zed?)"
3785 ));
3786 };
3787
3788 let mut new_item = task.await?;
3789 pane.update(cx, |pane, cx| {
3790 let mut item_to_remove = None;
3791 for (ix, item) in pane.items().enumerate() {
3792 if let Some(item) = item.to_followable_item_handle(cx) {
3793 match new_item.dedup(item.as_ref(), cx) {
3794 Some(item::Dedup::KeepExisting) => {
3795 new_item =
3796 item.boxed_clone().to_followable_item_handle(cx).unwrap();
3797 break;
3798 }
3799 Some(item::Dedup::ReplaceExisting) => {
3800 item_to_remove = Some((ix, item.item_id()));
3801 break;
3802 }
3803 None => {}
3804 }
3805 }
3806 }
3807
3808 if let Some((ix, id)) = item_to_remove {
3809 pane.remove_item(id, false, false, cx);
3810 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3811 }
3812 })?;
3813
3814 new_item
3815 };
3816
3817 this.update(cx, |this, cx| {
3818 let state = this.follower_states.get_mut(&leader_id)?;
3819 item.set_leader_peer_id(Some(leader_id), cx);
3820 state.items_by_leader_view_id.insert(
3821 id,
3822 FollowerView {
3823 view: item,
3824 location: panel_id,
3825 },
3826 );
3827
3828 Some(())
3829 })?;
3830
3831 Ok(())
3832 }
3833
3834 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3835 let mut is_project_item = true;
3836 let mut update = proto::UpdateActiveView::default();
3837 if cx.is_window_active() {
3838 let (active_item, panel_id) = self.active_item_for_followers(cx);
3839
3840 if let Some(item) = active_item {
3841 if item.focus_handle(cx).contains_focused(cx) {
3842 let leader_id = self
3843 .pane_for(&*item)
3844 .and_then(|pane| self.leader_for_pane(&pane));
3845
3846 if let Some(item) = item.to_followable_item_handle(cx) {
3847 let id = item
3848 .remote_id(&self.app_state.client, cx)
3849 .map(|id| id.to_proto());
3850
3851 if let Some(id) = id.clone() {
3852 if let Some(variant) = item.to_state_proto(cx) {
3853 let view = Some(proto::View {
3854 id: Some(id.clone()),
3855 leader_id,
3856 variant: Some(variant),
3857 panel_id: panel_id.map(|id| id as i32),
3858 });
3859
3860 is_project_item = item.is_project_item(cx);
3861 update = proto::UpdateActiveView {
3862 view,
3863 // TODO: Remove after version 0.145.x stabilizes.
3864 id: Some(id.clone()),
3865 leader_id,
3866 };
3867 }
3868 };
3869 }
3870 }
3871 }
3872 }
3873
3874 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3875 if active_view_id != self.last_active_view_id.as_ref() {
3876 self.last_active_view_id = active_view_id.cloned();
3877 self.update_followers(
3878 is_project_item,
3879 proto::update_followers::Variant::UpdateActiveView(update),
3880 cx,
3881 );
3882 }
3883 }
3884
3885 fn active_item_for_followers(
3886 &self,
3887 cx: &mut WindowContext,
3888 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3889 let mut active_item = None;
3890 let mut panel_id = None;
3891 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3892 if dock.focus_handle(cx).contains_focused(cx) {
3893 if let Some(panel) = dock.read(cx).active_panel() {
3894 if let Some(pane) = panel.pane(cx) {
3895 if let Some(item) = pane.read(cx).active_item() {
3896 active_item = Some(item);
3897 panel_id = panel.remote_id();
3898 break;
3899 }
3900 }
3901 }
3902 }
3903 }
3904
3905 if active_item.is_none() {
3906 active_item = self.active_pane().read(cx).active_item();
3907 }
3908 (active_item, panel_id)
3909 }
3910
3911 fn update_followers(
3912 &self,
3913 project_only: bool,
3914 update: proto::update_followers::Variant,
3915 cx: &mut WindowContext,
3916 ) -> Option<()> {
3917 // If this update only applies to for followers in the current project,
3918 // then skip it unless this project is shared. If it applies to all
3919 // followers, regardless of project, then set `project_id` to none,
3920 // indicating that it goes to all followers.
3921 let project_id = if project_only {
3922 Some(self.project.read(cx).remote_id()?)
3923 } else {
3924 None
3925 };
3926 self.app_state().workspace_store.update(cx, |store, cx| {
3927 store.update_followers(project_id, update, cx)
3928 })
3929 }
3930
3931 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3932 self.follower_states.iter().find_map(|(leader_id, state)| {
3933 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3934 Some(*leader_id)
3935 } else {
3936 None
3937 }
3938 })
3939 }
3940
3941 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3942 cx.notify();
3943
3944 let call = self.active_call()?;
3945 let room = call.read(cx).room()?.read(cx);
3946 let participant = room.remote_participant_for_peer_id(leader_id)?;
3947
3948 let leader_in_this_app;
3949 let leader_in_this_project;
3950 match participant.location {
3951 call::ParticipantLocation::SharedProject { project_id } => {
3952 leader_in_this_app = true;
3953 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3954 }
3955 call::ParticipantLocation::UnsharedProject => {
3956 leader_in_this_app = true;
3957 leader_in_this_project = false;
3958 }
3959 call::ParticipantLocation::External => {
3960 leader_in_this_app = false;
3961 leader_in_this_project = false;
3962 }
3963 };
3964
3965 let state = self.follower_states.get(&leader_id)?;
3966 let mut item_to_activate = None;
3967 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3968 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3969 if leader_in_this_project || !item.view.is_project_item(cx) {
3970 item_to_activate = Some((item.location, item.view.boxed_clone()));
3971 }
3972 }
3973 } else if let Some(shared_screen) =
3974 self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3975 {
3976 item_to_activate = Some((None, Box::new(shared_screen)));
3977 }
3978
3979 let (panel_id, item) = item_to_activate?;
3980
3981 let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3982 let pane;
3983 if let Some(panel_id) = panel_id {
3984 pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3985 let state = self.follower_states.get_mut(&leader_id)?;
3986 state.dock_pane = Some(pane.clone());
3987 } else {
3988 pane = state.center_pane.clone();
3989 let state = self.follower_states.get_mut(&leader_id)?;
3990 if let Some(dock_pane) = state.dock_pane.take() {
3991 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3992 }
3993 }
3994
3995 pane.update(cx, |pane, cx| {
3996 let focus_active_item = pane.has_focus(cx) || transfer_focus;
3997 if let Some(index) = pane.index_for_item(item.as_ref()) {
3998 pane.activate_item(index, false, false, cx);
3999 } else {
4000 pane.add_item(item.boxed_clone(), false, false, None, cx)
4001 }
4002
4003 if focus_active_item {
4004 pane.focus_active_item(cx)
4005 }
4006 });
4007
4008 None
4009 }
4010
4011 #[cfg(target_os = "windows")]
4012 fn shared_screen_for_peer(
4013 &self,
4014 _peer_id: PeerId,
4015 _pane: &View<Pane>,
4016 _cx: &mut WindowContext,
4017 ) -> Option<View<SharedScreen>> {
4018 None
4019 }
4020
4021 #[cfg(not(target_os = "windows"))]
4022 fn shared_screen_for_peer(
4023 &self,
4024 peer_id: PeerId,
4025 pane: &View<Pane>,
4026 cx: &mut WindowContext,
4027 ) -> Option<View<SharedScreen>> {
4028 let call = self.active_call()?;
4029 let room = call.read(cx).room()?.read(cx);
4030 let participant = room.remote_participant_for_peer_id(peer_id)?;
4031 let track = participant.video_tracks.values().next()?.clone();
4032 let user = participant.user.clone();
4033
4034 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4035 if item.read(cx).peer_id == peer_id {
4036 return Some(item);
4037 }
4038 }
4039
4040 Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4041 }
4042
4043 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4044 if cx.is_window_active() {
4045 self.update_active_view_for_followers(cx);
4046
4047 if let Some(database_id) = self.database_id {
4048 cx.background_executor()
4049 .spawn(persistence::DB.update_timestamp(database_id))
4050 .detach();
4051 }
4052 } else {
4053 for pane in &self.panes {
4054 pane.update(cx, |pane, cx| {
4055 if let Some(item) = pane.active_item() {
4056 item.workspace_deactivated(cx);
4057 }
4058 for item in pane.items() {
4059 if matches!(
4060 item.workspace_settings(cx).autosave,
4061 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4062 ) {
4063 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4064 .detach_and_log_err(cx);
4065 }
4066 }
4067 });
4068 }
4069 }
4070 }
4071
4072 fn active_call(&self) -> Option<&Model<ActiveCall>> {
4073 self.active_call.as_ref().map(|(call, _)| call)
4074 }
4075
4076 fn on_active_call_event(
4077 &mut self,
4078 _: Model<ActiveCall>,
4079 event: &call::room::Event,
4080 cx: &mut ViewContext<Self>,
4081 ) {
4082 match event {
4083 call::room::Event::ParticipantLocationChanged { participant_id }
4084 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4085 self.leader_updated(*participant_id, cx);
4086 }
4087 _ => {}
4088 }
4089 }
4090
4091 pub fn database_id(&self) -> Option<WorkspaceId> {
4092 self.database_id
4093 }
4094
4095 fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4096 let project = self.project().read(cx);
4097
4098 if project.is_local() {
4099 Some(
4100 project
4101 .visible_worktrees(cx)
4102 .map(|worktree| worktree.read(cx).abs_path())
4103 .collect::<Vec<_>>(),
4104 )
4105 } else {
4106 None
4107 }
4108 }
4109
4110 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4111 match member {
4112 Member::Axis(PaneAxis { members, .. }) => {
4113 for child in members.iter() {
4114 self.remove_panes(child.clone(), cx)
4115 }
4116 }
4117 Member::Pane(pane) => {
4118 self.force_remove_pane(&pane, &None, cx);
4119 }
4120 }
4121 }
4122
4123 fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4124 self.session_id.take();
4125 self.serialize_workspace_internal(cx)
4126 }
4127
4128 fn force_remove_pane(
4129 &mut self,
4130 pane: &View<Pane>,
4131 focus_on: &Option<View<Pane>>,
4132 cx: &mut ViewContext<Workspace>,
4133 ) {
4134 self.panes.retain(|p| p != pane);
4135 if let Some(focus_on) = focus_on {
4136 focus_on.update(cx, |pane, cx| pane.focus(cx));
4137 } else {
4138 self.panes
4139 .last()
4140 .unwrap()
4141 .update(cx, |pane, cx| pane.focus(cx));
4142 }
4143 if self.last_active_center_pane == Some(pane.downgrade()) {
4144 self.last_active_center_pane = None;
4145 }
4146 cx.notify();
4147 }
4148
4149 fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4150 if self._schedule_serialize.is_none() {
4151 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4152 cx.background_executor()
4153 .timer(Duration::from_millis(100))
4154 .await;
4155 this.update(&mut cx, |this, cx| {
4156 this.serialize_workspace_internal(cx).detach();
4157 this._schedule_serialize.take();
4158 })
4159 .log_err();
4160 }));
4161 }
4162 }
4163
4164 fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4165 let Some(database_id) = self.database_id() else {
4166 return Task::ready(());
4167 };
4168
4169 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4170 let (items, active, pinned_count) = {
4171 let pane = pane_handle.read(cx);
4172 let active_item_id = pane.active_item().map(|item| item.item_id());
4173 (
4174 pane.items()
4175 .filter_map(|handle| {
4176 let handle = handle.to_serializable_item_handle(cx)?;
4177
4178 Some(SerializedItem {
4179 kind: Arc::from(handle.serialized_item_kind()),
4180 item_id: handle.item_id().as_u64(),
4181 active: Some(handle.item_id()) == active_item_id,
4182 preview: pane.is_active_preview_item(handle.item_id()),
4183 })
4184 })
4185 .collect::<Vec<_>>(),
4186 pane.has_focus(cx),
4187 pane.pinned_count(),
4188 )
4189 };
4190
4191 SerializedPane::new(items, active, pinned_count)
4192 }
4193
4194 fn build_serialized_pane_group(
4195 pane_group: &Member,
4196 cx: &WindowContext,
4197 ) -> SerializedPaneGroup {
4198 match pane_group {
4199 Member::Axis(PaneAxis {
4200 axis,
4201 members,
4202 flexes,
4203 bounding_boxes: _,
4204 }) => SerializedPaneGroup::Group {
4205 axis: SerializedAxis(*axis),
4206 children: members
4207 .iter()
4208 .map(|member| build_serialized_pane_group(member, cx))
4209 .collect::<Vec<_>>(),
4210 flexes: Some(flexes.lock().clone()),
4211 },
4212 Member::Pane(pane_handle) => {
4213 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4214 }
4215 }
4216 }
4217
4218 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4219 let left_dock = this.left_dock.read(cx);
4220 let left_visible = left_dock.is_open();
4221 let left_active_panel = left_dock
4222 .active_panel()
4223 .map(|panel| panel.persistent_name().to_string());
4224 let left_dock_zoom = left_dock
4225 .active_panel()
4226 .map(|panel| panel.is_zoomed(cx))
4227 .unwrap_or(false);
4228
4229 let right_dock = this.right_dock.read(cx);
4230 let right_visible = right_dock.is_open();
4231 let right_active_panel = right_dock
4232 .active_panel()
4233 .map(|panel| panel.persistent_name().to_string());
4234 let right_dock_zoom = right_dock
4235 .active_panel()
4236 .map(|panel| panel.is_zoomed(cx))
4237 .unwrap_or(false);
4238
4239 let bottom_dock = this.bottom_dock.read(cx);
4240 let bottom_visible = bottom_dock.is_open();
4241 let bottom_active_panel = bottom_dock
4242 .active_panel()
4243 .map(|panel| panel.persistent_name().to_string());
4244 let bottom_dock_zoom = bottom_dock
4245 .active_panel()
4246 .map(|panel| panel.is_zoomed(cx))
4247 .unwrap_or(false);
4248
4249 DockStructure {
4250 left: DockData {
4251 visible: left_visible,
4252 active_panel: left_active_panel,
4253 zoom: left_dock_zoom,
4254 },
4255 right: DockData {
4256 visible: right_visible,
4257 active_panel: right_active_panel,
4258 zoom: right_dock_zoom,
4259 },
4260 bottom: DockData {
4261 visible: bottom_visible,
4262 active_panel: bottom_active_panel,
4263 zoom: bottom_dock_zoom,
4264 },
4265 }
4266 }
4267
4268 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4269 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4270 } else if let Some(local_paths) = self.local_paths(cx) {
4271 if !local_paths.is_empty() {
4272 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4273 } else {
4274 None
4275 }
4276 } else {
4277 None
4278 };
4279
4280 if let Some(location) = location {
4281 let center_group = build_serialized_pane_group(&self.center.root, cx);
4282 let docks = build_serialized_docks(self, cx);
4283 let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4284 let serialized_workspace = SerializedWorkspace {
4285 id: database_id,
4286 location,
4287 center_group,
4288 window_bounds,
4289 display: Default::default(),
4290 docks,
4291 centered_layout: self.centered_layout,
4292 session_id: self.session_id.clone(),
4293 window_id: Some(cx.window_handle().window_id().as_u64()),
4294 };
4295 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4296 }
4297 Task::ready(())
4298 }
4299
4300 async fn serialize_items(
4301 this: &WeakView<Self>,
4302 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4303 cx: &mut AsyncWindowContext,
4304 ) -> Result<()> {
4305 const CHUNK_SIZE: usize = 200;
4306 const THROTTLE_TIME: Duration = Duration::from_millis(200);
4307
4308 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4309
4310 while let Some(items_received) = serializable_items.next().await {
4311 let unique_items =
4312 items_received
4313 .into_iter()
4314 .fold(HashMap::default(), |mut acc, item| {
4315 acc.entry(item.item_id()).or_insert(item);
4316 acc
4317 });
4318
4319 // We use into_iter() here so that the references to the items are moved into
4320 // the tasks and not kept alive while we're sleeping.
4321 for (_, item) in unique_items.into_iter() {
4322 if let Ok(Some(task)) =
4323 this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4324 {
4325 cx.background_executor()
4326 .spawn(async move { task.await.log_err() })
4327 .detach();
4328 }
4329 }
4330
4331 cx.background_executor().timer(THROTTLE_TIME).await;
4332 }
4333
4334 Ok(())
4335 }
4336
4337 pub(crate) fn enqueue_item_serialization(
4338 &mut self,
4339 item: Box<dyn SerializableItemHandle>,
4340 ) -> Result<()> {
4341 self.serializable_items_tx
4342 .unbounded_send(item)
4343 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4344 }
4345
4346 pub(crate) fn load_workspace(
4347 serialized_workspace: SerializedWorkspace,
4348 paths_to_open: Vec<Option<ProjectPath>>,
4349 cx: &mut ViewContext<Workspace>,
4350 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4351 cx.spawn(|workspace, mut cx| async move {
4352 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4353
4354 let mut center_group = None;
4355 let mut center_items = None;
4356
4357 // Traverse the splits tree and add to things
4358 if let Some((group, active_pane, items)) = serialized_workspace
4359 .center_group
4360 .deserialize(
4361 &project,
4362 serialized_workspace.id,
4363 workspace.clone(),
4364 &mut cx,
4365 )
4366 .await
4367 {
4368 center_items = Some(items);
4369 center_group = Some((group, active_pane))
4370 }
4371
4372 let mut items_by_project_path = HashMap::default();
4373 let mut item_ids_by_kind = HashMap::default();
4374 let mut all_deserialized_items = Vec::default();
4375 cx.update(|cx| {
4376 for item in center_items.unwrap_or_default().into_iter().flatten() {
4377 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4378 item_ids_by_kind
4379 .entry(serializable_item_handle.serialized_item_kind())
4380 .or_insert(Vec::new())
4381 .push(item.item_id().as_u64() as ItemId);
4382 }
4383
4384 if let Some(project_path) = item.project_path(cx) {
4385 items_by_project_path.insert(project_path, item.clone());
4386 }
4387 all_deserialized_items.push(item);
4388 }
4389 })?;
4390
4391 let opened_items = paths_to_open
4392 .into_iter()
4393 .map(|path_to_open| {
4394 path_to_open
4395 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4396 })
4397 .collect::<Vec<_>>();
4398
4399 // Remove old panes from workspace panes list
4400 workspace.update(&mut cx, |workspace, cx| {
4401 if let Some((center_group, active_pane)) = center_group {
4402 workspace.remove_panes(workspace.center.root.clone(), cx);
4403
4404 // Swap workspace center group
4405 workspace.center = PaneGroup::with_root(center_group);
4406 if let Some(active_pane) = active_pane {
4407 workspace.set_active_pane(&active_pane, cx);
4408 cx.focus_self();
4409 } else {
4410 workspace.set_active_pane(&workspace.center.first_pane(), cx);
4411 }
4412 }
4413
4414 let docks = serialized_workspace.docks;
4415
4416 for (dock, serialized_dock) in [
4417 (&mut workspace.right_dock, docks.right),
4418 (&mut workspace.left_dock, docks.left),
4419 (&mut workspace.bottom_dock, docks.bottom),
4420 ]
4421 .iter_mut()
4422 {
4423 dock.update(cx, |dock, cx| {
4424 dock.serialized_dock = Some(serialized_dock.clone());
4425 dock.restore_state(cx);
4426 });
4427 }
4428
4429 cx.notify();
4430 })?;
4431
4432 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4433 // after loading the items, we might have different items and in order to avoid
4434 // the database filling up, we delete items that haven't been loaded now.
4435 //
4436 // The items that have been loaded, have been saved after they've been added to the workspace.
4437 let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4438 item_ids_by_kind
4439 .into_iter()
4440 .map(|(item_kind, loaded_items)| {
4441 SerializableItemRegistry::cleanup(
4442 item_kind,
4443 serialized_workspace.id,
4444 loaded_items,
4445 cx,
4446 )
4447 .log_err()
4448 })
4449 .collect::<Vec<_>>()
4450 })?;
4451
4452 futures::future::join_all(clean_up_tasks).await;
4453
4454 workspace
4455 .update(&mut cx, |workspace, cx| {
4456 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4457 workspace.serialize_workspace_internal(cx).detach();
4458
4459 // Ensure that we mark the window as edited if we did load dirty items
4460 workspace.update_window_edited(cx);
4461 })
4462 .ok();
4463
4464 Ok(opened_items)
4465 })
4466 }
4467
4468 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4469 self.add_workspace_actions_listeners(div, cx)
4470 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4471 .on_action(cx.listener(Self::close_all_items_and_panes))
4472 .on_action(cx.listener(Self::save_all))
4473 .on_action(cx.listener(Self::send_keystrokes))
4474 .on_action(cx.listener(Self::add_folder_to_project))
4475 .on_action(cx.listener(Self::follow_next_collaborator))
4476 .on_action(cx.listener(Self::close_window))
4477 .on_action(cx.listener(Self::activate_pane_at_index))
4478 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4479 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4480 let pane = workspace.active_pane().clone();
4481 workspace.unfollow_in_pane(&pane, cx);
4482 }))
4483 .on_action(cx.listener(|workspace, action: &Save, cx| {
4484 workspace
4485 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4486 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4487 }))
4488 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4489 workspace
4490 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4491 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4492 }))
4493 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4494 workspace
4495 .save_active_item(SaveIntent::SaveAs, cx)
4496 .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4497 }))
4498 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4499 workspace.activate_previous_pane(cx)
4500 }))
4501 .on_action(
4502 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4503 )
4504 .on_action(
4505 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4506 workspace.activate_pane_in_direction(action.0, cx)
4507 }),
4508 )
4509 .on_action(
4510 cx.listener(|workspace, action: &MoveItemToPaneInDirection, cx| {
4511 workspace.move_item_to_pane_in_direction(action, cx)
4512 }),
4513 )
4514 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4515 workspace.swap_pane_in_direction(action.0, cx)
4516 }))
4517 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4518 this.toggle_dock(DockPosition::Left, cx);
4519 }))
4520 .on_action(
4521 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4522 workspace.toggle_dock(DockPosition::Right, cx);
4523 }),
4524 )
4525 .on_action(
4526 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4527 workspace.toggle_dock(DockPosition::Bottom, cx);
4528 }),
4529 )
4530 .on_action(
4531 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4532 workspace.close_all_docks(cx);
4533 }),
4534 )
4535 .on_action(
4536 cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4537 workspace.clear_all_notifications(cx);
4538 }),
4539 )
4540 .on_action(
4541 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4542 workspace.reopen_closed_item(cx).detach();
4543 }),
4544 )
4545 .on_action(cx.listener(Workspace::toggle_centered_layout))
4546 }
4547
4548 #[cfg(any(test, feature = "test-support"))]
4549 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4550 use node_runtime::NodeRuntime;
4551 use session::Session;
4552
4553 let client = project.read(cx).client();
4554 let user_store = project.read(cx).user_store();
4555
4556 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4557 let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4558 cx.activate_window();
4559 let app_state = Arc::new(AppState {
4560 languages: project.read(cx).languages().clone(),
4561 workspace_store,
4562 client,
4563 user_store,
4564 fs: project.read(cx).fs().clone(),
4565 build_window_options: |_, _| Default::default(),
4566 node_runtime: NodeRuntime::unavailable(),
4567 session,
4568 });
4569 let workspace = Self::new(Default::default(), project, app_state, cx);
4570 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4571 workspace
4572 }
4573
4574 pub fn register_action<A: Action>(
4575 &mut self,
4576 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4577 ) -> &mut Self {
4578 let callback = Arc::new(callback);
4579
4580 self.workspace_actions.push(Box::new(move |div, cx| {
4581 let callback = callback.clone();
4582 div.on_action(
4583 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4584 )
4585 }));
4586 self
4587 }
4588
4589 fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4590 for action in self.workspace_actions.iter() {
4591 div = (action)(div, cx)
4592 }
4593 div
4594 }
4595
4596 pub fn has_active_modal(&self, cx: &WindowContext) -> bool {
4597 self.modal_layer.read(cx).has_active_modal()
4598 }
4599
4600 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4601 self.modal_layer.read(cx).active_modal()
4602 }
4603
4604 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4605 where
4606 B: FnOnce(&mut ViewContext<V>) -> V,
4607 {
4608 self.modal_layer
4609 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4610 }
4611
4612 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4613 self.centered_layout = !self.centered_layout;
4614 if let Some(database_id) = self.database_id() {
4615 cx.background_executor()
4616 .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4617 .detach_and_log_err(cx);
4618 }
4619 cx.notify();
4620 }
4621
4622 fn adjust_padding(padding: Option<f32>) -> f32 {
4623 padding
4624 .unwrap_or(Self::DEFAULT_PADDING)
4625 .clamp(0.0, Self::MAX_PADDING)
4626 }
4627
4628 fn render_dock(
4629 &self,
4630 position: DockPosition,
4631 dock: &View<Dock>,
4632 cx: &WindowContext,
4633 ) -> Option<Div> {
4634 if self.zoomed_position == Some(position) {
4635 return None;
4636 }
4637
4638 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4639 let pane = panel.pane(cx)?;
4640 let follower_states = &self.follower_states;
4641 leader_border_for_pane(follower_states, &pane, cx)
4642 });
4643
4644 Some(
4645 div()
4646 .flex()
4647 .flex_none()
4648 .overflow_hidden()
4649 .child(dock.clone())
4650 .children(leader_border),
4651 )
4652 }
4653
4654 pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4655 let window = cx.window_handle().downcast::<Workspace>()?;
4656 cx.read_window(&window, |workspace, _| workspace).ok()
4657 }
4658
4659 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4660 self.zoomed.as_ref()
4661 }
4662}
4663
4664fn leader_border_for_pane(
4665 follower_states: &HashMap<PeerId, FollowerState>,
4666 pane: &View<Pane>,
4667 cx: &WindowContext,
4668) -> Option<Div> {
4669 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4670 if state.pane() == pane {
4671 Some((*leader_id, state))
4672 } else {
4673 None
4674 }
4675 })?;
4676
4677 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4678 let leader = room.remote_participant_for_peer_id(leader_id)?;
4679
4680 let mut leader_color = cx
4681 .theme()
4682 .players()
4683 .color_for_participant(leader.participant_index.0)
4684 .cursor;
4685 leader_color.fade_out(0.3);
4686 Some(
4687 div()
4688 .absolute()
4689 .size_full()
4690 .left_0()
4691 .top_0()
4692 .border_2()
4693 .border_color(leader_color),
4694 )
4695}
4696
4697fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4698 ZED_WINDOW_POSITION
4699 .zip(*ZED_WINDOW_SIZE)
4700 .map(|(position, size)| Bounds {
4701 origin: position,
4702 size,
4703 })
4704}
4705
4706fn open_items(
4707 serialized_workspace: Option<SerializedWorkspace>,
4708 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4709 cx: &mut ViewContext<Workspace>,
4710) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4711 let restored_items = serialized_workspace.map(|serialized_workspace| {
4712 Workspace::load_workspace(
4713 serialized_workspace,
4714 project_paths_to_open
4715 .iter()
4716 .map(|(_, project_path)| project_path)
4717 .cloned()
4718 .collect(),
4719 cx,
4720 )
4721 });
4722
4723 cx.spawn(|workspace, mut cx| async move {
4724 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4725
4726 if let Some(restored_items) = restored_items {
4727 let restored_items = restored_items.await?;
4728
4729 let restored_project_paths = restored_items
4730 .iter()
4731 .filter_map(|item| {
4732 cx.update(|cx| item.as_ref()?.project_path(cx))
4733 .ok()
4734 .flatten()
4735 })
4736 .collect::<HashSet<_>>();
4737
4738 for restored_item in restored_items {
4739 opened_items.push(restored_item.map(Ok));
4740 }
4741
4742 project_paths_to_open
4743 .iter_mut()
4744 .for_each(|(_, project_path)| {
4745 if let Some(project_path_to_open) = project_path {
4746 if restored_project_paths.contains(project_path_to_open) {
4747 *project_path = None;
4748 }
4749 }
4750 });
4751 } else {
4752 for _ in 0..project_paths_to_open.len() {
4753 opened_items.push(None);
4754 }
4755 }
4756 assert!(opened_items.len() == project_paths_to_open.len());
4757
4758 let tasks =
4759 project_paths_to_open
4760 .into_iter()
4761 .enumerate()
4762 .map(|(ix, (abs_path, project_path))| {
4763 let workspace = workspace.clone();
4764 cx.spawn(|mut cx| async move {
4765 let file_project_path = project_path?;
4766 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4767 workspace.project().update(cx, |project, cx| {
4768 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4769 })
4770 });
4771
4772 // We only want to open file paths here. If one of the items
4773 // here is a directory, it was already opened further above
4774 // with a `find_or_create_worktree`.
4775 if let Ok(task) = abs_path_task {
4776 if task.await.map_or(true, |p| p.is_file()) {
4777 return Some((
4778 ix,
4779 workspace
4780 .update(&mut cx, |workspace, cx| {
4781 workspace.open_path(file_project_path, None, true, cx)
4782 })
4783 .log_err()?
4784 .await,
4785 ));
4786 }
4787 }
4788 None
4789 })
4790 });
4791
4792 let tasks = tasks.collect::<Vec<_>>();
4793
4794 let tasks = futures::future::join_all(tasks);
4795 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4796 opened_items[ix] = Some(path_open_result);
4797 }
4798
4799 Ok(opened_items)
4800 })
4801}
4802
4803enum ActivateInDirectionTarget {
4804 Pane(View<Pane>),
4805 Dock(View<Dock>),
4806}
4807
4808fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4809 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";
4810
4811 workspace
4812 .update(cx, |workspace, cx| {
4813 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4814 struct DatabaseFailedNotification;
4815
4816 workspace.show_notification_once(
4817 NotificationId::unique::<DatabaseFailedNotification>(),
4818 cx,
4819 |cx| {
4820 cx.new_view(|_| {
4821 MessageNotification::new("Failed to load the database file.")
4822 .with_click_message("File an issue")
4823 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4824 })
4825 },
4826 );
4827 }
4828 })
4829 .log_err();
4830}
4831
4832impl FocusableView for Workspace {
4833 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4834 self.active_pane.focus_handle(cx)
4835 }
4836}
4837
4838#[derive(Clone, Render)]
4839struct DraggedDock(DockPosition);
4840
4841impl Render for Workspace {
4842 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4843 let mut context = KeyContext::new_with_defaults();
4844 context.add("Workspace");
4845 context.set("keyboard_layout", cx.keyboard_layout().clone());
4846 let centered_layout = self.centered_layout
4847 && self.center.panes().len() == 1
4848 && self.active_item(cx).is_some();
4849 let render_padding = |size| {
4850 (size > 0.0).then(|| {
4851 div()
4852 .h_full()
4853 .w(relative(size))
4854 .bg(cx.theme().colors().editor_background)
4855 .border_color(cx.theme().colors().pane_group_border)
4856 })
4857 };
4858 let paddings = if centered_layout {
4859 let settings = WorkspaceSettings::get_global(cx).centered_layout;
4860 (
4861 render_padding(Self::adjust_padding(settings.left_padding)),
4862 render_padding(Self::adjust_padding(settings.right_padding)),
4863 )
4864 } else {
4865 (None, None)
4866 };
4867 let ui_font = theme::setup_ui_font(cx);
4868
4869 let theme = cx.theme().clone();
4870 let colors = theme.colors();
4871
4872 client_side_decorations(
4873 self.actions(div(), cx)
4874 .key_context(context)
4875 .relative()
4876 .size_full()
4877 .flex()
4878 .flex_col()
4879 .font(ui_font)
4880 .gap_0()
4881 .justify_start()
4882 .items_start()
4883 .text_color(colors.text)
4884 .overflow_hidden()
4885 .children(self.titlebar_item.clone())
4886 .child(
4887 div()
4888 .size_full()
4889 .relative()
4890 .flex_1()
4891 .flex()
4892 .flex_col()
4893 .child(
4894 div()
4895 .id("workspace")
4896 .bg(colors.background)
4897 .relative()
4898 .flex_1()
4899 .w_full()
4900 .flex()
4901 .flex_col()
4902 .overflow_hidden()
4903 .border_t_1()
4904 .border_b_1()
4905 .border_color(colors.border)
4906 .child({
4907 let this = cx.view().clone();
4908 canvas(
4909 move |bounds, cx| {
4910 this.update(cx, |this, cx| {
4911 let bounds_changed = this.bounds != bounds;
4912 this.bounds = bounds;
4913
4914 if bounds_changed {
4915 this.left_dock.update(cx, |dock, cx| {
4916 dock.clamp_panel_size(bounds.size.width, cx)
4917 });
4918
4919 this.right_dock.update(cx, |dock, cx| {
4920 dock.clamp_panel_size(bounds.size.width, cx)
4921 });
4922
4923 this.bottom_dock.update(cx, |dock, cx| {
4924 dock.clamp_panel_size(
4925 bounds.size.height,
4926 cx,
4927 )
4928 });
4929 }
4930 })
4931 },
4932 |_, _, _| {},
4933 )
4934 .absolute()
4935 .size_full()
4936 })
4937 .when(self.zoomed.is_none(), |this| {
4938 this.on_drag_move(cx.listener(
4939 move |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4940 if workspace.previous_dock_drag_coordinates
4941 != Some(e.event.position)
4942 {
4943 workspace.previous_dock_drag_coordinates =
4944 Some(e.event.position);
4945 match e.drag(cx).0 {
4946 DockPosition::Left => {
4947 resize_left_dock(
4948 e.event.position.x
4949 - workspace.bounds.left(),
4950 workspace,
4951 cx,
4952 );
4953 }
4954 DockPosition::Right => {
4955 resize_right_dock(
4956 workspace.bounds.right()
4957 - e.event.position.x,
4958 workspace,
4959 cx,
4960 );
4961 }
4962 DockPosition::Bottom => {
4963 resize_bottom_dock(
4964 workspace.bounds.bottom()
4965 - e.event.position.y,
4966 workspace,
4967 cx,
4968 );
4969 }
4970 };
4971 workspace.serialize_workspace(cx);
4972 }
4973 },
4974 ))
4975 })
4976 .child(
4977 div()
4978 .flex()
4979 .flex_row()
4980 .h_full()
4981 // Left Dock
4982 .children(self.render_dock(
4983 DockPosition::Left,
4984 &self.left_dock,
4985 cx,
4986 ))
4987 // Panes
4988 .child(
4989 div()
4990 .flex()
4991 .flex_col()
4992 .flex_1()
4993 .overflow_hidden()
4994 .child(
4995 h_flex()
4996 .flex_1()
4997 .when_some(paddings.0, |this, p| {
4998 this.child(p.border_r_1())
4999 })
5000 .child(self.center.render(
5001 &self.project,
5002 &self.follower_states,
5003 self.active_call(),
5004 &self.active_pane,
5005 self.zoomed.as_ref(),
5006 &self.app_state,
5007 cx,
5008 ))
5009 .when_some(paddings.1, |this, p| {
5010 this.child(p.border_l_1())
5011 }),
5012 )
5013 .children(self.render_dock(
5014 DockPosition::Bottom,
5015 &self.bottom_dock,
5016 cx,
5017 )),
5018 )
5019 // Right Dock
5020 .children(self.render_dock(
5021 DockPosition::Right,
5022 &self.right_dock,
5023 cx,
5024 )),
5025 )
5026 .children(self.zoomed.as_ref().and_then(|view| {
5027 let zoomed_view = view.upgrade()?;
5028 let div = div()
5029 .occlude()
5030 .absolute()
5031 .overflow_hidden()
5032 .border_color(colors.border)
5033 .bg(colors.background)
5034 .child(zoomed_view)
5035 .inset_0()
5036 .shadow_lg();
5037
5038 Some(match self.zoomed_position {
5039 Some(DockPosition::Left) => div.right_2().border_r_1(),
5040 Some(DockPosition::Right) => div.left_2().border_l_1(),
5041 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5042 None => {
5043 div.top_2().bottom_2().left_2().right_2().border_1()
5044 }
5045 })
5046 }))
5047 .children(self.render_notifications(cx)),
5048 )
5049 .child(self.status_bar.clone())
5050 .child(self.modal_layer.clone()),
5051 ),
5052 cx,
5053 )
5054 }
5055}
5056
5057fn resize_bottom_dock(
5058 new_size: Pixels,
5059 workspace: &mut Workspace,
5060 cx: &mut ViewContext<Workspace>,
5061) {
5062 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5063 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5064 bottom_dock.resize_active_panel(Some(size), cx);
5065 });
5066}
5067
5068fn resize_right_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5069 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5070 workspace.right_dock.update(cx, |right_dock, cx| {
5071 right_dock.resize_active_panel(Some(size), cx);
5072 });
5073}
5074
5075fn resize_left_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5076 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5077
5078 workspace.left_dock.update(cx, |left_dock, cx| {
5079 left_dock.resize_active_panel(Some(size), cx);
5080 });
5081}
5082
5083impl WorkspaceStore {
5084 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5085 Self {
5086 workspaces: Default::default(),
5087 _subscriptions: vec![
5088 client.add_request_handler(cx.weak_model(), Self::handle_follow),
5089 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5090 ],
5091 client,
5092 }
5093 }
5094
5095 pub fn update_followers(
5096 &self,
5097 project_id: Option<u64>,
5098 update: proto::update_followers::Variant,
5099 cx: &AppContext,
5100 ) -> Option<()> {
5101 let active_call = ActiveCall::try_global(cx)?;
5102 let room_id = active_call.read(cx).room()?.read(cx).id();
5103 self.client
5104 .send(proto::UpdateFollowers {
5105 room_id,
5106 project_id,
5107 variant: Some(update),
5108 })
5109 .log_err()
5110 }
5111
5112 pub async fn handle_follow(
5113 this: Model<Self>,
5114 envelope: TypedEnvelope<proto::Follow>,
5115 mut cx: AsyncAppContext,
5116 ) -> Result<proto::FollowResponse> {
5117 this.update(&mut cx, |this, cx| {
5118 let follower = Follower {
5119 project_id: envelope.payload.project_id,
5120 peer_id: envelope.original_sender_id()?,
5121 };
5122
5123 let mut response = proto::FollowResponse::default();
5124 this.workspaces.retain(|workspace| {
5125 workspace
5126 .update(cx, |workspace, cx| {
5127 let handler_response = workspace.handle_follow(follower.project_id, cx);
5128 if let Some(active_view) = handler_response.active_view.clone() {
5129 if workspace.project.read(cx).remote_id() == follower.project_id {
5130 response.active_view = Some(active_view)
5131 }
5132 }
5133 })
5134 .is_ok()
5135 });
5136
5137 Ok(response)
5138 })?
5139 }
5140
5141 async fn handle_update_followers(
5142 this: Model<Self>,
5143 envelope: TypedEnvelope<proto::UpdateFollowers>,
5144 mut cx: AsyncAppContext,
5145 ) -> Result<()> {
5146 let leader_id = envelope.original_sender_id()?;
5147 let update = envelope.payload;
5148
5149 this.update(&mut cx, |this, cx| {
5150 this.workspaces.retain(|workspace| {
5151 workspace
5152 .update(cx, |workspace, cx| {
5153 let project_id = workspace.project.read(cx).remote_id();
5154 if update.project_id != project_id && update.project_id.is_some() {
5155 return;
5156 }
5157 workspace.handle_update_followers(leader_id, update.clone(), cx);
5158 })
5159 .is_ok()
5160 });
5161 Ok(())
5162 })?
5163 }
5164}
5165
5166impl ViewId {
5167 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5168 Ok(Self {
5169 creator: message
5170 .creator
5171 .ok_or_else(|| anyhow!("creator is missing"))?,
5172 id: message.id,
5173 })
5174 }
5175
5176 pub(crate) fn to_proto(self) -> proto::ViewId {
5177 proto::ViewId {
5178 creator: Some(self.creator),
5179 id: self.id,
5180 }
5181 }
5182}
5183
5184impl FollowerState {
5185 fn pane(&self) -> &View<Pane> {
5186 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5187 }
5188}
5189
5190pub trait WorkspaceHandle {
5191 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5192}
5193
5194impl WorkspaceHandle for View<Workspace> {
5195 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5196 self.read(cx)
5197 .worktrees(cx)
5198 .flat_map(|worktree| {
5199 let worktree_id = worktree.read(cx).id();
5200 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5201 worktree_id,
5202 path: f.path.clone(),
5203 })
5204 })
5205 .collect::<Vec<_>>()
5206 }
5207}
5208
5209impl std::fmt::Debug for OpenPaths {
5210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5211 f.debug_struct("OpenPaths")
5212 .field("paths", &self.paths)
5213 .finish()
5214 }
5215}
5216
5217pub fn activate_workspace_for_project(
5218 cx: &mut AppContext,
5219 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5220) -> Option<WindowHandle<Workspace>> {
5221 for window in cx.windows() {
5222 let Some(workspace) = window.downcast::<Workspace>() else {
5223 continue;
5224 };
5225
5226 let predicate = workspace
5227 .update(cx, |workspace, cx| {
5228 let project = workspace.project.read(cx);
5229 if predicate(project, cx) {
5230 cx.activate_window();
5231 true
5232 } else {
5233 false
5234 }
5235 })
5236 .log_err()
5237 .unwrap_or(false);
5238
5239 if predicate {
5240 return Some(workspace);
5241 }
5242 }
5243
5244 None
5245}
5246
5247pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5248 DB.last_workspace().await.log_err().flatten()
5249}
5250
5251pub fn last_session_workspace_locations(
5252 last_session_id: &str,
5253 last_session_window_stack: Option<Vec<WindowId>>,
5254) -> Option<Vec<SerializedWorkspaceLocation>> {
5255 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5256 .log_err()
5257}
5258
5259actions!(collab, [OpenChannelNotes]);
5260actions!(zed, [OpenLog]);
5261
5262async fn join_channel_internal(
5263 channel_id: ChannelId,
5264 app_state: &Arc<AppState>,
5265 requesting_window: Option<WindowHandle<Workspace>>,
5266 active_call: &Model<ActiveCall>,
5267 cx: &mut AsyncAppContext,
5268) -> Result<bool> {
5269 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5270 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5271 return (false, None);
5272 };
5273
5274 let already_in_channel = room.channel_id() == Some(channel_id);
5275 let should_prompt = room.is_sharing_project()
5276 && !room.remote_participants().is_empty()
5277 && !already_in_channel;
5278 let open_room = if already_in_channel {
5279 active_call.room().cloned()
5280 } else {
5281 None
5282 };
5283 (should_prompt, open_room)
5284 })?;
5285
5286 if let Some(room) = open_room {
5287 let task = room.update(cx, |room, cx| {
5288 if let Some((project, host)) = room.most_active_project(cx) {
5289 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5290 }
5291
5292 None
5293 })?;
5294 if let Some(task) = task {
5295 task.await?;
5296 }
5297 return anyhow::Ok(true);
5298 }
5299
5300 if should_prompt {
5301 if let Some(workspace) = requesting_window {
5302 let answer = workspace
5303 .update(cx, |_, cx| {
5304 cx.prompt(
5305 PromptLevel::Warning,
5306 "Do you want to switch channels?",
5307 Some("Leaving this call will unshare your current project."),
5308 &["Yes, Join Channel", "Cancel"],
5309 )
5310 })?
5311 .await;
5312
5313 if answer == Ok(1) {
5314 return Ok(false);
5315 }
5316 } else {
5317 return Ok(false); // unreachable!() hopefully
5318 }
5319 }
5320
5321 let client = cx.update(|cx| active_call.read(cx).client())?;
5322
5323 let mut client_status = client.status();
5324
5325 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5326 'outer: loop {
5327 let Some(status) = client_status.recv().await else {
5328 return Err(anyhow!("error connecting"));
5329 };
5330
5331 match status {
5332 Status::Connecting
5333 | Status::Authenticating
5334 | Status::Reconnecting
5335 | Status::Reauthenticating => continue,
5336 Status::Connected { .. } => break 'outer,
5337 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5338 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5339 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5340 return Err(ErrorCode::Disconnected.into());
5341 }
5342 }
5343 }
5344
5345 let room = active_call
5346 .update(cx, |active_call, cx| {
5347 active_call.join_channel(channel_id, cx)
5348 })?
5349 .await?;
5350
5351 let Some(room) = room else {
5352 return anyhow::Ok(true);
5353 };
5354
5355 room.update(cx, |room, _| room.room_update_completed())?
5356 .await;
5357
5358 let task = room.update(cx, |room, cx| {
5359 if let Some((project, host)) = room.most_active_project(cx) {
5360 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5361 }
5362
5363 // If you are the first to join a channel, see if you should share your project.
5364 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5365 if let Some(workspace) = requesting_window {
5366 let project = workspace.update(cx, |workspace, cx| {
5367 let project = workspace.project.read(cx);
5368
5369 if !CallSettings::get_global(cx).share_on_join {
5370 return None;
5371 }
5372
5373 if (project.is_local() || project.is_via_ssh())
5374 && project.visible_worktrees(cx).any(|tree| {
5375 tree.read(cx)
5376 .root_entry()
5377 .map_or(false, |entry| entry.is_dir())
5378 })
5379 {
5380 Some(workspace.project.clone())
5381 } else {
5382 None
5383 }
5384 });
5385 if let Ok(Some(project)) = project {
5386 return Some(cx.spawn(|room, mut cx| async move {
5387 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5388 .await?;
5389 Ok(())
5390 }));
5391 }
5392 }
5393 }
5394
5395 None
5396 })?;
5397 if let Some(task) = task {
5398 task.await?;
5399 return anyhow::Ok(true);
5400 }
5401 anyhow::Ok(false)
5402}
5403
5404pub fn join_channel(
5405 channel_id: ChannelId,
5406 app_state: Arc<AppState>,
5407 requesting_window: Option<WindowHandle<Workspace>>,
5408 cx: &mut AppContext,
5409) -> Task<Result<()>> {
5410 let active_call = ActiveCall::global(cx);
5411 cx.spawn(|mut cx| async move {
5412 let result = join_channel_internal(
5413 channel_id,
5414 &app_state,
5415 requesting_window,
5416 &active_call,
5417 &mut cx,
5418 )
5419 .await;
5420
5421 // join channel succeeded, and opened a window
5422 if matches!(result, Ok(true)) {
5423 return anyhow::Ok(());
5424 }
5425
5426 // find an existing workspace to focus and show call controls
5427 let mut active_window =
5428 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5429 if active_window.is_none() {
5430 // no open workspaces, make one to show the error in (blergh)
5431 let (window_handle, _) = cx
5432 .update(|cx| {
5433 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5434 })?
5435 .await?;
5436
5437 if result.is_ok() {
5438 cx.update(|cx| {
5439 cx.dispatch_action(&OpenChannelNotes);
5440 }).log_err();
5441 }
5442
5443 active_window = Some(window_handle);
5444 }
5445
5446 if let Err(err) = result {
5447 log::error!("failed to join channel: {}", err);
5448 if let Some(active_window) = active_window {
5449 active_window
5450 .update(&mut cx, |_, cx| {
5451 let detail: SharedString = match err.error_code() {
5452 ErrorCode::SignedOut => {
5453 "Please sign in to continue.".into()
5454 }
5455 ErrorCode::UpgradeRequired => {
5456 "Your are running an unsupported version of Zed. Please update to continue.".into()
5457 }
5458 ErrorCode::NoSuchChannel => {
5459 "No matching channel was found. Please check the link and try again.".into()
5460 }
5461 ErrorCode::Forbidden => {
5462 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5463 }
5464 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5465 _ => format!("{}\n\nPlease try again.", err).into(),
5466 };
5467 cx.prompt(
5468 PromptLevel::Critical,
5469 "Failed to join channel",
5470 Some(&detail),
5471 &["Ok"],
5472 )
5473 })?
5474 .await
5475 .ok();
5476 }
5477 }
5478
5479 // return ok, we showed the error to the user.
5480 anyhow::Ok(())
5481 })
5482}
5483
5484pub async fn get_any_active_workspace(
5485 app_state: Arc<AppState>,
5486 mut cx: AsyncAppContext,
5487) -> anyhow::Result<WindowHandle<Workspace>> {
5488 // find an existing workspace to focus and show call controls
5489 let active_window = activate_any_workspace_window(&mut cx);
5490 if active_window.is_none() {
5491 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5492 .await?;
5493 }
5494 activate_any_workspace_window(&mut cx).context("could not open zed")
5495}
5496
5497fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5498 cx.update(|cx| {
5499 if let Some(workspace_window) = cx
5500 .active_window()
5501 .and_then(|window| window.downcast::<Workspace>())
5502 {
5503 return Some(workspace_window);
5504 }
5505
5506 for window in cx.windows() {
5507 if let Some(workspace_window) = window.downcast::<Workspace>() {
5508 workspace_window
5509 .update(cx, |_, cx| cx.activate_window())
5510 .ok();
5511 return Some(workspace_window);
5512 }
5513 }
5514 None
5515 })
5516 .ok()
5517 .flatten()
5518}
5519
5520pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5521 cx.windows()
5522 .into_iter()
5523 .filter_map(|window| window.downcast::<Workspace>())
5524 .filter(|workspace| {
5525 workspace
5526 .read(cx)
5527 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5528 })
5529 .collect()
5530}
5531
5532#[derive(Default)]
5533pub struct OpenOptions {
5534 pub open_new_workspace: Option<bool>,
5535 pub replace_window: Option<WindowHandle<Workspace>>,
5536 pub env: Option<HashMap<String, String>>,
5537}
5538
5539#[allow(clippy::type_complexity)]
5540pub fn open_paths(
5541 abs_paths: &[PathBuf],
5542 app_state: Arc<AppState>,
5543 open_options: OpenOptions,
5544 cx: &mut AppContext,
5545) -> Task<
5546 anyhow::Result<(
5547 WindowHandle<Workspace>,
5548 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5549 )>,
5550> {
5551 let abs_paths = abs_paths.to_vec();
5552 let mut existing = None;
5553 let mut best_match = None;
5554 let mut open_visible = OpenVisible::All;
5555
5556 if open_options.open_new_workspace != Some(true) {
5557 for window in local_workspace_windows(cx) {
5558 if let Ok(workspace) = window.read(cx) {
5559 let m = workspace
5560 .project
5561 .read(cx)
5562 .visibility_for_paths(&abs_paths, cx);
5563 if m > best_match {
5564 existing = Some(window);
5565 best_match = m;
5566 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5567 existing = Some(window)
5568 }
5569 }
5570 }
5571 }
5572
5573 cx.spawn(move |mut cx| async move {
5574 if open_options.open_new_workspace.is_none() && existing.is_none() {
5575 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5576 if futures::future::join_all(all_files)
5577 .await
5578 .into_iter()
5579 .filter_map(|result| result.ok().flatten())
5580 .all(|file| !file.is_dir)
5581 {
5582 cx.update(|cx| {
5583 for window in local_workspace_windows(cx) {
5584 if let Ok(workspace) = window.read(cx) {
5585 let project = workspace.project().read(cx);
5586 if project.is_via_collab() {
5587 continue;
5588 }
5589 existing = Some(window);
5590 open_visible = OpenVisible::None;
5591 break;
5592 }
5593 }
5594 })?;
5595 }
5596 }
5597
5598 if let Some(existing) = existing {
5599 let open_task = existing
5600 .update(&mut cx, |workspace, cx| {
5601 cx.activate_window();
5602 workspace.open_paths(abs_paths, open_visible, None, cx)
5603 })?
5604 .await;
5605
5606 _ = existing.update(&mut cx, |workspace, cx| {
5607 for item in open_task.iter().flatten() {
5608 if let Err(e) = item {
5609 workspace.show_error(&e, cx);
5610 }
5611 }
5612 });
5613
5614 Ok((existing, open_task))
5615 } else {
5616 cx.update(move |cx| {
5617 Workspace::new_local(
5618 abs_paths,
5619 app_state.clone(),
5620 open_options.replace_window,
5621 open_options.env,
5622 cx,
5623 )
5624 })?
5625 .await
5626 }
5627 })
5628}
5629
5630pub fn open_new(
5631 open_options: OpenOptions,
5632 app_state: Arc<AppState>,
5633 cx: &mut AppContext,
5634 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5635) -> Task<anyhow::Result<()>> {
5636 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5637 cx.spawn(|mut cx| async move {
5638 let (workspace, opened_paths) = task.await?;
5639 workspace.update(&mut cx, |workspace, cx| {
5640 if opened_paths.is_empty() {
5641 init(workspace, cx)
5642 }
5643 })?;
5644 Ok(())
5645 })
5646}
5647
5648pub fn create_and_open_local_file(
5649 path: &'static Path,
5650 cx: &mut ViewContext<Workspace>,
5651 default_content: impl 'static + Send + FnOnce() -> Rope,
5652) -> Task<Result<Box<dyn ItemHandle>>> {
5653 cx.spawn(|workspace, mut cx| async move {
5654 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5655 if !fs.is_file(path).await {
5656 fs.create_file(path, Default::default()).await?;
5657 fs.save(path, &default_content(), Default::default())
5658 .await?;
5659 }
5660
5661 let mut items = workspace
5662 .update(&mut cx, |workspace, cx| {
5663 workspace.with_local_workspace(cx, |workspace, cx| {
5664 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5665 })
5666 })?
5667 .await?
5668 .await;
5669
5670 let item = items.pop().flatten();
5671 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5672 })
5673}
5674
5675pub fn open_ssh_project(
5676 window: WindowHandle<Workspace>,
5677 connection_options: SshConnectionOptions,
5678 cancel_rx: oneshot::Receiver<()>,
5679 delegate: Arc<dyn SshClientDelegate>,
5680 app_state: Arc<AppState>,
5681 paths: Vec<PathBuf>,
5682 cx: &mut AppContext,
5683) -> Task<Result<()>> {
5684 cx.spawn(|mut cx| async move {
5685 let (serialized_ssh_project, workspace_id, serialized_workspace) =
5686 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5687
5688 let session = match cx
5689 .update(|cx| {
5690 remote::SshRemoteClient::new(
5691 ConnectionIdentifier::Workspace(workspace_id.0),
5692 connection_options,
5693 cancel_rx,
5694 delegate,
5695 cx,
5696 )
5697 })?
5698 .await?
5699 {
5700 Some(result) => result,
5701 None => return Ok(()),
5702 };
5703
5704 let project = cx.update(|cx| {
5705 project::Project::ssh(
5706 session,
5707 app_state.client.clone(),
5708 app_state.node_runtime.clone(),
5709 app_state.user_store.clone(),
5710 app_state.languages.clone(),
5711 app_state.fs.clone(),
5712 cx,
5713 )
5714 })?;
5715
5716 let toolchains = DB.toolchains(workspace_id).await?;
5717 for (toolchain, worktree_id) in toolchains {
5718 project
5719 .update(&mut cx, |this, cx| {
5720 this.activate_toolchain(worktree_id, toolchain, cx)
5721 })?
5722 .await;
5723 }
5724 let mut project_paths_to_open = vec![];
5725 let mut project_path_errors = vec![];
5726
5727 for path in paths {
5728 let result = cx
5729 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5730 .await;
5731 match result {
5732 Ok((_, project_path)) => {
5733 project_paths_to_open.push((path.clone(), Some(project_path)));
5734 }
5735 Err(error) => {
5736 project_path_errors.push(error);
5737 }
5738 };
5739 }
5740
5741 if project_paths_to_open.is_empty() {
5742 return Err(project_path_errors
5743 .pop()
5744 .unwrap_or_else(|| anyhow!("no paths given")));
5745 }
5746
5747 cx.update_window(window.into(), |_, cx| {
5748 cx.replace_root_view(|cx| {
5749 let mut workspace =
5750 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5751
5752 workspace
5753 .client()
5754 .telemetry()
5755 .report_app_event("open ssh project".to_string());
5756
5757 workspace.set_serialized_ssh_project(serialized_ssh_project);
5758 workspace
5759 });
5760 })?;
5761
5762 window
5763 .update(&mut cx, |_, cx| {
5764 cx.activate_window();
5765
5766 open_items(serialized_workspace, project_paths_to_open, cx)
5767 })?
5768 .await?;
5769
5770 window.update(&mut cx, |workspace, cx| {
5771 for error in project_path_errors {
5772 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5773 if let Some(path) = error.error_tag("path") {
5774 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5775 }
5776 } else {
5777 workspace.show_error(&error, cx)
5778 }
5779 }
5780 })
5781 })
5782}
5783
5784fn serialize_ssh_project(
5785 connection_options: SshConnectionOptions,
5786 paths: Vec<PathBuf>,
5787 cx: &AsyncAppContext,
5788) -> Task<
5789 Result<(
5790 SerializedSshProject,
5791 WorkspaceId,
5792 Option<SerializedWorkspace>,
5793 )>,
5794> {
5795 cx.background_executor().spawn(async move {
5796 let serialized_ssh_project = persistence::DB
5797 .get_or_create_ssh_project(
5798 connection_options.host.clone(),
5799 connection_options.port,
5800 paths
5801 .iter()
5802 .map(|path| path.to_string_lossy().to_string())
5803 .collect::<Vec<_>>(),
5804 connection_options.username.clone(),
5805 )
5806 .await?;
5807
5808 let serialized_workspace =
5809 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5810
5811 let workspace_id = if let Some(workspace_id) =
5812 serialized_workspace.as_ref().map(|workspace| workspace.id)
5813 {
5814 workspace_id
5815 } else {
5816 persistence::DB.next_id().await?
5817 };
5818
5819 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5820 })
5821}
5822
5823pub fn join_in_room_project(
5824 project_id: u64,
5825 follow_user_id: u64,
5826 app_state: Arc<AppState>,
5827 cx: &mut AppContext,
5828) -> Task<Result<()>> {
5829 let windows = cx.windows();
5830 cx.spawn(|mut cx| async move {
5831 let existing_workspace = windows.into_iter().find_map(|window| {
5832 window.downcast::<Workspace>().and_then(|window| {
5833 window
5834 .update(&mut cx, |workspace, cx| {
5835 if workspace.project().read(cx).remote_id() == Some(project_id) {
5836 Some(window)
5837 } else {
5838 None
5839 }
5840 })
5841 .unwrap_or(None)
5842 })
5843 });
5844
5845 let workspace = if let Some(existing_workspace) = existing_workspace {
5846 existing_workspace
5847 } else {
5848 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5849 let room = active_call
5850 .read_with(&cx, |call, _| call.room().cloned())?
5851 .ok_or_else(|| anyhow!("not in a call"))?;
5852 let project = room
5853 .update(&mut cx, |room, cx| {
5854 room.join_project(
5855 project_id,
5856 app_state.languages.clone(),
5857 app_state.fs.clone(),
5858 cx,
5859 )
5860 })?
5861 .await?;
5862
5863 let window_bounds_override = window_bounds_env_override();
5864 cx.update(|cx| {
5865 let mut options = (app_state.build_window_options)(None, cx);
5866 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5867 cx.open_window(options, |cx| {
5868 cx.new_view(|cx| {
5869 Workspace::new(Default::default(), project, app_state.clone(), cx)
5870 })
5871 })
5872 })??
5873 };
5874
5875 workspace.update(&mut cx, |workspace, cx| {
5876 cx.activate(true);
5877 cx.activate_window();
5878
5879 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5880 let follow_peer_id = room
5881 .read(cx)
5882 .remote_participants()
5883 .iter()
5884 .find(|(_, participant)| participant.user.id == follow_user_id)
5885 .map(|(_, p)| p.peer_id)
5886 .or_else(|| {
5887 // If we couldn't follow the given user, follow the host instead.
5888 let collaborator = workspace
5889 .project()
5890 .read(cx)
5891 .collaborators()
5892 .values()
5893 .find(|collaborator| collaborator.is_host)?;
5894 Some(collaborator.peer_id)
5895 });
5896
5897 if let Some(follow_peer_id) = follow_peer_id {
5898 workspace.follow(follow_peer_id, cx);
5899 }
5900 }
5901 })?;
5902
5903 anyhow::Ok(())
5904 })
5905}
5906
5907pub fn reload(reload: &Reload, cx: &mut AppContext) {
5908 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5909 let mut workspace_windows = cx
5910 .windows()
5911 .into_iter()
5912 .filter_map(|window| window.downcast::<Workspace>())
5913 .collect::<Vec<_>>();
5914
5915 // If multiple windows have unsaved changes, and need a save prompt,
5916 // prompt in the active window before switching to a different window.
5917 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5918
5919 let mut prompt = None;
5920 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5921 prompt = window
5922 .update(cx, |_, cx| {
5923 cx.prompt(
5924 PromptLevel::Info,
5925 "Are you sure you want to restart?",
5926 None,
5927 &["Restart", "Cancel"],
5928 )
5929 })
5930 .ok();
5931 }
5932
5933 let binary_path = reload.binary_path.clone();
5934 cx.spawn(|mut cx| async move {
5935 if let Some(prompt) = prompt {
5936 let answer = prompt.await?;
5937 if answer != 0 {
5938 return Ok(());
5939 }
5940 }
5941
5942 // If the user cancels any save prompt, then keep the app open.
5943 for window in workspace_windows {
5944 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5945 workspace.prepare_to_close(CloseIntent::Quit, cx)
5946 }) {
5947 if !should_close.await? {
5948 return Ok(());
5949 }
5950 }
5951 }
5952
5953 cx.update(|cx| cx.restart(binary_path))
5954 })
5955 .detach_and_log_err(cx);
5956}
5957
5958fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5959 let mut parts = value.split(',');
5960 let x: usize = parts.next()?.parse().ok()?;
5961 let y: usize = parts.next()?.parse().ok()?;
5962 Some(point(px(x as f32), px(y as f32)))
5963}
5964
5965fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5966 let mut parts = value.split(',');
5967 let width: usize = parts.next()?.parse().ok()?;
5968 let height: usize = parts.next()?.parse().ok()?;
5969 Some(size(px(width as f32), px(height as f32)))
5970}
5971
5972pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5973 const BORDER_SIZE: Pixels = px(1.0);
5974 let decorations = cx.window_decorations();
5975
5976 if matches!(decorations, Decorations::Client { .. }) {
5977 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5978 }
5979
5980 struct GlobalResizeEdge(ResizeEdge);
5981 impl Global for GlobalResizeEdge {}
5982
5983 div()
5984 .id("window-backdrop")
5985 .bg(transparent_black())
5986 .map(|div| match decorations {
5987 Decorations::Server => div,
5988 Decorations::Client { tiling, .. } => div
5989 .when(!(tiling.top || tiling.right), |div| {
5990 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5991 })
5992 .when(!(tiling.top || tiling.left), |div| {
5993 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5994 })
5995 .when(!(tiling.bottom || tiling.right), |div| {
5996 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5997 })
5998 .when(!(tiling.bottom || tiling.left), |div| {
5999 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6000 })
6001 .when(!tiling.top, |div| {
6002 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6003 })
6004 .when(!tiling.bottom, |div| {
6005 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6006 })
6007 .when(!tiling.left, |div| {
6008 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6009 })
6010 .when(!tiling.right, |div| {
6011 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6012 })
6013 .on_mouse_move(move |e, cx| {
6014 let size = cx.window_bounds().get_bounds().size;
6015 let pos = e.position;
6016
6017 let new_edge =
6018 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6019
6020 let edge = cx.try_global::<GlobalResizeEdge>();
6021 if new_edge != edge.map(|edge| edge.0) {
6022 cx.window_handle()
6023 .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
6024 .ok();
6025 }
6026 })
6027 .on_mouse_down(MouseButton::Left, move |e, cx| {
6028 let size = cx.window_bounds().get_bounds().size;
6029 let pos = e.position;
6030
6031 let edge = match resize_edge(
6032 pos,
6033 theme::CLIENT_SIDE_DECORATION_SHADOW,
6034 size,
6035 tiling,
6036 ) {
6037 Some(value) => value,
6038 None => return,
6039 };
6040
6041 cx.start_window_resize(edge);
6042 }),
6043 })
6044 .size_full()
6045 .child(
6046 div()
6047 .cursor(CursorStyle::Arrow)
6048 .map(|div| match decorations {
6049 Decorations::Server => div,
6050 Decorations::Client { tiling } => div
6051 .border_color(cx.theme().colors().border)
6052 .when(!(tiling.top || tiling.right), |div| {
6053 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6054 })
6055 .when(!(tiling.top || tiling.left), |div| {
6056 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6057 })
6058 .when(!(tiling.bottom || tiling.right), |div| {
6059 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6060 })
6061 .when(!(tiling.bottom || tiling.left), |div| {
6062 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6063 })
6064 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6065 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6066 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6067 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6068 .when(!tiling.is_tiled(), |div| {
6069 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6070 color: Hsla {
6071 h: 0.,
6072 s: 0.,
6073 l: 0.,
6074 a: 0.4,
6075 },
6076 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6077 spread_radius: px(0.),
6078 offset: point(px(0.0), px(0.0)),
6079 }])
6080 }),
6081 })
6082 .on_mouse_move(|_e, cx| {
6083 cx.stop_propagation();
6084 })
6085 .size_full()
6086 .child(element),
6087 )
6088 .map(|div| match decorations {
6089 Decorations::Server => div,
6090 Decorations::Client { tiling, .. } => div.child(
6091 canvas(
6092 |_bounds, cx| {
6093 cx.insert_hitbox(
6094 Bounds::new(
6095 point(px(0.0), px(0.0)),
6096 cx.window_bounds().get_bounds().size,
6097 ),
6098 false,
6099 )
6100 },
6101 move |_bounds, hitbox, cx| {
6102 let mouse = cx.mouse_position();
6103 let size = cx.window_bounds().get_bounds().size;
6104 let Some(edge) =
6105 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6106 else {
6107 return;
6108 };
6109 cx.set_global(GlobalResizeEdge(edge));
6110 cx.set_cursor_style(
6111 match edge {
6112 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6113 ResizeEdge::Left | ResizeEdge::Right => {
6114 CursorStyle::ResizeLeftRight
6115 }
6116 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6117 CursorStyle::ResizeUpLeftDownRight
6118 }
6119 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6120 CursorStyle::ResizeUpRightDownLeft
6121 }
6122 },
6123 &hitbox,
6124 );
6125 },
6126 )
6127 .size_full()
6128 .absolute(),
6129 ),
6130 })
6131}
6132
6133fn resize_edge(
6134 pos: Point<Pixels>,
6135 shadow_size: Pixels,
6136 window_size: Size<Pixels>,
6137 tiling: Tiling,
6138) -> Option<ResizeEdge> {
6139 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6140 if bounds.contains(&pos) {
6141 return None;
6142 }
6143
6144 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6145 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6146 if !tiling.top && top_left_bounds.contains(&pos) {
6147 return Some(ResizeEdge::TopLeft);
6148 }
6149
6150 let top_right_bounds = Bounds::new(
6151 Point::new(window_size.width - corner_size.width, px(0.)),
6152 corner_size,
6153 );
6154 if !tiling.top && top_right_bounds.contains(&pos) {
6155 return Some(ResizeEdge::TopRight);
6156 }
6157
6158 let bottom_left_bounds = Bounds::new(
6159 Point::new(px(0.), window_size.height - corner_size.height),
6160 corner_size,
6161 );
6162 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6163 return Some(ResizeEdge::BottomLeft);
6164 }
6165
6166 let bottom_right_bounds = Bounds::new(
6167 Point::new(
6168 window_size.width - corner_size.width,
6169 window_size.height - corner_size.height,
6170 ),
6171 corner_size,
6172 );
6173 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6174 return Some(ResizeEdge::BottomRight);
6175 }
6176
6177 if !tiling.top && pos.y < shadow_size {
6178 Some(ResizeEdge::Top)
6179 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6180 Some(ResizeEdge::Bottom)
6181 } else if !tiling.left && pos.x < shadow_size {
6182 Some(ResizeEdge::Left)
6183 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6184 Some(ResizeEdge::Right)
6185 } else {
6186 None
6187 }
6188}
6189
6190fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext) {
6191 if pane == active_pane {
6192 return;
6193 } else if pane.read(cx).items_len() == 0 {
6194 pane.update(cx, |_, cx| {
6195 cx.emit(pane::Event::Remove {
6196 focus_on_pane: None,
6197 });
6198 })
6199 } else {
6200 move_all_items(pane, active_pane, cx);
6201 }
6202}
6203
6204fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext) {
6205 let destination_is_different = from_pane != to_pane;
6206 let mut moved_items = 0;
6207 for (item_ix, item_handle) in from_pane
6208 .read(cx)
6209 .items()
6210 .enumerate()
6211 .map(|(ix, item)| (ix, item.clone()))
6212 .collect::<Vec<_>>()
6213 {
6214 let ix = item_ix - moved_items;
6215 if destination_is_different {
6216 // Close item from previous pane
6217 from_pane.update(cx, |source, cx| {
6218 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6219 });
6220 moved_items += 1;
6221 }
6222
6223 // This automatically removes duplicate items in the pane
6224 to_pane.update(cx, |destination, cx| {
6225 destination.add_item(item_handle, true, true, None, cx);
6226 destination.focus(cx)
6227 });
6228 }
6229}
6230
6231pub fn move_item(
6232 source: &View<Pane>,
6233 destination: &View<Pane>,
6234 item_id_to_move: EntityId,
6235 destination_index: usize,
6236 cx: &mut WindowContext,
6237) {
6238 let Some((item_ix, item_handle)) = source
6239 .read(cx)
6240 .items()
6241 .enumerate()
6242 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6243 .map(|(ix, item)| (ix, item.clone()))
6244 else {
6245 // Tab was closed during drag
6246 return;
6247 };
6248
6249 if source != destination {
6250 // Close item from previous pane
6251 source.update(cx, |source, cx| {
6252 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6253 });
6254 }
6255
6256 // This automatically removes duplicate items in the pane
6257 destination.update(cx, |destination, cx| {
6258 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6259 destination.focus(cx)
6260 });
6261}
6262
6263pub fn move_active_item(
6264 source: &View<Pane>,
6265 destination: &View<Pane>,
6266 focus_destination: bool,
6267 close_if_empty: bool,
6268 cx: &mut WindowContext,
6269) {
6270 if source == destination {
6271 return;
6272 }
6273 let Some(active_item) = source.read(cx).active_item() else {
6274 return;
6275 };
6276 source.update(cx, |source_pane, cx| {
6277 let item_id = active_item.item_id();
6278 source_pane.remove_item(item_id, false, close_if_empty, cx);
6279 destination.update(cx, |target_pane, cx| {
6280 target_pane.add_item(
6281 active_item,
6282 focus_destination,
6283 focus_destination,
6284 Some(target_pane.items_len()),
6285 cx,
6286 );
6287 });
6288 });
6289}
6290
6291#[cfg(test)]
6292mod tests {
6293 use std::{cell::RefCell, rc::Rc};
6294
6295 use super::*;
6296 use crate::{
6297 dock::{test::TestPanel, PanelEvent},
6298 item::{
6299 test::{TestItem, TestProjectItem},
6300 ItemEvent,
6301 },
6302 };
6303 use fs::FakeFs;
6304 use gpui::{
6305 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6306 UpdateGlobal, VisualTestContext,
6307 };
6308 use project::{Project, ProjectEntryId};
6309 use serde_json::json;
6310 use settings::SettingsStore;
6311
6312 #[gpui::test]
6313 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6314 init_test(cx);
6315
6316 let fs = FakeFs::new(cx.executor());
6317 let project = Project::test(fs, [], cx).await;
6318 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6319
6320 // Adding an item with no ambiguity renders the tab without detail.
6321 let item1 = cx.new_view(|cx| {
6322 let mut item = TestItem::new(cx);
6323 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6324 item
6325 });
6326 workspace.update(cx, |workspace, cx| {
6327 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6328 });
6329 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6330
6331 // Adding an item that creates ambiguity increases the level of detail on
6332 // both tabs.
6333 let item2 = cx.new_view(|cx| {
6334 let mut item = TestItem::new(cx);
6335 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6336 item
6337 });
6338 workspace.update(cx, |workspace, cx| {
6339 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6340 });
6341 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6342 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6343
6344 // Adding an item that creates ambiguity increases the level of detail only
6345 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6346 // we stop at the highest detail available.
6347 let item3 = cx.new_view(|cx| {
6348 let mut item = TestItem::new(cx);
6349 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6350 item
6351 });
6352 workspace.update(cx, |workspace, cx| {
6353 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6354 });
6355 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6356 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6357 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6358 }
6359
6360 #[gpui::test]
6361 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6362 init_test(cx);
6363
6364 let fs = FakeFs::new(cx.executor());
6365 fs.insert_tree(
6366 "/root1",
6367 json!({
6368 "one.txt": "",
6369 "two.txt": "",
6370 }),
6371 )
6372 .await;
6373 fs.insert_tree(
6374 "/root2",
6375 json!({
6376 "three.txt": "",
6377 }),
6378 )
6379 .await;
6380
6381 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6382 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6383 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6384 let worktree_id = project.update(cx, |project, cx| {
6385 project.worktrees(cx).next().unwrap().read(cx).id()
6386 });
6387
6388 let item1 = cx.new_view(|cx| {
6389 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6390 });
6391 let item2 = cx.new_view(|cx| {
6392 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6393 });
6394
6395 // Add an item to an empty pane
6396 workspace.update(cx, |workspace, cx| {
6397 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6398 });
6399 project.update(cx, |project, cx| {
6400 assert_eq!(
6401 project.active_entry(),
6402 project
6403 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6404 .map(|e| e.id)
6405 );
6406 });
6407 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6408
6409 // Add a second item to a non-empty pane
6410 workspace.update(cx, |workspace, cx| {
6411 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6412 });
6413 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6414 project.update(cx, |project, cx| {
6415 assert_eq!(
6416 project.active_entry(),
6417 project
6418 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6419 .map(|e| e.id)
6420 );
6421 });
6422
6423 // Close the active item
6424 pane.update(cx, |pane, cx| {
6425 pane.close_active_item(&Default::default(), cx).unwrap()
6426 })
6427 .await
6428 .unwrap();
6429 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6430 project.update(cx, |project, cx| {
6431 assert_eq!(
6432 project.active_entry(),
6433 project
6434 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6435 .map(|e| e.id)
6436 );
6437 });
6438
6439 // Add a project folder
6440 project
6441 .update(cx, |project, cx| {
6442 project.find_or_create_worktree("root2", true, cx)
6443 })
6444 .await
6445 .unwrap();
6446 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6447
6448 // Remove a project folder
6449 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6450 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6451 }
6452
6453 #[gpui::test]
6454 async fn test_close_window(cx: &mut TestAppContext) {
6455 init_test(cx);
6456
6457 let fs = FakeFs::new(cx.executor());
6458 fs.insert_tree("/root", json!({ "one": "" })).await;
6459
6460 let project = Project::test(fs, ["root".as_ref()], cx).await;
6461 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6462
6463 // When there are no dirty items, there's nothing to do.
6464 let item1 = cx.new_view(TestItem::new);
6465 workspace.update(cx, |w, cx| {
6466 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6467 });
6468 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6469 assert!(task.await.unwrap());
6470
6471 // When there are dirty untitled items, prompt to save each one. If the user
6472 // cancels any prompt, then abort.
6473 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6474 let item3 = cx.new_view(|cx| {
6475 TestItem::new(cx)
6476 .with_dirty(true)
6477 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6478 });
6479 workspace.update(cx, |w, cx| {
6480 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6481 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6482 });
6483 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6484 cx.executor().run_until_parked();
6485 cx.simulate_prompt_answer(2); // cancel save all
6486 cx.executor().run_until_parked();
6487 cx.simulate_prompt_answer(2); // cancel save all
6488 cx.executor().run_until_parked();
6489 assert!(!cx.has_pending_prompt());
6490 assert!(!task.await.unwrap());
6491 }
6492
6493 #[gpui::test]
6494 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6495 init_test(cx);
6496
6497 // Register TestItem as a serializable item
6498 cx.update(|cx| {
6499 register_serializable_item::<TestItem>(cx);
6500 });
6501
6502 let fs = FakeFs::new(cx.executor());
6503 fs.insert_tree("/root", json!({ "one": "" })).await;
6504
6505 let project = Project::test(fs, ["root".as_ref()], cx).await;
6506 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6507
6508 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6509 let item1 = cx.new_view(|cx| {
6510 TestItem::new(cx)
6511 .with_dirty(true)
6512 .with_serialize(|| Some(Task::ready(Ok(()))))
6513 });
6514 let item2 = cx.new_view(|cx| {
6515 TestItem::new(cx)
6516 .with_dirty(true)
6517 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6518 .with_serialize(|| Some(Task::ready(Ok(()))))
6519 });
6520 workspace.update(cx, |w, cx| {
6521 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6522 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6523 });
6524 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6525 assert!(task.await.unwrap());
6526 }
6527
6528 #[gpui::test]
6529 async fn test_close_pane_items(cx: &mut TestAppContext) {
6530 init_test(cx);
6531
6532 let fs = FakeFs::new(cx.executor());
6533
6534 let project = Project::test(fs, None, cx).await;
6535 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6536
6537 let item1 = cx.new_view(|cx| {
6538 TestItem::new(cx)
6539 .with_dirty(true)
6540 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6541 });
6542 let item2 = cx.new_view(|cx| {
6543 TestItem::new(cx)
6544 .with_dirty(true)
6545 .with_conflict(true)
6546 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6547 });
6548 let item3 = cx.new_view(|cx| {
6549 TestItem::new(cx)
6550 .with_dirty(true)
6551 .with_conflict(true)
6552 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6553 });
6554 let item4 = cx.new_view(|cx| {
6555 TestItem::new(cx).with_dirty(true).with_project_items(&[{
6556 let project_item = TestProjectItem::new_untitled(cx);
6557 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6558 project_item
6559 }])
6560 });
6561 let pane = workspace.update(cx, |workspace, cx| {
6562 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6563 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6564 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6565 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6566 workspace.active_pane().clone()
6567 });
6568
6569 let close_items = pane.update(cx, |pane, cx| {
6570 pane.activate_item(1, true, true, cx);
6571 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6572 let item1_id = item1.item_id();
6573 let item3_id = item3.item_id();
6574 let item4_id = item4.item_id();
6575 pane.close_items(cx, SaveIntent::Close, move |id| {
6576 [item1_id, item3_id, item4_id].contains(&id)
6577 })
6578 });
6579 cx.executor().run_until_parked();
6580
6581 assert!(cx.has_pending_prompt());
6582 // Ignore "Save all" prompt
6583 cx.simulate_prompt_answer(2);
6584 cx.executor().run_until_parked();
6585 // There's a prompt to save item 1.
6586 pane.update(cx, |pane, _| {
6587 assert_eq!(pane.items_len(), 4);
6588 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6589 });
6590 // Confirm saving item 1.
6591 cx.simulate_prompt_answer(0);
6592 cx.executor().run_until_parked();
6593
6594 // Item 1 is saved. There's a prompt to save item 3.
6595 pane.update(cx, |pane, cx| {
6596 assert_eq!(item1.read(cx).save_count, 1);
6597 assert_eq!(item1.read(cx).save_as_count, 0);
6598 assert_eq!(item1.read(cx).reload_count, 0);
6599 assert_eq!(pane.items_len(), 3);
6600 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6601 });
6602 assert!(cx.has_pending_prompt());
6603
6604 // Cancel saving item 3.
6605 cx.simulate_prompt_answer(1);
6606 cx.executor().run_until_parked();
6607
6608 // Item 3 is reloaded. There's a prompt to save item 4.
6609 pane.update(cx, |pane, cx| {
6610 assert_eq!(item3.read(cx).save_count, 0);
6611 assert_eq!(item3.read(cx).save_as_count, 0);
6612 assert_eq!(item3.read(cx).reload_count, 1);
6613 assert_eq!(pane.items_len(), 2);
6614 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6615 });
6616 assert!(cx.has_pending_prompt());
6617
6618 // Confirm saving item 4.
6619 cx.simulate_prompt_answer(0);
6620 cx.executor().run_until_parked();
6621
6622 // There's a prompt for a path for item 4.
6623 cx.simulate_new_path_selection(|_| Some(Default::default()));
6624 close_items.await.unwrap();
6625
6626 // The requested items are closed.
6627 pane.update(cx, |pane, cx| {
6628 assert_eq!(item4.read(cx).save_count, 0);
6629 assert_eq!(item4.read(cx).save_as_count, 1);
6630 assert_eq!(item4.read(cx).reload_count, 0);
6631 assert_eq!(pane.items_len(), 1);
6632 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6633 });
6634 }
6635
6636 #[gpui::test]
6637 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6638 init_test(cx);
6639
6640 let fs = FakeFs::new(cx.executor());
6641 let project = Project::test(fs, [], cx).await;
6642 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6643
6644 // Create several workspace items with single project entries, and two
6645 // workspace items with multiple project entries.
6646 let single_entry_items = (0..=4)
6647 .map(|project_entry_id| {
6648 cx.new_view(|cx| {
6649 TestItem::new(cx)
6650 .with_dirty(true)
6651 .with_project_items(&[dirty_project_item(
6652 project_entry_id,
6653 &format!("{project_entry_id}.txt"),
6654 cx,
6655 )])
6656 })
6657 })
6658 .collect::<Vec<_>>();
6659 let item_2_3 = cx.new_view(|cx| {
6660 TestItem::new(cx)
6661 .with_dirty(true)
6662 .with_singleton(false)
6663 .with_project_items(&[
6664 single_entry_items[2].read(cx).project_items[0].clone(),
6665 single_entry_items[3].read(cx).project_items[0].clone(),
6666 ])
6667 });
6668 let item_3_4 = cx.new_view(|cx| {
6669 TestItem::new(cx)
6670 .with_dirty(true)
6671 .with_singleton(false)
6672 .with_project_items(&[
6673 single_entry_items[3].read(cx).project_items[0].clone(),
6674 single_entry_items[4].read(cx).project_items[0].clone(),
6675 ])
6676 });
6677
6678 // Create two panes that contain the following project entries:
6679 // left pane:
6680 // multi-entry items: (2, 3)
6681 // single-entry items: 0, 1, 2, 3, 4
6682 // right pane:
6683 // single-entry items: 1
6684 // multi-entry items: (3, 4)
6685 let left_pane = workspace.update(cx, |workspace, cx| {
6686 let left_pane = workspace.active_pane().clone();
6687 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6688 for item in single_entry_items {
6689 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6690 }
6691 left_pane.update(cx, |pane, cx| {
6692 pane.activate_item(2, true, true, cx);
6693 });
6694
6695 let right_pane = workspace
6696 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6697 .unwrap();
6698
6699 right_pane.update(cx, |pane, cx| {
6700 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6701 });
6702
6703 left_pane
6704 });
6705
6706 cx.focus_view(&left_pane);
6707
6708 // When closing all of the items in the left pane, we should be prompted twice:
6709 // once for project entry 0, and once for project entry 2. Project entries 1,
6710 // 3, and 4 are all still open in the other paten. After those two
6711 // prompts, the task should complete.
6712
6713 let close = left_pane.update(cx, |pane, cx| {
6714 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6715 });
6716 cx.executor().run_until_parked();
6717
6718 // Discard "Save all" prompt
6719 cx.simulate_prompt_answer(2);
6720
6721 cx.executor().run_until_parked();
6722 left_pane.update(cx, |pane, cx| {
6723 assert_eq!(
6724 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6725 &[ProjectEntryId::from_proto(0)]
6726 );
6727 });
6728 cx.simulate_prompt_answer(0);
6729
6730 cx.executor().run_until_parked();
6731 left_pane.update(cx, |pane, cx| {
6732 assert_eq!(
6733 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6734 &[ProjectEntryId::from_proto(2)]
6735 );
6736 });
6737 cx.simulate_prompt_answer(0);
6738
6739 cx.executor().run_until_parked();
6740 close.await.unwrap();
6741 left_pane.update(cx, |pane, _| {
6742 assert_eq!(pane.items_len(), 0);
6743 });
6744 }
6745
6746 #[gpui::test]
6747 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6748 init_test(cx);
6749
6750 let fs = FakeFs::new(cx.executor());
6751 let project = Project::test(fs, [], cx).await;
6752 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6753 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6754
6755 let item = cx.new_view(|cx| {
6756 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6757 });
6758 let item_id = item.entity_id();
6759 workspace.update(cx, |workspace, cx| {
6760 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6761 });
6762
6763 // Autosave on window change.
6764 item.update(cx, |item, cx| {
6765 SettingsStore::update_global(cx, |settings, cx| {
6766 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6767 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6768 })
6769 });
6770 item.is_dirty = true;
6771 });
6772
6773 // Deactivating the window saves the file.
6774 cx.deactivate_window();
6775 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6776
6777 // Re-activating the window doesn't save the file.
6778 cx.update(|cx| cx.activate_window());
6779 cx.executor().run_until_parked();
6780 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6781
6782 // Autosave on focus change.
6783 item.update(cx, |item, cx| {
6784 cx.focus_self();
6785 SettingsStore::update_global(cx, |settings, cx| {
6786 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6787 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6788 })
6789 });
6790 item.is_dirty = true;
6791 });
6792
6793 // Blurring the item saves the file.
6794 item.update(cx, |_, cx| cx.blur());
6795 cx.executor().run_until_parked();
6796 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6797
6798 // Deactivating the window still saves the file.
6799 item.update(cx, |item, cx| {
6800 cx.focus_self();
6801 item.is_dirty = true;
6802 });
6803 cx.deactivate_window();
6804 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6805
6806 // Autosave after delay.
6807 item.update(cx, |item, cx| {
6808 SettingsStore::update_global(cx, |settings, cx| {
6809 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6810 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6811 })
6812 });
6813 item.is_dirty = true;
6814 cx.emit(ItemEvent::Edit);
6815 });
6816
6817 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6818 cx.executor().advance_clock(Duration::from_millis(250));
6819 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6820
6821 // After delay expires, the file is saved.
6822 cx.executor().advance_clock(Duration::from_millis(250));
6823 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6824
6825 // Autosave on focus change, ensuring closing the tab counts as such.
6826 item.update(cx, |item, cx| {
6827 SettingsStore::update_global(cx, |settings, cx| {
6828 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6829 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6830 })
6831 });
6832 item.is_dirty = true;
6833 for project_item in &mut item.project_items {
6834 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6835 }
6836 });
6837
6838 pane.update(cx, |pane, cx| {
6839 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6840 })
6841 .await
6842 .unwrap();
6843 assert!(!cx.has_pending_prompt());
6844 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6845
6846 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6847 workspace.update(cx, |workspace, cx| {
6848 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6849 });
6850 item.update(cx, |item, cx| {
6851 item.project_items[0].update(cx, |item, _| {
6852 item.entry_id = None;
6853 });
6854 item.is_dirty = true;
6855 cx.blur();
6856 });
6857 cx.run_until_parked();
6858 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6859
6860 // Ensure autosave is prevented for deleted files also when closing the buffer.
6861 let _close_items = pane.update(cx, |pane, cx| {
6862 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6863 });
6864 cx.run_until_parked();
6865 assert!(cx.has_pending_prompt());
6866 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6867 }
6868
6869 #[gpui::test]
6870 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6871 init_test(cx);
6872
6873 let fs = FakeFs::new(cx.executor());
6874
6875 let project = Project::test(fs, [], cx).await;
6876 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6877
6878 let item = cx.new_view(|cx| {
6879 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6880 });
6881 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6882 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6883 let toolbar_notify_count = Rc::new(RefCell::new(0));
6884
6885 workspace.update(cx, |workspace, cx| {
6886 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6887 let toolbar_notification_count = toolbar_notify_count.clone();
6888 cx.observe(&toolbar, move |_, _, _| {
6889 *toolbar_notification_count.borrow_mut() += 1
6890 })
6891 .detach();
6892 });
6893
6894 pane.update(cx, |pane, _| {
6895 assert!(!pane.can_navigate_backward());
6896 assert!(!pane.can_navigate_forward());
6897 });
6898
6899 item.update(cx, |item, cx| {
6900 item.set_state("one".to_string(), cx);
6901 });
6902
6903 // Toolbar must be notified to re-render the navigation buttons
6904 assert_eq!(*toolbar_notify_count.borrow(), 1);
6905
6906 pane.update(cx, |pane, _| {
6907 assert!(pane.can_navigate_backward());
6908 assert!(!pane.can_navigate_forward());
6909 });
6910
6911 workspace
6912 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6913 .await
6914 .unwrap();
6915
6916 assert_eq!(*toolbar_notify_count.borrow(), 2);
6917 pane.update(cx, |pane, _| {
6918 assert!(!pane.can_navigate_backward());
6919 assert!(pane.can_navigate_forward());
6920 });
6921 }
6922
6923 #[gpui::test]
6924 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6925 init_test(cx);
6926 let fs = FakeFs::new(cx.executor());
6927
6928 let project = Project::test(fs, [], cx).await;
6929 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6930
6931 let panel = workspace.update(cx, |workspace, cx| {
6932 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6933 workspace.add_panel(panel.clone(), cx);
6934
6935 workspace
6936 .right_dock()
6937 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6938
6939 panel
6940 });
6941
6942 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6943 pane.update(cx, |pane, cx| {
6944 let item = cx.new_view(TestItem::new);
6945 pane.add_item(Box::new(item), true, true, None, cx);
6946 });
6947
6948 // Transfer focus from center to panel
6949 workspace.update(cx, |workspace, cx| {
6950 workspace.toggle_panel_focus::<TestPanel>(cx);
6951 });
6952
6953 workspace.update(cx, |workspace, cx| {
6954 assert!(workspace.right_dock().read(cx).is_open());
6955 assert!(!panel.is_zoomed(cx));
6956 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6957 });
6958
6959 // Transfer focus from panel to center
6960 workspace.update(cx, |workspace, cx| {
6961 workspace.toggle_panel_focus::<TestPanel>(cx);
6962 });
6963
6964 workspace.update(cx, |workspace, cx| {
6965 assert!(workspace.right_dock().read(cx).is_open());
6966 assert!(!panel.is_zoomed(cx));
6967 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6968 });
6969
6970 // Close the dock
6971 workspace.update(cx, |workspace, cx| {
6972 workspace.toggle_dock(DockPosition::Right, cx);
6973 });
6974
6975 workspace.update(cx, |workspace, cx| {
6976 assert!(!workspace.right_dock().read(cx).is_open());
6977 assert!(!panel.is_zoomed(cx));
6978 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6979 });
6980
6981 // Open the dock
6982 workspace.update(cx, |workspace, cx| {
6983 workspace.toggle_dock(DockPosition::Right, cx);
6984 });
6985
6986 workspace.update(cx, |workspace, cx| {
6987 assert!(workspace.right_dock().read(cx).is_open());
6988 assert!(!panel.is_zoomed(cx));
6989 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6990 });
6991
6992 // Focus and zoom panel
6993 panel.update(cx, |panel, cx| {
6994 cx.focus_self();
6995 panel.set_zoomed(true, cx)
6996 });
6997
6998 workspace.update(cx, |workspace, cx| {
6999 assert!(workspace.right_dock().read(cx).is_open());
7000 assert!(panel.is_zoomed(cx));
7001 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7002 });
7003
7004 // Transfer focus to the center closes the dock
7005 workspace.update(cx, |workspace, cx| {
7006 workspace.toggle_panel_focus::<TestPanel>(cx);
7007 });
7008
7009 workspace.update(cx, |workspace, cx| {
7010 assert!(!workspace.right_dock().read(cx).is_open());
7011 assert!(panel.is_zoomed(cx));
7012 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7013 });
7014
7015 // Transferring focus back to the panel keeps it zoomed
7016 workspace.update(cx, |workspace, cx| {
7017 workspace.toggle_panel_focus::<TestPanel>(cx);
7018 });
7019
7020 workspace.update(cx, |workspace, cx| {
7021 assert!(workspace.right_dock().read(cx).is_open());
7022 assert!(panel.is_zoomed(cx));
7023 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7024 });
7025
7026 // Close the dock while it is zoomed
7027 workspace.update(cx, |workspace, cx| {
7028 workspace.toggle_dock(DockPosition::Right, cx)
7029 });
7030
7031 workspace.update(cx, |workspace, cx| {
7032 assert!(!workspace.right_dock().read(cx).is_open());
7033 assert!(panel.is_zoomed(cx));
7034 assert!(workspace.zoomed.is_none());
7035 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
7036 });
7037
7038 // Opening the dock, when it's zoomed, retains focus
7039 workspace.update(cx, |workspace, cx| {
7040 workspace.toggle_dock(DockPosition::Right, cx)
7041 });
7042
7043 workspace.update(cx, |workspace, cx| {
7044 assert!(workspace.right_dock().read(cx).is_open());
7045 assert!(panel.is_zoomed(cx));
7046 assert!(workspace.zoomed.is_some());
7047 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7048 });
7049
7050 // Unzoom and close the panel, zoom the active pane.
7051 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
7052 workspace.update(cx, |workspace, cx| {
7053 workspace.toggle_dock(DockPosition::Right, cx)
7054 });
7055 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
7056
7057 // Opening a dock unzooms the pane.
7058 workspace.update(cx, |workspace, cx| {
7059 workspace.toggle_dock(DockPosition::Right, cx)
7060 });
7061 workspace.update(cx, |workspace, cx| {
7062 let pane = pane.read(cx);
7063 assert!(!pane.is_zoomed());
7064 assert!(!pane.focus_handle(cx).is_focused(cx));
7065 assert!(workspace.right_dock().read(cx).is_open());
7066 assert!(workspace.zoomed.is_none());
7067 });
7068 }
7069
7070 #[gpui::test]
7071 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7072 init_test(cx);
7073
7074 let fs = FakeFs::new(cx.executor());
7075
7076 let project = Project::test(fs, None, cx).await;
7077 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7078
7079 // Let's arrange the panes like this:
7080 //
7081 // +-----------------------+
7082 // | top |
7083 // +------+--------+-------+
7084 // | left | center | right |
7085 // +------+--------+-------+
7086 // | bottom |
7087 // +-----------------------+
7088
7089 let top_item = cx.new_view(|cx| {
7090 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7091 });
7092 let bottom_item = cx.new_view(|cx| {
7093 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7094 });
7095 let left_item = cx.new_view(|cx| {
7096 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7097 });
7098 let right_item = cx.new_view(|cx| {
7099 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7100 });
7101 let center_item = cx.new_view(|cx| {
7102 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7103 });
7104
7105 let top_pane_id = workspace.update(cx, |workspace, cx| {
7106 let top_pane_id = workspace.active_pane().entity_id();
7107 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7108 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7109 top_pane_id
7110 });
7111 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7112 let bottom_pane_id = workspace.active_pane().entity_id();
7113 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7114 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7115 bottom_pane_id
7116 });
7117 let left_pane_id = workspace.update(cx, |workspace, cx| {
7118 let left_pane_id = workspace.active_pane().entity_id();
7119 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7120 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7121 left_pane_id
7122 });
7123 let right_pane_id = workspace.update(cx, |workspace, cx| {
7124 let right_pane_id = workspace.active_pane().entity_id();
7125 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7126 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7127 right_pane_id
7128 });
7129 let center_pane_id = workspace.update(cx, |workspace, cx| {
7130 let center_pane_id = workspace.active_pane().entity_id();
7131 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7132 center_pane_id
7133 });
7134 cx.executor().run_until_parked();
7135
7136 workspace.update(cx, |workspace, cx| {
7137 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7138
7139 // Join into next from center pane into right
7140 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7141 });
7142
7143 workspace.update(cx, |workspace, cx| {
7144 let active_pane = workspace.active_pane();
7145 assert_eq!(right_pane_id, active_pane.entity_id());
7146 assert_eq!(2, active_pane.read(cx).items_len());
7147 let item_ids_in_pane =
7148 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7149 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7150 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7151
7152 // Join into next from right pane into bottom
7153 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7154 });
7155
7156 workspace.update(cx, |workspace, cx| {
7157 let active_pane = workspace.active_pane();
7158 assert_eq!(bottom_pane_id, active_pane.entity_id());
7159 assert_eq!(3, active_pane.read(cx).items_len());
7160 let item_ids_in_pane =
7161 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7162 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7163 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7164 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7165
7166 // Join into next from bottom pane into left
7167 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7168 });
7169
7170 workspace.update(cx, |workspace, cx| {
7171 let active_pane = workspace.active_pane();
7172 assert_eq!(left_pane_id, active_pane.entity_id());
7173 assert_eq!(4, active_pane.read(cx).items_len());
7174 let item_ids_in_pane =
7175 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7176 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7177 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7178 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7179 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7180
7181 // Join into next from left pane into top
7182 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7183 });
7184
7185 workspace.update(cx, |workspace, cx| {
7186 let active_pane = workspace.active_pane();
7187 assert_eq!(top_pane_id, active_pane.entity_id());
7188 assert_eq!(5, active_pane.read(cx).items_len());
7189 let item_ids_in_pane =
7190 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7191 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7192 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7193 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7194 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7195 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7196
7197 // Single pane left: no-op
7198 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7199 });
7200
7201 workspace.update(cx, |workspace, _cx| {
7202 let active_pane = workspace.active_pane();
7203 assert_eq!(top_pane_id, active_pane.entity_id());
7204 });
7205 }
7206
7207 fn add_an_item_to_active_pane(
7208 cx: &mut VisualTestContext,
7209 workspace: &View<Workspace>,
7210 item_id: u64,
7211 ) -> View<TestItem> {
7212 let item = cx.new_view(|cx| {
7213 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7214 item_id,
7215 "item{item_id}.txt",
7216 cx,
7217 )])
7218 });
7219 workspace.update(cx, |workspace, cx| {
7220 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7221 });
7222 return item;
7223 }
7224
7225 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7226 return workspace.update(cx, |workspace, cx| {
7227 let new_pane =
7228 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7229 new_pane
7230 });
7231 }
7232
7233 #[gpui::test]
7234 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7235 init_test(cx);
7236 let fs = FakeFs::new(cx.executor());
7237 let project = Project::test(fs, None, cx).await;
7238 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7239
7240 add_an_item_to_active_pane(cx, &workspace, 1);
7241 split_pane(cx, &workspace);
7242 add_an_item_to_active_pane(cx, &workspace, 2);
7243 split_pane(cx, &workspace); // empty pane
7244 split_pane(cx, &workspace);
7245 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7246
7247 cx.executor().run_until_parked();
7248
7249 workspace.update(cx, |workspace, cx| {
7250 let num_panes = workspace.panes().len();
7251 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7252 let active_item = workspace
7253 .active_pane()
7254 .read(cx)
7255 .active_item()
7256 .expect("item is in focus");
7257
7258 assert_eq!(num_panes, 4);
7259 assert_eq!(num_items_in_current_pane, 1);
7260 assert_eq!(active_item.item_id(), last_item.item_id());
7261 });
7262
7263 workspace.update(cx, |workspace, cx| {
7264 workspace.join_all_panes(cx);
7265 });
7266
7267 workspace.update(cx, |workspace, cx| {
7268 let num_panes = workspace.panes().len();
7269 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7270 let active_item = workspace
7271 .active_pane()
7272 .read(cx)
7273 .active_item()
7274 .expect("item is in focus");
7275
7276 assert_eq!(num_panes, 1);
7277 assert_eq!(num_items_in_current_pane, 3);
7278 assert_eq!(active_item.item_id(), last_item.item_id());
7279 });
7280 }
7281 struct TestModal(FocusHandle);
7282
7283 impl TestModal {
7284 fn new(cx: &mut ViewContext<Self>) -> Self {
7285 Self(cx.focus_handle())
7286 }
7287 }
7288
7289 impl EventEmitter<DismissEvent> for TestModal {}
7290
7291 impl FocusableView for TestModal {
7292 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7293 self.0.clone()
7294 }
7295 }
7296
7297 impl ModalView for TestModal {}
7298
7299 impl Render for TestModal {
7300 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7301 div().track_focus(&self.0)
7302 }
7303 }
7304
7305 #[gpui::test]
7306 async fn test_panels(cx: &mut gpui::TestAppContext) {
7307 init_test(cx);
7308 let fs = FakeFs::new(cx.executor());
7309
7310 let project = Project::test(fs, [], cx).await;
7311 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7312
7313 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7314 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7315 workspace.add_panel(panel_1.clone(), cx);
7316 workspace.toggle_dock(DockPosition::Left, cx);
7317 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7318 workspace.add_panel(panel_2.clone(), cx);
7319 workspace.toggle_dock(DockPosition::Right, cx);
7320
7321 let left_dock = workspace.left_dock();
7322 assert_eq!(
7323 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7324 panel_1.panel_id()
7325 );
7326 assert_eq!(
7327 left_dock.read(cx).active_panel_size(cx).unwrap(),
7328 panel_1.size(cx)
7329 );
7330
7331 left_dock.update(cx, |left_dock, cx| {
7332 left_dock.resize_active_panel(Some(px(1337.)), cx)
7333 });
7334 assert_eq!(
7335 workspace
7336 .right_dock()
7337 .read(cx)
7338 .visible_panel()
7339 .unwrap()
7340 .panel_id(),
7341 panel_2.panel_id(),
7342 );
7343
7344 (panel_1, panel_2)
7345 });
7346
7347 // Move panel_1 to the right
7348 panel_1.update(cx, |panel_1, cx| {
7349 panel_1.set_position(DockPosition::Right, cx)
7350 });
7351
7352 workspace.update(cx, |workspace, cx| {
7353 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7354 // Since it was the only panel on the left, the left dock should now be closed.
7355 assert!(!workspace.left_dock().read(cx).is_open());
7356 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7357 let right_dock = workspace.right_dock();
7358 assert_eq!(
7359 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7360 panel_1.panel_id()
7361 );
7362 assert_eq!(
7363 right_dock.read(cx).active_panel_size(cx).unwrap(),
7364 px(1337.)
7365 );
7366
7367 // Now we move panel_2 to the left
7368 panel_2.set_position(DockPosition::Left, cx);
7369 });
7370
7371 workspace.update(cx, |workspace, cx| {
7372 // Since panel_2 was not visible on the right, we don't open the left dock.
7373 assert!(!workspace.left_dock().read(cx).is_open());
7374 // And the right dock is unaffected in its displaying of panel_1
7375 assert!(workspace.right_dock().read(cx).is_open());
7376 assert_eq!(
7377 workspace
7378 .right_dock()
7379 .read(cx)
7380 .visible_panel()
7381 .unwrap()
7382 .panel_id(),
7383 panel_1.panel_id(),
7384 );
7385 });
7386
7387 // Move panel_1 back to the left
7388 panel_1.update(cx, |panel_1, cx| {
7389 panel_1.set_position(DockPosition::Left, cx)
7390 });
7391
7392 workspace.update(cx, |workspace, cx| {
7393 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7394 let left_dock = workspace.left_dock();
7395 assert!(left_dock.read(cx).is_open());
7396 assert_eq!(
7397 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7398 panel_1.panel_id()
7399 );
7400 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7401 // And the right dock should be closed as it no longer has any panels.
7402 assert!(!workspace.right_dock().read(cx).is_open());
7403
7404 // Now we move panel_1 to the bottom
7405 panel_1.set_position(DockPosition::Bottom, cx);
7406 });
7407
7408 workspace.update(cx, |workspace, cx| {
7409 // Since panel_1 was visible on the left, we close the left dock.
7410 assert!(!workspace.left_dock().read(cx).is_open());
7411 // The bottom dock is sized based on the panel's default size,
7412 // since the panel orientation changed from vertical to horizontal.
7413 let bottom_dock = workspace.bottom_dock();
7414 assert_eq!(
7415 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7416 panel_1.size(cx),
7417 );
7418 // Close bottom dock and move panel_1 back to the left.
7419 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7420 panel_1.set_position(DockPosition::Left, cx);
7421 });
7422
7423 // Emit activated event on panel 1
7424 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7425
7426 // Now the left dock is open and panel_1 is active and focused.
7427 workspace.update(cx, |workspace, cx| {
7428 let left_dock = workspace.left_dock();
7429 assert!(left_dock.read(cx).is_open());
7430 assert_eq!(
7431 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7432 panel_1.panel_id(),
7433 );
7434 assert!(panel_1.focus_handle(cx).is_focused(cx));
7435 });
7436
7437 // Emit closed event on panel 2, which is not active
7438 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7439
7440 // Wo don't close the left dock, because panel_2 wasn't the active panel
7441 workspace.update(cx, |workspace, cx| {
7442 let left_dock = workspace.left_dock();
7443 assert!(left_dock.read(cx).is_open());
7444 assert_eq!(
7445 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7446 panel_1.panel_id(),
7447 );
7448 });
7449
7450 // Emitting a ZoomIn event shows the panel as zoomed.
7451 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7452 workspace.update(cx, |workspace, _| {
7453 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7454 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7455 });
7456
7457 // Move panel to another dock while it is zoomed
7458 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7459 workspace.update(cx, |workspace, _| {
7460 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7461
7462 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7463 });
7464
7465 // This is a helper for getting a:
7466 // - valid focus on an element,
7467 // - that isn't a part of the panes and panels system of the Workspace,
7468 // - and doesn't trigger the 'on_focus_lost' API.
7469 let focus_other_view = {
7470 let workspace = workspace.clone();
7471 move |cx: &mut VisualTestContext| {
7472 workspace.update(cx, |workspace, cx| {
7473 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7474 workspace.toggle_modal(cx, TestModal::new);
7475 workspace.toggle_modal(cx, TestModal::new);
7476 } else {
7477 workspace.toggle_modal(cx, TestModal::new);
7478 }
7479 })
7480 }
7481 };
7482
7483 // If focus is transferred to another view that's not a panel or another pane, we still show
7484 // the panel as zoomed.
7485 focus_other_view(cx);
7486 workspace.update(cx, |workspace, _| {
7487 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7488 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7489 });
7490
7491 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7492 workspace.update(cx, |_, cx| cx.focus_self());
7493 workspace.update(cx, |workspace, _| {
7494 assert_eq!(workspace.zoomed, None);
7495 assert_eq!(workspace.zoomed_position, None);
7496 });
7497
7498 // If focus is transferred again to another view that's not a panel or a pane, we won't
7499 // show the panel as zoomed because it wasn't zoomed before.
7500 focus_other_view(cx);
7501 workspace.update(cx, |workspace, _| {
7502 assert_eq!(workspace.zoomed, None);
7503 assert_eq!(workspace.zoomed_position, None);
7504 });
7505
7506 // When the panel is activated, it is zoomed again.
7507 cx.dispatch_action(ToggleRightDock);
7508 workspace.update(cx, |workspace, _| {
7509 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7510 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7511 });
7512
7513 // Emitting a ZoomOut event unzooms the panel.
7514 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7515 workspace.update(cx, |workspace, _| {
7516 assert_eq!(workspace.zoomed, None);
7517 assert_eq!(workspace.zoomed_position, None);
7518 });
7519
7520 // Emit closed event on panel 1, which is active
7521 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7522
7523 // Now the left dock is closed, because panel_1 was the active panel
7524 workspace.update(cx, |workspace, cx| {
7525 let right_dock = workspace.right_dock();
7526 assert!(!right_dock.read(cx).is_open());
7527 });
7528 }
7529
7530 #[gpui::test]
7531 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7532 init_test(cx);
7533
7534 let fs = FakeFs::new(cx.background_executor.clone());
7535 let project = Project::test(fs, [], cx).await;
7536 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7537 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7538
7539 let dirty_regular_buffer = cx.new_view(|cx| {
7540 TestItem::new(cx)
7541 .with_dirty(true)
7542 .with_label("1.txt")
7543 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7544 });
7545 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7546 TestItem::new(cx)
7547 .with_dirty(true)
7548 .with_label("2.txt")
7549 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7550 });
7551 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7552 TestItem::new(cx)
7553 .with_dirty(true)
7554 .with_singleton(false)
7555 .with_label("Fake Project Search")
7556 .with_project_items(&[
7557 dirty_regular_buffer.read(cx).project_items[0].clone(),
7558 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7559 ])
7560 });
7561 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7562 workspace.update(cx, |workspace, cx| {
7563 workspace.add_item(
7564 pane.clone(),
7565 Box::new(dirty_regular_buffer.clone()),
7566 None,
7567 false,
7568 false,
7569 cx,
7570 );
7571 workspace.add_item(
7572 pane.clone(),
7573 Box::new(dirty_regular_buffer_2.clone()),
7574 None,
7575 false,
7576 false,
7577 cx,
7578 );
7579 workspace.add_item(
7580 pane.clone(),
7581 Box::new(dirty_multi_buffer_with_both.clone()),
7582 None,
7583 false,
7584 false,
7585 cx,
7586 );
7587 });
7588
7589 pane.update(cx, |pane, cx| {
7590 pane.activate_item(2, true, true, cx);
7591 assert_eq!(
7592 pane.active_item().unwrap().item_id(),
7593 multi_buffer_with_both_files_id,
7594 "Should select the multi buffer in the pane"
7595 );
7596 });
7597 let close_all_but_multi_buffer_task = pane
7598 .update(cx, |pane, cx| {
7599 pane.close_inactive_items(
7600 &CloseInactiveItems {
7601 save_intent: Some(SaveIntent::Save),
7602 close_pinned: true,
7603 },
7604 cx,
7605 )
7606 })
7607 .expect("should have inactive files to close");
7608 cx.background_executor.run_until_parked();
7609 assert!(
7610 !cx.has_pending_prompt(),
7611 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7612 );
7613 close_all_but_multi_buffer_task
7614 .await
7615 .expect("Closing all buffers but the multi buffer failed");
7616 pane.update(cx, |pane, cx| {
7617 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7618 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7619 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7620 assert_eq!(pane.items_len(), 1);
7621 assert_eq!(
7622 pane.active_item().unwrap().item_id(),
7623 multi_buffer_with_both_files_id,
7624 "Should have only the multi buffer left in the pane"
7625 );
7626 assert!(
7627 dirty_multi_buffer_with_both.read(cx).is_dirty,
7628 "The multi buffer containing the unsaved buffer should still be dirty"
7629 );
7630 });
7631
7632 let close_multi_buffer_task = pane
7633 .update(cx, |pane, cx| {
7634 pane.close_active_item(
7635 &CloseActiveItem {
7636 save_intent: Some(SaveIntent::Close),
7637 },
7638 cx,
7639 )
7640 })
7641 .expect("should have the multi buffer to close");
7642 cx.background_executor.run_until_parked();
7643 assert!(
7644 cx.has_pending_prompt(),
7645 "Dirty multi buffer should prompt a save dialog"
7646 );
7647 cx.simulate_prompt_answer(0);
7648 cx.background_executor.run_until_parked();
7649 close_multi_buffer_task
7650 .await
7651 .expect("Closing the multi buffer failed");
7652 pane.update(cx, |pane, cx| {
7653 assert_eq!(
7654 dirty_multi_buffer_with_both.read(cx).save_count,
7655 1,
7656 "Multi buffer item should get be saved"
7657 );
7658 // Test impl does not save inner items, so we do not assert them
7659 assert_eq!(
7660 pane.items_len(),
7661 0,
7662 "No more items should be left in the pane"
7663 );
7664 assert!(pane.active_item().is_none());
7665 });
7666 }
7667
7668 #[gpui::test]
7669 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7670 cx: &mut TestAppContext,
7671 ) {
7672 init_test(cx);
7673
7674 let fs = FakeFs::new(cx.background_executor.clone());
7675 let project = Project::test(fs, [], cx).await;
7676 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7677 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7678
7679 let dirty_regular_buffer = cx.new_view(|cx| {
7680 TestItem::new(cx)
7681 .with_dirty(true)
7682 .with_label("1.txt")
7683 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7684 });
7685 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7686 TestItem::new(cx)
7687 .with_dirty(true)
7688 .with_label("2.txt")
7689 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7690 });
7691 let clear_regular_buffer = cx.new_view(|cx| {
7692 TestItem::new(cx)
7693 .with_label("3.txt")
7694 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7695 });
7696
7697 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7698 TestItem::new(cx)
7699 .with_dirty(true)
7700 .with_singleton(false)
7701 .with_label("Fake Project Search")
7702 .with_project_items(&[
7703 dirty_regular_buffer.read(cx).project_items[0].clone(),
7704 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7705 clear_regular_buffer.read(cx).project_items[0].clone(),
7706 ])
7707 });
7708 workspace.update(cx, |workspace, cx| {
7709 workspace.add_item(
7710 pane.clone(),
7711 Box::new(dirty_regular_buffer.clone()),
7712 None,
7713 false,
7714 false,
7715 cx,
7716 );
7717 workspace.add_item(
7718 pane.clone(),
7719 Box::new(dirty_multi_buffer_with_both.clone()),
7720 None,
7721 false,
7722 false,
7723 cx,
7724 );
7725 });
7726
7727 pane.update(cx, |pane, cx| {
7728 pane.activate_item(0, true, true, cx);
7729 assert_eq!(
7730 pane.active_item().unwrap().item_id(),
7731 dirty_regular_buffer.item_id(),
7732 "Should select the dirty singleton buffer in the pane"
7733 );
7734 });
7735 let close_singleton_buffer_task = pane
7736 .update(cx, |pane, cx| {
7737 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7738 })
7739 .expect("should have active singleton buffer to close");
7740 cx.background_executor.run_until_parked();
7741 assert!(
7742 !cx.has_pending_prompt(),
7743 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7744 );
7745
7746 close_singleton_buffer_task
7747 .await
7748 .expect("Should not fail closing the singleton buffer");
7749 pane.update(cx, |pane, cx| {
7750 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7751 assert_eq!(
7752 dirty_multi_buffer_with_both.read(cx).save_count,
7753 0,
7754 "Multi buffer itself should not be saved"
7755 );
7756 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7757 assert_eq!(
7758 pane.items_len(),
7759 1,
7760 "A dirty multi buffer should be present in the pane"
7761 );
7762 assert_eq!(
7763 pane.active_item().unwrap().item_id(),
7764 dirty_multi_buffer_with_both.item_id(),
7765 "Should activate the only remaining item in the pane"
7766 );
7767 });
7768 }
7769
7770 #[gpui::test]
7771 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7772 cx: &mut TestAppContext,
7773 ) {
7774 init_test(cx);
7775
7776 let fs = FakeFs::new(cx.background_executor.clone());
7777 let project = Project::test(fs, [], cx).await;
7778 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7779 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7780
7781 let dirty_regular_buffer = cx.new_view(|cx| {
7782 TestItem::new(cx)
7783 .with_dirty(true)
7784 .with_label("1.txt")
7785 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7786 });
7787 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7788 TestItem::new(cx)
7789 .with_dirty(true)
7790 .with_label("2.txt")
7791 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7792 });
7793 let clear_regular_buffer = cx.new_view(|cx| {
7794 TestItem::new(cx)
7795 .with_label("3.txt")
7796 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7797 });
7798
7799 let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7800 TestItem::new(cx)
7801 .with_dirty(true)
7802 .with_singleton(false)
7803 .with_label("Fake Project Search")
7804 .with_project_items(&[
7805 dirty_regular_buffer.read(cx).project_items[0].clone(),
7806 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7807 clear_regular_buffer.read(cx).project_items[0].clone(),
7808 ])
7809 });
7810 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7811 workspace.update(cx, |workspace, cx| {
7812 workspace.add_item(
7813 pane.clone(),
7814 Box::new(dirty_regular_buffer.clone()),
7815 None,
7816 false,
7817 false,
7818 cx,
7819 );
7820 workspace.add_item(
7821 pane.clone(),
7822 Box::new(dirty_multi_buffer_with_both.clone()),
7823 None,
7824 false,
7825 false,
7826 cx,
7827 );
7828 });
7829
7830 pane.update(cx, |pane, cx| {
7831 pane.activate_item(1, true, true, cx);
7832 assert_eq!(
7833 pane.active_item().unwrap().item_id(),
7834 multi_buffer_with_both_files_id,
7835 "Should select the multi buffer in the pane"
7836 );
7837 });
7838 let _close_multi_buffer_task = pane
7839 .update(cx, |pane, cx| {
7840 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7841 })
7842 .expect("should have active multi buffer to close");
7843 cx.background_executor.run_until_parked();
7844 assert!(
7845 cx.has_pending_prompt(),
7846 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7847 );
7848 }
7849
7850 #[gpui::test]
7851 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7852 cx: &mut TestAppContext,
7853 ) {
7854 init_test(cx);
7855
7856 let fs = FakeFs::new(cx.background_executor.clone());
7857 let project = Project::test(fs, [], cx).await;
7858 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7859 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7860
7861 let dirty_regular_buffer = cx.new_view(|cx| {
7862 TestItem::new(cx)
7863 .with_dirty(true)
7864 .with_label("1.txt")
7865 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7866 });
7867 let dirty_regular_buffer_2 = cx.new_view(|cx| {
7868 TestItem::new(cx)
7869 .with_dirty(true)
7870 .with_label("2.txt")
7871 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7872 });
7873 let clear_regular_buffer = cx.new_view(|cx| {
7874 TestItem::new(cx)
7875 .with_label("3.txt")
7876 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7877 });
7878
7879 let dirty_multi_buffer = cx.new_view(|cx| {
7880 TestItem::new(cx)
7881 .with_dirty(true)
7882 .with_singleton(false)
7883 .with_label("Fake Project Search")
7884 .with_project_items(&[
7885 dirty_regular_buffer.read(cx).project_items[0].clone(),
7886 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7887 clear_regular_buffer.read(cx).project_items[0].clone(),
7888 ])
7889 });
7890 workspace.update(cx, |workspace, cx| {
7891 workspace.add_item(
7892 pane.clone(),
7893 Box::new(dirty_regular_buffer.clone()),
7894 None,
7895 false,
7896 false,
7897 cx,
7898 );
7899 workspace.add_item(
7900 pane.clone(),
7901 Box::new(dirty_regular_buffer_2.clone()),
7902 None,
7903 false,
7904 false,
7905 cx,
7906 );
7907 workspace.add_item(
7908 pane.clone(),
7909 Box::new(dirty_multi_buffer.clone()),
7910 None,
7911 false,
7912 false,
7913 cx,
7914 );
7915 });
7916
7917 pane.update(cx, |pane, cx| {
7918 pane.activate_item(2, true, true, cx);
7919 assert_eq!(
7920 pane.active_item().unwrap().item_id(),
7921 dirty_multi_buffer.item_id(),
7922 "Should select the multi buffer in the pane"
7923 );
7924 });
7925 let close_multi_buffer_task = pane
7926 .update(cx, |pane, cx| {
7927 pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7928 })
7929 .expect("should have active multi buffer to close");
7930 cx.background_executor.run_until_parked();
7931 assert!(
7932 !cx.has_pending_prompt(),
7933 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
7934 );
7935 close_multi_buffer_task
7936 .await
7937 .expect("Closing multi buffer failed");
7938 pane.update(cx, |pane, cx| {
7939 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7940 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
7941 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7942 assert_eq!(
7943 pane.items()
7944 .map(|item| item.item_id())
7945 .sorted()
7946 .collect::<Vec<_>>(),
7947 vec![
7948 dirty_regular_buffer.item_id(),
7949 dirty_regular_buffer_2.item_id(),
7950 ],
7951 "Should have no multi buffer left in the pane"
7952 );
7953 assert!(dirty_regular_buffer.read(cx).is_dirty);
7954 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
7955 });
7956 }
7957
7958 mod register_project_item_tests {
7959 use gpui::Context as _;
7960
7961 use super::*;
7962
7963 // View
7964 struct TestPngItemView {
7965 focus_handle: FocusHandle,
7966 }
7967 // Model
7968 struct TestPngItem {}
7969
7970 impl project::ProjectItem for TestPngItem {
7971 fn try_open(
7972 _project: &Model<Project>,
7973 path: &ProjectPath,
7974 cx: &mut AppContext,
7975 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7976 if path.path.extension().unwrap() == "png" {
7977 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7978 } else {
7979 None
7980 }
7981 }
7982
7983 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7984 None
7985 }
7986
7987 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7988 None
7989 }
7990
7991 fn is_dirty(&self) -> bool {
7992 false
7993 }
7994 }
7995
7996 impl Item for TestPngItemView {
7997 type Event = ();
7998 }
7999 impl EventEmitter<()> for TestPngItemView {}
8000 impl FocusableView for TestPngItemView {
8001 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8002 self.focus_handle.clone()
8003 }
8004 }
8005
8006 impl Render for TestPngItemView {
8007 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8008 Empty
8009 }
8010 }
8011
8012 impl ProjectItem for TestPngItemView {
8013 type Item = TestPngItem;
8014
8015 fn for_project_item(
8016 _project: Model<Project>,
8017 _item: Model<Self::Item>,
8018 cx: &mut ViewContext<Self>,
8019 ) -> Self
8020 where
8021 Self: Sized,
8022 {
8023 Self {
8024 focus_handle: cx.focus_handle(),
8025 }
8026 }
8027 }
8028
8029 // View
8030 struct TestIpynbItemView {
8031 focus_handle: FocusHandle,
8032 }
8033 // Model
8034 struct TestIpynbItem {}
8035
8036 impl project::ProjectItem for TestIpynbItem {
8037 fn try_open(
8038 _project: &Model<Project>,
8039 path: &ProjectPath,
8040 cx: &mut AppContext,
8041 ) -> Option<Task<gpui::Result<Model<Self>>>> {
8042 if path.path.extension().unwrap() == "ipynb" {
8043 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
8044 } else {
8045 None
8046 }
8047 }
8048
8049 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
8050 None
8051 }
8052
8053 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
8054 None
8055 }
8056
8057 fn is_dirty(&self) -> bool {
8058 false
8059 }
8060 }
8061
8062 impl Item for TestIpynbItemView {
8063 type Event = ();
8064 }
8065 impl EventEmitter<()> for TestIpynbItemView {}
8066 impl FocusableView for TestIpynbItemView {
8067 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8068 self.focus_handle.clone()
8069 }
8070 }
8071
8072 impl Render for TestIpynbItemView {
8073 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8074 Empty
8075 }
8076 }
8077
8078 impl ProjectItem for TestIpynbItemView {
8079 type Item = TestIpynbItem;
8080
8081 fn for_project_item(
8082 _project: Model<Project>,
8083 _item: Model<Self::Item>,
8084 cx: &mut ViewContext<Self>,
8085 ) -> Self
8086 where
8087 Self: Sized,
8088 {
8089 Self {
8090 focus_handle: cx.focus_handle(),
8091 }
8092 }
8093 }
8094
8095 struct TestAlternatePngItemView {
8096 focus_handle: FocusHandle,
8097 }
8098
8099 impl Item for TestAlternatePngItemView {
8100 type Event = ();
8101 }
8102
8103 impl EventEmitter<()> for TestAlternatePngItemView {}
8104 impl FocusableView for TestAlternatePngItemView {
8105 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8106 self.focus_handle.clone()
8107 }
8108 }
8109
8110 impl Render for TestAlternatePngItemView {
8111 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8112 Empty
8113 }
8114 }
8115
8116 impl ProjectItem for TestAlternatePngItemView {
8117 type Item = TestPngItem;
8118
8119 fn for_project_item(
8120 _project: Model<Project>,
8121 _item: Model<Self::Item>,
8122 cx: &mut ViewContext<Self>,
8123 ) -> Self
8124 where
8125 Self: Sized,
8126 {
8127 Self {
8128 focus_handle: cx.focus_handle(),
8129 }
8130 }
8131 }
8132
8133 #[gpui::test]
8134 async fn test_register_project_item(cx: &mut TestAppContext) {
8135 init_test(cx);
8136
8137 cx.update(|cx| {
8138 register_project_item::<TestPngItemView>(cx);
8139 register_project_item::<TestIpynbItemView>(cx);
8140 });
8141
8142 let fs = FakeFs::new(cx.executor());
8143 fs.insert_tree(
8144 "/root1",
8145 json!({
8146 "one.png": "BINARYDATAHERE",
8147 "two.ipynb": "{ totally a notebook }",
8148 "three.txt": "editing text, sure why not?"
8149 }),
8150 )
8151 .await;
8152
8153 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8154 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8155
8156 let worktree_id = project.update(cx, |project, cx| {
8157 project.worktrees(cx).next().unwrap().read(cx).id()
8158 });
8159
8160 let handle = workspace
8161 .update(cx, |workspace, cx| {
8162 let project_path = (worktree_id, "one.png");
8163 workspace.open_path(project_path, None, true, cx)
8164 })
8165 .await
8166 .unwrap();
8167
8168 // Now we can check if the handle we got back errored or not
8169 assert_eq!(
8170 handle.to_any().entity_type(),
8171 TypeId::of::<TestPngItemView>()
8172 );
8173
8174 let handle = workspace
8175 .update(cx, |workspace, cx| {
8176 let project_path = (worktree_id, "two.ipynb");
8177 workspace.open_path(project_path, None, true, cx)
8178 })
8179 .await
8180 .unwrap();
8181
8182 assert_eq!(
8183 handle.to_any().entity_type(),
8184 TypeId::of::<TestIpynbItemView>()
8185 );
8186
8187 let handle = workspace
8188 .update(cx, |workspace, cx| {
8189 let project_path = (worktree_id, "three.txt");
8190 workspace.open_path(project_path, None, true, cx)
8191 })
8192 .await;
8193 assert!(handle.is_err());
8194 }
8195
8196 #[gpui::test]
8197 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8198 init_test(cx);
8199
8200 cx.update(|cx| {
8201 register_project_item::<TestPngItemView>(cx);
8202 register_project_item::<TestAlternatePngItemView>(cx);
8203 });
8204
8205 let fs = FakeFs::new(cx.executor());
8206 fs.insert_tree(
8207 "/root1",
8208 json!({
8209 "one.png": "BINARYDATAHERE",
8210 "two.ipynb": "{ totally a notebook }",
8211 "three.txt": "editing text, sure why not?"
8212 }),
8213 )
8214 .await;
8215
8216 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8217 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8218
8219 let worktree_id = project.update(cx, |project, cx| {
8220 project.worktrees(cx).next().unwrap().read(cx).id()
8221 });
8222
8223 let handle = workspace
8224 .update(cx, |workspace, cx| {
8225 let project_path = (worktree_id, "one.png");
8226 workspace.open_path(project_path, None, true, cx)
8227 })
8228 .await
8229 .unwrap();
8230
8231 // This _must_ be the second item registered
8232 assert_eq!(
8233 handle.to_any().entity_type(),
8234 TypeId::of::<TestAlternatePngItemView>()
8235 );
8236
8237 let handle = workspace
8238 .update(cx, |workspace, cx| {
8239 let project_path = (worktree_id, "three.txt");
8240 workspace.open_path(project_path, None, true, cx)
8241 })
8242 .await;
8243 assert!(handle.is_err());
8244 }
8245 }
8246
8247 pub fn init_test(cx: &mut TestAppContext) {
8248 cx.update(|cx| {
8249 let settings_store = SettingsStore::test(cx);
8250 cx.set_global(settings_store);
8251 theme::init(theme::LoadThemes::JustBase, cx);
8252 language::init(cx);
8253 crate::init_settings(cx);
8254 Project::init_settings(cx);
8255 });
8256 }
8257
8258 fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8259 let item = TestProjectItem::new(id, path, cx);
8260 item.update(cx, |item, _| {
8261 item.is_dirty = true;
8262 });
8263 item
8264 }
8265}