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