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