zed.rs

   1mod app_menus;
   2pub mod inline_completion_registry;
   3#[cfg(not(target_os = "linux"))]
   4pub(crate) mod only_instance;
   5mod open_listener;
   6
   7pub use app_menus::*;
   8use breadcrumbs::Breadcrumbs;
   9use client::ZED_URL_SCHEME;
  10use collections::VecDeque;
  11use editor::{scroll::Autoscroll, Editor, MultiBuffer};
  12use gpui::{
  13    actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel,
  14    TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
  15};
  16pub use open_listener::*;
  17
  18use anyhow::Context as _;
  19use assets::Assets;
  20use futures::{channel::mpsc, select_biased, StreamExt};
  21use project::TaskSourceKind;
  22use project_panel::ProjectPanel;
  23use quick_action_bar::QuickActionBar;
  24use release_channel::{AppCommitSha, ReleaseChannel};
  25use rope::Rope;
  26use search::project_search::ProjectSearchBar;
  27use settings::{
  28    initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings,
  29    SettingsStore, DEFAULT_KEYMAP_PATH,
  30};
  31use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
  32use task::static_source::{StaticSource, TrackedFile};
  33use theme::ActiveTheme;
  34use workspace::notifications::NotificationId;
  35
  36use terminal_view::terminal_panel::{self, TerminalPanel};
  37use util::{
  38    asset_str,
  39    paths::{self, LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH},
  40    ResultExt,
  41};
  42use uuid::Uuid;
  43use vim::VimModeSetting;
  44use welcome::BaseKeymap;
  45use workspace::{
  46    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
  47    open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
  48};
  49use workspace::{notifications::DetachAndPromptErr, Pane};
  50use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit};
  51
  52actions!(
  53    zed,
  54    [
  55        About,
  56        DebugElements,
  57        DecreaseBufferFontSize,
  58        Hide,
  59        HideOthers,
  60        IncreaseBufferFontSize,
  61        Minimize,
  62        OpenDefaultKeymap,
  63        OpenDefaultSettings,
  64        OpenKeymap,
  65        OpenLicenses,
  66        OpenLocalSettings,
  67        OpenLocalTasks,
  68        OpenTasks,
  69        OpenTelemetryLog,
  70        ResetBufferFontSize,
  71        ResetDatabase,
  72        ShowAll,
  73        ToggleFullScreen,
  74        Zoom,
  75    ]
  76);
  77
  78pub fn init(cx: &mut AppContext) {
  79    cx.on_action(|_: &Hide, cx| cx.hide());
  80    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
  81    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
  82    cx.on_action(quit);
  83}
  84
  85pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) -> WindowOptions {
  86    let display = display_uuid.and_then(|uuid| {
  87        cx.displays()
  88            .into_iter()
  89            .find(|display| display.uuid().ok() == Some(uuid))
  90    });
  91    let app_id = ReleaseChannel::global(cx).app_id();
  92
  93    WindowOptions {
  94        titlebar: Some(TitlebarOptions {
  95            title: None,
  96            appears_transparent: true,
  97            traffic_light_position: Some(point(px(9.0), px(9.0))),
  98        }),
  99        window_bounds: None,
 100        focus: false,
 101        show: false,
 102        kind: WindowKind::Normal,
 103        is_movable: true,
 104        display_id: display.map(|display| display.id()),
 105        window_background: cx.theme().window_background_appearance(),
 106        app_id: Some(app_id.to_owned()),
 107    }
 108}
 109
 110pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
 111    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
 112        let workspace_handle = cx.view().clone();
 113        let center_pane = workspace.active_pane().clone();
 114        initialize_pane(workspace, &center_pane, cx);
 115        cx.subscribe(&workspace_handle, {
 116            move |workspace, _, event, cx| match event {
 117                workspace::Event::PaneAdded(pane) => {
 118                    initialize_pane(workspace, pane, cx);
 119                }
 120                workspace::Event::OpenBundledFile {
 121                    text,
 122                    title,
 123                    language,
 124                } => open_bundled_file(workspace, text.clone(), title, language, cx),
 125                _ => {}
 126            }
 127        })
 128        .detach();
 129
 130        let inline_completion_button = cx.new_view(|cx| {
 131            inline_completion_button::InlineCompletionButton::new(app_state.fs.clone(), cx)
 132        });
 133
 134        let diagnostic_summary =
 135            cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 136        let activity_indicator =
 137            activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
 138        let active_buffer_language =
 139            cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
 140        let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx));
 141        let cursor_position =
 142            cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
 143        workspace.status_bar().update(cx, |status_bar, cx| {
 144            status_bar.add_left_item(diagnostic_summary, cx);
 145            status_bar.add_left_item(activity_indicator, cx);
 146            status_bar.add_right_item(inline_completion_button, cx);
 147            status_bar.add_right_item(active_buffer_language, cx);
 148            status_bar.add_right_item(vim_mode_indicator, cx);
 149            status_bar.add_right_item(cursor_position, cx);
 150        });
 151
 152        auto_update::notify_of_any_new_update(cx);
 153
 154        let handle = cx.view().downgrade();
 155        cx.on_window_should_close(move |cx| {
 156            handle
 157                .update(cx, |workspace, cx| {
 158                    // We'll handle closing asynchronously
 159                    workspace.close_window(&Default::default(), cx);
 160                    false
 161                })
 162                .unwrap_or(true)
 163        });
 164
 165        let project = workspace.project().clone();
 166        if project.read(cx).is_local() {
 167            project.update(cx, |project, cx| {
 168                let fs = app_state.fs.clone();
 169                project.task_inventory().update(cx, |inventory, cx| {
 170                    let tasks_file_rx =
 171                        watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone());
 172                    inventory.add_source(
 173                        TaskSourceKind::AbsPath {
 174                            id_base: "global_tasks",
 175                            abs_path: paths::TASKS.clone(),
 176                        },
 177                        StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
 178                        cx,
 179                    );
 180                })
 181            });
 182        }
 183
 184        cx.spawn(|workspace_handle, mut cx| async move {
 185            let assistant_panel =
 186                assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
 187            let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 188            let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 189            let channels_panel =
 190                collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 191            let chat_panel =
 192                collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
 193            let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
 194                workspace_handle.clone(),
 195                cx.clone(),
 196            );
 197
 198            let (
 199                project_panel,
 200                terminal_panel,
 201                assistant_panel,
 202                channels_panel,
 203                chat_panel,
 204                notification_panel,
 205            ) = futures::try_join!(
 206                project_panel,
 207                terminal_panel,
 208                assistant_panel,
 209                channels_panel,
 210                chat_panel,
 211                notification_panel,
 212            )?;
 213
 214            workspace_handle.update(&mut cx, |workspace, cx| {
 215                workspace.add_panel(assistant_panel, cx);
 216                workspace.add_panel(project_panel, cx);
 217                if !workspace.project().read(cx).is_remote() {
 218                    workspace.add_panel(terminal_panel, cx);
 219                }
 220                workspace.add_panel(channels_panel, cx);
 221                workspace.add_panel(chat_panel, cx);
 222                workspace.add_panel(notification_panel, cx);
 223                cx.focus_self();
 224            })
 225        })
 226        .detach();
 227
 228        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 229
 230        cx.spawn(|workspace_handle, mut cx| async move {
 231            while let Some(user) = current_user.next().await {
 232                if user.is_some() {
 233                    // User known now, can check feature flags / staff
 234                    // At this point, should have the user with staff status available
 235                    let use_assistant2 = cx.update(|cx| assistant2::enabled(cx))?;
 236                    if use_assistant2 {
 237                        let panel =
 238                            assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone())
 239                                .await?;
 240                        workspace_handle.update(&mut cx, |workspace, cx| {
 241                            workspace.add_panel(panel, cx);
 242                        })?;
 243                    }
 244
 245                    break;
 246                }
 247            }
 248            anyhow::Ok(())
 249        })
 250        .detach();
 251
 252        workspace
 253            .register_action(about)
 254            .register_action(|_, _: &Minimize, cx| {
 255                cx.minimize_window();
 256            })
 257            .register_action(|_, _: &Zoom, cx| {
 258                cx.zoom_window();
 259            })
 260            .register_action(|_, _: &ToggleFullScreen, cx| {
 261                cx.toggle_fullscreen();
 262            })
 263            .register_action(|_, action: &OpenZedUrl, cx| {
 264                OpenListener::global(cx).open_urls(vec![action.url.clone()])
 265            })
 266            .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
 267            .register_action(move |_, _: &IncreaseBufferFontSize, cx| {
 268                theme::adjust_font_size(cx, |size| *size += px(1.0))
 269            })
 270            .register_action(move |_, _: &DecreaseBufferFontSize, cx| {
 271                theme::adjust_font_size(cx, |size| *size -= px(1.0))
 272            })
 273            .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx))
 274            .register_action(|_, _: &install_cli::Install, cx| {
 275                cx.spawn(|workspace, mut cx| async move {
 276                    let path = install_cli::install_cli(cx.deref())
 277                        .await
 278                        .context("error creating CLI symlink")?;
 279                    workspace.update(&mut cx, |workspace, cx| {
 280                        struct InstalledZedCli;
 281
 282                        workspace.show_toast(
 283                            Toast::new(
 284                                NotificationId::unique::<InstalledZedCli>(),
 285                                format!(
 286                                    "Installed `zed` to {}. You can launch {} from your terminal.",
 287                                    path.to_string_lossy(),
 288                                    ReleaseChannel::global(cx).display_name()
 289                                ),
 290                            ),
 291                            cx,
 292                        )
 293                    })?;
 294                    register_zed_scheme(&cx).await.log_err();
 295                    Ok(())
 296                })
 297                .detach_and_prompt_err("Error installing zed cli", cx, |_, _| None);
 298            })
 299            .register_action(|_, _: &install_cli::RegisterZedScheme, cx| {
 300                cx.spawn(|workspace, mut cx| async move {
 301                    register_zed_scheme(&cx).await?;
 302                    workspace.update(&mut cx, |workspace, cx| {
 303                        struct RegisterZedScheme;
 304
 305                        workspace.show_toast(
 306                            Toast::new(
 307                                NotificationId::unique::<RegisterZedScheme>(),
 308                                format!(
 309                                    "zed:// links will now open in {}.",
 310                                    ReleaseChannel::global(cx).display_name()
 311                                ),
 312                            ),
 313                            cx,
 314                        )
 315                    })?;
 316                    Ok(())
 317                })
 318                .detach_and_prompt_err(
 319                    "Error registering zed:// scheme",
 320                    cx,
 321                    |_, _| None,
 322                );
 323            })
 324            .register_action(|workspace, _: &OpenLog, cx| {
 325                open_log_file(workspace, cx);
 326            })
 327            .register_action(|workspace, _: &OpenLicenses, cx| {
 328                open_bundled_file(
 329                    workspace,
 330                    asset_str::<Assets>("licenses.md"),
 331                    "Open Source License Attribution",
 332                    "Markdown",
 333                    cx,
 334                );
 335            })
 336            .register_action(
 337                move |workspace: &mut Workspace,
 338                      _: &OpenTelemetryLog,
 339                      cx: &mut ViewContext<Workspace>| {
 340                    open_telemetry_log_file(workspace, cx);
 341                },
 342            )
 343            .register_action(
 344                move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
 345                    open_settings_file(&paths::KEYMAP, Rope::default, cx);
 346                },
 347            )
 348            .register_action(
 349                move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 350                    open_settings_file(
 351                        &paths::SETTINGS,
 352                        || settings::initial_user_settings_content().as_ref().into(),
 353                        cx,
 354                    );
 355                },
 356            )
 357            .register_action(
 358                move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext<Workspace>| {
 359                    open_settings_file(
 360                        &paths::TASKS,
 361                        || settings::initial_tasks_content().as_ref().into(),
 362                        cx,
 363                    );
 364                },
 365            )
 366            .register_action(open_local_settings_file)
 367            .register_action(open_local_tasks_file)
 368            .register_action(
 369                move |workspace: &mut Workspace,
 370                      _: &OpenDefaultKeymap,
 371                      cx: &mut ViewContext<Workspace>| {
 372                    open_bundled_file(
 373                        workspace,
 374                        settings::default_keymap(),
 375                        "Default Key Bindings",
 376                        "JSON",
 377                        cx,
 378                    );
 379                },
 380            )
 381            .register_action(
 382                move |workspace: &mut Workspace,
 383                      _: &OpenDefaultSettings,
 384                      cx: &mut ViewContext<Workspace>| {
 385                    open_bundled_file(
 386                        workspace,
 387                        settings::default_settings(),
 388                        "Default Settings",
 389                        "JSON",
 390                        cx,
 391                    );
 392                },
 393            )
 394            .register_action(
 395                |workspace: &mut Workspace,
 396                 _: &project_panel::ToggleFocus,
 397                 cx: &mut ViewContext<Workspace>| {
 398                    workspace.toggle_panel_focus::<ProjectPanel>(cx);
 399                },
 400            )
 401            .register_action(
 402                |workspace: &mut Workspace,
 403                 _: &collab_ui::collab_panel::ToggleFocus,
 404                 cx: &mut ViewContext<Workspace>| {
 405                    workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
 406                },
 407            )
 408            .register_action(
 409                |workspace: &mut Workspace,
 410                 _: &collab_ui::chat_panel::ToggleFocus,
 411                 cx: &mut ViewContext<Workspace>| {
 412                    workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
 413                },
 414            )
 415            .register_action(
 416                |workspace: &mut Workspace,
 417                 _: &collab_ui::notification_panel::ToggleFocus,
 418                 cx: &mut ViewContext<Workspace>| {
 419                    workspace
 420                        .toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
 421                },
 422            )
 423            .register_action(
 424                |workspace: &mut Workspace,
 425                 _: &terminal_panel::ToggleFocus,
 426                 cx: &mut ViewContext<Workspace>| {
 427                    workspace.toggle_panel_focus::<TerminalPanel>(cx);
 428                },
 429            )
 430            .register_action({
 431                let app_state = Arc::downgrade(&app_state);
 432                move |_, _: &NewWindow, cx| {
 433                    if let Some(app_state) = app_state.upgrade() {
 434                        open_new(app_state, cx, |workspace, cx| {
 435                            Editor::new_file(workspace, &Default::default(), cx)
 436                        })
 437                        .detach();
 438                    }
 439                }
 440            })
 441            .register_action({
 442                let app_state = Arc::downgrade(&app_state);
 443                move |_, _: &NewFile, cx| {
 444                    if let Some(app_state) = app_state.upgrade() {
 445                        open_new(app_state, cx, |workspace, cx| {
 446                            Editor::new_file(workspace, &Default::default(), cx)
 447                        })
 448                        .detach();
 449                    }
 450                }
 451            });
 452
 453        workspace.focus_handle(cx).focus(cx);
 454    })
 455    .detach();
 456}
 457
 458fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
 459    pane.update(cx, |pane, cx| {
 460        pane.toolbar().update(cx, |toolbar, cx| {
 461            let breadcrumbs = cx.new_view(|_| Breadcrumbs::new());
 462            toolbar.add_item(breadcrumbs, cx);
 463            let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
 464            toolbar.add_item(buffer_search_bar.clone(), cx);
 465
 466            let quick_action_bar =
 467                cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
 468            toolbar.add_item(quick_action_bar, cx);
 469            let diagnostic_editor_controls = cx.new_view(|_| diagnostics::ToolbarControls::new());
 470            toolbar.add_item(diagnostic_editor_controls, cx);
 471            let project_search_bar = cx.new_view(|_| ProjectSearchBar::new());
 472            toolbar.add_item(project_search_bar, cx);
 473            let lsp_log_item = cx.new_view(|_| language_tools::LspLogToolbarItemView::new());
 474            toolbar.add_item(lsp_log_item, cx);
 475            let syntax_tree_item =
 476                cx.new_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
 477            toolbar.add_item(syntax_tree_item, cx);
 478        })
 479    });
 480}
 481
 482fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
 483    let release_channel = ReleaseChannel::global(cx).display_name();
 484    let version = env!("CARGO_PKG_VERSION");
 485    let message = format!("{release_channel} {version}");
 486    let detail = AppCommitSha::try_global(cx).map(|sha| sha.0.clone());
 487
 488    let prompt = cx.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"]);
 489    cx.foreground_executor()
 490        .spawn(async {
 491            prompt.await.ok();
 492        })
 493        .detach();
 494}
 495
 496fn quit(_: &Quit, cx: &mut AppContext) {
 497    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 498    cx.spawn(|mut cx| async move {
 499        let mut workspace_windows = cx.update(|cx| {
 500            cx.windows()
 501                .into_iter()
 502                .filter_map(|window| window.downcast::<Workspace>())
 503                .collect::<Vec<_>>()
 504        })?;
 505
 506        // If multiple windows have unsaved changes, and need a save prompt,
 507        // prompt in the active window before switching to a different window.
 508        cx.update(|mut cx| {
 509            workspace_windows.sort_by_key(|window| window.is_active(&mut cx) == Some(false));
 510        })
 511        .log_err();
 512
 513        if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) {
 514            let answer = workspace
 515                .update(&mut cx, |_, cx| {
 516                    cx.prompt(
 517                        PromptLevel::Info,
 518                        "Are you sure you want to quit?",
 519                        None,
 520                        &["Quit", "Cancel"],
 521                    )
 522                })
 523                .log_err();
 524
 525            if let Some(answer) = answer {
 526                let answer = answer.await.ok();
 527                if answer != Some(0) {
 528                    return Ok(());
 529                }
 530            }
 531        }
 532
 533        // If the user cancels any save prompt, then keep the app open.
 534        for window in workspace_windows {
 535            if let Some(should_close) = window
 536                .update(&mut cx, |workspace, cx| {
 537                    workspace.prepare_to_close(true, cx)
 538                })
 539                .log_err()
 540            {
 541                if !should_close.await? {
 542                    return Ok(());
 543                }
 544            }
 545        }
 546        cx.update(|cx| cx.quit())?;
 547        anyhow::Ok(())
 548    })
 549    .detach_and_log_err(cx);
 550}
 551
 552fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 553    const MAX_LINES: usize = 1000;
 554    workspace
 555        .with_local_workspace(cx, move |workspace, cx| {
 556            let fs = workspace.app_state().fs.clone();
 557            cx.spawn(|workspace, mut cx| async move {
 558                let (old_log, new_log) =
 559                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
 560                let log = match (old_log, new_log) {
 561                    (Err(_), Err(_)) => None,
 562                    (old_log, new_log) => {
 563                        let mut lines = VecDeque::with_capacity(MAX_LINES);
 564                        for line in old_log
 565                            .iter()
 566                            .flat_map(|log| log.lines())
 567                            .chain(new_log.iter().flat_map(|log| log.lines()))
 568                        {
 569                            if lines.len() == MAX_LINES {
 570                                lines.pop_front();
 571                            }
 572                            lines.push_back(line);
 573                        }
 574                        Some(
 575                            lines
 576                                .into_iter()
 577                                .flat_map(|line| [line, "\n"])
 578                                .collect::<String>(),
 579                        )
 580                    }
 581                };
 582
 583                workspace
 584                    .update(&mut cx, |workspace, cx| {
 585                        let Some(log) = log else {
 586                            struct OpenLogError;
 587
 588                            workspace.show_notification(
 589                                NotificationId::unique::<OpenLogError>(),
 590                                cx,
 591                                |cx| {
 592                                    cx.new_view(|_| {
 593                                        MessageNotification::new(format!(
 594                                            "Unable to access/open log file at path {:?}",
 595                                            paths::LOG.as_path()
 596                                        ))
 597                                    })
 598                                },
 599                            );
 600                            return;
 601                        };
 602                        let project = workspace.project().clone();
 603                        let buffer = project.update(cx, |project, cx| {
 604                            project.create_local_buffer(&log, None, cx)
 605                        });
 606
 607                        let buffer = cx.new_model(|cx| {
 608                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
 609                        });
 610                        let editor =
 611                            cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
 612
 613                        editor.update(cx, |editor, cx| {
 614                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
 615                            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 616                                s.select_ranges(Some(
 617                                    last_multi_buffer_offset..last_multi_buffer_offset,
 618                                ));
 619                            })
 620                        });
 621
 622                        workspace.add_item_to_active_pane(Box::new(editor), None, cx);
 623                    })
 624                    .log_err();
 625            })
 626            .detach();
 627        })
 628        .detach();
 629}
 630
 631pub fn handle_keymap_file_changes(
 632    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
 633    cx: &mut AppContext,
 634) {
 635    BaseKeymap::register(cx);
 636    VimModeSetting::register(cx);
 637
 638    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
 639    let mut old_base_keymap = *BaseKeymap::get_global(cx);
 640    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
 641    cx.observe_global::<SettingsStore>(move |cx| {
 642        let new_base_keymap = *BaseKeymap::get_global(cx);
 643        let new_vim_enabled = VimModeSetting::get_global(cx).0;
 644
 645        if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
 646            old_base_keymap = new_base_keymap;
 647            old_vim_enabled = new_vim_enabled;
 648            base_keymap_tx.unbounded_send(()).unwrap();
 649        }
 650    })
 651    .detach();
 652
 653    load_default_keymap(cx);
 654
 655    cx.spawn(move |cx| async move {
 656        let mut user_keymap = KeymapFile::default();
 657        loop {
 658            select_biased! {
 659                _ = base_keymap_rx.next() => {}
 660                user_keymap_content = user_keymap_file_rx.next() => {
 661                    if let Some(user_keymap_content) = user_keymap_content {
 662                        if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
 663                            user_keymap = keymap_content;
 664                        } else {
 665                            continue
 666                        }
 667                    }
 668                }
 669            }
 670            cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
 671        }
 672    })
 673    .detach();
 674}
 675
 676fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
 677    cx.clear_key_bindings();
 678    load_default_keymap(cx);
 679    keymap_content.clone().add_to_cx(cx).log_err();
 680    cx.set_menus(app_menus());
 681}
 682
 683pub fn load_default_keymap(cx: &mut AppContext) {
 684    let base_keymap = *BaseKeymap::get_global(cx);
 685    if base_keymap == BaseKeymap::None {
 686        return;
 687    }
 688
 689    KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
 690    if VimModeSetting::get_global(cx).0 {
 691        KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
 692    }
 693
 694    if let Some(asset_path) = base_keymap.asset_path() {
 695        KeymapFile::load_asset(asset_path, cx).unwrap();
 696    }
 697}
 698
 699fn open_local_settings_file(
 700    workspace: &mut Workspace,
 701    _: &OpenLocalSettings,
 702    cx: &mut ViewContext<Workspace>,
 703) {
 704    open_local_file(
 705        workspace,
 706        &LOCAL_SETTINGS_RELATIVE_PATH,
 707        initial_local_settings_content(),
 708        cx,
 709    )
 710}
 711
 712fn open_local_tasks_file(
 713    workspace: &mut Workspace,
 714    _: &OpenLocalTasks,
 715    cx: &mut ViewContext<Workspace>,
 716) {
 717    open_local_file(
 718        workspace,
 719        &LOCAL_TASKS_RELATIVE_PATH,
 720        initial_tasks_content(),
 721        cx,
 722    )
 723}
 724
 725fn open_local_file(
 726    workspace: &mut Workspace,
 727    settings_relative_path: &'static Path,
 728    initial_contents: Cow<'static, str>,
 729    cx: &mut ViewContext<Workspace>,
 730) {
 731    let project = workspace.project().clone();
 732    let worktree = project
 733        .read(cx)
 734        .visible_worktrees(cx)
 735        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
 736    if let Some(worktree) = worktree {
 737        let tree_id = worktree.read(cx).id();
 738        cx.spawn(|workspace, mut cx| async move {
 739            if let Some(dir_path) = settings_relative_path.parent() {
 740                if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
 741                    project
 742                        .update(&mut cx, |project, cx| {
 743                            project.create_entry((tree_id, dir_path), true, cx)
 744                        })?
 745                        .await
 746                        .context("worktree was removed")?;
 747                }
 748            }
 749
 750            if worktree.update(&mut cx, |tree, _| {
 751                tree.entry_for_path(settings_relative_path).is_none()
 752            })? {
 753                project
 754                    .update(&mut cx, |project, cx| {
 755                        project.create_entry((tree_id, settings_relative_path), false, cx)
 756                    })?
 757                    .await
 758                    .context("worktree was removed")?;
 759            }
 760
 761            let editor = workspace
 762                .update(&mut cx, |workspace, cx| {
 763                    workspace.open_path((tree_id, settings_relative_path), None, true, cx)
 764                })?
 765                .await?
 766                .downcast::<Editor>()
 767                .context("unexpected item type: expected editor item")?;
 768
 769            editor
 770                .downgrade()
 771                .update(&mut cx, |editor, cx| {
 772                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 773                        if buffer.read(cx).is_empty() {
 774                            buffer.update(cx, |buffer, cx| {
 775                                buffer.edit([(0..0, initial_contents)], None, cx)
 776                            });
 777                        }
 778                    }
 779                })
 780                .ok();
 781
 782            anyhow::Ok(())
 783        })
 784        .detach();
 785    } else {
 786        struct NoOpenFolders;
 787
 788        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
 789            cx.new_view(|_| MessageNotification::new("This project has no folders open."))
 790        })
 791    }
 792}
 793
 794fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 795    workspace.with_local_workspace(cx, move |workspace, cx| {
 796        let app_state = workspace.app_state().clone();
 797        cx.spawn(|workspace, mut cx| async move {
 798            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
 799                let path = app_state.client.telemetry().log_file_path()?;
 800                app_state.fs.load(&path).await.log_err()
 801            }
 802
 803            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
 804
 805            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
 806            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
 807            if let Some(newline_offset) = log[start_offset..].find('\n') {
 808                start_offset += newline_offset + 1;
 809            }
 810            let log_suffix = &log[start_offset..];
 811            let json = app_state.languages.language_for_name("JSON").await.log_err();
 812
 813            workspace.update(&mut cx, |workspace, cx| {
 814                let project = workspace.project().clone();
 815                let buffer = project
 816                    .update(cx, |project, cx| project.create_local_buffer("", None, cx));
 817                buffer.update(cx, |buffer, cx| {
 818                    buffer.set_language(json, cx);
 819                    buffer.edit(
 820                        [(
 821                            0..0,
 822                            concat!(
 823                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
 824                                "// Telemetry can be disabled via the `settings.json` file.\n",
 825                                "// Here is the data that has been reported for the current session:\n",
 826                                "\n"
 827                            ),
 828                        )],
 829                        None,
 830                        cx,
 831                    );
 832                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
 833                });
 834
 835                let buffer = cx.new_model(|cx| {
 836                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
 837                });
 838                workspace.add_item_to_active_pane(
 839                    Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
 840                    None,cx,
 841                );
 842            }).log_err()?;
 843
 844            Some(())
 845        })
 846        .detach();
 847    }).detach();
 848}
 849
 850fn open_bundled_file(
 851    workspace: &mut Workspace,
 852    text: Cow<'static, str>,
 853    title: &'static str,
 854    language: &'static str,
 855    cx: &mut ViewContext<Workspace>,
 856) {
 857    let language = workspace.app_state().languages.language_for_name(language);
 858    cx.spawn(|workspace, mut cx| async move {
 859        let language = language.await.log_err();
 860        workspace
 861            .update(&mut cx, |workspace, cx| {
 862                workspace.with_local_workspace(cx, |workspace, cx| {
 863                    let project = workspace.project();
 864                    let buffer = project.update(cx, move |project, cx| {
 865                        project.create_local_buffer(text.as_ref(), language, cx)
 866                    });
 867                    let buffer = cx.new_model(|cx| {
 868                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
 869                    });
 870                    workspace.add_item_to_active_pane(
 871                        Box::new(cx.new_view(|cx| {
 872                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
 873                        })),
 874                        None,
 875                        cx,
 876                    );
 877                })
 878            })?
 879            .await
 880    })
 881    .detach_and_log_err(cx);
 882}
 883
 884fn open_settings_file(
 885    abs_path: &'static Path,
 886    default_content: impl FnOnce() -> Rope + Send + 'static,
 887    cx: &mut ViewContext<Workspace>,
 888) {
 889    cx.spawn(|workspace, mut cx| async move {
 890        let (worktree_creation_task, settings_open_task) =
 891            workspace.update(&mut cx, |workspace, cx| {
 892                let worktree_creation_task = workspace.project().update(cx, |project, cx| {
 893                    // Set up a dedicated worktree for settings, since otherwise we're dropping and re-starting LSP servers for each file inside on every settings file close/open
 894                    // TODO: Do note that all other external files (e.g. drag and drop from OS) still have their worktrees released on file close, causing LSP servers' restarts.
 895                    project.find_or_create_local_worktree(paths::CONFIG_DIR.as_path(), false, cx)
 896                });
 897                let settings_open_task = create_and_open_local_file(&abs_path, cx, default_content);
 898                (worktree_creation_task, settings_open_task)
 899            })?;
 900
 901        let _ = worktree_creation_task.await?;
 902        let _ = settings_open_task.await?;
 903        anyhow::Ok(())
 904    })
 905    .detach_and_log_err(cx);
 906}
 907
 908#[cfg(test)]
 909mod tests {
 910    use super::*;
 911    use assets::Assets;
 912    use collections::HashSet;
 913    use editor::{scroll::Autoscroll, DisplayPoint, Editor};
 914    use gpui::{
 915        actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
 916        TestAppContext, VisualTestContext, WindowHandle,
 917    };
 918    use language::{LanguageMatcher, LanguageRegistry};
 919    use project::{Project, ProjectPath, WorktreeSettings};
 920    use serde_json::json;
 921    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
 922    use std::path::{Path, PathBuf};
 923    use theme::{ThemeRegistry, ThemeSettings};
 924    use workspace::{
 925        item::{Item, ItemHandle},
 926        open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection,
 927        WorkspaceHandle,
 928    };
 929
 930    #[gpui::test]
 931    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
 932        let app_state = init_test(cx);
 933        app_state
 934            .fs
 935            .as_fake()
 936            .insert_tree(
 937                "/root",
 938                json!({
 939                    "a": {
 940                    },
 941                }),
 942            )
 943            .await;
 944
 945        cx.update(|cx| {
 946            open_paths(
 947                &[PathBuf::from("/root/a/new")],
 948                app_state.clone(),
 949                workspace::OpenOptions::default(),
 950                cx,
 951            )
 952        })
 953        .await
 954        .unwrap();
 955        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
 956
 957        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
 958        workspace
 959            .update(cx, |workspace, cx| {
 960                assert!(workspace.active_item_as::<Editor>(cx).is_some())
 961            })
 962            .unwrap();
 963    }
 964
 965    #[gpui::test]
 966    async fn test_open_paths_action(cx: &mut TestAppContext) {
 967        let app_state = init_test(cx);
 968        app_state
 969            .fs
 970            .as_fake()
 971            .insert_tree(
 972                "/root",
 973                json!({
 974                    "a": {
 975                        "aa": null,
 976                        "ab": null,
 977                    },
 978                    "b": {
 979                        "ba": null,
 980                        "bb": null,
 981                    },
 982                    "c": {
 983                        "ca": null,
 984                        "cb": null,
 985                    },
 986                    "d": {
 987                        "da": null,
 988                        "db": null,
 989                    },
 990                    "e": {
 991                        "ea": null,
 992                        "eb": null,
 993                    }
 994                }),
 995            )
 996            .await;
 997
 998        cx.update(|cx| {
 999            open_paths(
1000                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1001                app_state.clone(),
1002                workspace::OpenOptions::default(),
1003                cx,
1004            )
1005        })
1006        .await
1007        .unwrap();
1008        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1009
1010        cx.update(|cx| {
1011            open_paths(
1012                &[PathBuf::from("/root/a")],
1013                app_state.clone(),
1014                workspace::OpenOptions::default(),
1015                cx,
1016            )
1017        })
1018        .await
1019        .unwrap();
1020        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1021        let workspace_1 = cx
1022            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1023            .unwrap();
1024        workspace_1
1025            .update(cx, |workspace, cx| {
1026                assert_eq!(workspace.worktrees(cx).count(), 2);
1027                assert!(workspace.left_dock().read(cx).is_open());
1028                assert!(workspace
1029                    .active_pane()
1030                    .read(cx)
1031                    .focus_handle(cx)
1032                    .is_focused(cx));
1033            })
1034            .unwrap();
1035
1036        cx.update(|cx| {
1037            open_paths(
1038                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1039                app_state.clone(),
1040                workspace::OpenOptions::default(),
1041                cx,
1042            )
1043        })
1044        .await
1045        .unwrap();
1046        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1047
1048        // Replace existing windows
1049        let window = cx
1050            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1051            .unwrap();
1052        cx.update(|cx| {
1053            open_paths(
1054                &[PathBuf::from("/root/e")],
1055                app_state,
1056                workspace::OpenOptions {
1057                    replace_window: Some(window),
1058                    ..Default::default()
1059                },
1060                cx,
1061            )
1062        })
1063        .await
1064        .unwrap();
1065        cx.background_executor.run_until_parked();
1066        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1067        let workspace_1 = cx
1068            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1069            .unwrap();
1070        workspace_1
1071            .update(cx, |workspace, cx| {
1072                assert_eq!(
1073                    workspace
1074                        .worktrees(cx)
1075                        .map(|w| w.read(cx).abs_path())
1076                        .collect::<Vec<_>>(),
1077                    &[Path::new("/root/e").into()]
1078                );
1079                assert!(workspace.left_dock().read(cx).is_open());
1080                assert!(workspace.active_pane().focus_handle(cx).is_focused(cx));
1081            })
1082            .unwrap();
1083    }
1084
1085    #[gpui::test]
1086    async fn test_open_add_new(cx: &mut TestAppContext) {
1087        let app_state = init_test(cx);
1088        app_state
1089            .fs
1090            .as_fake()
1091            .insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}}))
1092            .await;
1093
1094        cx.update(|cx| {
1095            open_paths(
1096                &[PathBuf::from("/root/dir")],
1097                app_state.clone(),
1098                workspace::OpenOptions::default(),
1099                cx,
1100            )
1101        })
1102        .await
1103        .unwrap();
1104        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1105
1106        cx.update(|cx| {
1107            open_paths(
1108                &[PathBuf::from("/root/a")],
1109                app_state.clone(),
1110                workspace::OpenOptions {
1111                    open_new_workspace: Some(false),
1112                    ..Default::default()
1113                },
1114                cx,
1115            )
1116        })
1117        .await
1118        .unwrap();
1119        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1120
1121        cx.update(|cx| {
1122            open_paths(
1123                &[PathBuf::from("/root/dir/c")],
1124                app_state.clone(),
1125                workspace::OpenOptions {
1126                    open_new_workspace: Some(true),
1127                    ..Default::default()
1128                },
1129                cx,
1130            )
1131        })
1132        .await
1133        .unwrap();
1134        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1135    }
1136
1137    #[gpui::test]
1138    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1139        let app_state = init_test(cx);
1140        app_state
1141            .fs
1142            .as_fake()
1143            .insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}))
1144            .await;
1145
1146        cx.update(|cx| {
1147            open_paths(
1148                &[PathBuf::from("/root/dir1/a")],
1149                app_state.clone(),
1150                workspace::OpenOptions::default(),
1151                cx,
1152            )
1153        })
1154        .await
1155        .unwrap();
1156        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1157        let window1 = cx.update(|cx| cx.active_window().unwrap());
1158
1159        cx.update(|cx| {
1160            open_paths(
1161                &[PathBuf::from("/root/dir2/c")],
1162                app_state.clone(),
1163                workspace::OpenOptions::default(),
1164                cx,
1165            )
1166        })
1167        .await
1168        .unwrap();
1169        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1170
1171        cx.update(|cx| {
1172            open_paths(
1173                &[PathBuf::from("/root/dir2")],
1174                app_state.clone(),
1175                workspace::OpenOptions::default(),
1176                cx,
1177            )
1178        })
1179        .await
1180        .unwrap();
1181        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1182        let window2 = cx.update(|cx| cx.active_window().unwrap());
1183        assert!(window1 != window2);
1184        cx.update_window(window1, |_, cx| cx.activate_window())
1185            .unwrap();
1186
1187        cx.update(|cx| {
1188            open_paths(
1189                &[PathBuf::from("/root/dir2/c")],
1190                app_state.clone(),
1191                workspace::OpenOptions::default(),
1192                cx,
1193            )
1194        })
1195        .await
1196        .unwrap();
1197        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1198        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
1199        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
1200    }
1201
1202    #[gpui::test]
1203    async fn test_window_edit_state(cx: &mut TestAppContext) {
1204        let executor = cx.executor();
1205        let app_state = init_test(cx);
1206        app_state
1207            .fs
1208            .as_fake()
1209            .insert_tree("/root", json!({"a": "hey"}))
1210            .await;
1211
1212        cx.update(|cx| {
1213            open_paths(
1214                &[PathBuf::from("/root/a")],
1215                app_state.clone(),
1216                workspace::OpenOptions::default(),
1217                cx,
1218            )
1219        })
1220        .await
1221        .unwrap();
1222        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1223
1224        // When opening the workspace, the window is not in a edited state.
1225        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1226
1227        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
1228            cx.update(|cx| window.read(cx).unwrap().is_edited())
1229        };
1230        let pane = window
1231            .read_with(cx, |workspace, _| workspace.active_pane().clone())
1232            .unwrap();
1233        let editor = window
1234            .read_with(cx, |workspace, cx| {
1235                workspace
1236                    .active_item(cx)
1237                    .unwrap()
1238                    .downcast::<Editor>()
1239                    .unwrap()
1240            })
1241            .unwrap();
1242
1243        assert!(!window_is_edited(window, cx));
1244
1245        // Editing a buffer marks the window as edited.
1246        window
1247            .update(cx, |_, cx| {
1248                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1249            })
1250            .unwrap();
1251
1252        assert!(window_is_edited(window, cx));
1253
1254        // Undoing the edit restores the window's edited state.
1255        window
1256            .update(cx, |_, cx| {
1257                editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
1258            })
1259            .unwrap();
1260        assert!(!window_is_edited(window, cx));
1261
1262        // Redoing the edit marks the window as edited again.
1263        window
1264            .update(cx, |_, cx| {
1265                editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
1266            })
1267            .unwrap();
1268        assert!(window_is_edited(window, cx));
1269
1270        // Closing the item restores the window's edited state.
1271        let close = window
1272            .update(cx, |_, cx| {
1273                pane.update(cx, |pane, cx| {
1274                    drop(editor);
1275                    pane.close_active_item(&Default::default(), cx).unwrap()
1276                })
1277            })
1278            .unwrap();
1279        executor.run_until_parked();
1280
1281        cx.simulate_prompt_answer(1);
1282        close.await.unwrap();
1283        assert!(!window_is_edited(window, cx));
1284
1285        // Opening the buffer again doesn't impact the window's edited state.
1286        cx.update(|cx| {
1287            open_paths(
1288                &[PathBuf::from("/root/a")],
1289                app_state,
1290                workspace::OpenOptions::default(),
1291                cx,
1292            )
1293        })
1294        .await
1295        .unwrap();
1296        let editor = window
1297            .read_with(cx, |workspace, cx| {
1298                workspace
1299                    .active_item(cx)
1300                    .unwrap()
1301                    .downcast::<Editor>()
1302                    .unwrap()
1303            })
1304            .unwrap();
1305        assert!(!window_is_edited(window, cx));
1306
1307        // Editing the buffer marks the window as edited.
1308        window
1309            .update(cx, |_, cx| {
1310                editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
1311            })
1312            .unwrap();
1313        assert!(window_is_edited(window, cx));
1314
1315        // Ensure closing the window via the mouse gets preempted due to the
1316        // buffer having unsaved changes.
1317        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
1318        executor.run_until_parked();
1319        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1320
1321        // The window is successfully closed after the user dismisses the prompt.
1322        cx.simulate_prompt_answer(1);
1323        executor.run_until_parked();
1324        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
1325    }
1326
1327    #[gpui::test]
1328    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
1329        let app_state = init_test(cx);
1330        cx.update(|cx| {
1331            open_new(app_state.clone(), cx, |workspace, cx| {
1332                Editor::new_file(workspace, &Default::default(), cx)
1333            })
1334        })
1335        .await;
1336        cx.run_until_parked();
1337
1338        let workspace = cx
1339            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
1340            .unwrap();
1341
1342        let editor = workspace
1343            .update(cx, |workspace, cx| {
1344                let editor = workspace
1345                    .active_item(cx)
1346                    .unwrap()
1347                    .downcast::<editor::Editor>()
1348                    .unwrap();
1349                editor.update(cx, |editor, cx| {
1350                    assert!(editor.text(cx).is_empty());
1351                    assert!(!editor.is_dirty(cx));
1352                });
1353
1354                editor
1355            })
1356            .unwrap();
1357
1358        let save_task = workspace
1359            .update(cx, |workspace, cx| {
1360                workspace.save_active_item(SaveIntent::Save, cx)
1361            })
1362            .unwrap();
1363        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1364        cx.background_executor.run_until_parked();
1365        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
1366        save_task.await.unwrap();
1367        workspace
1368            .update(cx, |_, cx| {
1369                editor.update(cx, |editor, cx| {
1370                    assert!(!editor.is_dirty(cx));
1371                    assert_eq!(editor.title(cx), "the-new-name");
1372                });
1373            })
1374            .unwrap();
1375    }
1376
1377    #[gpui::test]
1378    async fn test_open_entry(cx: &mut TestAppContext) {
1379        let app_state = init_test(cx);
1380        app_state
1381            .fs
1382            .as_fake()
1383            .insert_tree(
1384                "/root",
1385                json!({
1386                    "a": {
1387                        "file1": "contents 1",
1388                        "file2": "contents 2",
1389                        "file3": "contents 3",
1390                    },
1391                }),
1392            )
1393            .await;
1394
1395        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1396        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1397        let workspace = window.root(cx).unwrap();
1398
1399        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1400        let file1 = entries[0].clone();
1401        let file2 = entries[1].clone();
1402        let file3 = entries[2].clone();
1403
1404        // Open the first entry
1405        let entry_1 = window
1406            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1407            .unwrap()
1408            .await
1409            .unwrap();
1410        cx.read(|cx| {
1411            let pane = workspace.read(cx).active_pane().read(cx);
1412            assert_eq!(
1413                pane.active_item().unwrap().project_path(cx),
1414                Some(file1.clone())
1415            );
1416            assert_eq!(pane.items_len(), 1);
1417        });
1418
1419        // Open the second entry
1420        window
1421            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1422            .unwrap()
1423            .await
1424            .unwrap();
1425        cx.read(|cx| {
1426            let pane = workspace.read(cx).active_pane().read(cx);
1427            assert_eq!(
1428                pane.active_item().unwrap().project_path(cx),
1429                Some(file2.clone())
1430            );
1431            assert_eq!(pane.items_len(), 2);
1432        });
1433
1434        // Open the first entry again. The existing pane item is activated.
1435        let entry_1b = window
1436            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1437            .unwrap()
1438            .await
1439            .unwrap();
1440        assert_eq!(entry_1.item_id(), entry_1b.item_id());
1441
1442        cx.read(|cx| {
1443            let pane = workspace.read(cx).active_pane().read(cx);
1444            assert_eq!(
1445                pane.active_item().unwrap().project_path(cx),
1446                Some(file1.clone())
1447            );
1448            assert_eq!(pane.items_len(), 2);
1449        });
1450
1451        // Split the pane with the first entry, then open the second entry again.
1452        window
1453            .update(cx, |w, cx| {
1454                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1455                w.open_path(file2.clone(), None, true, cx)
1456            })
1457            .unwrap()
1458            .await
1459            .unwrap();
1460
1461        window
1462            .read_with(cx, |w, cx| {
1463                assert_eq!(
1464                    w.active_pane()
1465                        .read(cx)
1466                        .active_item()
1467                        .unwrap()
1468                        .project_path(cx),
1469                    Some(file2.clone())
1470                );
1471            })
1472            .unwrap();
1473
1474        // Open the third entry twice concurrently. Only one pane item is added.
1475        let (t1, t2) = window
1476            .update(cx, |w, cx| {
1477                (
1478                    w.open_path(file3.clone(), None, true, cx),
1479                    w.open_path(file3.clone(), None, true, cx),
1480                )
1481            })
1482            .unwrap();
1483        t1.await.unwrap();
1484        t2.await.unwrap();
1485        cx.read(|cx| {
1486            let pane = workspace.read(cx).active_pane().read(cx);
1487            assert_eq!(
1488                pane.active_item().unwrap().project_path(cx),
1489                Some(file3.clone())
1490            );
1491            let pane_entries = pane
1492                .items()
1493                .map(|i| i.project_path(cx).unwrap())
1494                .collect::<Vec<_>>();
1495            assert_eq!(pane_entries, &[file1, file2, file3]);
1496        });
1497    }
1498
1499    #[gpui::test]
1500    async fn test_open_paths(cx: &mut TestAppContext) {
1501        let app_state = init_test(cx);
1502
1503        app_state
1504            .fs
1505            .as_fake()
1506            .insert_tree(
1507                "/",
1508                json!({
1509                    "dir1": {
1510                        "a.txt": ""
1511                    },
1512                    "dir2": {
1513                        "b.txt": ""
1514                    },
1515                    "dir3": {
1516                        "c.txt": ""
1517                    },
1518                    "d.txt": ""
1519                }),
1520            )
1521            .await;
1522
1523        cx.update(|cx| {
1524            open_paths(
1525                &[PathBuf::from("/dir1/")],
1526                app_state,
1527                workspace::OpenOptions::default(),
1528                cx,
1529            )
1530        })
1531        .await
1532        .unwrap();
1533        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1534        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
1535        let workspace = window.root(cx).unwrap();
1536
1537        #[track_caller]
1538        fn assert_project_panel_selection(
1539            workspace: &Workspace,
1540            expected_worktree_path: &Path,
1541            expected_entry_path: &Path,
1542            cx: &AppContext,
1543        ) {
1544            let project_panel = [
1545                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
1546                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
1547                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
1548            ]
1549            .into_iter()
1550            .find_map(std::convert::identity)
1551            .expect("found no project panels")
1552            .read(cx);
1553            let (selected_worktree, selected_entry) = project_panel
1554                .selected_entry(cx)
1555                .expect("project panel should have a selected entry");
1556            assert_eq!(
1557                selected_worktree.abs_path().as_ref(),
1558                expected_worktree_path,
1559                "Unexpected project panel selected worktree path"
1560            );
1561            assert_eq!(
1562                selected_entry.path.as_ref(),
1563                expected_entry_path,
1564                "Unexpected project panel selected entry path"
1565            );
1566        }
1567
1568        // Open a file within an existing worktree.
1569        window
1570            .update(cx, |view, cx| {
1571                view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx)
1572            })
1573            .unwrap()
1574            .await;
1575        cx.read(|cx| {
1576            let workspace = workspace.read(cx);
1577            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
1578            assert_eq!(
1579                workspace
1580                    .active_pane()
1581                    .read(cx)
1582                    .active_item()
1583                    .unwrap()
1584                    .act_as::<Editor>(cx)
1585                    .unwrap()
1586                    .read(cx)
1587                    .title(cx),
1588                "a.txt"
1589            );
1590        });
1591
1592        // Open a file outside of any existing worktree.
1593        window
1594            .update(cx, |view, cx| {
1595                view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx)
1596            })
1597            .unwrap()
1598            .await;
1599        cx.read(|cx| {
1600            let workspace = workspace.read(cx);
1601            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
1602            let worktree_roots = workspace
1603                .worktrees(cx)
1604                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1605                .collect::<HashSet<_>>();
1606            assert_eq!(
1607                worktree_roots,
1608                vec!["/dir1", "/dir2/b.txt"]
1609                    .into_iter()
1610                    .map(Path::new)
1611                    .collect(),
1612            );
1613            assert_eq!(
1614                workspace
1615                    .active_pane()
1616                    .read(cx)
1617                    .active_item()
1618                    .unwrap()
1619                    .act_as::<Editor>(cx)
1620                    .unwrap()
1621                    .read(cx)
1622                    .title(cx),
1623                "b.txt"
1624            );
1625        });
1626
1627        // Ensure opening a directory and one of its children only adds one worktree.
1628        window
1629            .update(cx, |view, cx| {
1630                view.open_paths(
1631                    vec!["/dir3".into(), "/dir3/c.txt".into()],
1632                    OpenVisible::All,
1633                    None,
1634                    cx,
1635                )
1636            })
1637            .unwrap()
1638            .await;
1639        cx.read(|cx| {
1640            let workspace = workspace.read(cx);
1641            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
1642            let worktree_roots = workspace
1643                .worktrees(cx)
1644                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1645                .collect::<HashSet<_>>();
1646            assert_eq!(
1647                worktree_roots,
1648                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1649                    .into_iter()
1650                    .map(Path::new)
1651                    .collect(),
1652            );
1653            assert_eq!(
1654                workspace
1655                    .active_pane()
1656                    .read(cx)
1657                    .active_item()
1658                    .unwrap()
1659                    .act_as::<Editor>(cx)
1660                    .unwrap()
1661                    .read(cx)
1662                    .title(cx),
1663                "c.txt"
1664            );
1665        });
1666
1667        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1668        window
1669            .update(cx, |view, cx| {
1670                view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx)
1671            })
1672            .unwrap()
1673            .await;
1674        cx.read(|cx| {
1675            let workspace = workspace.read(cx);
1676            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
1677            let worktree_roots = workspace
1678                .worktrees(cx)
1679                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1680                .collect::<HashSet<_>>();
1681            assert_eq!(
1682                worktree_roots,
1683                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1684                    .into_iter()
1685                    .map(Path::new)
1686                    .collect(),
1687            );
1688
1689            let visible_worktree_roots = workspace
1690                .visible_worktrees(cx)
1691                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1692                .collect::<HashSet<_>>();
1693            assert_eq!(
1694                visible_worktree_roots,
1695                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1696                    .into_iter()
1697                    .map(Path::new)
1698                    .collect(),
1699            );
1700
1701            assert_eq!(
1702                workspace
1703                    .active_pane()
1704                    .read(cx)
1705                    .active_item()
1706                    .unwrap()
1707                    .act_as::<Editor>(cx)
1708                    .unwrap()
1709                    .read(cx)
1710                    .title(cx),
1711                "d.txt"
1712            );
1713        });
1714    }
1715
1716    #[gpui::test]
1717    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
1718        let app_state = init_test(cx);
1719        cx.update(|cx| {
1720            cx.update_global::<SettingsStore, _>(|store, cx| {
1721                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1722                    project_settings.file_scan_exclusions =
1723                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
1724                });
1725            });
1726        });
1727        app_state
1728            .fs
1729            .as_fake()
1730            .insert_tree(
1731                "/root",
1732                json!({
1733                    ".gitignore": "ignored_dir\n",
1734                    ".git": {
1735                        "HEAD": "ref: refs/heads/main",
1736                    },
1737                    "regular_dir": {
1738                        "file": "regular file contents",
1739                    },
1740                    "ignored_dir": {
1741                        "ignored_subdir": {
1742                            "file": "ignored subfile contents",
1743                        },
1744                        "file": "ignored file contents",
1745                    },
1746                    "excluded_dir": {
1747                        "file": "excluded file contents",
1748                        "ignored_subdir": {
1749                            "file": "ignored subfile contents",
1750                        },
1751                    },
1752                }),
1753            )
1754            .await;
1755
1756        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1757        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1758        let workspace = window.root(cx).unwrap();
1759
1760        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
1761        let paths_to_open = [
1762            Path::new("/root/excluded_dir/file").to_path_buf(),
1763            Path::new("/root/.git/HEAD").to_path_buf(),
1764            Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
1765        ];
1766        let (opened_workspace, new_items) = cx
1767            .update(|cx| {
1768                workspace::open_paths(
1769                    &paths_to_open,
1770                    app_state,
1771                    workspace::OpenOptions::default(),
1772                    cx,
1773                )
1774            })
1775            .await
1776            .unwrap();
1777
1778        assert_eq!(
1779            opened_workspace.root_view(cx).unwrap().entity_id(),
1780            workspace.entity_id(),
1781            "Excluded files in subfolders of a workspace root should be opened in the workspace"
1782        );
1783        let mut opened_paths = cx.read(|cx| {
1784            assert_eq!(
1785                new_items.len(),
1786                paths_to_open.len(),
1787                "Expect to get the same number of opened items as submitted paths to open"
1788            );
1789            new_items
1790                .iter()
1791                .zip(paths_to_open.iter())
1792                .map(|(i, path)| {
1793                    match i {
1794                        Some(Ok(i)) => {
1795                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
1796                        }
1797                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
1798                        None => None,
1799                    }
1800                    .flatten()
1801                })
1802                .collect::<Vec<_>>()
1803        });
1804        opened_paths.sort();
1805        assert_eq!(
1806            opened_paths,
1807            vec![
1808                None,
1809                Some(".git/HEAD".to_string()),
1810                Some("excluded_dir/file".to_string()),
1811            ],
1812            "Excluded files should get opened, excluded dir should not get opened"
1813        );
1814
1815        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1816        assert_eq!(
1817                initial_entries, entries,
1818                "Workspace entries should not change after opening excluded files and directories paths"
1819            );
1820
1821        cx.read(|cx| {
1822                let pane = workspace.read(cx).active_pane().read(cx);
1823                let mut opened_buffer_paths = pane
1824                    .items()
1825                    .map(|i| {
1826                        i.project_path(cx)
1827                            .expect("all excluded files that got open should have a path")
1828                            .path
1829                            .display()
1830                            .to_string()
1831                    })
1832                    .collect::<Vec<_>>();
1833                opened_buffer_paths.sort();
1834                assert_eq!(
1835                    opened_buffer_paths,
1836                    vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
1837                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
1838                );
1839            });
1840    }
1841
1842    #[gpui::test]
1843    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1844        let app_state = init_test(cx);
1845        app_state
1846            .fs
1847            .as_fake()
1848            .insert_tree("/root", json!({ "a.txt": "" }))
1849            .await;
1850
1851        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1852        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1853        let workspace = window.root(cx).unwrap();
1854
1855        // Open a file within an existing worktree.
1856        window
1857            .update(cx, |view, cx| {
1858                view.open_paths(
1859                    vec![PathBuf::from("/root/a.txt")],
1860                    OpenVisible::All,
1861                    None,
1862                    cx,
1863                )
1864            })
1865            .unwrap()
1866            .await;
1867        let editor = cx.read(|cx| {
1868            let pane = workspace.read(cx).active_pane().read(cx);
1869            let item = pane.active_item().unwrap();
1870            item.downcast::<Editor>().unwrap()
1871        });
1872
1873        window
1874            .update(cx, |_, cx| {
1875                editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1876            })
1877            .unwrap();
1878
1879        app_state
1880            .fs
1881            .as_fake()
1882            .insert_file("/root/a.txt", b"changed".to_vec())
1883            .await;
1884
1885        cx.run_until_parked();
1886        cx.read(|cx| assert!(editor.is_dirty(cx)));
1887        cx.read(|cx| assert!(editor.has_conflict(cx)));
1888
1889        let save_task = window
1890            .update(cx, |workspace, cx| {
1891                workspace.save_active_item(SaveIntent::Save, cx)
1892            })
1893            .unwrap();
1894        cx.background_executor.run_until_parked();
1895        cx.simulate_prompt_answer(0);
1896        save_task.await.unwrap();
1897        window
1898            .update(cx, |_, cx| {
1899                editor.update(cx, |editor, cx| {
1900                    assert!(!editor.is_dirty(cx));
1901                    assert!(!editor.has_conflict(cx));
1902                });
1903            })
1904            .unwrap();
1905    }
1906
1907    #[gpui::test]
1908    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1909        let app_state = init_test(cx);
1910        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1911
1912        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1913        project.update(cx, |project, _| project.languages().add(rust_lang()));
1914        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1915        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
1916
1917        // Create a new untitled buffer
1918        cx.dispatch_action(window.into(), NewFile);
1919        let editor = window
1920            .read_with(cx, |workspace, cx| {
1921                workspace
1922                    .active_item(cx)
1923                    .unwrap()
1924                    .downcast::<Editor>()
1925                    .unwrap()
1926            })
1927            .unwrap();
1928
1929        window
1930            .update(cx, |_, cx| {
1931                editor.update(cx, |editor, cx| {
1932                    assert!(!editor.is_dirty(cx));
1933                    assert_eq!(editor.title(cx), "untitled");
1934                    assert!(Arc::ptr_eq(
1935                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
1936                        &languages::PLAIN_TEXT
1937                    ));
1938                    editor.handle_input("hi", cx);
1939                    assert!(editor.is_dirty(cx));
1940                });
1941            })
1942            .unwrap();
1943
1944        // Save the buffer. This prompts for a filename.
1945        let save_task = window
1946            .update(cx, |workspace, cx| {
1947                workspace.save_active_item(SaveIntent::Save, cx)
1948            })
1949            .unwrap();
1950        cx.background_executor.run_until_parked();
1951        cx.simulate_new_path_selection(|parent_dir| {
1952            assert_eq!(parent_dir, Path::new("/root"));
1953            Some(parent_dir.join("the-new-name.rs"))
1954        });
1955        cx.read(|cx| {
1956            assert!(editor.is_dirty(cx));
1957            assert_eq!(editor.read(cx).title(cx), "untitled");
1958        });
1959
1960        // When the save completes, the buffer's title is updated and the language is assigned based
1961        // on the path.
1962        save_task.await.unwrap();
1963        window
1964            .update(cx, |_, cx| {
1965                editor.update(cx, |editor, cx| {
1966                    assert!(!editor.is_dirty(cx));
1967                    assert_eq!(editor.title(cx), "the-new-name.rs");
1968                    assert_eq!(
1969                        editor
1970                            .buffer()
1971                            .read(cx)
1972                            .language_at(0, cx)
1973                            .unwrap()
1974                            .name()
1975                            .as_ref(),
1976                        "Rust"
1977                    );
1978                });
1979            })
1980            .unwrap();
1981
1982        // Edit the file and save it again. This time, there is no filename prompt.
1983        window
1984            .update(cx, |_, cx| {
1985                editor.update(cx, |editor, cx| {
1986                    editor.handle_input(" there", cx);
1987                    assert!(editor.is_dirty(cx));
1988                });
1989            })
1990            .unwrap();
1991
1992        let save_task = window
1993            .update(cx, |workspace, cx| {
1994                workspace.save_active_item(SaveIntent::Save, cx)
1995            })
1996            .unwrap();
1997        save_task.await.unwrap();
1998
1999        assert!(!cx.did_prompt_for_new_path());
2000        window
2001            .update(cx, |_, cx| {
2002                editor.update(cx, |editor, cx| {
2003                    assert!(!editor.is_dirty(cx));
2004                    assert_eq!(editor.title(cx), "the-new-name.rs")
2005                });
2006            })
2007            .unwrap();
2008
2009        // Open the same newly-created file in another pane item. The new editor should reuse
2010        // the same buffer.
2011        cx.dispatch_action(window.into(), NewFile);
2012        window
2013            .update(cx, |workspace, cx| {
2014                workspace.split_and_clone(
2015                    workspace.active_pane().clone(),
2016                    SplitDirection::Right,
2017                    cx,
2018                );
2019                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
2020            })
2021            .unwrap()
2022            .await
2023            .unwrap();
2024        let editor2 = window
2025            .update(cx, |workspace, cx| {
2026                workspace
2027                    .active_item(cx)
2028                    .unwrap()
2029                    .downcast::<Editor>()
2030                    .unwrap()
2031            })
2032            .unwrap();
2033        cx.read(|cx| {
2034            assert_eq!(
2035                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
2036                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
2037            );
2038        })
2039    }
2040
2041    #[gpui::test]
2042    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
2043        let app_state = init_test(cx);
2044        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2045
2046        let project = Project::test(app_state.fs.clone(), [], cx).await;
2047        project.update(cx, |project, _| project.languages().add(rust_lang()));
2048        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2049
2050        // Create a new untitled buffer
2051        cx.dispatch_action(window.into(), NewFile);
2052        let editor = window
2053            .read_with(cx, |workspace, cx| {
2054                workspace
2055                    .active_item(cx)
2056                    .unwrap()
2057                    .downcast::<Editor>()
2058                    .unwrap()
2059            })
2060            .unwrap();
2061        window
2062            .update(cx, |_, cx| {
2063                editor.update(cx, |editor, cx| {
2064                    assert!(Arc::ptr_eq(
2065                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2066                        &languages::PLAIN_TEXT
2067                    ));
2068                    editor.handle_input("hi", cx);
2069                    assert!(editor.is_dirty(cx));
2070                });
2071            })
2072            .unwrap();
2073
2074        // Save the buffer. This prompts for a filename.
2075        let save_task = window
2076            .update(cx, |workspace, cx| {
2077                workspace.save_active_item(SaveIntent::Save, cx)
2078            })
2079            .unwrap();
2080        cx.background_executor.run_until_parked();
2081        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
2082        save_task.await.unwrap();
2083        // The buffer is not dirty anymore and the language is assigned based on the path.
2084        window
2085            .update(cx, |_, cx| {
2086                editor.update(cx, |editor, cx| {
2087                    assert!(!editor.is_dirty(cx));
2088                    assert_eq!(
2089                        editor
2090                            .buffer()
2091                            .read(cx)
2092                            .language_at(0, cx)
2093                            .unwrap()
2094                            .name()
2095                            .as_ref(),
2096                        "Rust"
2097                    )
2098                });
2099            })
2100            .unwrap();
2101    }
2102
2103    #[gpui::test]
2104    async fn test_pane_actions(cx: &mut TestAppContext) {
2105        let app_state = init_test(cx);
2106        app_state
2107            .fs
2108            .as_fake()
2109            .insert_tree(
2110                "/root",
2111                json!({
2112                    "a": {
2113                        "file1": "contents 1",
2114                        "file2": "contents 2",
2115                        "file3": "contents 3",
2116                    },
2117                }),
2118            )
2119            .await;
2120
2121        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2122        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
2123        let workspace = window.root(cx).unwrap();
2124
2125        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2126        let file1 = entries[0].clone();
2127
2128        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
2129
2130        window
2131            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2132            .unwrap()
2133            .await
2134            .unwrap();
2135
2136        let (editor_1, buffer) = window
2137            .update(cx, |_, cx| {
2138                pane_1.update(cx, |pane_1, cx| {
2139                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
2140                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
2141                    let buffer = editor.update(cx, |editor, cx| {
2142                        editor.insert("dirt", cx);
2143                        editor.buffer().downgrade()
2144                    });
2145                    (editor.downgrade(), buffer)
2146                })
2147            })
2148            .unwrap();
2149
2150        cx.dispatch_action(window.into(), pane::SplitRight);
2151        let editor_2 = cx.update(|cx| {
2152            let pane_2 = workspace.read(cx).active_pane().clone();
2153            assert_ne!(pane_1, pane_2);
2154
2155            let pane2_item = pane_2.read(cx).active_item().unwrap();
2156            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
2157
2158            pane2_item.downcast::<Editor>().unwrap().downgrade()
2159        });
2160        cx.dispatch_action(
2161            window.into(),
2162            workspace::CloseActiveItem { save_intent: None },
2163        );
2164
2165        cx.background_executor.run_until_parked();
2166        window
2167            .read_with(cx, |workspace, _| {
2168                assert_eq!(workspace.panes().len(), 1);
2169                assert_eq!(workspace.active_pane(), &pane_1);
2170            })
2171            .unwrap();
2172
2173        cx.dispatch_action(
2174            window.into(),
2175            workspace::CloseActiveItem { save_intent: None },
2176        );
2177        cx.background_executor.run_until_parked();
2178        cx.simulate_prompt_answer(1);
2179        cx.background_executor.run_until_parked();
2180
2181        window
2182            .read_with(cx, |workspace, cx| {
2183                assert_eq!(workspace.panes().len(), 1);
2184                assert!(workspace.active_item(cx).is_none());
2185            })
2186            .unwrap();
2187        editor_1.assert_released();
2188        editor_2.assert_released();
2189        buffer.assert_released();
2190    }
2191
2192    #[gpui::test]
2193    async fn test_navigation(cx: &mut TestAppContext) {
2194        let app_state = init_test(cx);
2195        app_state
2196            .fs
2197            .as_fake()
2198            .insert_tree(
2199                "/root",
2200                json!({
2201                    "a": {
2202                        "file1": "contents 1\n".repeat(20),
2203                        "file2": "contents 2\n".repeat(20),
2204                        "file3": "contents 3\n".repeat(20),
2205                    },
2206                }),
2207            )
2208            .await;
2209
2210        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2211        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2212        let pane = workspace
2213            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2214            .unwrap();
2215
2216        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2217        let file1 = entries[0].clone();
2218        let file2 = entries[1].clone();
2219        let file3 = entries[2].clone();
2220
2221        let editor1 = workspace
2222            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2223            .unwrap()
2224            .await
2225            .unwrap()
2226            .downcast::<Editor>()
2227            .unwrap();
2228        workspace
2229            .update(cx, |_, cx| {
2230                editor1.update(cx, |editor, cx| {
2231                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2232                        s.select_display_ranges(
2233                            [DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)],
2234                        )
2235                    });
2236                });
2237            })
2238            .unwrap();
2239
2240        let editor2 = workspace
2241            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2242            .unwrap()
2243            .await
2244            .unwrap()
2245            .downcast::<Editor>()
2246            .unwrap();
2247        let editor3 = workspace
2248            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2249            .unwrap()
2250            .await
2251            .unwrap()
2252            .downcast::<Editor>()
2253            .unwrap();
2254
2255        workspace
2256            .update(cx, |_, cx| {
2257                editor3.update(cx, |editor, cx| {
2258                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
2259                        s.select_display_ranges(
2260                            [DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)],
2261                        )
2262                    });
2263                    editor.newline(&Default::default(), cx);
2264                    editor.newline(&Default::default(), cx);
2265                    editor.move_down(&Default::default(), cx);
2266                    editor.move_down(&Default::default(), cx);
2267                    editor.save(true, project.clone(), cx)
2268                })
2269            })
2270            .unwrap()
2271            .await
2272            .unwrap();
2273        workspace
2274            .update(cx, |_, cx| {
2275                editor3.update(cx, |editor, cx| {
2276                    editor.set_scroll_position(point(0., 12.5), cx)
2277                });
2278            })
2279            .unwrap();
2280        assert_eq!(
2281            active_location(&workspace, cx),
2282            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
2283        );
2284
2285        workspace
2286            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2287            .unwrap()
2288            .await
2289            .unwrap();
2290        assert_eq!(
2291            active_location(&workspace, cx),
2292            (file3.clone(), DisplayPoint::new(0, 0), 0.)
2293        );
2294
2295        workspace
2296            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2297            .unwrap()
2298            .await
2299            .unwrap();
2300        assert_eq!(
2301            active_location(&workspace, cx),
2302            (file2.clone(), DisplayPoint::new(0, 0), 0.)
2303        );
2304
2305        workspace
2306            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2307            .unwrap()
2308            .await
2309            .unwrap();
2310        assert_eq!(
2311            active_location(&workspace, cx),
2312            (file1.clone(), DisplayPoint::new(10, 0), 0.)
2313        );
2314
2315        workspace
2316            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2317            .unwrap()
2318            .await
2319            .unwrap();
2320        assert_eq!(
2321            active_location(&workspace, cx),
2322            (file1.clone(), DisplayPoint::new(0, 0), 0.)
2323        );
2324
2325        // Go back one more time and ensure we don't navigate past the first item in the history.
2326        workspace
2327            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2328            .unwrap()
2329            .await
2330            .unwrap();
2331        assert_eq!(
2332            active_location(&workspace, cx),
2333            (file1.clone(), DisplayPoint::new(0, 0), 0.)
2334        );
2335
2336        workspace
2337            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2338            .unwrap()
2339            .await
2340            .unwrap();
2341        assert_eq!(
2342            active_location(&workspace, cx),
2343            (file1.clone(), DisplayPoint::new(10, 0), 0.)
2344        );
2345
2346        workspace
2347            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2348            .unwrap()
2349            .await
2350            .unwrap();
2351        assert_eq!(
2352            active_location(&workspace, cx),
2353            (file2.clone(), DisplayPoint::new(0, 0), 0.)
2354        );
2355
2356        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
2357        // location.
2358        workspace
2359            .update(cx, |_, cx| {
2360                pane.update(cx, |pane, cx| {
2361                    let editor3_id = editor3.entity_id();
2362                    drop(editor3);
2363                    pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
2364                })
2365            })
2366            .unwrap()
2367            .await
2368            .unwrap();
2369        workspace
2370            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2371            .unwrap()
2372            .await
2373            .unwrap();
2374        assert_eq!(
2375            active_location(&workspace, cx),
2376            (file3.clone(), DisplayPoint::new(0, 0), 0.)
2377        );
2378
2379        workspace
2380            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2381            .unwrap()
2382            .await
2383            .unwrap();
2384        assert_eq!(
2385            active_location(&workspace, cx),
2386            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
2387        );
2388
2389        workspace
2390            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2391            .unwrap()
2392            .await
2393            .unwrap();
2394        assert_eq!(
2395            active_location(&workspace, cx),
2396            (file3.clone(), DisplayPoint::new(0, 0), 0.)
2397        );
2398
2399        // Go back to an item that has been closed and removed from disk
2400        workspace
2401            .update(cx, |_, cx| {
2402                pane.update(cx, |pane, cx| {
2403                    let editor2_id = editor2.entity_id();
2404                    drop(editor2);
2405                    pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
2406                })
2407            })
2408            .unwrap()
2409            .await
2410            .unwrap();
2411        app_state
2412            .fs
2413            .remove_file(Path::new("/root/a/file2"), Default::default())
2414            .await
2415            .unwrap();
2416        cx.background_executor.run_until_parked();
2417
2418        workspace
2419            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2420            .unwrap()
2421            .await
2422            .unwrap();
2423        assert_eq!(
2424            active_location(&workspace, cx),
2425            (file2.clone(), DisplayPoint::new(0, 0), 0.)
2426        );
2427        workspace
2428            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
2429            .unwrap()
2430            .await
2431            .unwrap();
2432        assert_eq!(
2433            active_location(&workspace, cx),
2434            (file3.clone(), DisplayPoint::new(0, 0), 0.)
2435        );
2436
2437        // Modify file to collapse multiple nav history entries into the same location.
2438        // Ensure we don't visit the same location twice when navigating.
2439        workspace
2440            .update(cx, |_, cx| {
2441                editor1.update(cx, |editor, cx| {
2442                    editor.change_selections(None, cx, |s| {
2443                        s.select_display_ranges(
2444                            [DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)],
2445                        )
2446                    })
2447                });
2448            })
2449            .unwrap();
2450        for _ in 0..5 {
2451            workspace
2452                .update(cx, |_, cx| {
2453                    editor1.update(cx, |editor, cx| {
2454                        editor.change_selections(None, cx, |s| {
2455                            s.select_display_ranges([
2456                                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)
2457                            ])
2458                        });
2459                    });
2460                })
2461                .unwrap();
2462
2463            workspace
2464                .update(cx, |_, cx| {
2465                    editor1.update(cx, |editor, cx| {
2466                        editor.change_selections(None, cx, |s| {
2467                            s.select_display_ranges([
2468                                DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)
2469                            ])
2470                        })
2471                    });
2472                })
2473                .unwrap();
2474        }
2475        workspace
2476            .update(cx, |_, cx| {
2477                editor1.update(cx, |editor, cx| {
2478                    editor.transact(cx, |editor, cx| {
2479                        editor.change_selections(None, cx, |s| {
2480                            s.select_display_ranges([
2481                                DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)
2482                            ])
2483                        });
2484                        editor.insert("", cx);
2485                    })
2486                });
2487            })
2488            .unwrap();
2489
2490        workspace
2491            .update(cx, |_, cx| {
2492                editor1.update(cx, |editor, cx| {
2493                    editor.change_selections(None, cx, |s| {
2494                        s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
2495                    })
2496                });
2497            })
2498            .unwrap();
2499        workspace
2500            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2501            .unwrap()
2502            .await
2503            .unwrap();
2504        assert_eq!(
2505            active_location(&workspace, cx),
2506            (file1.clone(), DisplayPoint::new(2, 0), 0.)
2507        );
2508        workspace
2509            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
2510            .unwrap()
2511            .await
2512            .unwrap();
2513        assert_eq!(
2514            active_location(&workspace, cx),
2515            (file1.clone(), DisplayPoint::new(3, 0), 0.)
2516        );
2517
2518        fn active_location(
2519            workspace: &WindowHandle<Workspace>,
2520            cx: &mut TestAppContext,
2521        ) -> (ProjectPath, DisplayPoint, f32) {
2522            workspace
2523                .update(cx, |workspace, cx| {
2524                    let item = workspace.active_item(cx).unwrap();
2525                    let editor = item.downcast::<Editor>().unwrap();
2526                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
2527                        (
2528                            editor.selections.display_ranges(cx),
2529                            editor.scroll_position(cx),
2530                        )
2531                    });
2532                    (
2533                        item.project_path(cx).unwrap(),
2534                        selections[0].start,
2535                        scroll_position.y,
2536                    )
2537                })
2538                .unwrap()
2539        }
2540    }
2541
2542    #[gpui::test]
2543    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
2544        let app_state = init_test(cx);
2545        app_state
2546            .fs
2547            .as_fake()
2548            .insert_tree(
2549                "/root",
2550                json!({
2551                    "a": {
2552                        "file1": "",
2553                        "file2": "",
2554                        "file3": "",
2555                        "file4": "",
2556                    },
2557                }),
2558            )
2559            .await;
2560
2561        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
2562        let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
2563        let pane = workspace
2564            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2565            .unwrap();
2566
2567        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
2568        let file1 = entries[0].clone();
2569        let file2 = entries[1].clone();
2570        let file3 = entries[2].clone();
2571        let file4 = entries[3].clone();
2572
2573        let file1_item_id = workspace
2574            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2575            .unwrap()
2576            .await
2577            .unwrap()
2578            .item_id();
2579        let file2_item_id = workspace
2580            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2581            .unwrap()
2582            .await
2583            .unwrap()
2584            .item_id();
2585        let file3_item_id = workspace
2586            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2587            .unwrap()
2588            .await
2589            .unwrap()
2590            .item_id();
2591        let file4_item_id = workspace
2592            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
2593            .unwrap()
2594            .await
2595            .unwrap()
2596            .item_id();
2597        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2598
2599        // Close all the pane items in some arbitrary order.
2600        workspace
2601            .update(cx, |_, cx| {
2602                pane.update(cx, |pane, cx| {
2603                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
2604                })
2605            })
2606            .unwrap()
2607            .await
2608            .unwrap();
2609        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2610
2611        workspace
2612            .update(cx, |_, cx| {
2613                pane.update(cx, |pane, cx| {
2614                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
2615                })
2616            })
2617            .unwrap()
2618            .await
2619            .unwrap();
2620        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2621
2622        workspace
2623            .update(cx, |_, cx| {
2624                pane.update(cx, |pane, cx| {
2625                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
2626                })
2627            })
2628            .unwrap()
2629            .await
2630            .unwrap();
2631        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2632        workspace
2633            .update(cx, |_, cx| {
2634                pane.update(cx, |pane, cx| {
2635                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
2636                })
2637            })
2638            .unwrap()
2639            .await
2640            .unwrap();
2641
2642        assert_eq!(active_path(&workspace, cx), None);
2643
2644        // Reopen all the closed items, ensuring they are reopened in the same order
2645        // in which they were closed.
2646        workspace
2647            .update(cx, Workspace::reopen_closed_item)
2648            .unwrap()
2649            .await
2650            .unwrap();
2651        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2652
2653        workspace
2654            .update(cx, Workspace::reopen_closed_item)
2655            .unwrap()
2656            .await
2657            .unwrap();
2658        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2659
2660        workspace
2661            .update(cx, Workspace::reopen_closed_item)
2662            .unwrap()
2663            .await
2664            .unwrap();
2665        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2666
2667        workspace
2668            .update(cx, Workspace::reopen_closed_item)
2669            .unwrap()
2670            .await
2671            .unwrap();
2672        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2673
2674        // Reopening past the last closed item is a no-op.
2675        workspace
2676            .update(cx, Workspace::reopen_closed_item)
2677            .unwrap()
2678            .await
2679            .unwrap();
2680        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2681
2682        // Reopening closed items doesn't interfere with navigation history.
2683        workspace
2684            .update(cx, |workspace, cx| {
2685                workspace.go_back(workspace.active_pane().downgrade(), cx)
2686            })
2687            .unwrap()
2688            .await
2689            .unwrap();
2690        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2691
2692        workspace
2693            .update(cx, |workspace, cx| {
2694                workspace.go_back(workspace.active_pane().downgrade(), cx)
2695            })
2696            .unwrap()
2697            .await
2698            .unwrap();
2699        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2700
2701        workspace
2702            .update(cx, |workspace, cx| {
2703                workspace.go_back(workspace.active_pane().downgrade(), cx)
2704            })
2705            .unwrap()
2706            .await
2707            .unwrap();
2708        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2709
2710        workspace
2711            .update(cx, |workspace, cx| {
2712                workspace.go_back(workspace.active_pane().downgrade(), cx)
2713            })
2714            .unwrap()
2715            .await
2716            .unwrap();
2717        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2718
2719        workspace
2720            .update(cx, |workspace, cx| {
2721                workspace.go_back(workspace.active_pane().downgrade(), cx)
2722            })
2723            .unwrap()
2724            .await
2725            .unwrap();
2726        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2727
2728        workspace
2729            .update(cx, |workspace, cx| {
2730                workspace.go_back(workspace.active_pane().downgrade(), cx)
2731            })
2732            .unwrap()
2733            .await
2734            .unwrap();
2735        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2736
2737        workspace
2738            .update(cx, |workspace, cx| {
2739                workspace.go_back(workspace.active_pane().downgrade(), cx)
2740            })
2741            .unwrap()
2742            .await
2743            .unwrap();
2744        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2745
2746        workspace
2747            .update(cx, |workspace, cx| {
2748                workspace.go_back(workspace.active_pane().downgrade(), cx)
2749            })
2750            .unwrap()
2751            .await
2752            .unwrap();
2753        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2754
2755        fn active_path(
2756            workspace: &WindowHandle<Workspace>,
2757            cx: &TestAppContext,
2758        ) -> Option<ProjectPath> {
2759            workspace
2760                .read_with(cx, |workspace, cx| {
2761                    let item = workspace.active_item(cx)?;
2762                    item.project_path(cx)
2763                })
2764                .unwrap()
2765        }
2766    }
2767
2768    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
2769        cx.update(|cx| {
2770            let app_state = AppState::test(cx);
2771
2772            theme::init(theme::LoadThemes::JustBase, cx);
2773            client::init(&app_state.client, cx);
2774            language::init(cx);
2775            workspace::init(app_state.clone(), cx);
2776            welcome::init(cx);
2777            Project::init_settings(cx);
2778            app_state
2779        })
2780    }
2781
2782    #[gpui::test]
2783    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
2784        let executor = cx.executor();
2785        let app_state = init_keymap_test(cx);
2786        let project = Project::test(app_state.fs.clone(), [], cx).await;
2787        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2788
2789        actions!(test1, [A, B]);
2790        // From the Atom keymap
2791        use workspace::ActivatePreviousPane;
2792        // From the JetBrains keymap
2793        use workspace::ActivatePrevItem;
2794
2795        app_state
2796            .fs
2797            .save(
2798                "/settings.json".as_ref(),
2799                &r#"
2800                {
2801                    "base_keymap": "Atom"
2802                }
2803                "#
2804                .into(),
2805                Default::default(),
2806            )
2807            .await
2808            .unwrap();
2809
2810        app_state
2811            .fs
2812            .save(
2813                "/keymap.json".as_ref(),
2814                &r#"
2815                [
2816                    {
2817                        "bindings": {
2818                            "backspace": "test1::A"
2819                        }
2820                    }
2821                ]
2822                "#
2823                .into(),
2824                Default::default(),
2825            )
2826            .await
2827            .unwrap();
2828        executor.run_until_parked();
2829        cx.update(|cx| {
2830            let settings_rx = watch_config_file(
2831                &executor,
2832                app_state.fs.clone(),
2833                PathBuf::from("/settings.json"),
2834            );
2835            let keymap_rx = watch_config_file(
2836                &executor,
2837                app_state.fs.clone(),
2838                PathBuf::from("/keymap.json"),
2839            );
2840            handle_settings_file_changes(settings_rx, cx);
2841            handle_keymap_file_changes(keymap_rx, cx);
2842        });
2843        workspace
2844            .update(cx, |workspace, cx| {
2845                workspace.register_action(|_, _: &A, _cx| {});
2846                workspace.register_action(|_, _: &B, _cx| {});
2847                workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {});
2848                workspace.register_action(|_, _: &ActivatePrevItem, _cx| {});
2849                cx.notify();
2850            })
2851            .unwrap();
2852        executor.run_until_parked();
2853        // Test loading the keymap base at all
2854        assert_key_bindings_for(
2855            workspace.into(),
2856            cx,
2857            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2858            line!(),
2859        );
2860
2861        // Test modifying the users keymap, while retaining the base keymap
2862        app_state
2863            .fs
2864            .save(
2865                "/keymap.json".as_ref(),
2866                &r#"
2867                [
2868                    {
2869                        "bindings": {
2870                            "backspace": "test1::B"
2871                        }
2872                    }
2873                ]
2874                "#
2875                .into(),
2876                Default::default(),
2877            )
2878            .await
2879            .unwrap();
2880
2881        executor.run_until_parked();
2882
2883        assert_key_bindings_for(
2884            workspace.into(),
2885            cx,
2886            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
2887            line!(),
2888        );
2889
2890        // Test modifying the base, while retaining the users keymap
2891        app_state
2892            .fs
2893            .save(
2894                "/settings.json".as_ref(),
2895                &r#"
2896                {
2897                    "base_keymap": "JetBrains"
2898                }
2899                "#
2900                .into(),
2901                Default::default(),
2902            )
2903            .await
2904            .unwrap();
2905
2906        executor.run_until_parked();
2907
2908        assert_key_bindings_for(
2909            workspace.into(),
2910            cx,
2911            vec![("backspace", &B), ("[", &ActivatePrevItem)],
2912            line!(),
2913        );
2914    }
2915
2916    #[gpui::test]
2917    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
2918        let executor = cx.executor();
2919        let app_state = init_keymap_test(cx);
2920        let project = Project::test(app_state.fs.clone(), [], cx).await;
2921        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
2922
2923        actions!(test2, [A, B]);
2924        // From the Atom keymap
2925        use workspace::ActivatePreviousPane;
2926        // From the JetBrains keymap
2927        use pane::ActivatePrevItem;
2928        workspace
2929            .update(cx, |workspace, _| {
2930                workspace
2931                    .register_action(|_, _: &A, _| {})
2932                    .register_action(|_, _: &B, _| {});
2933            })
2934            .unwrap();
2935        app_state
2936            .fs
2937            .save(
2938                "/settings.json".as_ref(),
2939                &r#"
2940                {
2941                    "base_keymap": "Atom"
2942                }
2943                "#
2944                .into(),
2945                Default::default(),
2946            )
2947            .await
2948            .unwrap();
2949        app_state
2950            .fs
2951            .save(
2952                "/keymap.json".as_ref(),
2953                &r#"
2954                [
2955                    {
2956                        "bindings": {
2957                            "backspace": "test2::A"
2958                        }
2959                    }
2960                ]
2961                "#
2962                .into(),
2963                Default::default(),
2964            )
2965            .await
2966            .unwrap();
2967
2968        cx.update(|cx| {
2969            let settings_rx = watch_config_file(
2970                &executor,
2971                app_state.fs.clone(),
2972                PathBuf::from("/settings.json"),
2973            );
2974            let keymap_rx = watch_config_file(
2975                &executor,
2976                app_state.fs.clone(),
2977                PathBuf::from("/keymap.json"),
2978            );
2979
2980            handle_settings_file_changes(settings_rx, cx);
2981            handle_keymap_file_changes(keymap_rx, cx);
2982        });
2983
2984        cx.background_executor.run_until_parked();
2985
2986        cx.background_executor.run_until_parked();
2987        // Test loading the keymap base at all
2988        assert_key_bindings_for(
2989            workspace.into(),
2990            cx,
2991            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2992            line!(),
2993        );
2994
2995        // Test disabling the key binding for the base keymap
2996        app_state
2997            .fs
2998            .save(
2999                "/keymap.json".as_ref(),
3000                &r#"
3001                [
3002                    {
3003                        "bindings": {
3004                            "backspace": null
3005                        }
3006                    }
3007                ]
3008                "#
3009                .into(),
3010                Default::default(),
3011            )
3012            .await
3013            .unwrap();
3014
3015        cx.background_executor.run_until_parked();
3016
3017        assert_key_bindings_for(
3018            workspace.into(),
3019            cx,
3020            vec![("k", &ActivatePreviousPane)],
3021            line!(),
3022        );
3023
3024        // Test modifying the base, while retaining the users keymap
3025        app_state
3026            .fs
3027            .save(
3028                "/settings.json".as_ref(),
3029                &r#"
3030                {
3031                    "base_keymap": "JetBrains"
3032                }
3033                "#
3034                .into(),
3035                Default::default(),
3036            )
3037            .await
3038            .unwrap();
3039
3040        cx.background_executor.run_until_parked();
3041
3042        assert_key_bindings_for(
3043            workspace.into(),
3044            cx,
3045            vec![("[", &ActivatePrevItem)],
3046            line!(),
3047        );
3048    }
3049
3050    #[gpui::test]
3051    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
3052        cx.text_system()
3053            .add_fonts(vec![
3054                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap(),
3055                Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap(),
3056            ])
3057            .unwrap();
3058        let themes = ThemeRegistry::default();
3059        settings::init(cx);
3060        theme::init(theme::LoadThemes::JustBase, cx);
3061
3062        let mut has_default_theme = false;
3063        for theme_name in themes.list(false).into_iter().map(|meta| meta.name) {
3064            let theme = themes.get(&theme_name).unwrap();
3065            assert_eq!(theme.name, theme_name);
3066            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
3067                has_default_theme = true;
3068            }
3069        }
3070        assert!(has_default_theme);
3071    }
3072
3073    #[gpui::test]
3074    async fn test_bundled_languages(cx: &mut TestAppContext) {
3075        let settings = cx.update(|cx| SettingsStore::test(cx));
3076        cx.set_global(settings);
3077        let languages = LanguageRegistry::test(cx.executor());
3078        let languages = Arc::new(languages);
3079        let node_runtime = node_runtime::FakeNodeRuntime::new();
3080        cx.update(|cx| {
3081            languages::init(languages.clone(), node_runtime, cx);
3082        });
3083        for name in languages.language_names() {
3084            languages
3085                .language_for_name(&name)
3086                .await
3087                .with_context(|| format!("language name {name}"))
3088                .unwrap();
3089        }
3090        cx.run_until_parked();
3091    }
3092
3093    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3094        cx.update(|cx| {
3095            let mut app_state = AppState::test(cx);
3096
3097            let state = Arc::get_mut(&mut app_state).unwrap();
3098            env_logger::try_init().ok();
3099
3100            state.build_window_options = build_window_options;
3101            theme::init(theme::LoadThemes::JustBase, cx);
3102            audio::init((), cx);
3103            channel::init(&app_state.client, app_state.user_store.clone(), cx);
3104            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3105            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
3106            workspace::init(app_state.clone(), cx);
3107            Project::init_settings(cx);
3108            release_channel::init("0.0.0", cx);
3109            command_palette::init(cx);
3110            language::init(cx);
3111            editor::init(cx);
3112            project_panel::init_settings(cx);
3113            collab_ui::init(&app_state, cx);
3114            project_panel::init((), cx);
3115            terminal_view::init(cx);
3116            assistant::init(app_state.client.clone(), cx);
3117            tasks_ui::init(cx);
3118            initialize_workspace(app_state.clone(), cx);
3119            app_state
3120        })
3121    }
3122
3123    fn rust_lang() -> Arc<language::Language> {
3124        Arc::new(language::Language::new(
3125            language::LanguageConfig {
3126                name: "Rust".into(),
3127                matcher: LanguageMatcher {
3128                    path_suffixes: vec!["rs".to_string()],
3129                    ..Default::default()
3130                },
3131                ..Default::default()
3132            },
3133            Some(tree_sitter_rust::language()),
3134        ))
3135    }
3136    #[track_caller]
3137    fn assert_key_bindings_for(
3138        window: AnyWindowHandle,
3139        cx: &TestAppContext,
3140        actions: Vec<(&'static str, &dyn Action)>,
3141        line: u32,
3142    ) {
3143        let available_actions = cx
3144            .update(|cx| window.update(cx, |_, cx| cx.available_actions()))
3145            .unwrap();
3146        for (key, action) in actions {
3147            let bindings = cx
3148                .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action)))
3149                .unwrap();
3150            // assert that...
3151            assert!(
3152                available_actions.iter().any(|bound_action| {
3153                    // actions match...
3154                    bound_action.partial_eq(action)
3155                }),
3156                "On {} Failed to find {}",
3157                line,
3158                action.name(),
3159            );
3160            assert!(
3161                // and key strokes contain the given key
3162                bindings
3163                    .into_iter()
3164                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
3165                "On {} Failed to find {} with key binding {}",
3166                line,
3167                action.name(),
3168                key
3169            );
3170        }
3171    }
3172}
3173
3174async fn register_zed_scheme(cx: &AsyncAppContext) -> anyhow::Result<()> {
3175    cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
3176        .await
3177}