zed.rs

   1mod app_menus;
   2pub mod component_preview;
   3pub mod inline_completion_registry;
   4#[cfg(target_os = "macos")]
   5pub(crate) mod mac_only_instance;
   6mod migrate;
   7mod open_listener;
   8mod quick_action_bar;
   9#[cfg(target_os = "windows")]
  10pub(crate) mod windows_only_instance;
  11
  12use agent::AgentDiffToolbar;
  13use anyhow::Context as _;
  14pub use app_menus::*;
  15use assets::Assets;
  16use assistant_context_editor::AgentPanelDelegate;
  17use breadcrumbs::Breadcrumbs;
  18use client::zed_urls;
  19use collections::VecDeque;
  20use debugger_ui::debugger_panel::DebugPanel;
  21use editor::ProposedChangesEditorToolbar;
  22use editor::{Editor, MultiBuffer, scroll::Autoscroll};
  23use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
  24use futures::future::Either;
  25use futures::{StreamExt, channel::mpsc, select_biased};
  26use git_ui::git_panel::GitPanel;
  27use git_ui::project_diff::ProjectDiffToolbar;
  28use gpui::{
  29    Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
  30    Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
  31    Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions,
  32    image_cache, point, px, retain_all,
  33};
  34use image_viewer::ImageInfo;
  35use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
  36use migrator::{migrate_keymap, migrate_settings};
  37pub use open_listener::*;
  38use outline_panel::OutlinePanel;
  39use paths::{
  40    local_debug_file_relative_path, local_settings_file_relative_path,
  41    local_tasks_file_relative_path,
  42};
  43use project::{DirectoryLister, ProjectItem};
  44use project_panel::ProjectPanel;
  45use prompt_store::PromptBuilder;
  46use quick_action_bar::QuickActionBar;
  47use recent_projects::open_ssh_project;
  48use release_channel::{AppCommitSha, ReleaseChannel};
  49use rope::Rope;
  50use search::project_search::ProjectSearchBar;
  51use settings::{
  52    DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings,
  53    SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
  54    initial_project_settings_content, initial_tasks_content, update_settings_file,
  55};
  56use std::path::PathBuf;
  57use std::sync::atomic::{self, AtomicBool};
  58use std::{borrow::Cow, path::Path, sync::Arc};
  59use terminal_view::terminal_panel::{self, TerminalPanel};
  60use theme::{ActiveTheme, ThemeSettings};
  61use ui::{PopoverMenuHandle, prelude::*};
  62use util::markdown::MarkdownString;
  63use util::{ResultExt, asset_str};
  64use uuid::Uuid;
  65use vim_mode_setting::VimModeSetting;
  66use welcome::{BaseKeymap, DOCS_URL, MultibufferHint};
  67use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};
  68use workspace::{
  69    AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
  70    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
  71    open_new,
  72};
  73use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace};
  74use workspace::{Pane, notifications::DetachAndPromptErr};
  75use zed_actions::{
  76    OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
  77};
  78
  79actions!(
  80    zed,
  81    [
  82        DebugElements,
  83        Hide,
  84        HideOthers,
  85        Minimize,
  86        OpenDefaultSettings,
  87        OpenProjectSettings,
  88        OpenProjectTasks,
  89        OpenProjectDebugTasks,
  90        OpenTasks,
  91        OpenDebugTasks,
  92        ResetDatabase,
  93        ShowAll,
  94        ToggleFullScreen,
  95        Zoom,
  96        TestPanic,
  97    ]
  98);
  99
 100pub fn init(cx: &mut App) {
 101    #[cfg(target_os = "macos")]
 102    cx.on_action(|_: &Hide, cx| cx.hide());
 103    #[cfg(target_os = "macos")]
 104    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
 105    #[cfg(target_os = "macos")]
 106    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
 107    cx.on_action(quit);
 108
 109    cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
 110
 111    if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
 112        cx.on_action(test_panic);
 113    }
 114
 115    cx.on_action(|_: &OpenLog, cx| {
 116        with_active_or_new_workspace(cx, |workspace, window, cx| {
 117            open_log_file(workspace, window, cx);
 118        });
 119    });
 120    cx.on_action(|_: &zed_actions::OpenLicenses, cx| {
 121        with_active_or_new_workspace(cx, |workspace, window, cx| {
 122            open_bundled_file(
 123                workspace,
 124                asset_str::<Assets>("licenses.md"),
 125                "Open Source License Attribution",
 126                "Markdown",
 127                window,
 128                cx,
 129            );
 130        });
 131    });
 132    cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| {
 133        with_active_or_new_workspace(cx, |workspace, window, cx| {
 134            open_telemetry_log_file(workspace, window, cx);
 135        });
 136    });
 137    cx.on_action(|&zed_actions::OpenKeymap, cx| {
 138        with_active_or_new_workspace(cx, |_, window, cx| {
 139            open_settings_file(
 140                paths::keymap_file(),
 141                || settings::initial_keymap_content().as_ref().into(),
 142                window,
 143                cx,
 144            );
 145        });
 146    });
 147    cx.on_action(|_: &OpenSettings, cx| {
 148        with_active_or_new_workspace(cx, |_, window, cx| {
 149            open_settings_file(
 150                paths::settings_file(),
 151                || settings::initial_user_settings_content().as_ref().into(),
 152                window,
 153                cx,
 154            );
 155        });
 156    });
 157    cx.on_action(|_: &OpenAccountSettings, cx| {
 158        with_active_or_new_workspace(cx, |_, _, cx| {
 159            cx.open_url(&zed_urls::account_url(cx));
 160        });
 161    });
 162    cx.on_action(|_: &OpenTasks, cx| {
 163        with_active_or_new_workspace(cx, |_, window, cx| {
 164            open_settings_file(
 165                paths::tasks_file(),
 166                || settings::initial_tasks_content().as_ref().into(),
 167                window,
 168                cx,
 169            );
 170        });
 171    });
 172    cx.on_action(|_: &OpenDebugTasks, cx| {
 173        with_active_or_new_workspace(cx, |_, window, cx| {
 174            open_settings_file(
 175                paths::debug_scenarios_file(),
 176                || settings::initial_debug_tasks_content().as_ref().into(),
 177                window,
 178                cx,
 179            );
 180        });
 181    });
 182    cx.on_action(|_: &OpenDefaultSettings, cx| {
 183        with_active_or_new_workspace(cx, |workspace, window, cx| {
 184            open_bundled_file(
 185                workspace,
 186                settings::default_settings(),
 187                "Default Settings",
 188                "JSON",
 189                window,
 190                cx,
 191            );
 192        });
 193    });
 194    cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
 195        with_active_or_new_workspace(cx, |workspace, window, cx| {
 196            open_bundled_file(
 197                workspace,
 198                settings::default_keymap(),
 199                "Default Key Bindings",
 200                "JSON",
 201                window,
 202                cx,
 203            );
 204        });
 205    });
 206}
 207
 208fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
 209    WorkspaceSettings::get_global(cx)
 210        .on_last_window_closed
 211        .is_quit_app()
 212        .then(|| {
 213            cx.on_window_closed(|cx| {
 214                if cx.windows().is_empty() {
 215                    cx.quit();
 216                }
 217            })
 218        })
 219}
 220
 221pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
 222    let display = display_uuid.and_then(|uuid| {
 223        cx.displays()
 224            .into_iter()
 225            .find(|display| display.uuid().ok() == Some(uuid))
 226    });
 227    let app_id = ReleaseChannel::global(cx).app_id();
 228    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 229        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 230        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 231        _ => gpui::WindowDecorations::Client,
 232    };
 233
 234    WindowOptions {
 235        titlebar: Some(TitlebarOptions {
 236            title: None,
 237            appears_transparent: true,
 238            traffic_light_position: Some(point(px(9.0), px(9.0))),
 239        }),
 240        window_bounds: None,
 241        focus: false,
 242        show: false,
 243        kind: WindowKind::Normal,
 244        is_movable: true,
 245        display_id: display.map(|display| display.id()),
 246        window_background: cx.theme().window_background_appearance(),
 247        app_id: Some(app_id.to_owned()),
 248        window_decorations: Some(window_decorations),
 249        window_min_size: Some(gpui::Size {
 250            width: px(360.0),
 251            height: px(240.0),
 252        }),
 253    }
 254}
 255
 256pub fn initialize_workspace(
 257    app_state: Arc<AppState>,
 258    prompt_builder: Arc<PromptBuilder>,
 259    cx: &mut App,
 260) {
 261    let mut _on_close_subscription = bind_on_window_closed(cx);
 262    cx.observe_global::<SettingsStore>(move |cx| {
 263        _on_close_subscription = bind_on_window_closed(cx);
 264    })
 265    .detach();
 266
 267    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
 268        let Some(window) = window else {
 269            return;
 270        };
 271
 272        let workspace_handle = cx.entity().clone();
 273        let center_pane = workspace.active_pane().clone();
 274        initialize_pane(workspace, &center_pane, window, cx);
 275
 276        cx.subscribe_in(&workspace_handle, window, {
 277            move |workspace, _, event, window, cx| match event {
 278                workspace::Event::PaneAdded(pane) => {
 279                    initialize_pane(workspace, &pane, window, cx);
 280                }
 281                workspace::Event::OpenBundledFile {
 282                    text,
 283                    title,
 284                    language,
 285                } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
 286                _ => {}
 287            }
 288        })
 289        .detach();
 290
 291        #[cfg(not(target_os = "macos"))]
 292        initialize_file_watcher(window, cx);
 293
 294        if let Some(specs) = window.gpu_specs() {
 295            log::info!("Using GPU: {:?}", specs);
 296            show_software_emulation_warning_if_needed(specs, window, cx);
 297        }
 298
 299        let popover_menu_handle = PopoverMenuHandle::default();
 300
 301        let inline_completion_button = cx.new(|cx| {
 302            inline_completion_button::InlineCompletionButton::new(
 303                app_state.fs.clone(),
 304                app_state.user_store.clone(),
 305                popover_menu_handle.clone(),
 306                cx,
 307            )
 308        });
 309
 310        workspace.register_action({
 311            move |_, _: &inline_completion_button::ToggleMenu, window, cx| {
 312                popover_menu_handle.toggle(window, cx);
 313            }
 314        });
 315
 316        let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
 317        let diagnostic_summary =
 318            cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 319        let activity_indicator = activity_indicator::ActivityIndicator::new(
 320            workspace,
 321            app_state.languages.clone(),
 322            window,
 323            cx,
 324        );
 325        let active_buffer_language =
 326            cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
 327        let active_toolchain_language =
 328            cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
 329        let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
 330        let image_info = cx.new(|_cx| ImageInfo::new(workspace));
 331        let cursor_position =
 332            cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
 333        workspace.status_bar().update(cx, |status_bar, cx| {
 334            status_bar.add_left_item(search_button, window, cx);
 335            status_bar.add_left_item(diagnostic_summary, window, cx);
 336            status_bar.add_left_item(activity_indicator, window, cx);
 337            status_bar.add_right_item(inline_completion_button, window, cx);
 338            status_bar.add_right_item(active_buffer_language, window, cx);
 339            status_bar.add_right_item(active_toolchain_language, window, cx);
 340            status_bar.add_right_item(vim_mode_indicator, window, cx);
 341            status_bar.add_right_item(cursor_position, window, cx);
 342            status_bar.add_right_item(image_info, window, cx);
 343        });
 344
 345        let handle = cx.entity().downgrade();
 346        window.on_window_should_close(cx, move |window, cx| {
 347            handle
 348                .update(cx, |workspace, cx| {
 349                    // We'll handle closing asynchronously
 350                    workspace.close_window(&CloseWindow, window, cx);
 351                    false
 352                })
 353                .unwrap_or(true)
 354        });
 355
 356        initialize_panels(prompt_builder.clone(), window, cx);
 357        register_actions(app_state.clone(), workspace, window, cx);
 358
 359        workspace.focus_handle(cx).focus(window);
 360    })
 361    .detach();
 362}
 363
 364#[cfg(any(target_os = "linux", target_os = "freebsd"))]
 365fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 366    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 367        let message = format!(
 368            db::indoc! {r#"
 369            inotify_init returned {}
 370
 371            This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
 372            "#},
 373            e
 374        );
 375        let prompt = window.prompt(
 376            PromptLevel::Critical,
 377            "Could not start inotify",
 378            Some(&message),
 379            &["Troubleshoot and Quit"],
 380            cx,
 381        );
 382        cx.spawn(async move |_, cx| {
 383            if prompt.await == Ok(0) {
 384                cx.update(|cx| {
 385                    cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
 386                    cx.quit();
 387                })
 388                .ok();
 389            }
 390        })
 391        .detach()
 392    }
 393}
 394
 395#[cfg(target_os = "windows")]
 396fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 397    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 398        let message = format!(
 399            db::indoc! {r#"
 400            ReadDirectoryChangesW initialization failed: {}
 401
 402            This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
 403            "#},
 404            e
 405        );
 406        let prompt = window.prompt(
 407            PromptLevel::Critical,
 408            "Could not start ReadDirectoryChangesW",
 409            Some(&message),
 410            &["Troubleshoot and Quit"],
 411            cx,
 412        );
 413        cx.spawn(async move |_, cx| {
 414            if prompt.await == Ok(0) {
 415                cx.update(|cx| {
 416                    cx.open_url("https://zed.dev/docs/windows");
 417                    cx.quit()
 418                })
 419                .ok();
 420            }
 421        })
 422        .detach()
 423    }
 424}
 425
 426fn show_software_emulation_warning_if_needed(
 427    specs: gpui::GpuSpecs,
 428    window: &mut Window,
 429    cx: &mut Context<Workspace>,
 430) {
 431    if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
 432        let message = format!(
 433            db::indoc! {r#"
 434            Zed uses Vulkan for rendering and requires a compatible GPU.
 435
 436            Currently you are using a software emulated GPU ({}) which
 437            will result in awful performance.
 438
 439            For troubleshooting see: https://zed.dev/docs/linux
 440            Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
 441            "#},
 442            specs.device_name
 443        );
 444        let prompt = window.prompt(
 445            PromptLevel::Critical,
 446            "Unsupported GPU",
 447            Some(&message),
 448            &["Skip", "Troubleshoot and Quit"],
 449            cx,
 450        );
 451        cx.spawn(async move |_, cx| {
 452            if prompt.await == Ok(1) {
 453                cx.update(|cx| {
 454                    cx.open_url("https://zed.dev/docs/linux#zed-fails-to-open-windows");
 455                    cx.quit();
 456                })
 457                .ok();
 458            }
 459        })
 460        .detach()
 461    }
 462}
 463
 464fn initialize_panels(
 465    prompt_builder: Arc<PromptBuilder>,
 466    window: &mut Window,
 467    cx: &mut Context<Workspace>,
 468) {
 469    let prompt_builder = prompt_builder.clone();
 470
 471    cx.spawn_in(window, async move |workspace_handle, cx| {
 472        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 473        let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
 474        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 475        let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
 476        let channels_panel =
 477            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 478        let chat_panel =
 479            collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
 480        let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
 481            workspace_handle.clone(),
 482            cx.clone(),
 483        );
 484
 485        let (
 486            project_panel,
 487            outline_panel,
 488            terminal_panel,
 489            git_panel,
 490            channels_panel,
 491            chat_panel,
 492            notification_panel,
 493        ) = futures::try_join!(
 494            project_panel,
 495            outline_panel,
 496            git_panel,
 497            terminal_panel,
 498            channels_panel,
 499            chat_panel,
 500            notification_panel,
 501        )?;
 502
 503        workspace_handle.update_in(cx, |workspace, window, cx| {
 504            workspace.add_panel(project_panel, window, cx);
 505            workspace.add_panel(outline_panel, window, cx);
 506            workspace.add_panel(terminal_panel, window, cx);
 507            workspace.add_panel(git_panel, window, cx);
 508            workspace.add_panel(channels_panel, window, cx);
 509            workspace.add_panel(chat_panel, window, cx);
 510            workspace.add_panel(notification_panel, window, cx);
 511            cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |_, window, cx| {
 512                cx.spawn_in(
 513                    window,
 514                    async move |workspace: gpui::WeakEntity<Workspace>,
 515                                cx: &mut AsyncWindowContext| {
 516                        let debug_panel = DebugPanel::load(workspace.clone(), cx).await?;
 517                        workspace.update_in(cx, |workspace, window, cx| {
 518                            workspace.add_panel(debug_panel, window, cx);
 519                        })?;
 520                        anyhow::Ok(())
 521                    },
 522                )
 523                .detach()
 524            });
 525        })?;
 526
 527        let is_assistant2_enabled = !cfg!(test);
 528        let agent_panel = if is_assistant2_enabled {
 529            let agent_panel =
 530                agent::AgentPanel::load(workspace_handle.clone(), prompt_builder, cx.clone())
 531                    .await?;
 532
 533            Some(agent_panel)
 534        } else {
 535            None
 536        };
 537
 538        workspace_handle.update_in(cx, |workspace, window, cx| {
 539            if let Some(agent_panel) = agent_panel {
 540                workspace.add_panel(agent_panel, window, cx);
 541            }
 542
 543            // Register the actions that are shared between `assistant` and `assistant2`.
 544            //
 545            // We need to do this here instead of within the individual `init`
 546            // functions so that we only register the actions once.
 547            //
 548            // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
 549            if is_assistant2_enabled {
 550                <dyn AgentPanelDelegate>::set_global(
 551                    Arc::new(agent::ConcreteAssistantPanelDelegate),
 552                    cx,
 553                );
 554
 555                workspace
 556                    .register_action(agent::AgentPanel::toggle_focus)
 557                    .register_action(agent::InlineAssistant::inline_assist);
 558            }
 559        })?;
 560
 561        anyhow::Ok(())
 562    })
 563    .detach();
 564}
 565
 566fn register_actions(
 567    app_state: Arc<AppState>,
 568    workspace: &mut Workspace,
 569    _: &mut Window,
 570    cx: &mut Context<Workspace>,
 571) {
 572    workspace
 573        .register_action(about)
 574        .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
 575        .register_action(|_, _: &Minimize, window, _| {
 576            window.minimize_window();
 577        })
 578        .register_action(|_, _: &Zoom, window, _| {
 579            window.zoom_window();
 580        })
 581        .register_action(|_, _: &ToggleFullScreen, window, _| {
 582            window.toggle_fullscreen();
 583        })
 584        .register_action(|_, action: &OpenZedUrl, _, cx| {
 585            OpenListener::global(cx).open_urls(vec![action.url.clone()])
 586        })
 587        .register_action(|_, action: &OpenBrowser, _window, cx| cx.open_url(&action.url))
 588        .register_action(|workspace, _: &workspace::Open, window, cx| {
 589            telemetry::event!("Project Opened");
 590            let paths = workspace.prompt_for_open_path(
 591                PathPromptOptions {
 592                    files: true,
 593                    directories: true,
 594                    multiple: true,
 595                },
 596                DirectoryLister::Local(
 597                    workspace.project().clone(),
 598                    workspace.app_state().fs.clone(),
 599                ),
 600                window,
 601                cx,
 602            );
 603
 604            cx.spawn_in(window, async move |this, cx| {
 605                let Some(paths) = paths.await.log_err().flatten() else {
 606                    return;
 607                };
 608
 609                if let Some(task) = this
 610                    .update_in(cx, |this, window, cx| {
 611                        this.open_workspace_for_paths(false, paths, window, cx)
 612                    })
 613                    .log_err()
 614                {
 615                    task.await.log_err();
 616                }
 617            })
 618            .detach()
 619        })
 620        .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
 621            if !action.from_existing_connection {
 622                cx.propagate();
 623                return;
 624            }
 625            // You need existing remote connection to open it this way
 626            if workspace.project().read(cx).is_local() {
 627                return;
 628            }
 629            telemetry::event!("Project Opened");
 630            let paths = workspace.prompt_for_open_path(
 631                PathPromptOptions {
 632                    files: true,
 633                    directories: true,
 634                    multiple: true,
 635                },
 636                DirectoryLister::Project(workspace.project().clone()),
 637                window,
 638                cx,
 639            );
 640            cx.spawn_in(window, async move |this, cx| {
 641                let Some(paths) = paths.await.log_err().flatten() else {
 642                    return;
 643                };
 644                if let Some(task) = this
 645                    .update_in(cx, |this, window, cx| {
 646                        open_new_ssh_project_from_project(this, paths, window, cx)
 647                    })
 648                    .log_err()
 649                {
 650                    task.await.log_err();
 651                }
 652            })
 653            .detach()
 654        })
 655        .register_action({
 656            let fs = app_state.fs.clone();
 657            move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
 658                if action.persist {
 659                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 660                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
 661                        let _ = settings
 662                            .ui_font_size
 663                            .insert(theme::clamp_font_size(ui_font_size).0);
 664                    });
 665                } else {
 666                    theme::adjust_ui_font_size(cx, |size| {
 667                        *size += px(1.0);
 668                    });
 669                }
 670            }
 671        })
 672        .register_action({
 673            let fs = app_state.fs.clone();
 674            move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
 675                if action.persist {
 676                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 677                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
 678                        let _ = settings
 679                            .ui_font_size
 680                            .insert(theme::clamp_font_size(ui_font_size).0);
 681                    });
 682                } else {
 683                    theme::adjust_ui_font_size(cx, |size| {
 684                        *size -= px(1.0);
 685                    });
 686                }
 687            }
 688        })
 689        .register_action({
 690            let fs = app_state.fs.clone();
 691            move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
 692                if action.persist {
 693                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
 694                        settings.ui_font_size = None;
 695                    });
 696                } else {
 697                    theme::reset_ui_font_size(cx);
 698                }
 699            }
 700        })
 701        .register_action({
 702            let fs = app_state.fs.clone();
 703            move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
 704                if action.persist {
 705                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 706                        let buffer_font_size =
 707                            ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
 708                        let _ = settings
 709                            .buffer_font_size
 710                            .insert(theme::clamp_font_size(buffer_font_size).0);
 711                    });
 712                } else {
 713                    theme::adjust_buffer_font_size(cx, |size| {
 714                        *size += px(1.0);
 715                    });
 716                }
 717            }
 718        })
 719        .register_action({
 720            let fs = app_state.fs.clone();
 721            move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
 722                if action.persist {
 723                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 724                        let buffer_font_size =
 725                            ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
 726                        let _ = settings
 727                            .buffer_font_size
 728                            .insert(theme::clamp_font_size(buffer_font_size).0);
 729                    });
 730                } else {
 731                    theme::adjust_buffer_font_size(cx, |size| {
 732                        *size -= px(1.0);
 733                    });
 734                }
 735            }
 736        })
 737        .register_action({
 738            let fs = app_state.fs.clone();
 739            move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
 740                if action.persist {
 741                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
 742                        settings.buffer_font_size = None;
 743                    });
 744                } else {
 745                    theme::reset_buffer_font_size(cx);
 746                }
 747            }
 748        })
 749        .register_action(install_cli)
 750        .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
 751            cx.spawn_in(window, async move |workspace, cx| {
 752                install_cli::register_zed_scheme(&cx).await?;
 753                workspace.update_in(cx, |workspace, _, cx| {
 754                    struct RegisterZedScheme;
 755
 756                    workspace.show_toast(
 757                        Toast::new(
 758                            NotificationId::unique::<RegisterZedScheme>(),
 759                            format!(
 760                                "zed:// links will now open in {}.",
 761                                ReleaseChannel::global(cx).display_name()
 762                            ),
 763                        ),
 764                        cx,
 765                    )
 766                })?;
 767                Ok(())
 768            })
 769            .detach_and_prompt_err(
 770                "Error registering zed:// scheme",
 771                window,
 772                cx,
 773                |_, _, _| None,
 774            );
 775        })
 776        .register_action(open_project_settings_file)
 777        .register_action(open_project_tasks_file)
 778        .register_action(open_project_debug_tasks_file)
 779        .register_action(
 780            |workspace: &mut Workspace,
 781             _: &project_panel::ToggleFocus,
 782             window: &mut Window,
 783             cx: &mut Context<Workspace>| {
 784                workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
 785            },
 786        )
 787        .register_action(
 788            |workspace: &mut Workspace,
 789             _: &outline_panel::ToggleFocus,
 790             window: &mut Window,
 791             cx: &mut Context<Workspace>| {
 792                workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
 793            },
 794        )
 795        .register_action(
 796            |workspace: &mut Workspace,
 797             _: &collab_ui::collab_panel::ToggleFocus,
 798             window: &mut Window,
 799             cx: &mut Context<Workspace>| {
 800                workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
 801            },
 802        )
 803        .register_action(
 804            |workspace: &mut Workspace,
 805             _: &collab_ui::chat_panel::ToggleFocus,
 806             window: &mut Window,
 807             cx: &mut Context<Workspace>| {
 808                workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(window, cx);
 809            },
 810        )
 811        .register_action(
 812            |workspace: &mut Workspace,
 813             _: &collab_ui::notification_panel::ToggleFocus,
 814             window: &mut Window,
 815             cx: &mut Context<Workspace>| {
 816                workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
 817                    window, cx,
 818                );
 819            },
 820        )
 821        .register_action(
 822            |workspace: &mut Workspace,
 823             _: &terminal_panel::ToggleFocus,
 824             window: &mut Window,
 825             cx: &mut Context<Workspace>| {
 826                workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
 827            },
 828        )
 829        .register_action({
 830            let app_state = Arc::downgrade(&app_state);
 831            move |_, _: &NewWindow, _, cx| {
 832                if let Some(app_state) = app_state.upgrade() {
 833                    open_new(
 834                        Default::default(),
 835                        app_state,
 836                        cx,
 837                        |workspace, window, cx| {
 838                            cx.activate(true);
 839                            Editor::new_file(workspace, &Default::default(), window, cx)
 840                        },
 841                    )
 842                    .detach();
 843                }
 844            }
 845        })
 846        .register_action({
 847            let app_state = Arc::downgrade(&app_state);
 848            move |_, _: &NewFile, _, cx| {
 849                if let Some(app_state) = app_state.upgrade() {
 850                    open_new(
 851                        Default::default(),
 852                        app_state,
 853                        cx,
 854                        |workspace, window, cx| {
 855                            Editor::new_file(workspace, &Default::default(), window, cx)
 856                        },
 857                    )
 858                    .detach();
 859                }
 860            }
 861        });
 862    if workspace.project().read(cx).is_via_ssh() {
 863        workspace.register_action({
 864            move |workspace, _: &OpenServerSettings, window, cx| {
 865                let open_server_settings = workspace
 866                    .project()
 867                    .update(cx, |project, cx| project.open_server_settings(cx));
 868
 869                cx.spawn_in(window, async move |workspace, cx| {
 870                    let buffer = open_server_settings.await?;
 871
 872                    workspace
 873                        .update_in(cx, |workspace, window, cx| {
 874                            workspace.open_path(
 875                                buffer
 876                                    .read(cx)
 877                                    .project_path(cx)
 878                                    .expect("Settings file must have a location"),
 879                                None,
 880                                true,
 881                                window,
 882                                cx,
 883                            )
 884                        })?
 885                        .await?;
 886
 887                    anyhow::Ok(())
 888                })
 889                .detach_and_log_err(cx);
 890            }
 891        });
 892    }
 893}
 894
 895fn initialize_pane(
 896    workspace: &Workspace,
 897    pane: &Entity<Pane>,
 898    window: &mut Window,
 899    cx: &mut Context<Workspace>,
 900) {
 901    pane.update(cx, |pane, cx| {
 902        pane.toolbar().update(cx, |toolbar, cx| {
 903            let multibuffer_hint = cx.new(|_| MultibufferHint::new());
 904            toolbar.add_item(multibuffer_hint, window, cx);
 905            let breadcrumbs = cx.new(|_| Breadcrumbs::new());
 906            toolbar.add_item(breadcrumbs, window, cx);
 907            let buffer_search_bar = cx.new(|cx| {
 908                search::BufferSearchBar::new(
 909                    Some(workspace.project().read(cx).languages().clone()),
 910                    window,
 911                    cx,
 912                )
 913            });
 914            toolbar.add_item(buffer_search_bar.clone(), window, cx);
 915            let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
 916            toolbar.add_item(proposed_change_bar, window, cx);
 917            let quick_action_bar =
 918                cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
 919            toolbar.add_item(quick_action_bar, window, cx);
 920            let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
 921            toolbar.add_item(diagnostic_editor_controls, window, cx);
 922            let project_search_bar = cx.new(|_| ProjectSearchBar::new());
 923            toolbar.add_item(project_search_bar, window, cx);
 924            let lsp_log_item = cx.new(|_| language_tools::LspLogToolbarItemView::new());
 925            toolbar.add_item(lsp_log_item, window, cx);
 926            let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
 927            toolbar.add_item(dap_log_item, window, cx);
 928            let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
 929            toolbar.add_item(syntax_tree_item, window, cx);
 930            let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
 931            toolbar.add_item(migration_banner, window, cx);
 932            let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
 933            toolbar.add_item(project_diff_toolbar, window, cx);
 934            let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
 935            toolbar.add_item(agent_diff_toolbar, window, cx);
 936        })
 937    });
 938}
 939
 940fn about(
 941    _: &mut Workspace,
 942    _: &zed_actions::About,
 943    window: &mut Window,
 944    cx: &mut Context<Workspace>,
 945) {
 946    let release_channel = ReleaseChannel::global(cx).display_name();
 947    let version = env!("CARGO_PKG_VERSION");
 948    let debug = if cfg!(debug_assertions) {
 949        "(debug)"
 950    } else {
 951        ""
 952    };
 953    let message = format!("{release_channel} {version} {debug}");
 954    let detail = AppCommitSha::try_global(cx).map(|sha| sha.full());
 955
 956    let prompt = window.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"], cx);
 957    cx.foreground_executor()
 958        .spawn(async {
 959            prompt.await.ok();
 960        })
 961        .detach();
 962}
 963
 964fn test_panic(_: &TestPanic, _: &mut App) {
 965    panic!("Ran the TestPanic action")
 966}
 967
 968fn install_cli(
 969    _: &mut Workspace,
 970    _: &install_cli::Install,
 971    window: &mut Window,
 972    cx: &mut Context<Workspace>,
 973) {
 974    install_cli::install_cli(window, cx);
 975}
 976
 977static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
 978fn quit(_: &Quit, cx: &mut App) {
 979    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
 980        return;
 981    }
 982
 983    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 984    cx.spawn(async move |cx| {
 985        let mut workspace_windows = cx.update(|cx| {
 986            cx.windows()
 987                .into_iter()
 988                .filter_map(|window| window.downcast::<Workspace>())
 989                .collect::<Vec<_>>()
 990        })?;
 991
 992        // If multiple windows have unsaved changes, and need a save prompt,
 993        // prompt in the active window before switching to a different window.
 994        cx.update(|cx| {
 995            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
 996        })
 997        .log_err();
 998
 999        if should_confirm {
1000            if let Some(workspace) = workspace_windows.first() {
1001                let answer = workspace
1002                    .update(cx, |_, window, cx| {
1003                        window.prompt(
1004                            PromptLevel::Info,
1005                            "Are you sure you want to quit?",
1006                            None,
1007                            &["Quit", "Cancel"],
1008                            cx,
1009                        )
1010                    })
1011                    .log_err();
1012
1013                if let Some(answer) = answer {
1014                    WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1015                    let answer = answer.await.ok();
1016                    WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1017                    if answer != Some(0) {
1018                        return Ok(());
1019                    }
1020                }
1021            }
1022        }
1023
1024        // If the user cancels any save prompt, then keep the app open.
1025        for window in workspace_windows {
1026            if let Some(should_close) = window
1027                .update(cx, |workspace, window, cx| {
1028                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1029                })
1030                .log_err()
1031            {
1032                if !should_close.await? {
1033                    return Ok(());
1034                }
1035            }
1036        }
1037        cx.update(|cx| cx.quit())?;
1038        anyhow::Ok(())
1039    })
1040    .detach_and_log_err(cx);
1041}
1042
1043fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1044    const MAX_LINES: usize = 1000;
1045    workspace
1046        .with_local_workspace(window, cx, move |workspace, window, cx| {
1047            let fs = workspace.app_state().fs.clone();
1048            cx.spawn_in(window, async move |workspace, cx| {
1049                let (old_log, new_log) =
1050                    futures::join!(fs.load(paths::old_log_file()), fs.load(paths::log_file()));
1051                let log = match (old_log, new_log) {
1052                    (Err(_), Err(_)) => None,
1053                    (old_log, new_log) => {
1054                        let mut lines = VecDeque::with_capacity(MAX_LINES);
1055                        for line in old_log
1056                            .iter()
1057                            .flat_map(|log| log.lines())
1058                            .chain(new_log.iter().flat_map(|log| log.lines()))
1059                        {
1060                            if lines.len() == MAX_LINES {
1061                                lines.pop_front();
1062                            }
1063                            lines.push_back(line);
1064                        }
1065                        Some(
1066                            lines
1067                                .into_iter()
1068                                .flat_map(|line| [line, "\n"])
1069                                .collect::<String>(),
1070                        )
1071                    }
1072                };
1073
1074                workspace
1075                    .update_in(cx, |workspace, window, cx| {
1076                        let Some(log) = log else {
1077                            struct OpenLogError;
1078
1079                            workspace.show_notification(
1080                                NotificationId::unique::<OpenLogError>(),
1081                                cx,
1082                                |cx| {
1083                                    cx.new(|cx| {
1084                                        MessageNotification::new(
1085                                            format!(
1086                                                "Unable to access/open log file at path {:?}",
1087                                                paths::log_file().as_path()
1088                                            ),
1089                                            cx,
1090                                        )
1091                                    })
1092                                },
1093                            );
1094                            return;
1095                        };
1096                        let project = workspace.project().clone();
1097                        let buffer = project.update(cx, |project, cx| {
1098                            project.create_local_buffer(&log, None, cx)
1099                        });
1100
1101                        let buffer = cx
1102                            .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1103                        let editor = cx.new(|cx| {
1104                            let mut editor =
1105                                Editor::for_multibuffer(buffer, Some(project), window, cx);
1106                            editor.set_read_only(true);
1107                            editor.set_breadcrumb_header(format!(
1108                                "Last {} lines in {}",
1109                                MAX_LINES,
1110                                paths::log_file().display()
1111                            ));
1112                            editor
1113                        });
1114
1115                        editor.update(cx, |editor, cx| {
1116                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1117                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1118                                s.select_ranges(Some(
1119                                    last_multi_buffer_offset..last_multi_buffer_offset,
1120                                ));
1121                            })
1122                        });
1123
1124                        workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1125                    })
1126                    .log_err();
1127            })
1128            .detach();
1129        })
1130        .detach();
1131}
1132
1133pub fn handle_settings_file_changes(
1134    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1135    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1136    cx: &mut App,
1137    settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
1138) {
1139    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1140
1141    // Helper function to process settings content
1142    let process_settings =
1143        move |content: String, is_user: bool, store: &mut SettingsStore, cx: &mut App| -> bool {
1144            // Apply migrations to both user and global settings
1145            let (processed_content, content_migrated) =
1146                if let Ok(Some(migrated_content)) = migrate_settings(&content) {
1147                    (migrated_content, true)
1148                } else {
1149                    (content, false)
1150                };
1151
1152            let result = if is_user {
1153                store.set_user_settings(&processed_content, cx)
1154            } else {
1155                store.set_global_settings(&processed_content, cx)
1156            };
1157
1158            if let Err(err) = &result {
1159                let settings_type = if is_user { "user" } else { "global" };
1160                log::error!("Failed to load {} settings: {err}", settings_type);
1161            }
1162
1163            settings_changed(result.err(), cx);
1164
1165            content_migrated
1166        };
1167
1168    // Initial load of both settings files
1169    let global_content = cx
1170        .background_executor()
1171        .block(global_settings_file_rx.next())
1172        .unwrap();
1173    let user_content = cx
1174        .background_executor()
1175        .block(user_settings_file_rx.next())
1176        .unwrap();
1177
1178    SettingsStore::update_global(cx, |store, cx| {
1179        process_settings(global_content, false, store, cx);
1180        process_settings(user_content, true, store, cx);
1181    });
1182
1183    // Watch for changes in both files
1184    cx.spawn(async move |cx| {
1185        let mut settings_streams = futures::stream::select(
1186            global_settings_file_rx.map(Either::Left),
1187            user_settings_file_rx.map(Either::Right),
1188        );
1189
1190        while let Some(content) = settings_streams.next().await {
1191            let (content, is_user) = match content {
1192                Either::Left(content) => (content, false),
1193                Either::Right(content) => (content, true),
1194            };
1195
1196            let result = cx.update_global(|store: &mut SettingsStore, cx| {
1197                let migrating_in_memory = process_settings(content, is_user, store, cx);
1198                if let Some(notifier) = MigrationNotification::try_global(cx) {
1199                    notifier.update(cx, |_, cx| {
1200                        cx.emit(MigrationEvent::ContentChanged {
1201                            migration_type: MigrationType::Settings,
1202                            migrating_in_memory,
1203                        });
1204                    });
1205                }
1206                cx.refresh_windows();
1207            });
1208
1209            if result.is_err() {
1210                break; // App dropped
1211            }
1212        }
1213    })
1214    .detach();
1215}
1216
1217pub fn handle_keymap_file_changes(
1218    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1219    cx: &mut App,
1220) {
1221    BaseKeymap::register(cx);
1222    VimModeSetting::register(cx);
1223
1224    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1225    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1226    let mut old_base_keymap = *BaseKeymap::get_global(cx);
1227    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1228    cx.observe_global::<SettingsStore>(move |cx| {
1229        let new_base_keymap = *BaseKeymap::get_global(cx);
1230        let new_vim_enabled = VimModeSetting::get_global(cx).0;
1231
1232        if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
1233            old_base_keymap = new_base_keymap;
1234            old_vim_enabled = new_vim_enabled;
1235            base_keymap_tx.unbounded_send(()).unwrap();
1236        }
1237    })
1238    .detach();
1239
1240    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1241    cx.on_keyboard_layout_change(move |cx| {
1242        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1243        if next_mapping != current_mapping {
1244            current_mapping = next_mapping;
1245            keyboard_layout_tx.unbounded_send(()).ok();
1246        }
1247    })
1248    .detach();
1249
1250    load_default_keymap(cx);
1251
1252    struct KeymapParseErrorNotification;
1253    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1254
1255    cx.spawn(async move |cx| {
1256        let mut user_keymap_content = String::new();
1257        let mut migrating_in_memory = false;
1258        loop {
1259            select_biased! {
1260                _ = base_keymap_rx.next() => {},
1261                _ = keyboard_layout_rx.next() => {},
1262                content = user_keymap_file_rx.next() => {
1263                    if let Some(content) = content {
1264                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1265                            user_keymap_content = migrated_content;
1266                            migrating_in_memory = true;
1267                        } else {
1268                            user_keymap_content = content;
1269                            migrating_in_memory = false;
1270                        }
1271                    }
1272                }
1273            };
1274            cx.update(|cx| {
1275                if let Some(notifier) = MigrationNotification::try_global(cx) {
1276                    notifier.update(cx, |_, cx| {
1277                        cx.emit(MigrationEvent::ContentChanged {
1278                            migration_type: MigrationType::Keymap,
1279                            migrating_in_memory,
1280                        });
1281                    });
1282                }
1283                let load_result = KeymapFile::load(&user_keymap_content, cx);
1284                match load_result {
1285                    KeymapFileLoadResult::Success { key_bindings } => {
1286                        reload_keymaps(cx, key_bindings);
1287                        dismiss_app_notification(&notification_id.clone(), cx);
1288                    }
1289                    KeymapFileLoadResult::SomeFailedToLoad {
1290                        key_bindings,
1291                        error_message,
1292                    } => {
1293                        if !key_bindings.is_empty() {
1294                            reload_keymaps(cx, key_bindings);
1295                        }
1296                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1297                    }
1298                    KeymapFileLoadResult::JsonParseFailure { error } => {
1299                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
1300                    }
1301                }
1302            })
1303            .ok();
1304        }
1305    })
1306    .detach();
1307}
1308
1309fn show_keymap_file_json_error(
1310    notification_id: NotificationId,
1311    error: &anyhow::Error,
1312    cx: &mut App,
1313) {
1314    let message: SharedString =
1315        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1316    show_app_notification(notification_id, cx, move |cx| {
1317        cx.new(|cx| {
1318            MessageNotification::new(message.clone(), cx)
1319                .primary_message("Open Keymap File")
1320                .primary_on_click(|window, cx| {
1321                    window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1322                    cx.emit(DismissEvent);
1323                })
1324        })
1325    });
1326}
1327
1328fn show_keymap_file_load_error(
1329    notification_id: NotificationId,
1330    error_message: MarkdownString,
1331    cx: &mut App,
1332) {
1333    show_markdown_app_notification(
1334        notification_id.clone(),
1335        error_message,
1336        "Open Keymap File".into(),
1337        |window, cx| {
1338            window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1339            cx.emit(DismissEvent);
1340        },
1341        cx,
1342    )
1343}
1344
1345fn show_markdown_app_notification<F>(
1346    notification_id: NotificationId,
1347    message: MarkdownString,
1348    primary_button_message: SharedString,
1349    primary_button_on_click: F,
1350    cx: &mut App,
1351) where
1352    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1353{
1354    let parsed_markdown = cx.background_spawn(async move {
1355        let file_location_directory = None;
1356        let language_registry = None;
1357        markdown_preview::markdown_parser::parse_markdown(
1358            &message.0,
1359            file_location_directory,
1360            language_registry,
1361        )
1362        .await
1363    });
1364
1365    cx.spawn(async move |cx| {
1366        let parsed_markdown = Arc::new(parsed_markdown.await);
1367        let primary_button_message = primary_button_message.clone();
1368        let primary_button_on_click = Arc::new(primary_button_on_click);
1369        cx.update(|cx| {
1370            show_app_notification(notification_id, cx, move |cx| {
1371                let workspace_handle = cx.entity().downgrade();
1372                let parsed_markdown = parsed_markdown.clone();
1373                let primary_button_message = primary_button_message.clone();
1374                let primary_button_on_click = primary_button_on_click.clone();
1375                cx.new(move |cx| {
1376                    MessageNotification::new_from_builder(cx, move |window, cx| {
1377                        image_cache(retain_all("notification-cache"))
1378                            .text_xs()
1379                            .child(markdown_preview::markdown_renderer::render_parsed_markdown(
1380                                &parsed_markdown.clone(),
1381                                Some(workspace_handle.clone()),
1382                                window,
1383                                cx,
1384                            ))
1385                            .into_any()
1386                    })
1387                    .primary_message(primary_button_message)
1388                    .primary_on_click_arc(primary_button_on_click)
1389                })
1390            })
1391        })
1392        .ok();
1393    })
1394    .detach();
1395}
1396
1397fn reload_keymaps(cx: &mut App, user_key_bindings: Vec<KeyBinding>) {
1398    cx.clear_key_bindings();
1399    load_default_keymap(cx);
1400    cx.bind_keys(user_key_bindings);
1401    cx.set_menus(app_menus());
1402    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1403    #[cfg(not(target_os = "windows"))]
1404    cx.set_dock_menu(vec![gpui::MenuItem::action(
1405        "New Window",
1406        workspace::NewWindow,
1407    )]);
1408}
1409
1410pub fn load_default_keymap(cx: &mut App) {
1411    let base_keymap = *BaseKeymap::get_global(cx);
1412    if base_keymap == BaseKeymap::None {
1413        return;
1414    }
1415
1416    cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
1417
1418    if let Some(asset_path) = base_keymap.asset_path() {
1419        cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
1420    }
1421
1422    if VimModeSetting::get_global(cx).0 {
1423        cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
1424    }
1425}
1426
1427pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1428    struct SettingsParseErrorNotification;
1429    let id = NotificationId::unique::<SettingsParseErrorNotification>();
1430
1431    match error {
1432        Some(error) => {
1433            if let Some(InvalidSettingsError::LocalSettings { .. }) =
1434                error.downcast_ref::<InvalidSettingsError>()
1435            {
1436                // Local settings errors are displayed by the projects
1437                return;
1438            }
1439            show_app_notification(id, cx, move |cx| {
1440                cx.new(|cx| {
1441                    MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1442                        .primary_message("Open Settings File")
1443                        .primary_icon(IconName::Settings)
1444                        .primary_on_click(|window, cx| {
1445                            window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1446                            cx.emit(DismissEvent);
1447                        })
1448                })
1449            });
1450        }
1451        None => {
1452            dismiss_app_notification(&id, cx);
1453        }
1454    }
1455}
1456
1457pub fn open_new_ssh_project_from_project(
1458    workspace: &mut Workspace,
1459    paths: Vec<PathBuf>,
1460    window: &mut Window,
1461    cx: &mut Context<Workspace>,
1462) -> Task<anyhow::Result<()>> {
1463    let app_state = workspace.app_state().clone();
1464    let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
1465        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1466    };
1467    let connection_options = ssh_client.read(cx).connection_options();
1468    cx.spawn_in(window, async move |_, cx| {
1469        open_ssh_project(
1470            connection_options,
1471            paths,
1472            app_state,
1473            workspace::OpenOptions {
1474                open_new_workspace: Some(true),
1475                ..Default::default()
1476            },
1477            cx,
1478        )
1479        .await
1480    })
1481}
1482
1483fn open_project_settings_file(
1484    workspace: &mut Workspace,
1485    _: &OpenProjectSettings,
1486    window: &mut Window,
1487    cx: &mut Context<Workspace>,
1488) {
1489    open_local_file(
1490        workspace,
1491        local_settings_file_relative_path(),
1492        initial_project_settings_content(),
1493        window,
1494        cx,
1495    )
1496}
1497
1498fn open_project_tasks_file(
1499    workspace: &mut Workspace,
1500    _: &OpenProjectTasks,
1501    window: &mut Window,
1502    cx: &mut Context<Workspace>,
1503) {
1504    open_local_file(
1505        workspace,
1506        local_tasks_file_relative_path(),
1507        initial_tasks_content(),
1508        window,
1509        cx,
1510    )
1511}
1512
1513fn open_project_debug_tasks_file(
1514    workspace: &mut Workspace,
1515    _: &OpenProjectDebugTasks,
1516    window: &mut Window,
1517    cx: &mut Context<Workspace>,
1518) {
1519    open_local_file(
1520        workspace,
1521        local_debug_file_relative_path(),
1522        initial_local_debug_tasks_content(),
1523        window,
1524        cx,
1525    )
1526}
1527
1528fn open_local_file(
1529    workspace: &mut Workspace,
1530    settings_relative_path: &'static Path,
1531    initial_contents: Cow<'static, str>,
1532    window: &mut Window,
1533    cx: &mut Context<Workspace>,
1534) {
1535    let project = workspace.project().clone();
1536    let worktree = project
1537        .read(cx)
1538        .visible_worktrees(cx)
1539        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1540    if let Some(worktree) = worktree {
1541        let tree_id = worktree.read(cx).id();
1542        cx.spawn_in(window, async move |workspace, cx| {
1543            // Check if the file actually exists on disk (even if it's excluded from worktree)
1544            let file_exists = {
1545                let full_path = worktree
1546                    .read_with(cx, |tree, _| tree.abs_path().join(settings_relative_path))?;
1547
1548                let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1549                let file_exists = fs
1550                    .metadata(&full_path)
1551                    .await
1552                    .ok()
1553                    .flatten()
1554                    .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo);
1555                file_exists
1556            };
1557
1558            if !file_exists {
1559                if let Some(dir_path) = settings_relative_path.parent() {
1560                    if worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
1561                        project
1562                            .update(cx, |project, cx| {
1563                                project.create_entry((tree_id, dir_path), true, cx)
1564                            })?
1565                            .await
1566                            .context("worktree was removed")?;
1567                    }
1568                }
1569
1570                if worktree.read_with(cx, |tree, _| {
1571                    tree.entry_for_path(settings_relative_path).is_none()
1572                })? {
1573                    project
1574                        .update(cx, |project, cx| {
1575                            project.create_entry((tree_id, settings_relative_path), false, cx)
1576                        })?
1577                        .await
1578                        .context("worktree was removed")?;
1579                }
1580            }
1581
1582            let editor = workspace
1583                .update_in(cx, |workspace, window, cx| {
1584                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1585                })?
1586                .await?
1587                .downcast::<Editor>()
1588                .context("unexpected item type: expected editor item")?;
1589
1590            editor
1591                .downgrade()
1592                .update(cx, |editor, cx| {
1593                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1594                        if buffer.read(cx).is_empty() {
1595                            buffer.update(cx, |buffer, cx| {
1596                                buffer.edit([(0..0, initial_contents)], None, cx)
1597                            });
1598                        }
1599                    }
1600                })
1601                .ok();
1602
1603            anyhow::Ok(())
1604        })
1605        .detach();
1606    } else {
1607        struct NoOpenFolders;
1608
1609        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1610            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1611        })
1612    }
1613}
1614
1615fn open_telemetry_log_file(
1616    workspace: &mut Workspace,
1617    window: &mut Window,
1618    cx: &mut Context<Workspace>,
1619) {
1620    workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1621        let app_state = workspace.app_state().clone();
1622        cx.spawn_in(window, async move |workspace, cx| {
1623            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1624                let path = client::telemetry::Telemetry::log_file_path();
1625                app_state.fs.load(&path).await.log_err()
1626            }
1627
1628            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1629
1630            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1631            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1632            if let Some(newline_offset) = log[start_offset..].find('\n') {
1633                start_offset += newline_offset + 1;
1634            }
1635            let log_suffix = &log[start_offset..];
1636            let header = concat!(
1637                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1638                "// Telemetry can be disabled via the `settings.json` file.\n",
1639                "// Here is the data that has been reported for the current session:\n",
1640            );
1641            let content = format!("{}\n{}", header, log_suffix);
1642            let json = app_state.languages.language_for_name("JSON").await.log_err();
1643
1644            workspace.update_in( cx, |workspace, window, cx| {
1645                let project = workspace.project().clone();
1646                let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json, cx));
1647                let buffer = cx.new(|cx| {
1648                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1649                });
1650                workspace.add_item_to_active_pane(
1651                    Box::new(cx.new(|cx| {
1652                        let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1653                        editor.set_read_only(true);
1654                        editor.set_breadcrumb_header("Telemetry Log".into());
1655                        editor
1656                    })),
1657                    None,
1658                    true,
1659                    window, cx,
1660                );
1661            }).log_err()?;
1662
1663            Some(())
1664        })
1665        .detach();
1666    }).detach();
1667}
1668
1669fn open_bundled_file(
1670    workspace: &Workspace,
1671    text: Cow<'static, str>,
1672    title: &'static str,
1673    language: &'static str,
1674    window: &mut Window,
1675    cx: &mut Context<Workspace>,
1676) {
1677    let language = workspace.app_state().languages.language_for_name(language);
1678    cx.spawn_in(window, async move |workspace, cx| {
1679        let language = language.await.log_err();
1680        workspace
1681            .update_in(cx, |workspace, window, cx| {
1682                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1683                    let project = workspace.project();
1684                    let buffer = project.update(cx, move |project, cx| {
1685                        project.create_local_buffer(text.as_ref(), language, cx)
1686                    });
1687                    let buffer =
1688                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1689                    workspace.add_item_to_active_pane(
1690                        Box::new(cx.new(|cx| {
1691                            let mut editor =
1692                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1693                            editor.set_read_only(true);
1694                            editor.set_breadcrumb_header(title.into());
1695                            editor
1696                        })),
1697                        None,
1698                        true,
1699                        window,
1700                        cx,
1701                    );
1702                })
1703            })?
1704            .await
1705    })
1706    .detach_and_log_err(cx);
1707}
1708
1709fn open_settings_file(
1710    abs_path: &'static Path,
1711    default_content: impl FnOnce() -> Rope + Send + 'static,
1712    window: &mut Window,
1713    cx: &mut Context<Workspace>,
1714) {
1715    cx.spawn_in(window, async move |workspace, cx| {
1716        let (worktree_creation_task, settings_open_task) = workspace
1717            .update_in(cx, |workspace, window, cx| {
1718                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1719                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1720                        // Set up a dedicated worktree for settings, since
1721                        // otherwise we're dropping and re-starting LSP servers
1722                        // for each file inside on every settings file
1723                        // close/open
1724
1725                        // TODO: Do note that all other external files (e.g.
1726                        // drag and drop from OS) still have their worktrees
1727                        // released on file close, causing LSP servers'
1728                        // restarts.
1729                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1730                    });
1731                    let settings_open_task =
1732                        create_and_open_local_file(abs_path, window, cx, default_content);
1733                    (worktree_creation_task, settings_open_task)
1734                })
1735            })?
1736            .await?;
1737        let _ = worktree_creation_task.await?;
1738        let _ = settings_open_task.await?;
1739        anyhow::Ok(())
1740    })
1741    .detach_and_log_err(cx);
1742}
1743
1744#[cfg(test)]
1745mod tests {
1746    use super::*;
1747    use assets::Assets;
1748    use collections::HashSet;
1749    use editor::{DisplayPoint, Editor, display_map::DisplayRow, scroll::Autoscroll};
1750    use gpui::{
1751        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1752        TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1753    };
1754    use language::{LanguageMatcher, LanguageRegistry};
1755    use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings};
1756    use serde_json::json;
1757    use settings::{SettingsStore, watch_config_file};
1758    use std::{
1759        path::{Path, PathBuf},
1760        time::Duration,
1761    };
1762    use theme::{ThemeRegistry, ThemeSettings};
1763    use util::{path, separator};
1764    use workspace::{
1765        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1766        WorkspaceHandle,
1767        item::{Item, ItemHandle},
1768        open_new, open_paths, pane,
1769    };
1770
1771    #[gpui::test]
1772    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1773        let app_state = init_test(cx);
1774        app_state
1775            .fs
1776            .as_fake()
1777            .insert_tree(
1778                path!("/root"),
1779                json!({
1780                    "a": {
1781                    },
1782                }),
1783            )
1784            .await;
1785
1786        cx.update(|cx| {
1787            open_paths(
1788                &[PathBuf::from(path!("/root/a/new"))],
1789                app_state.clone(),
1790                workspace::OpenOptions::default(),
1791                cx,
1792            )
1793        })
1794        .await
1795        .unwrap();
1796        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1797
1798        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1799        workspace
1800            .update(cx, |workspace, _, cx| {
1801                assert!(workspace.active_item_as::<Editor>(cx).is_some())
1802            })
1803            .unwrap();
1804    }
1805
1806    #[gpui::test]
1807    async fn test_open_paths_action(cx: &mut TestAppContext) {
1808        let app_state = init_test(cx);
1809        app_state
1810            .fs
1811            .as_fake()
1812            .insert_tree(
1813                "/root",
1814                json!({
1815                    "a": {
1816                        "aa": null,
1817                        "ab": null,
1818                    },
1819                    "b": {
1820                        "ba": null,
1821                        "bb": null,
1822                    },
1823                    "c": {
1824                        "ca": null,
1825                        "cb": null,
1826                    },
1827                    "d": {
1828                        "da": null,
1829                        "db": null,
1830                    },
1831                    "e": {
1832                        "ea": null,
1833                        "eb": null,
1834                    }
1835                }),
1836            )
1837            .await;
1838
1839        cx.update(|cx| {
1840            open_paths(
1841                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1842                app_state.clone(),
1843                workspace::OpenOptions::default(),
1844                cx,
1845            )
1846        })
1847        .await
1848        .unwrap();
1849        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1850
1851        cx.update(|cx| {
1852            open_paths(
1853                &[PathBuf::from("/root/a")],
1854                app_state.clone(),
1855                workspace::OpenOptions::default(),
1856                cx,
1857            )
1858        })
1859        .await
1860        .unwrap();
1861        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1862        let workspace_1 = cx
1863            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1864            .unwrap();
1865        cx.run_until_parked();
1866        workspace_1
1867            .update(cx, |workspace, window, cx| {
1868                assert_eq!(workspace.worktrees(cx).count(), 2);
1869                assert!(workspace.left_dock().read(cx).is_open());
1870                assert!(
1871                    workspace
1872                        .active_pane()
1873                        .read(cx)
1874                        .focus_handle(cx)
1875                        .is_focused(window)
1876                );
1877            })
1878            .unwrap();
1879
1880        cx.update(|cx| {
1881            open_paths(
1882                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1883                app_state.clone(),
1884                workspace::OpenOptions::default(),
1885                cx,
1886            )
1887        })
1888        .await
1889        .unwrap();
1890        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1891
1892        // Replace existing windows
1893        let window = cx
1894            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1895            .unwrap();
1896        cx.update(|cx| {
1897            open_paths(
1898                &[PathBuf::from("/root/e")],
1899                app_state,
1900                workspace::OpenOptions {
1901                    replace_window: Some(window),
1902                    ..Default::default()
1903                },
1904                cx,
1905            )
1906        })
1907        .await
1908        .unwrap();
1909        cx.background_executor.run_until_parked();
1910        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1911        let workspace_1 = cx
1912            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1913            .unwrap();
1914        workspace_1
1915            .update(cx, |workspace, window, cx| {
1916                assert_eq!(
1917                    workspace
1918                        .worktrees(cx)
1919                        .map(|w| w.read(cx).abs_path())
1920                        .collect::<Vec<_>>(),
1921                    &[Path::new("/root/e").into()]
1922                );
1923                assert!(workspace.left_dock().read(cx).is_open());
1924                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
1925            })
1926            .unwrap();
1927    }
1928
1929    #[gpui::test]
1930    async fn test_open_add_new(cx: &mut TestAppContext) {
1931        let app_state = init_test(cx);
1932        app_state
1933            .fs
1934            .as_fake()
1935            .insert_tree(
1936                path!("/root"),
1937                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
1938            )
1939            .await;
1940
1941        cx.update(|cx| {
1942            open_paths(
1943                &[PathBuf::from(path!("/root/dir"))],
1944                app_state.clone(),
1945                workspace::OpenOptions::default(),
1946                cx,
1947            )
1948        })
1949        .await
1950        .unwrap();
1951        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1952
1953        cx.update(|cx| {
1954            open_paths(
1955                &[PathBuf::from(path!("/root/a"))],
1956                app_state.clone(),
1957                workspace::OpenOptions {
1958                    open_new_workspace: Some(false),
1959                    ..Default::default()
1960                },
1961                cx,
1962            )
1963        })
1964        .await
1965        .unwrap();
1966        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1967
1968        cx.update(|cx| {
1969            open_paths(
1970                &[PathBuf::from(path!("/root/dir/c"))],
1971                app_state.clone(),
1972                workspace::OpenOptions {
1973                    open_new_workspace: Some(true),
1974                    ..Default::default()
1975                },
1976                cx,
1977            )
1978        })
1979        .await
1980        .unwrap();
1981        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1982    }
1983
1984    #[gpui::test]
1985    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1986        let app_state = init_test(cx);
1987        app_state
1988            .fs
1989            .as_fake()
1990            .insert_tree(
1991                path!("/root"),
1992                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
1993            )
1994            .await;
1995
1996        cx.update(|cx| {
1997            open_paths(
1998                &[PathBuf::from(path!("/root/dir1/a"))],
1999                app_state.clone(),
2000                workspace::OpenOptions::default(),
2001                cx,
2002            )
2003        })
2004        .await
2005        .unwrap();
2006        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2007        let window1 = cx.update(|cx| cx.active_window().unwrap());
2008
2009        cx.update(|cx| {
2010            open_paths(
2011                &[PathBuf::from(path!("/root/dir2/c"))],
2012                app_state.clone(),
2013                workspace::OpenOptions::default(),
2014                cx,
2015            )
2016        })
2017        .await
2018        .unwrap();
2019        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2020
2021        cx.update(|cx| {
2022            open_paths(
2023                &[PathBuf::from(path!("/root/dir2"))],
2024                app_state.clone(),
2025                workspace::OpenOptions::default(),
2026                cx,
2027            )
2028        })
2029        .await
2030        .unwrap();
2031        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2032        let window2 = cx.update(|cx| cx.active_window().unwrap());
2033        assert!(window1 != window2);
2034        cx.update_window(window1, |_, window, _| window.activate_window())
2035            .unwrap();
2036
2037        cx.update(|cx| {
2038            open_paths(
2039                &[PathBuf::from(path!("/root/dir2/c"))],
2040                app_state.clone(),
2041                workspace::OpenOptions::default(),
2042                cx,
2043            )
2044        })
2045        .await
2046        .unwrap();
2047        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2048        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2049        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2050    }
2051
2052    #[gpui::test]
2053    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2054        let executor = cx.executor();
2055        let app_state = init_test(cx);
2056
2057        cx.update(|cx| {
2058            SettingsStore::update_global(cx, |store, cx| {
2059                store.update_user_settings::<ProjectSettings>(cx, |settings| {
2060                    settings.session.restore_unsaved_buffers = false
2061                });
2062            });
2063        });
2064
2065        app_state
2066            .fs
2067            .as_fake()
2068            .insert_tree(path!("/root"), json!({"a": "hey"}))
2069            .await;
2070
2071        cx.update(|cx| {
2072            open_paths(
2073                &[PathBuf::from(path!("/root/a"))],
2074                app_state.clone(),
2075                workspace::OpenOptions::default(),
2076                cx,
2077            )
2078        })
2079        .await
2080        .unwrap();
2081        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2082
2083        // When opening the workspace, the window is not in a edited state.
2084        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2085
2086        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2087            cx.update(|cx| window.read(cx).unwrap().is_edited())
2088        };
2089        let pane = window
2090            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2091            .unwrap();
2092        let editor = window
2093            .read_with(cx, |workspace, cx| {
2094                workspace
2095                    .active_item(cx)
2096                    .unwrap()
2097                    .downcast::<Editor>()
2098                    .unwrap()
2099            })
2100            .unwrap();
2101
2102        assert!(!window_is_edited(window, cx));
2103
2104        // Editing a buffer marks the window as edited.
2105        window
2106            .update(cx, |_, window, cx| {
2107                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2108            })
2109            .unwrap();
2110
2111        assert!(window_is_edited(window, cx));
2112
2113        // Undoing the edit restores the window's edited state.
2114        window
2115            .update(cx, |_, window, cx| {
2116                editor.update(cx, |editor, cx| {
2117                    editor.undo(&Default::default(), window, cx)
2118                });
2119            })
2120            .unwrap();
2121        assert!(!window_is_edited(window, cx));
2122
2123        // Redoing the edit marks the window as edited again.
2124        window
2125            .update(cx, |_, window, cx| {
2126                editor.update(cx, |editor, cx| {
2127                    editor.redo(&Default::default(), window, cx)
2128                });
2129            })
2130            .unwrap();
2131        assert!(window_is_edited(window, cx));
2132        let weak = editor.downgrade();
2133
2134        // Closing the item restores the window's edited state.
2135        let close = window
2136            .update(cx, |_, window, cx| {
2137                pane.update(cx, |pane, cx| {
2138                    drop(editor);
2139                    pane.close_active_item(&Default::default(), window, cx)
2140                })
2141            })
2142            .unwrap();
2143        executor.run_until_parked();
2144
2145        cx.simulate_prompt_answer("Don't Save");
2146        close.await.unwrap();
2147
2148        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2149        cx.executor().advance_clock(Duration::from_secs(1));
2150
2151        weak.assert_released();
2152        assert!(!window_is_edited(window, cx));
2153        // Opening the buffer again doesn't impact the window's edited state.
2154        cx.update(|cx| {
2155            open_paths(
2156                &[PathBuf::from(path!("/root/a"))],
2157                app_state,
2158                workspace::OpenOptions::default(),
2159                cx,
2160            )
2161        })
2162        .await
2163        .unwrap();
2164        executor.run_until_parked();
2165
2166        window
2167            .update(cx, |workspace, _, cx| {
2168                let editor = workspace
2169                    .active_item(cx)
2170                    .unwrap()
2171                    .downcast::<Editor>()
2172                    .unwrap();
2173
2174                editor.update(cx, |editor, cx| {
2175                    assert_eq!(editor.text(cx), "hey");
2176                });
2177            })
2178            .unwrap();
2179
2180        let editor = window
2181            .read_with(cx, |workspace, cx| {
2182                workspace
2183                    .active_item(cx)
2184                    .unwrap()
2185                    .downcast::<Editor>()
2186                    .unwrap()
2187            })
2188            .unwrap();
2189        assert!(!window_is_edited(window, cx));
2190
2191        // Editing the buffer marks the window as edited.
2192        window
2193            .update(cx, |_, window, cx| {
2194                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2195            })
2196            .unwrap();
2197        executor.run_until_parked();
2198        assert!(window_is_edited(window, cx));
2199
2200        // Ensure closing the window via the mouse gets preempted due to the
2201        // buffer having unsaved changes.
2202        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2203        executor.run_until_parked();
2204        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2205
2206        // The window is successfully closed after the user dismisses the prompt.
2207        cx.simulate_prompt_answer("Don't Save");
2208        executor.run_until_parked();
2209        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2210    }
2211
2212    #[gpui::test]
2213    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2214        let app_state = init_test(cx);
2215        app_state
2216            .fs
2217            .as_fake()
2218            .insert_tree(path!("/root"), json!({"a": "hey"}))
2219            .await;
2220
2221        cx.update(|cx| {
2222            open_paths(
2223                &[PathBuf::from(path!("/root/a"))],
2224                app_state.clone(),
2225                workspace::OpenOptions::default(),
2226                cx,
2227            )
2228        })
2229        .await
2230        .unwrap();
2231
2232        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2233
2234        // When opening the workspace, the window is not in a edited state.
2235        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2236
2237        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2238            cx.update(|cx| window.read(cx).unwrap().is_edited())
2239        };
2240
2241        let editor = window
2242            .read_with(cx, |workspace, cx| {
2243                workspace
2244                    .active_item(cx)
2245                    .unwrap()
2246                    .downcast::<Editor>()
2247                    .unwrap()
2248            })
2249            .unwrap();
2250
2251        assert!(!window_is_edited(window, cx));
2252
2253        // Editing a buffer marks the window as edited.
2254        window
2255            .update(cx, |_, window, cx| {
2256                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2257            })
2258            .unwrap();
2259
2260        assert!(window_is_edited(window, cx));
2261        cx.run_until_parked();
2262
2263        // Advance the clock to make sure the workspace is serialized
2264        cx.executor().advance_clock(Duration::from_secs(1));
2265
2266        // When closing the window, no prompt shows up and the window is closed.
2267        // buffer having unsaved changes.
2268        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2269        cx.run_until_parked();
2270        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2271
2272        // When we now reopen the window, the edited state and the edited buffer are back
2273        cx.update(|cx| {
2274            open_paths(
2275                &[PathBuf::from(path!("/root/a"))],
2276                app_state.clone(),
2277                workspace::OpenOptions::default(),
2278                cx,
2279            )
2280        })
2281        .await
2282        .unwrap();
2283
2284        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2285        assert!(cx.update(|cx| cx.active_window().is_some()));
2286
2287        cx.run_until_parked();
2288
2289        // When opening the workspace, the window is not in a edited state.
2290        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2291        assert!(window_is_edited(window, cx));
2292
2293        window
2294            .update(cx, |workspace, _, cx| {
2295                let editor = workspace
2296                    .active_item(cx)
2297                    .unwrap()
2298                    .downcast::<editor::Editor>()
2299                    .unwrap();
2300                editor.update(cx, |editor, cx| {
2301                    assert_eq!(editor.text(cx), "EDIThey");
2302                    assert!(editor.is_dirty(cx));
2303                });
2304
2305                editor
2306            })
2307            .unwrap();
2308    }
2309
2310    #[gpui::test]
2311    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2312        let app_state = init_test(cx);
2313        cx.update(|cx| {
2314            open_new(
2315                Default::default(),
2316                app_state.clone(),
2317                cx,
2318                |workspace, window, cx| {
2319                    Editor::new_file(workspace, &Default::default(), window, cx)
2320                },
2321            )
2322        })
2323        .await
2324        .unwrap();
2325        cx.run_until_parked();
2326
2327        let workspace = cx
2328            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2329            .unwrap();
2330
2331        let editor = workspace
2332            .update(cx, |workspace, _, cx| {
2333                let editor = workspace
2334                    .active_item(cx)
2335                    .unwrap()
2336                    .downcast::<editor::Editor>()
2337                    .unwrap();
2338                editor.update(cx, |editor, cx| {
2339                    assert!(editor.text(cx).is_empty());
2340                    assert!(!editor.is_dirty(cx));
2341                });
2342
2343                editor
2344            })
2345            .unwrap();
2346
2347        let save_task = workspace
2348            .update(cx, |workspace, window, cx| {
2349                workspace.save_active_item(SaveIntent::Save, window, cx)
2350            })
2351            .unwrap();
2352        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2353        cx.background_executor.run_until_parked();
2354        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2355        save_task.await.unwrap();
2356        workspace
2357            .update(cx, |_, _, cx| {
2358                editor.update(cx, |editor, cx| {
2359                    assert!(!editor.is_dirty(cx));
2360                    assert_eq!(editor.title(cx), "the-new-name");
2361                });
2362            })
2363            .unwrap();
2364    }
2365
2366    #[gpui::test]
2367    async fn test_open_entry(cx: &mut TestAppContext) {
2368        let app_state = init_test(cx);
2369        app_state
2370            .fs
2371            .as_fake()
2372            .insert_tree(
2373                path!("/root"),
2374                json!({
2375                    "a": {
2376                        "file1": "contents 1",
2377                        "file2": "contents 2",
2378                        "file3": "contents 3",
2379                    },
2380                }),
2381            )
2382            .await;
2383
2384        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2385        project.update(cx, |project, _cx| {
2386            project.languages().add(markdown_language())
2387        });
2388        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2389        let workspace = window.root(cx).unwrap();
2390
2391        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2392        let file1 = entries[0].clone();
2393        let file2 = entries[1].clone();
2394        let file3 = entries[2].clone();
2395
2396        // Open the first entry
2397        let entry_1 = window
2398            .update(cx, |w, window, cx| {
2399                w.open_path(file1.clone(), None, true, window, cx)
2400            })
2401            .unwrap()
2402            .await
2403            .unwrap();
2404        cx.read(|cx| {
2405            let pane = workspace.read(cx).active_pane().read(cx);
2406            assert_eq!(
2407                pane.active_item().unwrap().project_path(cx),
2408                Some(file1.clone())
2409            );
2410            assert_eq!(pane.items_len(), 1);
2411        });
2412
2413        // Open the second entry
2414        window
2415            .update(cx, |w, window, cx| {
2416                w.open_path(file2.clone(), None, true, window, cx)
2417            })
2418            .unwrap()
2419            .await
2420            .unwrap();
2421        cx.read(|cx| {
2422            let pane = workspace.read(cx).active_pane().read(cx);
2423            assert_eq!(
2424                pane.active_item().unwrap().project_path(cx),
2425                Some(file2.clone())
2426            );
2427            assert_eq!(pane.items_len(), 2);
2428        });
2429
2430        // Open the first entry again. The existing pane item is activated.
2431        let entry_1b = window
2432            .update(cx, |w, window, cx| {
2433                w.open_path(file1.clone(), None, true, window, cx)
2434            })
2435            .unwrap()
2436            .await
2437            .unwrap();
2438        assert_eq!(entry_1.item_id(), entry_1b.item_id());
2439
2440        cx.read(|cx| {
2441            let pane = workspace.read(cx).active_pane().read(cx);
2442            assert_eq!(
2443                pane.active_item().unwrap().project_path(cx),
2444                Some(file1.clone())
2445            );
2446            assert_eq!(pane.items_len(), 2);
2447        });
2448
2449        // Split the pane with the first entry, then open the second entry again.
2450        window
2451            .update(cx, |w, window, cx| {
2452                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2453                w.open_path(file2.clone(), None, true, window, cx)
2454            })
2455            .unwrap()
2456            .await
2457            .unwrap();
2458
2459        window
2460            .read_with(cx, |w, cx| {
2461                assert_eq!(
2462                    w.active_pane()
2463                        .read(cx)
2464                        .active_item()
2465                        .unwrap()
2466                        .project_path(cx),
2467                    Some(file2.clone())
2468                );
2469            })
2470            .unwrap();
2471
2472        // Open the third entry twice concurrently. Only one pane item is added.
2473        let (t1, t2) = window
2474            .update(cx, |w, window, cx| {
2475                (
2476                    w.open_path(file3.clone(), None, true, window, cx),
2477                    w.open_path(file3.clone(), None, true, window, cx),
2478                )
2479            })
2480            .unwrap();
2481        t1.await.unwrap();
2482        t2.await.unwrap();
2483        cx.read(|cx| {
2484            let pane = workspace.read(cx).active_pane().read(cx);
2485            assert_eq!(
2486                pane.active_item().unwrap().project_path(cx),
2487                Some(file3.clone())
2488            );
2489            let pane_entries = pane
2490                .items()
2491                .map(|i| i.project_path(cx).unwrap())
2492                .collect::<Vec<_>>();
2493            assert_eq!(pane_entries, &[file1, file2, file3]);
2494        });
2495    }
2496
2497    #[gpui::test]
2498    async fn test_open_paths(cx: &mut TestAppContext) {
2499        let app_state = init_test(cx);
2500
2501        app_state
2502            .fs
2503            .as_fake()
2504            .insert_tree(
2505                path!("/"),
2506                json!({
2507                    "dir1": {
2508                        "a.txt": ""
2509                    },
2510                    "dir2": {
2511                        "b.txt": ""
2512                    },
2513                    "dir3": {
2514                        "c.txt": ""
2515                    },
2516                    "d.txt": ""
2517                }),
2518            )
2519            .await;
2520
2521        cx.update(|cx| {
2522            open_paths(
2523                &[PathBuf::from(path!("/dir1/"))],
2524                app_state,
2525                workspace::OpenOptions::default(),
2526                cx,
2527            )
2528        })
2529        .await
2530        .unwrap();
2531        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2532        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2533        let workspace = window.root(cx).unwrap();
2534
2535        #[track_caller]
2536        fn assert_project_panel_selection(
2537            workspace: &Workspace,
2538            expected_worktree_path: &Path,
2539            expected_entry_path: &Path,
2540            cx: &App,
2541        ) {
2542            let project_panel = [
2543                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2544                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2545                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2546            ]
2547            .into_iter()
2548            .find_map(std::convert::identity)
2549            .expect("found no project panels")
2550            .read(cx);
2551            let (selected_worktree, selected_entry) = project_panel
2552                .selected_entry(cx)
2553                .expect("project panel should have a selected entry");
2554            assert_eq!(
2555                selected_worktree.abs_path().as_ref(),
2556                expected_worktree_path,
2557                "Unexpected project panel selected worktree path"
2558            );
2559            assert_eq!(
2560                selected_entry.path.as_ref(),
2561                expected_entry_path,
2562                "Unexpected project panel selected entry path"
2563            );
2564        }
2565
2566        // Open a file within an existing worktree.
2567        window
2568            .update(cx, |workspace, window, cx| {
2569                workspace.open_paths(
2570                    vec![path!("/dir1/a.txt").into()],
2571                    OpenOptions {
2572                        visible: Some(OpenVisible::All),
2573                        ..Default::default()
2574                    },
2575                    None,
2576                    window,
2577                    cx,
2578                )
2579            })
2580            .unwrap()
2581            .await;
2582        cx.read(|cx| {
2583            let workspace = workspace.read(cx);
2584            assert_project_panel_selection(
2585                workspace,
2586                Path::new(path!("/dir1")),
2587                Path::new("a.txt"),
2588                cx,
2589            );
2590            assert_eq!(
2591                workspace
2592                    .active_pane()
2593                    .read(cx)
2594                    .active_item()
2595                    .unwrap()
2596                    .act_as::<Editor>(cx)
2597                    .unwrap()
2598                    .read(cx)
2599                    .title(cx),
2600                "a.txt"
2601            );
2602        });
2603
2604        // Open a file outside of any existing worktree.
2605        window
2606            .update(cx, |workspace, window, cx| {
2607                workspace.open_paths(
2608                    vec![path!("/dir2/b.txt").into()],
2609                    OpenOptions {
2610                        visible: Some(OpenVisible::All),
2611                        ..Default::default()
2612                    },
2613                    None,
2614                    window,
2615                    cx,
2616                )
2617            })
2618            .unwrap()
2619            .await;
2620        cx.read(|cx| {
2621            let workspace = workspace.read(cx);
2622            assert_project_panel_selection(
2623                workspace,
2624                Path::new(path!("/dir2/b.txt")),
2625                Path::new(""),
2626                cx,
2627            );
2628            let worktree_roots = workspace
2629                .worktrees(cx)
2630                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2631                .collect::<HashSet<_>>();
2632            assert_eq!(
2633                worktree_roots,
2634                vec![path!("/dir1"), path!("/dir2/b.txt")]
2635                    .into_iter()
2636                    .map(Path::new)
2637                    .collect(),
2638            );
2639            assert_eq!(
2640                workspace
2641                    .active_pane()
2642                    .read(cx)
2643                    .active_item()
2644                    .unwrap()
2645                    .act_as::<Editor>(cx)
2646                    .unwrap()
2647                    .read(cx)
2648                    .title(cx),
2649                "b.txt"
2650            );
2651        });
2652
2653        // Ensure opening a directory and one of its children only adds one worktree.
2654        window
2655            .update(cx, |workspace, window, cx| {
2656                workspace.open_paths(
2657                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2658                    OpenOptions {
2659                        visible: Some(OpenVisible::All),
2660                        ..Default::default()
2661                    },
2662                    None,
2663                    window,
2664                    cx,
2665                )
2666            })
2667            .unwrap()
2668            .await;
2669        cx.read(|cx| {
2670            let workspace = workspace.read(cx);
2671            assert_project_panel_selection(
2672                workspace,
2673                Path::new(path!("/dir3")),
2674                Path::new("c.txt"),
2675                cx,
2676            );
2677            let worktree_roots = workspace
2678                .worktrees(cx)
2679                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2680                .collect::<HashSet<_>>();
2681            assert_eq!(
2682                worktree_roots,
2683                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2684                    .into_iter()
2685                    .map(Path::new)
2686                    .collect(),
2687            );
2688            assert_eq!(
2689                workspace
2690                    .active_pane()
2691                    .read(cx)
2692                    .active_item()
2693                    .unwrap()
2694                    .act_as::<Editor>(cx)
2695                    .unwrap()
2696                    .read(cx)
2697                    .title(cx),
2698                "c.txt"
2699            );
2700        });
2701
2702        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2703        window
2704            .update(cx, |workspace, window, cx| {
2705                workspace.open_paths(
2706                    vec![path!("/d.txt").into()],
2707                    OpenOptions {
2708                        visible: Some(OpenVisible::None),
2709                        ..Default::default()
2710                    },
2711                    None,
2712                    window,
2713                    cx,
2714                )
2715            })
2716            .unwrap()
2717            .await;
2718        cx.read(|cx| {
2719            let workspace = workspace.read(cx);
2720            assert_project_panel_selection(
2721                workspace,
2722                Path::new(path!("/d.txt")),
2723                Path::new(""),
2724                cx,
2725            );
2726            let worktree_roots = workspace
2727                .worktrees(cx)
2728                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2729                .collect::<HashSet<_>>();
2730            assert_eq!(
2731                worktree_roots,
2732                vec![
2733                    path!("/dir1"),
2734                    path!("/dir2/b.txt"),
2735                    path!("/dir3"),
2736                    path!("/d.txt")
2737                ]
2738                .into_iter()
2739                .map(Path::new)
2740                .collect(),
2741            );
2742
2743            let visible_worktree_roots = workspace
2744                .visible_worktrees(cx)
2745                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2746                .collect::<HashSet<_>>();
2747            assert_eq!(
2748                visible_worktree_roots,
2749                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2750                    .into_iter()
2751                    .map(Path::new)
2752                    .collect(),
2753            );
2754
2755            assert_eq!(
2756                workspace
2757                    .active_pane()
2758                    .read(cx)
2759                    .active_item()
2760                    .unwrap()
2761                    .act_as::<Editor>(cx)
2762                    .unwrap()
2763                    .read(cx)
2764                    .title(cx),
2765                "d.txt"
2766            );
2767        });
2768    }
2769
2770    #[gpui::test]
2771    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2772        let app_state = init_test(cx);
2773        cx.update(|cx| {
2774            cx.update_global::<SettingsStore, _>(|store, cx| {
2775                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
2776                    project_settings.file_scan_exclusions =
2777                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2778                });
2779            });
2780        });
2781        app_state
2782            .fs
2783            .as_fake()
2784            .insert_tree(
2785                path!("/root"),
2786                json!({
2787                    ".gitignore": "ignored_dir\n",
2788                    ".git": {
2789                        "HEAD": "ref: refs/heads/main",
2790                    },
2791                    "regular_dir": {
2792                        "file": "regular file contents",
2793                    },
2794                    "ignored_dir": {
2795                        "ignored_subdir": {
2796                            "file": "ignored subfile contents",
2797                        },
2798                        "file": "ignored file contents",
2799                    },
2800                    "excluded_dir": {
2801                        "file": "excluded file contents",
2802                        "ignored_subdir": {
2803                            "file": "ignored subfile contents",
2804                        },
2805                    },
2806                }),
2807            )
2808            .await;
2809
2810        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2811        project.update(cx, |project, _cx| {
2812            project.languages().add(markdown_language())
2813        });
2814        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2815        let workspace = window.root(cx).unwrap();
2816
2817        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
2818        let paths_to_open = [
2819            PathBuf::from(path!("/root/excluded_dir/file")),
2820            PathBuf::from(path!("/root/.git/HEAD")),
2821            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
2822        ];
2823        let (opened_workspace, new_items) = cx
2824            .update(|cx| {
2825                workspace::open_paths(
2826                    &paths_to_open,
2827                    app_state,
2828                    workspace::OpenOptions::default(),
2829                    cx,
2830                )
2831            })
2832            .await
2833            .unwrap();
2834
2835        assert_eq!(
2836            opened_workspace.root(cx).unwrap().entity_id(),
2837            workspace.entity_id(),
2838            "Excluded files in subfolders of a workspace root should be opened in the workspace"
2839        );
2840        let mut opened_paths = cx.read(|cx| {
2841            assert_eq!(
2842                new_items.len(),
2843                paths_to_open.len(),
2844                "Expect to get the same number of opened items as submitted paths to open"
2845            );
2846            new_items
2847                .iter()
2848                .zip(paths_to_open.iter())
2849                .map(|(i, path)| {
2850                    match i {
2851                        Some(Ok(i)) => {
2852                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
2853                        }
2854                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
2855                        None => None,
2856                    }
2857                    .flatten()
2858                })
2859                .collect::<Vec<_>>()
2860        });
2861        opened_paths.sort();
2862        assert_eq!(
2863            opened_paths,
2864            vec![
2865                None,
2866                Some(separator!(".git/HEAD").to_string()),
2867                Some(separator!("excluded_dir/file").to_string()),
2868            ],
2869            "Excluded files should get opened, excluded dir should not get opened"
2870        );
2871
2872        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2873        assert_eq!(
2874            initial_entries, entries,
2875            "Workspace entries should not change after opening excluded files and directories paths"
2876        );
2877
2878        cx.read(|cx| {
2879                let pane = workspace.read(cx).active_pane().read(cx);
2880                let mut opened_buffer_paths = pane
2881                    .items()
2882                    .map(|i| {
2883                        i.project_path(cx)
2884                            .expect("all excluded files that got open should have a path")
2885                            .path
2886                            .display()
2887                            .to_string()
2888                    })
2889                    .collect::<Vec<_>>();
2890                opened_buffer_paths.sort();
2891                assert_eq!(
2892                    opened_buffer_paths,
2893                    vec![separator!(".git/HEAD").to_string(), separator!("excluded_dir/file").to_string()],
2894                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2895                );
2896            });
2897    }
2898
2899    #[gpui::test]
2900    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2901        let app_state = init_test(cx);
2902        app_state
2903            .fs
2904            .as_fake()
2905            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
2906            .await;
2907
2908        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2909        project.update(cx, |project, _cx| {
2910            project.languages().add(markdown_language())
2911        });
2912        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2913        let workspace = window.root(cx).unwrap();
2914
2915        // Open a file within an existing worktree.
2916        window
2917            .update(cx, |workspace, window, cx| {
2918                workspace.open_paths(
2919                    vec![PathBuf::from(path!("/root/a.txt"))],
2920                    OpenOptions {
2921                        visible: Some(OpenVisible::All),
2922                        ..Default::default()
2923                    },
2924                    None,
2925                    window,
2926                    cx,
2927                )
2928            })
2929            .unwrap()
2930            .await;
2931        let editor = cx.read(|cx| {
2932            let pane = workspace.read(cx).active_pane().read(cx);
2933            let item = pane.active_item().unwrap();
2934            item.downcast::<Editor>().unwrap()
2935        });
2936
2937        window
2938            .update(cx, |_, window, cx| {
2939                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
2940            })
2941            .unwrap();
2942
2943        app_state
2944            .fs
2945            .as_fake()
2946            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
2947            .await;
2948
2949        cx.run_until_parked();
2950        cx.read(|cx| assert!(editor.is_dirty(cx)));
2951        cx.read(|cx| assert!(editor.has_conflict(cx)));
2952
2953        let save_task = window
2954            .update(cx, |workspace, window, cx| {
2955                workspace.save_active_item(SaveIntent::Save, window, cx)
2956            })
2957            .unwrap();
2958        cx.background_executor.run_until_parked();
2959        cx.simulate_prompt_answer("Overwrite");
2960        save_task.await.unwrap();
2961        window
2962            .update(cx, |_, _, cx| {
2963                editor.update(cx, |editor, cx| {
2964                    assert!(!editor.is_dirty(cx));
2965                    assert!(!editor.has_conflict(cx));
2966                });
2967            })
2968            .unwrap();
2969    }
2970
2971    #[gpui::test]
2972    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2973        let app_state = init_test(cx);
2974        app_state
2975            .fs
2976            .create_dir(Path::new(path!("/root")))
2977            .await
2978            .unwrap();
2979
2980        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2981        project.update(cx, |project, _| {
2982            project.languages().add(markdown_language());
2983            project.languages().add(rust_lang());
2984        });
2985        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2986        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2987
2988        // Create a new untitled buffer
2989        cx.dispatch_action(window.into(), NewFile);
2990        let editor = window
2991            .read_with(cx, |workspace, cx| {
2992                workspace
2993                    .active_item(cx)
2994                    .unwrap()
2995                    .downcast::<Editor>()
2996                    .unwrap()
2997            })
2998            .unwrap();
2999
3000        window
3001            .update(cx, |_, window, cx| {
3002                editor.update(cx, |editor, cx| {
3003                    assert!(!editor.is_dirty(cx));
3004                    assert_eq!(editor.title(cx), "untitled");
3005                    assert!(Arc::ptr_eq(
3006                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3007                        &languages::PLAIN_TEXT
3008                    ));
3009                    editor.handle_input("hi", window, cx);
3010                    assert!(editor.is_dirty(cx));
3011                });
3012            })
3013            .unwrap();
3014
3015        // Save the buffer. This prompts for a filename.
3016        let save_task = window
3017            .update(cx, |workspace, window, cx| {
3018                workspace.save_active_item(SaveIntent::Save, window, cx)
3019            })
3020            .unwrap();
3021        cx.background_executor.run_until_parked();
3022        cx.simulate_new_path_selection(|parent_dir| {
3023            assert_eq!(parent_dir, Path::new(path!("/root")));
3024            Some(parent_dir.join("the-new-name.rs"))
3025        });
3026        cx.read(|cx| {
3027            assert!(editor.is_dirty(cx));
3028            assert_eq!(editor.read(cx).title(cx), "hi");
3029        });
3030
3031        // When the save completes, the buffer's title is updated and the language is assigned based
3032        // on the path.
3033        save_task.await.unwrap();
3034        window
3035            .update(cx, |_, _, cx| {
3036                editor.update(cx, |editor, cx| {
3037                    assert!(!editor.is_dirty(cx));
3038                    assert_eq!(editor.title(cx), "the-new-name.rs");
3039                    assert_eq!(
3040                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3041                        "Rust".into()
3042                    );
3043                });
3044            })
3045            .unwrap();
3046
3047        // Edit the file and save it again. This time, there is no filename prompt.
3048        window
3049            .update(cx, |_, window, cx| {
3050                editor.update(cx, |editor, cx| {
3051                    editor.handle_input(" there", window, cx);
3052                    assert!(editor.is_dirty(cx));
3053                });
3054            })
3055            .unwrap();
3056
3057        let save_task = window
3058            .update(cx, |workspace, window, cx| {
3059                workspace.save_active_item(SaveIntent::Save, window, cx)
3060            })
3061            .unwrap();
3062        save_task.await.unwrap();
3063
3064        assert!(!cx.did_prompt_for_new_path());
3065        window
3066            .update(cx, |_, _, cx| {
3067                editor.update(cx, |editor, cx| {
3068                    assert!(!editor.is_dirty(cx));
3069                    assert_eq!(editor.title(cx), "the-new-name.rs")
3070                });
3071            })
3072            .unwrap();
3073
3074        // Open the same newly-created file in another pane item. The new editor should reuse
3075        // the same buffer.
3076        cx.dispatch_action(window.into(), NewFile);
3077        window
3078            .update(cx, |workspace, window, cx| {
3079                workspace.split_and_clone(
3080                    workspace.active_pane().clone(),
3081                    SplitDirection::Right,
3082                    window,
3083                    cx,
3084                );
3085                workspace.open_path(
3086                    (worktree.read(cx).id(), "the-new-name.rs"),
3087                    None,
3088                    true,
3089                    window,
3090                    cx,
3091                )
3092            })
3093            .unwrap()
3094            .await
3095            .unwrap();
3096        let editor2 = window
3097            .update(cx, |workspace, _, cx| {
3098                workspace
3099                    .active_item(cx)
3100                    .unwrap()
3101                    .downcast::<Editor>()
3102                    .unwrap()
3103            })
3104            .unwrap();
3105        cx.read(|cx| {
3106            assert_eq!(
3107                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3108                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3109            );
3110        })
3111    }
3112
3113    #[gpui::test]
3114    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3115        let app_state = init_test(cx);
3116        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3117
3118        let project = Project::test(app_state.fs.clone(), [], cx).await;
3119        project.update(cx, |project, _| {
3120            project.languages().add(rust_lang());
3121            project.languages().add(markdown_language());
3122        });
3123        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3124
3125        // Create a new untitled buffer
3126        cx.dispatch_action(window.into(), NewFile);
3127        let editor = window
3128            .read_with(cx, |workspace, cx| {
3129                workspace
3130                    .active_item(cx)
3131                    .unwrap()
3132                    .downcast::<Editor>()
3133                    .unwrap()
3134            })
3135            .unwrap();
3136        window
3137            .update(cx, |_, window, cx| {
3138                editor.update(cx, |editor, cx| {
3139                    assert!(Arc::ptr_eq(
3140                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3141                        &languages::PLAIN_TEXT
3142                    ));
3143                    editor.handle_input("hi", window, cx);
3144                    assert!(editor.is_dirty(cx));
3145                });
3146            })
3147            .unwrap();
3148
3149        // Save the buffer. This prompts for a filename.
3150        let save_task = window
3151            .update(cx, |workspace, window, cx| {
3152                workspace.save_active_item(SaveIntent::Save, window, cx)
3153            })
3154            .unwrap();
3155        cx.background_executor.run_until_parked();
3156        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3157        save_task.await.unwrap();
3158        // The buffer is not dirty anymore and the language is assigned based on the path.
3159        window
3160            .update(cx, |_, _, cx| {
3161                editor.update(cx, |editor, cx| {
3162                    assert!(!editor.is_dirty(cx));
3163                    assert_eq!(
3164                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3165                        "Rust".into()
3166                    )
3167                });
3168            })
3169            .unwrap();
3170    }
3171
3172    #[gpui::test]
3173    async fn test_pane_actions(cx: &mut TestAppContext) {
3174        let app_state = init_test(cx);
3175        app_state
3176            .fs
3177            .as_fake()
3178            .insert_tree(
3179                path!("/root"),
3180                json!({
3181                    "a": {
3182                        "file1": "contents 1",
3183                        "file2": "contents 2",
3184                        "file3": "contents 3",
3185                    },
3186                }),
3187            )
3188            .await;
3189
3190        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3191        project.update(cx, |project, _cx| {
3192            project.languages().add(markdown_language())
3193        });
3194        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3195        let workspace = window.root(cx).unwrap();
3196
3197        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3198        let file1 = entries[0].clone();
3199
3200        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3201
3202        window
3203            .update(cx, |w, window, cx| {
3204                w.open_path(file1.clone(), None, true, window, cx)
3205            })
3206            .unwrap()
3207            .await
3208            .unwrap();
3209
3210        let (editor_1, buffer) = window
3211            .update(cx, |_, window, cx| {
3212                pane_1.update(cx, |pane_1, cx| {
3213                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3214                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3215                    let buffer = editor.update(cx, |editor, cx| {
3216                        editor.insert("dirt", window, cx);
3217                        editor.buffer().downgrade()
3218                    });
3219                    (editor.downgrade(), buffer)
3220                })
3221            })
3222            .unwrap();
3223
3224        cx.dispatch_action(window.into(), pane::SplitRight);
3225        let editor_2 = cx.update(|cx| {
3226            let pane_2 = workspace.read(cx).active_pane().clone();
3227            assert_ne!(pane_1, pane_2);
3228
3229            let pane2_item = pane_2.read(cx).active_item().unwrap();
3230            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3231
3232            pane2_item.downcast::<Editor>().unwrap().downgrade()
3233        });
3234        cx.dispatch_action(
3235            window.into(),
3236            workspace::CloseActiveItem {
3237                save_intent: None,
3238                close_pinned: false,
3239            },
3240        );
3241
3242        cx.background_executor.run_until_parked();
3243        window
3244            .read_with(cx, |workspace, _| {
3245                assert_eq!(workspace.panes().len(), 1);
3246                assert_eq!(workspace.active_pane(), &pane_1);
3247            })
3248            .unwrap();
3249
3250        cx.dispatch_action(
3251            window.into(),
3252            workspace::CloseActiveItem {
3253                save_intent: None,
3254                close_pinned: false,
3255            },
3256        );
3257        cx.background_executor.run_until_parked();
3258        cx.simulate_prompt_answer("Don't Save");
3259        cx.background_executor.run_until_parked();
3260
3261        window
3262            .update(cx, |workspace, _, cx| {
3263                assert_eq!(workspace.panes().len(), 1);
3264                assert!(workspace.active_item(cx).is_none());
3265            })
3266            .unwrap();
3267
3268        cx.background_executor
3269            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3270        cx.update(|_| {});
3271        editor_1.assert_released();
3272        editor_2.assert_released();
3273        buffer.assert_released();
3274    }
3275
3276    #[gpui::test]
3277    async fn test_navigation(cx: &mut TestAppContext) {
3278        let app_state = init_test(cx);
3279        app_state
3280            .fs
3281            .as_fake()
3282            .insert_tree(
3283                path!("/root"),
3284                json!({
3285                    "a": {
3286                        "file1": "contents 1\n".repeat(20),
3287                        "file2": "contents 2\n".repeat(20),
3288                        "file3": "contents 3\n".repeat(20),
3289                    },
3290                }),
3291            )
3292            .await;
3293
3294        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3295        project.update(cx, |project, _cx| {
3296            project.languages().add(markdown_language())
3297        });
3298        let workspace =
3299            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3300        let pane = workspace
3301            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3302            .unwrap();
3303
3304        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3305        let file1 = entries[0].clone();
3306        let file2 = entries[1].clone();
3307        let file3 = entries[2].clone();
3308
3309        let editor1 = workspace
3310            .update(cx, |w, window, cx| {
3311                w.open_path(file1.clone(), None, true, window, cx)
3312            })
3313            .unwrap()
3314            .await
3315            .unwrap()
3316            .downcast::<Editor>()
3317            .unwrap();
3318        workspace
3319            .update(cx, |_, window, cx| {
3320                editor1.update(cx, |editor, cx| {
3321                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3322                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3323                            ..DisplayPoint::new(DisplayRow(10), 0)])
3324                    });
3325                });
3326            })
3327            .unwrap();
3328
3329        let editor2 = workspace
3330            .update(cx, |w, window, cx| {
3331                w.open_path(file2.clone(), None, true, window, cx)
3332            })
3333            .unwrap()
3334            .await
3335            .unwrap()
3336            .downcast::<Editor>()
3337            .unwrap();
3338        let editor3 = workspace
3339            .update(cx, |w, window, cx| {
3340                w.open_path(file3.clone(), None, true, window, cx)
3341            })
3342            .unwrap()
3343            .await
3344            .unwrap()
3345            .downcast::<Editor>()
3346            .unwrap();
3347
3348        workspace
3349            .update(cx, |_, window, cx| {
3350                editor3.update(cx, |editor, cx| {
3351                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3352                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3353                            ..DisplayPoint::new(DisplayRow(12), 0)])
3354                    });
3355                    editor.newline(&Default::default(), window, cx);
3356                    editor.newline(&Default::default(), window, cx);
3357                    editor.move_down(&Default::default(), window, cx);
3358                    editor.move_down(&Default::default(), window, cx);
3359                    editor.save(true, project.clone(), window, cx)
3360                })
3361            })
3362            .unwrap()
3363            .await
3364            .unwrap();
3365        workspace
3366            .update(cx, |_, window, cx| {
3367                editor3.update(cx, |editor, cx| {
3368                    editor.set_scroll_position(point(0., 12.5), window, cx)
3369                });
3370            })
3371            .unwrap();
3372        assert_eq!(
3373            active_location(&workspace, cx),
3374            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3375        );
3376
3377        workspace
3378            .update(cx, |w, window, cx| {
3379                w.go_back(w.active_pane().downgrade(), window, cx)
3380            })
3381            .unwrap()
3382            .await
3383            .unwrap();
3384        assert_eq!(
3385            active_location(&workspace, cx),
3386            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3387        );
3388
3389        workspace
3390            .update(cx, |w, window, cx| {
3391                w.go_back(w.active_pane().downgrade(), window, cx)
3392            })
3393            .unwrap()
3394            .await
3395            .unwrap();
3396        assert_eq!(
3397            active_location(&workspace, cx),
3398            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3399        );
3400
3401        workspace
3402            .update(cx, |w, window, cx| {
3403                w.go_back(w.active_pane().downgrade(), window, cx)
3404            })
3405            .unwrap()
3406            .await
3407            .unwrap();
3408        assert_eq!(
3409            active_location(&workspace, cx),
3410            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3411        );
3412
3413        workspace
3414            .update(cx, |w, window, cx| {
3415                w.go_back(w.active_pane().downgrade(), window, cx)
3416            })
3417            .unwrap()
3418            .await
3419            .unwrap();
3420        assert_eq!(
3421            active_location(&workspace, cx),
3422            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3423        );
3424
3425        // Go back one more time and ensure we don't navigate past the first item in the history.
3426        workspace
3427            .update(cx, |w, window, cx| {
3428                w.go_back(w.active_pane().downgrade(), window, cx)
3429            })
3430            .unwrap()
3431            .await
3432            .unwrap();
3433        assert_eq!(
3434            active_location(&workspace, cx),
3435            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3436        );
3437
3438        workspace
3439            .update(cx, |w, window, cx| {
3440                w.go_forward(w.active_pane().downgrade(), window, cx)
3441            })
3442            .unwrap()
3443            .await
3444            .unwrap();
3445        assert_eq!(
3446            active_location(&workspace, cx),
3447            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3448        );
3449
3450        workspace
3451            .update(cx, |w, window, cx| {
3452                w.go_forward(w.active_pane().downgrade(), window, cx)
3453            })
3454            .unwrap()
3455            .await
3456            .unwrap();
3457        assert_eq!(
3458            active_location(&workspace, cx),
3459            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3460        );
3461
3462        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3463        // location.
3464        workspace
3465            .update(cx, |_, window, cx| {
3466                pane.update(cx, |pane, cx| {
3467                    let editor3_id = editor3.entity_id();
3468                    drop(editor3);
3469                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3470                })
3471            })
3472            .unwrap()
3473            .await
3474            .unwrap();
3475        workspace
3476            .update(cx, |w, window, cx| {
3477                w.go_forward(w.active_pane().downgrade(), window, cx)
3478            })
3479            .unwrap()
3480            .await
3481            .unwrap();
3482        assert_eq!(
3483            active_location(&workspace, cx),
3484            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3485        );
3486
3487        workspace
3488            .update(cx, |w, window, cx| {
3489                w.go_forward(w.active_pane().downgrade(), window, cx)
3490            })
3491            .unwrap()
3492            .await
3493            .unwrap();
3494        assert_eq!(
3495            active_location(&workspace, cx),
3496            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3497        );
3498
3499        workspace
3500            .update(cx, |w, window, cx| {
3501                w.go_back(w.active_pane().downgrade(), window, cx)
3502            })
3503            .unwrap()
3504            .await
3505            .unwrap();
3506        assert_eq!(
3507            active_location(&workspace, cx),
3508            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3509        );
3510
3511        // Go back to an item that has been closed and removed from disk
3512        workspace
3513            .update(cx, |_, window, cx| {
3514                pane.update(cx, |pane, cx| {
3515                    let editor2_id = editor2.entity_id();
3516                    drop(editor2);
3517                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3518                })
3519            })
3520            .unwrap()
3521            .await
3522            .unwrap();
3523        app_state
3524            .fs
3525            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3526            .await
3527            .unwrap();
3528        cx.background_executor.run_until_parked();
3529
3530        workspace
3531            .update(cx, |w, window, cx| {
3532                w.go_back(w.active_pane().downgrade(), window, cx)
3533            })
3534            .unwrap()
3535            .await
3536            .unwrap();
3537        assert_eq!(
3538            active_location(&workspace, cx),
3539            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3540        );
3541        workspace
3542            .update(cx, |w, window, cx| {
3543                w.go_forward(w.active_pane().downgrade(), window, cx)
3544            })
3545            .unwrap()
3546            .await
3547            .unwrap();
3548        assert_eq!(
3549            active_location(&workspace, cx),
3550            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3551        );
3552
3553        // Modify file to collapse multiple nav history entries into the same location.
3554        // Ensure we don't visit the same location twice when navigating.
3555        workspace
3556            .update(cx, |_, window, cx| {
3557                editor1.update(cx, |editor, cx| {
3558                    editor.change_selections(None, window, cx, |s| {
3559                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
3560                            ..DisplayPoint::new(DisplayRow(15), 0)])
3561                    })
3562                });
3563            })
3564            .unwrap();
3565        for _ in 0..5 {
3566            workspace
3567                .update(cx, |_, window, cx| {
3568                    editor1.update(cx, |editor, cx| {
3569                        editor.change_selections(None, window, cx, |s| {
3570                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
3571                                ..DisplayPoint::new(DisplayRow(3), 0)])
3572                        });
3573                    });
3574                })
3575                .unwrap();
3576
3577            workspace
3578                .update(cx, |_, window, cx| {
3579                    editor1.update(cx, |editor, cx| {
3580                        editor.change_selections(None, window, cx, |s| {
3581                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3582                                ..DisplayPoint::new(DisplayRow(13), 0)])
3583                        })
3584                    });
3585                })
3586                .unwrap();
3587        }
3588        workspace
3589            .update(cx, |_, window, cx| {
3590                editor1.update(cx, |editor, cx| {
3591                    editor.transact(window, cx, |editor, window, cx| {
3592                        editor.change_selections(None, window, cx, |s| {
3593                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3594                                ..DisplayPoint::new(DisplayRow(14), 0)])
3595                        });
3596                        editor.insert("", window, cx);
3597                    })
3598                });
3599            })
3600            .unwrap();
3601
3602        workspace
3603            .update(cx, |_, window, cx| {
3604                editor1.update(cx, |editor, cx| {
3605                    editor.change_selections(None, window, cx, |s| {
3606                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3607                            ..DisplayPoint::new(DisplayRow(1), 0)])
3608                    })
3609                });
3610            })
3611            .unwrap();
3612        workspace
3613            .update(cx, |w, window, cx| {
3614                w.go_back(w.active_pane().downgrade(), window, cx)
3615            })
3616            .unwrap()
3617            .await
3618            .unwrap();
3619        assert_eq!(
3620            active_location(&workspace, cx),
3621            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
3622        );
3623        workspace
3624            .update(cx, |w, window, cx| {
3625                w.go_back(w.active_pane().downgrade(), window, cx)
3626            })
3627            .unwrap()
3628            .await
3629            .unwrap();
3630        assert_eq!(
3631            active_location(&workspace, cx),
3632            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3633        );
3634
3635        fn active_location(
3636            workspace: &WindowHandle<Workspace>,
3637            cx: &mut TestAppContext,
3638        ) -> (ProjectPath, DisplayPoint, f32) {
3639            workspace
3640                .update(cx, |workspace, _, cx| {
3641                    let item = workspace.active_item(cx).unwrap();
3642                    let editor = item.downcast::<Editor>().unwrap();
3643                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3644                        (
3645                            editor.selections.display_ranges(cx),
3646                            editor.scroll_position(cx),
3647                        )
3648                    });
3649                    (
3650                        item.project_path(cx).unwrap(),
3651                        selections[0].start,
3652                        scroll_position.y,
3653                    )
3654                })
3655                .unwrap()
3656        }
3657    }
3658
3659    #[gpui::test]
3660    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3661        let app_state = init_test(cx);
3662        app_state
3663            .fs
3664            .as_fake()
3665            .insert_tree(
3666                path!("/root"),
3667                json!({
3668                    "a": {
3669                        "file1": "",
3670                        "file2": "",
3671                        "file3": "",
3672                        "file4": "",
3673                    },
3674                }),
3675            )
3676            .await;
3677
3678        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3679        project.update(cx, |project, _cx| {
3680            project.languages().add(markdown_language())
3681        });
3682        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3683        let pane = workspace
3684            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3685            .unwrap();
3686
3687        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3688        let file1 = entries[0].clone();
3689        let file2 = entries[1].clone();
3690        let file3 = entries[2].clone();
3691        let file4 = entries[3].clone();
3692
3693        let file1_item_id = workspace
3694            .update(cx, |w, window, cx| {
3695                w.open_path(file1.clone(), None, true, window, cx)
3696            })
3697            .unwrap()
3698            .await
3699            .unwrap()
3700            .item_id();
3701        let file2_item_id = workspace
3702            .update(cx, |w, window, cx| {
3703                w.open_path(file2.clone(), None, true, window, cx)
3704            })
3705            .unwrap()
3706            .await
3707            .unwrap()
3708            .item_id();
3709        let file3_item_id = workspace
3710            .update(cx, |w, window, cx| {
3711                w.open_path(file3.clone(), None, true, window, cx)
3712            })
3713            .unwrap()
3714            .await
3715            .unwrap()
3716            .item_id();
3717        let file4_item_id = workspace
3718            .update(cx, |w, window, cx| {
3719                w.open_path(file4.clone(), None, true, window, cx)
3720            })
3721            .unwrap()
3722            .await
3723            .unwrap()
3724            .item_id();
3725        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3726
3727        // Close all the pane items in some arbitrary order.
3728        workspace
3729            .update(cx, |_, window, cx| {
3730                pane.update(cx, |pane, cx| {
3731                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3732                })
3733            })
3734            .unwrap()
3735            .await
3736            .unwrap();
3737        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3738
3739        workspace
3740            .update(cx, |_, window, cx| {
3741                pane.update(cx, |pane, cx| {
3742                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
3743                })
3744            })
3745            .unwrap()
3746            .await
3747            .unwrap();
3748        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3749
3750        workspace
3751            .update(cx, |_, window, cx| {
3752                pane.update(cx, |pane, cx| {
3753                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3754                })
3755            })
3756            .unwrap()
3757            .await
3758            .unwrap();
3759        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3760        workspace
3761            .update(cx, |_, window, cx| {
3762                pane.update(cx, |pane, cx| {
3763                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3764                })
3765            })
3766            .unwrap()
3767            .await
3768            .unwrap();
3769
3770        assert_eq!(active_path(&workspace, cx), None);
3771
3772        // Reopen all the closed items, ensuring they are reopened in the same order
3773        // in which they were closed.
3774        workspace
3775            .update(cx, Workspace::reopen_closed_item)
3776            .unwrap()
3777            .await
3778            .unwrap();
3779        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3780
3781        workspace
3782            .update(cx, Workspace::reopen_closed_item)
3783            .unwrap()
3784            .await
3785            .unwrap();
3786        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3787
3788        workspace
3789            .update(cx, Workspace::reopen_closed_item)
3790            .unwrap()
3791            .await
3792            .unwrap();
3793        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3794
3795        workspace
3796            .update(cx, Workspace::reopen_closed_item)
3797            .unwrap()
3798            .await
3799            .unwrap();
3800        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3801
3802        // Reopening past the last closed item is a no-op.
3803        workspace
3804            .update(cx, Workspace::reopen_closed_item)
3805            .unwrap()
3806            .await
3807            .unwrap();
3808        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3809
3810        // Reopening closed items doesn't interfere with navigation history.
3811        workspace
3812            .update(cx, |workspace, window, cx| {
3813                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3814            })
3815            .unwrap()
3816            .await
3817            .unwrap();
3818        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3819
3820        workspace
3821            .update(cx, |workspace, window, cx| {
3822                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3823            })
3824            .unwrap()
3825            .await
3826            .unwrap();
3827        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3828
3829        workspace
3830            .update(cx, |workspace, window, cx| {
3831                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3832            })
3833            .unwrap()
3834            .await
3835            .unwrap();
3836        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3837
3838        workspace
3839            .update(cx, |workspace, window, cx| {
3840                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3841            })
3842            .unwrap()
3843            .await
3844            .unwrap();
3845        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3846
3847        workspace
3848            .update(cx, |workspace, window, cx| {
3849                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3850            })
3851            .unwrap()
3852            .await
3853            .unwrap();
3854        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3855
3856        workspace
3857            .update(cx, |workspace, window, cx| {
3858                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3859            })
3860            .unwrap()
3861            .await
3862            .unwrap();
3863        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3864
3865        workspace
3866            .update(cx, |workspace, window, cx| {
3867                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3868            })
3869            .unwrap()
3870            .await
3871            .unwrap();
3872        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3873
3874        workspace
3875            .update(cx, |workspace, window, cx| {
3876                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3877            })
3878            .unwrap()
3879            .await
3880            .unwrap();
3881        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3882
3883        fn active_path(
3884            workspace: &WindowHandle<Workspace>,
3885            cx: &TestAppContext,
3886        ) -> Option<ProjectPath> {
3887            workspace
3888                .read_with(cx, |workspace, cx| {
3889                    let item = workspace.active_item(cx)?;
3890                    item.project_path(cx)
3891                })
3892                .unwrap()
3893        }
3894    }
3895
3896    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
3897        cx.update(|cx| {
3898            let app_state = AppState::test(cx);
3899
3900            theme::init(theme::LoadThemes::JustBase, cx);
3901            client::init(&app_state.client, cx);
3902            language::init(cx);
3903            workspace::init(app_state.clone(), cx);
3904            welcome::init(cx);
3905            Project::init_settings(cx);
3906            app_state
3907        })
3908    }
3909
3910    #[gpui::test]
3911    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
3912        let executor = cx.executor();
3913        let app_state = init_keymap_test(cx);
3914        let project = Project::test(app_state.fs.clone(), [], cx).await;
3915        let workspace =
3916            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3917
3918        actions!(test1, [A, B]);
3919        // From the Atom keymap
3920        use workspace::ActivatePreviousPane;
3921        // From the JetBrains keymap
3922        use workspace::ActivatePreviousItem;
3923
3924        app_state
3925            .fs
3926            .save(
3927                "/settings.json".as_ref(),
3928                &r#"{"base_keymap": "Atom"}"#.into(),
3929                Default::default(),
3930            )
3931            .await
3932            .unwrap();
3933
3934        app_state
3935            .fs
3936            .save(
3937                "/keymap.json".as_ref(),
3938                &r#"[{"bindings": {"backspace": "test1::A"}}]"#.into(),
3939                Default::default(),
3940            )
3941            .await
3942            .unwrap();
3943        executor.run_until_parked();
3944        cx.update(|cx| {
3945            let settings_rx = watch_config_file(
3946                &executor,
3947                app_state.fs.clone(),
3948                PathBuf::from("/settings.json"),
3949            );
3950            let keymap_rx = watch_config_file(
3951                &executor,
3952                app_state.fs.clone(),
3953                PathBuf::from("/keymap.json"),
3954            );
3955            let global_settings_rx = watch_config_file(
3956                &executor,
3957                app_state.fs.clone(),
3958                PathBuf::from("/global_settings.json"),
3959            );
3960            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
3961            handle_keymap_file_changes(keymap_rx, cx);
3962        });
3963        workspace
3964            .update(cx, |workspace, _, cx| {
3965                workspace.register_action(|_, _: &A, _window, _cx| {});
3966                workspace.register_action(|_, _: &B, _window, _cx| {});
3967                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
3968                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
3969                cx.notify();
3970            })
3971            .unwrap();
3972        executor.run_until_parked();
3973        // Test loading the keymap base at all
3974        assert_key_bindings_for(
3975            workspace.into(),
3976            cx,
3977            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3978            line!(),
3979        );
3980
3981        // Test modifying the users keymap, while retaining the base keymap
3982        app_state
3983            .fs
3984            .save(
3985                "/keymap.json".as_ref(),
3986                &r#"[{"bindings": {"backspace": "test1::B"}}]"#.into(),
3987                Default::default(),
3988            )
3989            .await
3990            .unwrap();
3991
3992        executor.run_until_parked();
3993
3994        assert_key_bindings_for(
3995            workspace.into(),
3996            cx,
3997            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
3998            line!(),
3999        );
4000
4001        // Test modifying the base, while retaining the users keymap
4002        app_state
4003            .fs
4004            .save(
4005                "/settings.json".as_ref(),
4006                &r#"{"base_keymap": "JetBrains"}"#.into(),
4007                Default::default(),
4008            )
4009            .await
4010            .unwrap();
4011
4012        executor.run_until_parked();
4013
4014        assert_key_bindings_for(
4015            workspace.into(),
4016            cx,
4017            vec![("backspace", &B), ("{", &ActivatePreviousItem)],
4018            line!(),
4019        );
4020    }
4021
4022    #[gpui::test]
4023    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4024        let executor = cx.executor();
4025        let app_state = init_keymap_test(cx);
4026        let project = Project::test(app_state.fs.clone(), [], cx).await;
4027        let workspace =
4028            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4029
4030        actions!(test2, [A, B]);
4031        // From the Atom keymap
4032        use workspace::ActivatePreviousPane;
4033        // From the JetBrains keymap
4034        use diagnostics::Deploy;
4035
4036        workspace
4037            .update(cx, |workspace, _, _| {
4038                workspace.register_action(|_, _: &A, _window, _cx| {});
4039                workspace.register_action(|_, _: &B, _window, _cx| {});
4040                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4041            })
4042            .unwrap();
4043        app_state
4044            .fs
4045            .save(
4046                "/settings.json".as_ref(),
4047                &r#"{"base_keymap": "Atom"}"#.into(),
4048                Default::default(),
4049            )
4050            .await
4051            .unwrap();
4052        app_state
4053            .fs
4054            .save(
4055                "/keymap.json".as_ref(),
4056                &r#"[{"bindings": {"backspace": "test2::A"}}]"#.into(),
4057                Default::default(),
4058            )
4059            .await
4060            .unwrap();
4061
4062        cx.update(|cx| {
4063            let settings_rx = watch_config_file(
4064                &executor,
4065                app_state.fs.clone(),
4066                PathBuf::from("/settings.json"),
4067            );
4068            let keymap_rx = watch_config_file(
4069                &executor,
4070                app_state.fs.clone(),
4071                PathBuf::from("/keymap.json"),
4072            );
4073
4074            let global_settings_rx = watch_config_file(
4075                &executor,
4076                app_state.fs.clone(),
4077                PathBuf::from("/global_settings.json"),
4078            );
4079            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4080            handle_keymap_file_changes(keymap_rx, cx);
4081        });
4082
4083        cx.background_executor.run_until_parked();
4084
4085        cx.background_executor.run_until_parked();
4086        // Test loading the keymap base at all
4087        assert_key_bindings_for(
4088            workspace.into(),
4089            cx,
4090            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
4091            line!(),
4092        );
4093
4094        // Test disabling the key binding for the base keymap
4095        app_state
4096            .fs
4097            .save(
4098                "/keymap.json".as_ref(),
4099                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4100                Default::default(),
4101            )
4102            .await
4103            .unwrap();
4104
4105        cx.background_executor.run_until_parked();
4106
4107        assert_key_bindings_for(
4108            workspace.into(),
4109            cx,
4110            vec![("k", &ActivatePreviousPane)],
4111            line!(),
4112        );
4113
4114        // Test modifying the base, while retaining the users keymap
4115        app_state
4116            .fs
4117            .save(
4118                "/settings.json".as_ref(),
4119                &r#"{"base_keymap": "JetBrains"}"#.into(),
4120                Default::default(),
4121            )
4122            .await
4123            .unwrap();
4124
4125        cx.background_executor.run_until_parked();
4126
4127        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4128    }
4129
4130    #[gpui::test]
4131    async fn test_generate_keymap_json_schema_for_registered_actions(
4132        cx: &mut gpui::TestAppContext,
4133    ) {
4134        init_keymap_test(cx);
4135        cx.update(|cx| {
4136            // Make sure it doesn't panic.
4137            KeymapFile::generate_json_schema_for_registered_actions(cx);
4138        });
4139    }
4140
4141    /// Actions that don't build from empty input won't work from command palette invocation.
4142    #[gpui::test]
4143    async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4144        init_keymap_test(cx);
4145        cx.update(|cx| {
4146            let all_actions = cx.all_action_names();
4147            let mut failing_names = Vec::new();
4148            let mut errors = Vec::new();
4149            for action in all_actions {
4150                match action.to_string().as_str() {
4151                    "vim::FindCommand"
4152                    | "vim::Literal"
4153                    | "vim::ResizePane"
4154                    | "vim::PushObject"
4155                    | "vim::PushFindForward"
4156                    | "vim::PushFindBackward"
4157                    | "vim::PushSneak"
4158                    | "vim::PushSneakBackward"
4159                    | "vim::PushChangeSurrounds"
4160                    | "vim::PushJump"
4161                    | "vim::PushDigraph"
4162                    | "vim::PushLiteral"
4163                    | "vim::Number"
4164                    | "vim::SelectRegister"
4165                    | "git::StageAndNext"
4166                    | "git::UnstageAndNext"
4167                    | "terminal::SendText"
4168                    | "terminal::SendKeystroke"
4169                    | "app_menu::OpenApplicationMenu"
4170                    | "picker::ConfirmInput"
4171                    | "editor::HandleInput"
4172                    | "editor::FoldAtLevel"
4173                    | "pane::ActivateItem"
4174                    | "workspace::ActivatePane"
4175                    | "workspace::MoveItemToPane"
4176                    | "workspace::MoveItemToPaneInDirection"
4177                    | "workspace::OpenTerminal"
4178                    | "workspace::SendKeystrokes"
4179                    | "zed::OpenBrowser"
4180                    | "zed::OpenZedUrl" => {}
4181                    _ => {
4182                        let result = cx.build_action(action, None);
4183                        match &result {
4184                            Ok(_) => {}
4185                            Err(err) => {
4186                                failing_names.push(action);
4187                                errors.push(format!("{action} failed to build: {err:?}"));
4188                            }
4189                        }
4190                    }
4191                }
4192            }
4193            if errors.len() > 0 {
4194                panic!(
4195                    "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4196                    failing_names,
4197                    errors.join("\n")
4198                );
4199            }
4200        });
4201    }
4202
4203    #[gpui::test]
4204    fn test_bundled_settings_and_themes(cx: &mut App) {
4205        cx.text_system()
4206            .add_fonts(vec![
4207                Assets
4208                    .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
4209                    .unwrap()
4210                    .unwrap(),
4211                Assets
4212                    .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
4213                    .unwrap()
4214                    .unwrap(),
4215            ])
4216            .unwrap();
4217        let themes = ThemeRegistry::default();
4218        settings::init(cx);
4219        theme::init(theme::LoadThemes::JustBase, cx);
4220
4221        let mut has_default_theme = false;
4222        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4223            let theme = themes.get(&theme_name).unwrap();
4224            assert_eq!(theme.name, theme_name);
4225            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4226                has_default_theme = true;
4227            }
4228        }
4229        assert!(has_default_theme);
4230    }
4231
4232    #[gpui::test]
4233    async fn test_bundled_languages(cx: &mut TestAppContext) {
4234        env_logger::builder().is_test(true).try_init().ok();
4235        let settings = cx.update(SettingsStore::test);
4236        cx.set_global(settings);
4237        let languages = LanguageRegistry::test(cx.executor());
4238        let languages = Arc::new(languages);
4239        let node_runtime = node_runtime::NodeRuntime::unavailable();
4240        cx.update(|cx| {
4241            languages::init(languages.clone(), node_runtime, cx);
4242        });
4243        for name in languages.language_names() {
4244            languages
4245                .language_for_name(&name)
4246                .await
4247                .with_context(|| format!("language name {name}"))
4248                .unwrap();
4249        }
4250        cx.run_until_parked();
4251    }
4252
4253    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4254        init_test_with_state(cx, cx.update(AppState::test))
4255    }
4256
4257    fn init_test_with_state(
4258        cx: &mut TestAppContext,
4259        mut app_state: Arc<AppState>,
4260    ) -> Arc<AppState> {
4261        cx.update(move |cx| {
4262            env_logger::builder().is_test(true).try_init().ok();
4263
4264            let state = Arc::get_mut(&mut app_state).unwrap();
4265            state.build_window_options = build_window_options;
4266
4267            app_state.languages.add(markdown_language());
4268
4269            gpui_tokio::init(cx);
4270            vim_mode_setting::init(cx);
4271            theme::init(theme::LoadThemes::JustBase, cx);
4272            audio::init((), cx);
4273            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4274            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4275            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4276            workspace::init(app_state.clone(), cx);
4277            Project::init_settings(cx);
4278            release_channel::init(SemanticVersion::default(), cx);
4279            command_palette::init(cx);
4280            language::init(cx);
4281            editor::init(cx);
4282            collab_ui::init(&app_state, cx);
4283            git_ui::init(cx);
4284            project_panel::init(cx);
4285            outline_panel::init(cx);
4286            terminal_view::init(cx);
4287            copilot::copilot_chat::init(app_state.fs.clone(), app_state.client.http_client(), cx);
4288            image_viewer::init(cx);
4289            language_model::init(app_state.client.clone(), cx);
4290            language_models::init(
4291                app_state.user_store.clone(),
4292                app_state.client.clone(),
4293                app_state.fs.clone(),
4294                cx,
4295            );
4296            web_search::init(cx);
4297            web_search_providers::init(app_state.client.clone(), cx);
4298            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4299            agent::init(
4300                app_state.fs.clone(),
4301                app_state.client.clone(),
4302                prompt_builder.clone(),
4303                app_state.languages.clone(),
4304                false,
4305                cx,
4306            );
4307            repl::init(app_state.fs.clone(), cx);
4308            repl::notebook::init(cx);
4309            tasks_ui::init(cx);
4310            project::debugger::breakpoint_store::BreakpointStore::init(
4311                &app_state.client.clone().into(),
4312            );
4313            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4314            debugger_ui::init(cx);
4315            initialize_workspace(app_state.clone(), prompt_builder, cx);
4316            search::init(cx);
4317            app_state
4318        })
4319    }
4320
4321    fn rust_lang() -> Arc<language::Language> {
4322        Arc::new(language::Language::new(
4323            language::LanguageConfig {
4324                name: "Rust".into(),
4325                matcher: LanguageMatcher {
4326                    path_suffixes: vec!["rs".to_string()],
4327                    ..Default::default()
4328                },
4329                ..Default::default()
4330            },
4331            Some(tree_sitter_rust::LANGUAGE.into()),
4332        ))
4333    }
4334
4335    fn markdown_language() -> Arc<language::Language> {
4336        Arc::new(language::Language::new(
4337            language::LanguageConfig {
4338                name: "Markdown".into(),
4339                matcher: LanguageMatcher {
4340                    path_suffixes: vec!["md".to_string()],
4341                    ..Default::default()
4342                },
4343                ..Default::default()
4344            },
4345            Some(tree_sitter_md::LANGUAGE.into()),
4346        ))
4347    }
4348
4349    #[track_caller]
4350    fn assert_key_bindings_for(
4351        window: AnyWindowHandle,
4352        cx: &TestAppContext,
4353        actions: Vec<(&'static str, &dyn Action)>,
4354        line: u32,
4355    ) {
4356        let available_actions = cx
4357            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4358            .unwrap();
4359        for (key, action) in actions {
4360            let bindings = cx
4361                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4362                .unwrap();
4363            // assert that...
4364            assert!(
4365                available_actions.iter().any(|bound_action| {
4366                    // actions match...
4367                    bound_action.partial_eq(action)
4368                }),
4369                "On {} Failed to find {}",
4370                line,
4371                action.name(),
4372            );
4373            assert!(
4374                // and key strokes contain the given key
4375                bindings
4376                    .into_iter()
4377                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
4378                "On {} Failed to find {} with key binding {}",
4379                line,
4380                action.name(),
4381                key
4382            );
4383        }
4384    }
4385
4386    #[gpui::test]
4387    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
4388        // Use the proper initialization for runtime state
4389        let app_state = init_keymap_test(cx);
4390
4391        eprintln!("Running test_opening_project_settings_when_excluded");
4392
4393        // 1. Set up a project with some project settings
4394        let settings_init =
4395            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
4396        app_state
4397            .fs
4398            .as_fake()
4399            .insert_tree(
4400                Path::new("/root"),
4401                json!({
4402                    ".zed": {
4403                        "settings.json": settings_init
4404                    }
4405                }),
4406            )
4407            .await;
4408
4409        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
4410
4411        // 2. Create a project with the file system and load it
4412        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
4413
4414        // Save original settings content for comparison
4415        let original_settings = app_state
4416            .fs
4417            .load(Path::new("/root/.zed/settings.json"))
4418            .await
4419            .unwrap();
4420
4421        let original_settings_str = original_settings.clone();
4422
4423        // Verify settings exist on disk and have expected content
4424        eprintln!("Original settings content: {}", original_settings_str);
4425        assert!(
4426            original_settings_str.contains("UNIQUEVALUE"),
4427            "Test setup failed - settings file doesn't contain our marker"
4428        );
4429
4430        // 3. Add .zed to file scan exclusions in user settings
4431        cx.update_global::<SettingsStore, _>(|store, cx| {
4432            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
4433                worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]);
4434            });
4435        });
4436
4437        eprintln!("Added .zed to file_scan_exclusions in settings");
4438
4439        // 4. Run tasks to apply settings
4440        cx.background_executor.run_until_parked();
4441
4442        // 5. Critical: Verify .zed is actually excluded from worktree
4443        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone());
4444
4445        let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
4446
4447        eprintln!(
4448            "Is .zed directory visible in worktree after exclusion: {}",
4449            has_zed_entry
4450        );
4451
4452        // This assertion verifies the test is set up correctly to show the bug
4453        // If .zed is not excluded, the test will fail here
4454        assert!(
4455            !has_zed_entry,
4456            "Test precondition failed: .zed directory should be excluded but was found in worktree"
4457        );
4458
4459        // 6. Create workspace and trigger the actual function that causes the bug
4460        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4461        window
4462            .update(cx, |workspace, window, cx| {
4463                // Call the exact function that contains the bug
4464                eprintln!("About to call open_project_settings_file");
4465                open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
4466            })
4467            .unwrap();
4468
4469        // 7. Run background tasks until completion
4470        cx.background_executor.run_until_parked();
4471
4472        // 8. Verify file contents after calling function
4473        let new_content = app_state
4474            .fs
4475            .load(Path::new("/root/.zed/settings.json"))
4476            .await
4477            .unwrap();
4478
4479        let new_content_str = new_content.clone();
4480        eprintln!("New settings content: {}", new_content_str);
4481
4482        // The bug causes the settings to be overwritten with empty settings
4483        // So if the unique value is no longer present, the bug has been reproduced
4484        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
4485        eprintln!("Bug reproduced: {}", bug_exists);
4486
4487        // This assertion should fail if the bug exists - showing the bug is real
4488        assert!(
4489            new_content_str.contains("UNIQUEVALUE"),
4490            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
4491        );
4492    }
4493}