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