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