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