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