zed.rs

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