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