zed.rs

   1pub mod assets;
   2pub mod languages;
   3pub mod menus;
   4pub mod only_instance;
   5#[cfg(any(test, feature = "test-support"))]
   6pub mod test;
   7
   8use ai::AssistantPanel;
   9use anyhow::Context;
  10use assets::Assets;
  11use breadcrumbs::Breadcrumbs;
  12pub use client;
  13use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
  14use collections::VecDeque;
  15pub use editor;
  16use editor::{Editor, MultiBuffer};
  17
  18use anyhow::anyhow;
  19use feedback::{
  20    feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
  21};
  22use futures::{channel::mpsc, StreamExt};
  23use gpui::{
  24    anyhow::{self, Result},
  25    geometry::vector::vec2f,
  26    impl_actions,
  27    platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
  28    AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
  29};
  30pub use lsp;
  31pub use project;
  32use project_panel::ProjectPanel;
  33use search::{BufferSearchBar, ProjectSearchBar};
  34use serde::Deserialize;
  35use serde_json::to_string_pretty;
  36use settings::{initial_local_settings_content, KeymapFile, SettingsStore};
  37use std::{borrow::Cow, str, sync::Arc};
  38use terminal_view::terminal_panel::{self, TerminalPanel};
  39use util::{
  40    asset_str,
  41    channel::ReleaseChannel,
  42    paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
  43    ResultExt,
  44};
  45use uuid::Uuid;
  46use welcome::BaseKeymap;
  47pub use workspace;
  48use workspace::{
  49    create_and_open_local_file, dock::PanelHandle,
  50    notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
  51    NewWindow, Workspace, WorkspaceSettings,
  52};
  53use zed_actions::*;
  54
  55#[derive(Deserialize, Clone, PartialEq)]
  56pub struct OpenBrowser {
  57    url: Arc<str>,
  58}
  59
  60impl_actions!(zed, [OpenBrowser]);
  61
  62pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
  63    cx.add_action(about);
  64    cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
  65        cx.platform().hide();
  66    });
  67    cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| {
  68        cx.platform().hide_other_apps();
  69    });
  70    cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| {
  71        cx.platform().unhide_other_apps();
  72    });
  73    cx.add_action(
  74        |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
  75            cx.minimize_window();
  76        },
  77    );
  78    cx.add_action(
  79        |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
  80            cx.zoom_window();
  81        },
  82    );
  83    cx.add_action(
  84        |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
  85            cx.toggle_full_screen();
  86        },
  87    );
  88    cx.add_action(
  89        |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
  90            if let Some(item) = workspace
  91                .titlebar_item()
  92                .and_then(|item| item.downcast::<CollabTitlebarItem>())
  93            {
  94                cx.defer(move |_, cx| {
  95                    item.update(cx, |item, cx| {
  96                        item.toggle_contacts_popover(&Default::default(), cx);
  97                    });
  98                });
  99            }
 100        },
 101    );
 102    cx.add_global_action(quit);
 103    cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
 104    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
 105        theme::adjust_font_size(cx, |size| *size += 1.0)
 106    });
 107    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
 108        theme::adjust_font_size(cx, |size| *size -= 1.0)
 109    });
 110    cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
 111    cx.add_global_action(move |_: &install_cli::Install, cx| {
 112        cx.spawn(|cx| async move {
 113            install_cli::install_cli(&cx)
 114                .await
 115                .context("error creating CLI symlink")
 116        })
 117        .detach_and_log_err(cx);
 118    });
 119    cx.add_action(
 120        move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
 121            open_log_file(workspace, cx);
 122        },
 123    );
 124    cx.add_action(
 125        move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
 126            open_bundled_file(
 127                workspace,
 128                asset_str::<Assets>("licenses.md"),
 129                "Open Source License Attribution",
 130                "Markdown",
 131                cx,
 132            );
 133        },
 134    );
 135    cx.add_action(
 136        move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
 137            open_telemetry_log_file(workspace, cx);
 138        },
 139    );
 140    cx.add_action(
 141        move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
 142            create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
 143        },
 144    );
 145    cx.add_action(
 146        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 147            create_and_open_local_file(&paths::SETTINGS, cx, || {
 148                settings::initial_user_settings_content().as_ref().into()
 149            })
 150            .detach_and_log_err(cx);
 151        },
 152    );
 153    cx.add_action(open_local_settings_file);
 154    cx.add_action(
 155        move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
 156            open_bundled_file(
 157                workspace,
 158                settings::default_keymap(),
 159                "Default Key Bindings",
 160                "JSON",
 161                cx,
 162            );
 163        },
 164    );
 165    cx.add_action(
 166        move |workspace: &mut Workspace,
 167              _: &OpenDefaultSettings,
 168              cx: &mut ViewContext<Workspace>| {
 169            open_bundled_file(
 170                workspace,
 171                settings::default_settings(),
 172                "Default Settings",
 173                "JSON",
 174                cx,
 175            );
 176        },
 177    );
 178    cx.add_action({
 179        move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
 180            let app_state = workspace.app_state().clone();
 181            let markdown = app_state.languages.language_for_name("JSON");
 182            let window_id = cx.window_id();
 183            cx.spawn(|workspace, mut cx| async move {
 184                let markdown = markdown.await.log_err();
 185                let content = to_string_pretty(
 186                    &cx.debug_elements(window_id)
 187                        .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
 188                )
 189                .unwrap();
 190                workspace
 191                    .update(&mut cx, |workspace, cx| {
 192                        workspace.with_local_workspace(cx, move |workspace, cx| {
 193                            let project = workspace.project().clone();
 194
 195                            let buffer = project
 196                                .update(cx, |project, cx| {
 197                                    project.create_buffer(&content, markdown, cx)
 198                                })
 199                                .expect("creating buffers on a local workspace always succeeds");
 200                            let buffer = cx.add_model(|cx| {
 201                                MultiBuffer::singleton(buffer, cx)
 202                                    .with_title("Debug Elements".into())
 203                            });
 204                            workspace.add_item(
 205                                Box::new(cx.add_view(|cx| {
 206                                    Editor::for_multibuffer(buffer, Some(project.clone()), cx)
 207                                })),
 208                                cx,
 209                            );
 210                        })
 211                    })?
 212                    .await
 213            })
 214            .detach_and_log_err(cx);
 215        }
 216    });
 217    cx.add_action(
 218        |workspace: &mut Workspace,
 219         _: &project_panel::ToggleFocus,
 220         cx: &mut ViewContext<Workspace>| {
 221            workspace.toggle_panel_focus::<ProjectPanel>(cx);
 222        },
 223    );
 224    cx.add_action(
 225        |workspace: &mut Workspace,
 226         _: &terminal_panel::ToggleFocus,
 227         cx: &mut ViewContext<Workspace>| {
 228            workspace.toggle_panel_focus::<TerminalPanel>(cx);
 229        },
 230    );
 231    cx.add_global_action({
 232        let app_state = Arc::downgrade(&app_state);
 233        move |_: &NewWindow, cx: &mut AppContext| {
 234            if let Some(app_state) = app_state.upgrade() {
 235                open_new(&app_state, cx, |workspace, cx| {
 236                    Editor::new_file(workspace, &Default::default(), cx)
 237                })
 238                .detach();
 239            }
 240        }
 241    });
 242    cx.add_global_action({
 243        let app_state = Arc::downgrade(&app_state);
 244        move |_: &NewFile, cx: &mut AppContext| {
 245            if let Some(app_state) = app_state.upgrade() {
 246                open_new(&app_state, cx, |workspace, cx| {
 247                    Editor::new_file(workspace, &Default::default(), cx)
 248                })
 249                .detach();
 250            }
 251        }
 252    });
 253    load_default_keymap(cx);
 254}
 255
 256pub fn initialize_workspace(
 257    workspace_handle: WeakViewHandle<Workspace>,
 258    was_deserialized: bool,
 259    app_state: Arc<AppState>,
 260    cx: AsyncAppContext,
 261) -> Task<Result<()>> {
 262    cx.spawn(|mut cx| async move {
 263        workspace_handle.update(&mut cx, |workspace, cx| {
 264            let workspace_handle = cx.handle();
 265            cx.subscribe(&workspace_handle, {
 266                move |workspace, _, event, cx| {
 267                    if let workspace::Event::PaneAdded(pane) = event {
 268                        pane.update(cx, |pane, cx| {
 269                            pane.toolbar().update(cx, |toolbar, cx| {
 270                                let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
 271                                toolbar.add_item(breadcrumbs, cx);
 272                                let buffer_search_bar = cx.add_view(BufferSearchBar::new);
 273                                toolbar.add_item(buffer_search_bar, cx);
 274                                let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
 275                                toolbar.add_item(project_search_bar, cx);
 276                                let submit_feedback_button =
 277                                    cx.add_view(|_| SubmitFeedbackButton::new());
 278                                toolbar.add_item(submit_feedback_button, cx);
 279                                let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
 280                                toolbar.add_item(feedback_info_text, cx);
 281                                let lsp_log_item =
 282                                    cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
 283                                toolbar.add_item(lsp_log_item, cx);
 284                                let syntax_tree_item = cx
 285                                    .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
 286                                toolbar.add_item(syntax_tree_item, cx);
 287                            })
 288                        });
 289                    }
 290                }
 291            })
 292            .detach();
 293
 294            cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 295
 296            let collab_titlebar_item =
 297                cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
 298            workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 299
 300            let copilot =
 301                cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
 302            let diagnostic_summary =
 303                cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 304            let activity_indicator = activity_indicator::ActivityIndicator::new(
 305                workspace,
 306                app_state.languages.clone(),
 307                cx,
 308            );
 309            let active_buffer_language =
 310                cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
 311            let feedback_button = cx.add_view(|_| {
 312                feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
 313            });
 314            let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
 315            workspace.status_bar().update(cx, |status_bar, cx| {
 316                status_bar.add_left_item(diagnostic_summary, cx);
 317                status_bar.add_left_item(activity_indicator, cx);
 318                status_bar.add_right_item(feedback_button, cx);
 319                status_bar.add_right_item(copilot, cx);
 320                status_bar.add_right_item(active_buffer_language, cx);
 321                status_bar.add_right_item(cursor_position, cx);
 322            });
 323
 324            auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 325
 326            vim::observe_keystrokes(cx);
 327
 328            cx.on_window_should_close(|workspace, cx| {
 329                if let Some(task) = workspace.close(&Default::default(), cx) {
 330                    task.detach_and_log_err(cx);
 331                }
 332                false
 333            });
 334        })?;
 335
 336        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 337        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 338        let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
 339        let (project_panel, terminal_panel, assistant_panel) =
 340            futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
 341
 342        cx.update(|cx| {
 343            if let Some(workspace) = workspace_handle.upgrade(cx) {
 344                cx.update_window(project_panel.window_id(), |cx| {
 345                    workspace.update(cx, |workspace, cx| {
 346                        let project_panel_subscription =
 347                            cx.subscribe(&project_panel, move |workspace, _, event, cx| {
 348                                if let project_panel::Event::NewSearchInDirectory { dir_entry } =
 349                                    event
 350                                {
 351                                    search::ProjectSearchView::new_search_in_directory(
 352                                        workspace, dir_entry, cx,
 353                                    )
 354                                }
 355                            });
 356                        workspace.push_subscription(project_panel_subscription);
 357                    });
 358                });
 359            }
 360        });
 361
 362        workspace_handle.update(&mut cx, |workspace, cx| {
 363            let project_panel_position = project_panel.position(cx);
 364            workspace.add_panel(project_panel, cx);
 365            workspace.add_panel(terminal_panel, cx);
 366            workspace.add_panel(assistant_panel, cx);
 367
 368            if !was_deserialized
 369                && workspace
 370                    .project()
 371                    .read(cx)
 372                    .visible_worktrees(cx)
 373                    .any(|tree| {
 374                        tree.read(cx)
 375                            .root_entry()
 376                            .map_or(false, |entry| entry.is_dir())
 377                    })
 378            {
 379                workspace.toggle_dock(project_panel_position, cx);
 380            }
 381            cx.focus_self();
 382        })?;
 383        Ok(())
 384    })
 385}
 386
 387pub fn build_window_options(
 388    bounds: Option<WindowBounds>,
 389    display: Option<Uuid>,
 390    platform: &dyn Platform,
 391) -> WindowOptions<'static> {
 392    let bounds = bounds.unwrap_or(WindowBounds::Maximized);
 393    let screen = display.and_then(|display| platform.screen_by_id(display));
 394
 395    WindowOptions {
 396        titlebar: Some(TitlebarOptions {
 397            title: None,
 398            appears_transparent: true,
 399            traffic_light_position: Some(vec2f(8., 8.)),
 400        }),
 401        center: false,
 402        focus: false,
 403        show: false,
 404        kind: WindowKind::Normal,
 405        is_movable: true,
 406        bounds,
 407        screen,
 408    }
 409}
 410
 411fn quit(_: &Quit, cx: &mut gpui::AppContext) {
 412    let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
 413    cx.spawn(|mut cx| async move {
 414        let mut workspaces = cx
 415            .window_ids()
 416            .into_iter()
 417            .filter_map(|window_id| {
 418                Some(
 419                    cx.root_view(window_id)?
 420                        .clone()
 421                        .downcast::<Workspace>()?
 422                        .downgrade(),
 423                )
 424            })
 425            .collect::<Vec<_>>();
 426
 427        // If multiple windows have unsaved changes, and need a save prompt,
 428        // prompt in the active window before switching to a different window.
 429        workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
 430
 431        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
 432            let answer = cx.prompt(
 433                workspace.window_id(),
 434                PromptLevel::Info,
 435                "Are you sure you want to quit?",
 436                &["Quit", "Cancel"],
 437            );
 438
 439            if let Some(mut answer) = answer {
 440                let answer = answer.next().await;
 441                if answer != Some(0) {
 442                    return Ok(());
 443                }
 444            }
 445        }
 446
 447        // If the user cancels any save prompt, then keep the app open.
 448        for workspace in workspaces {
 449            if !workspace
 450                .update(&mut cx, |workspace, cx| {
 451                    workspace.prepare_to_close(true, cx)
 452                })?
 453                .await?
 454            {
 455                return Ok(());
 456            }
 457        }
 458        cx.platform().quit();
 459        anyhow::Ok(())
 460    })
 461    .detach_and_log_err(cx);
 462}
 463
 464fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
 465    let app_name = cx.global::<ReleaseChannel>().display_name();
 466    let version = env!("CARGO_PKG_VERSION");
 467    cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
 468}
 469
 470fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 471    const MAX_LINES: usize = 1000;
 472
 473    workspace
 474        .with_local_workspace(cx, move |workspace, cx| {
 475            let fs = workspace.app_state().fs.clone();
 476            cx.spawn(|workspace, mut cx| async move {
 477                let (old_log, new_log) =
 478                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
 479
 480                let mut lines = VecDeque::with_capacity(MAX_LINES);
 481                for line in old_log
 482                    .iter()
 483                    .flat_map(|log| log.lines())
 484                    .chain(new_log.iter().flat_map(|log| log.lines()))
 485                {
 486                    if lines.len() == MAX_LINES {
 487                        lines.pop_front();
 488                    }
 489                    lines.push_back(line);
 490                }
 491                let log = lines
 492                    .into_iter()
 493                    .flat_map(|line| [line, "\n"])
 494                    .collect::<String>();
 495
 496                workspace
 497                    .update(&mut cx, |workspace, cx| {
 498                        let project = workspace.project().clone();
 499                        let buffer = project
 500                            .update(cx, |project, cx| project.create_buffer("", None, cx))
 501                            .expect("creating buffers on a local workspace always succeeds");
 502                        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
 503
 504                        let buffer = cx.add_model(|cx| {
 505                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
 506                        });
 507                        workspace.add_item(
 508                            Box::new(
 509                                cx.add_view(|cx| {
 510                                    Editor::for_multibuffer(buffer, Some(project), cx)
 511                                }),
 512                            ),
 513                            cx,
 514                        );
 515                    })
 516                    .log_err();
 517            })
 518            .detach();
 519        })
 520        .detach();
 521}
 522
 523pub fn load_default_keymap(cx: &mut AppContext) {
 524    for path in ["keymaps/default.json", "keymaps/vim.json"] {
 525        KeymapFile::load_asset(path, cx).unwrap();
 526    }
 527
 528    if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
 529        KeymapFile::load_asset(asset_path, cx).unwrap();
 530    }
 531}
 532
 533pub fn handle_keymap_file_changes(
 534    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
 535    cx: &mut AppContext,
 536) {
 537    cx.spawn(move |mut cx| async move {
 538        let mut settings_subscription = None;
 539        while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
 540            if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
 541                cx.update(|cx| reload_keymaps(cx, &keymap_content));
 542
 543                let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
 544                drop(settings_subscription);
 545                settings_subscription = Some(cx.update(|cx| {
 546                    cx.observe_global::<SettingsStore, _>(move |cx| {
 547                        let new_base_keymap = *settings::get::<BaseKeymap>(cx);
 548                        if new_base_keymap != old_base_keymap {
 549                            old_base_keymap = new_base_keymap.clone();
 550                            reload_keymaps(cx, &keymap_content);
 551                        }
 552                    })
 553                    .detach();
 554                }));
 555            }
 556        }
 557    })
 558    .detach();
 559}
 560
 561fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
 562    cx.clear_bindings();
 563    load_default_keymap(cx);
 564    keymap_content.clone().add_to_cx(cx).log_err();
 565    cx.set_menus(menus::menus());
 566}
 567
 568fn open_local_settings_file(
 569    workspace: &mut Workspace,
 570    _: &OpenLocalSettings,
 571    cx: &mut ViewContext<Workspace>,
 572) {
 573    let project = workspace.project().clone();
 574    let worktree = project
 575        .read(cx)
 576        .visible_worktrees(cx)
 577        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
 578    if let Some(worktree) = worktree {
 579        let tree_id = worktree.read(cx).id();
 580        cx.spawn(|workspace, mut cx| async move {
 581            let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
 582
 583            if let Some(dir_path) = file_path.parent() {
 584                if worktree.read_with(&cx, |tree, _| tree.entry_for_path(dir_path).is_none()) {
 585                    project
 586                        .update(&mut cx, |project, cx| {
 587                            project.create_entry((tree_id, dir_path), true, cx)
 588                        })
 589                        .ok_or_else(|| anyhow!("worktree was removed"))?
 590                        .await?;
 591                }
 592            }
 593
 594            if worktree.read_with(&cx, |tree, _| tree.entry_for_path(file_path).is_none()) {
 595                project
 596                    .update(&mut cx, |project, cx| {
 597                        project.create_entry((tree_id, file_path), false, cx)
 598                    })
 599                    .ok_or_else(|| anyhow!("worktree was removed"))?
 600                    .await?;
 601            }
 602
 603            let editor = workspace
 604                .update(&mut cx, |workspace, cx| {
 605                    workspace.open_path((tree_id, file_path), None, true, cx)
 606                })?
 607                .await?
 608                .downcast::<Editor>()
 609                .ok_or_else(|| anyhow!("unexpected item type"))?;
 610
 611            editor
 612                .downgrade()
 613                .update(&mut cx, |editor, cx| {
 614                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 615                        if buffer.read(cx).is_empty() {
 616                            buffer.update(cx, |buffer, cx| {
 617                                buffer.edit([(0..0, initial_local_settings_content())], None, cx)
 618                            });
 619                        }
 620                    }
 621                })
 622                .ok();
 623
 624            anyhow::Ok(())
 625        })
 626        .detach();
 627    } else {
 628        workspace.show_notification(0, cx, |cx| {
 629            cx.add_view(|_| MessageNotification::new("This project has no folders open."))
 630        })
 631    }
 632}
 633
 634fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 635    workspace.with_local_workspace(cx, move |workspace, cx| {
 636        let app_state = workspace.app_state().clone();
 637        cx.spawn(|workspace, mut cx| async move {
 638            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
 639                let path = app_state.client.telemetry().log_file_path()?;
 640                app_state.fs.load(&path).await.log_err()
 641            }
 642
 643            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
 644
 645            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
 646            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
 647            if let Some(newline_offset) = log[start_offset..].find('\n') {
 648                start_offset += newline_offset + 1;
 649            }
 650            let log_suffix = &log[start_offset..];
 651            let json = app_state.languages.language_for_name("JSON").await.log_err();
 652
 653            workspace.update(&mut cx, |workspace, cx| {
 654                let project = workspace.project().clone();
 655                let buffer = project
 656                    .update(cx, |project, cx| project.create_buffer("", None, cx))
 657                    .expect("creating buffers on a local workspace always succeeds");
 658                buffer.update(cx, |buffer, cx| {
 659                    buffer.set_language(json, cx);
 660                    buffer.edit(
 661                        [(
 662                            0..0,
 663                            concat!(
 664                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
 665                                "// Telemetry can be disabled via the `settings.json` file.\n",
 666                                "// Here is the data that has been reported for the current session:\n",
 667                                "\n"
 668                            ),
 669                        )],
 670                        None,
 671                        cx,
 672                    );
 673                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
 674                });
 675
 676                let buffer = cx.add_model(|cx| {
 677                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
 678                });
 679                workspace.add_item(
 680                    Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
 681                    cx,
 682                );
 683            }).log_err()?;
 684
 685            Some(())
 686        })
 687        .detach();
 688    }).detach();
 689}
 690
 691fn open_bundled_file(
 692    workspace: &mut Workspace,
 693    text: Cow<'static, str>,
 694    title: &'static str,
 695    language: &'static str,
 696    cx: &mut ViewContext<Workspace>,
 697) {
 698    let language = workspace.app_state().languages.language_for_name(language);
 699    cx.spawn(|workspace, mut cx| async move {
 700        let language = language.await.log_err();
 701        workspace
 702            .update(&mut cx, |workspace, cx| {
 703                workspace.with_local_workspace(cx, |workspace, cx| {
 704                    let project = workspace.project();
 705                    let buffer = project.update(cx, move |project, cx| {
 706                        project
 707                            .create_buffer(text.as_ref(), language, cx)
 708                            .expect("creating buffers on a local workspace always succeeds")
 709                    });
 710                    let buffer = cx.add_model(|cx| {
 711                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
 712                    });
 713                    workspace.add_item(
 714                        Box::new(cx.add_view(|cx| {
 715                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
 716                        })),
 717                        cx,
 718                    );
 719                })
 720            })?
 721            .await
 722    })
 723    .detach_and_log_err(cx);
 724}
 725
 726#[cfg(test)]
 727mod tests {
 728    use super::*;
 729    use assets::Assets;
 730    use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
 731    use fs::{FakeFs, Fs};
 732    use gpui::{
 733        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
 734        AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
 735    };
 736    use language::LanguageRegistry;
 737    use node_runtime::NodeRuntime;
 738    use project::{Project, ProjectPath};
 739    use serde_json::json;
 740    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
 741    use std::{
 742        collections::HashSet,
 743        path::{Path, PathBuf},
 744    };
 745    use theme::{ThemeRegistry, ThemeSettings};
 746    use util::http::FakeHttpClient;
 747    use workspace::{
 748        item::{Item, ItemHandle},
 749        open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
 750    };
 751
 752    #[gpui::test]
 753    async fn test_open_paths_action(cx: &mut TestAppContext) {
 754        let app_state = init_test(cx);
 755        app_state
 756            .fs
 757            .as_fake()
 758            .insert_tree(
 759                "/root",
 760                json!({
 761                    "a": {
 762                        "aa": null,
 763                        "ab": null,
 764                    },
 765                    "b": {
 766                        "ba": null,
 767                        "bb": null,
 768                    },
 769                    "c": {
 770                        "ca": null,
 771                        "cb": null,
 772                    },
 773                    "d": {
 774                        "da": null,
 775                        "db": null,
 776                    },
 777                }),
 778            )
 779            .await;
 780
 781        cx.update(|cx| {
 782            open_paths(
 783                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
 784                &app_state,
 785                None,
 786                cx,
 787            )
 788        })
 789        .await
 790        .unwrap();
 791        assert_eq!(cx.window_ids().len(), 1);
 792
 793        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 794            .await
 795            .unwrap();
 796        assert_eq!(cx.window_ids().len(), 1);
 797        let workspace_1 = cx
 798            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
 799            .unwrap()
 800            .downcast::<Workspace>()
 801            .unwrap();
 802        workspace_1.update(cx, |workspace, cx| {
 803            assert_eq!(workspace.worktrees(cx).count(), 2);
 804            assert!(workspace.left_dock().read(cx).is_open());
 805            assert!(workspace.active_pane().is_focused(cx));
 806        });
 807
 808        cx.update(|cx| {
 809            open_paths(
 810                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
 811                &app_state,
 812                None,
 813                cx,
 814            )
 815        })
 816        .await
 817        .unwrap();
 818        assert_eq!(cx.window_ids().len(), 2);
 819
 820        // Replace existing windows
 821        let window_id = cx.window_ids()[0];
 822        cx.update(|cx| {
 823            open_paths(
 824                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
 825                &app_state,
 826                Some(window_id),
 827                cx,
 828            )
 829        })
 830        .await
 831        .unwrap();
 832        assert_eq!(cx.window_ids().len(), 2);
 833        let workspace_1 = cx
 834            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
 835            .unwrap()
 836            .clone()
 837            .downcast::<Workspace>()
 838            .unwrap();
 839        workspace_1.update(cx, |workspace, cx| {
 840            assert_eq!(
 841                workspace
 842                    .worktrees(cx)
 843                    .map(|w| w.read(cx).abs_path())
 844                    .collect::<Vec<_>>(),
 845                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
 846            );
 847            assert!(workspace.left_dock().read(cx).is_open());
 848            assert!(workspace.active_pane().is_focused(cx));
 849        });
 850    }
 851
 852    #[gpui::test]
 853    async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
 854        let app_state = init_test(cx);
 855        app_state
 856            .fs
 857            .as_fake()
 858            .insert_tree("/root", json!({"a": "hey"}))
 859            .await;
 860
 861        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 862            .await
 863            .unwrap();
 864        assert_eq!(cx.window_ids().len(), 1);
 865
 866        // When opening the workspace, the window is not in a edited state.
 867        let workspace = cx
 868            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
 869            .unwrap()
 870            .downcast::<Workspace>()
 871            .unwrap();
 872        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 873        let editor = workspace.read_with(cx, |workspace, cx| {
 874            workspace
 875                .active_item(cx)
 876                .unwrap()
 877                .downcast::<Editor>()
 878                .unwrap()
 879        });
 880        assert!(!cx.is_window_edited(workspace.window_id()));
 881
 882        // Editing a buffer marks the window as edited.
 883        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 884        assert!(cx.is_window_edited(workspace.window_id()));
 885
 886        // Undoing the edit restores the window's edited state.
 887        editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
 888        assert!(!cx.is_window_edited(workspace.window_id()));
 889
 890        // Redoing the edit marks the window as edited again.
 891        editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
 892        assert!(cx.is_window_edited(workspace.window_id()));
 893
 894        // Closing the item restores the window's edited state.
 895        let close = pane.update(cx, |pane, cx| {
 896            drop(editor);
 897            pane.close_active_item(&Default::default(), cx).unwrap()
 898        });
 899        executor.run_until_parked();
 900        cx.simulate_prompt_answer(workspace.window_id(), 1);
 901        close.await.unwrap();
 902        assert!(!cx.is_window_edited(workspace.window_id()));
 903
 904        // Opening the buffer again doesn't impact the window's edited state.
 905        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 906            .await
 907            .unwrap();
 908        let editor = workspace.read_with(cx, |workspace, cx| {
 909            workspace
 910                .active_item(cx)
 911                .unwrap()
 912                .downcast::<Editor>()
 913                .unwrap()
 914        });
 915        assert!(!cx.is_window_edited(workspace.window_id()));
 916
 917        // Editing the buffer marks the window as edited.
 918        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 919        assert!(cx.is_window_edited(workspace.window_id()));
 920
 921        // Ensure closing the window via the mouse gets preempted due to the
 922        // buffer having unsaved changes.
 923        assert!(!cx.simulate_window_close(workspace.window_id()));
 924        executor.run_until_parked();
 925        assert_eq!(cx.window_ids().len(), 1);
 926
 927        // The window is successfully closed after the user dismisses the prompt.
 928        cx.simulate_prompt_answer(workspace.window_id(), 1);
 929        executor.run_until_parked();
 930        assert_eq!(cx.window_ids().len(), 0);
 931    }
 932
 933    #[gpui::test]
 934    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 935        let app_state = init_test(cx);
 936        cx.update(|cx| {
 937            open_new(&app_state, cx, |workspace, cx| {
 938                Editor::new_file(workspace, &Default::default(), cx)
 939            })
 940        })
 941        .await;
 942
 943        let window_id = *cx.window_ids().first().unwrap();
 944        let workspace = cx
 945            .read_window(window_id, |cx| cx.root_view().clone())
 946            .unwrap()
 947            .downcast::<Workspace>()
 948            .unwrap();
 949
 950        let editor = workspace.update(cx, |workspace, cx| {
 951            workspace
 952                .active_item(cx)
 953                .unwrap()
 954                .downcast::<editor::Editor>()
 955                .unwrap()
 956        });
 957
 958        editor.update(cx, |editor, cx| {
 959            assert!(editor.text(cx).is_empty());
 960        });
 961
 962        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 963        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 964        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 965        save_task.await.unwrap();
 966        editor.read_with(cx, |editor, cx| {
 967            assert!(!editor.is_dirty(cx));
 968            assert_eq!(editor.title(cx), "the-new-name");
 969        });
 970    }
 971
 972    #[gpui::test]
 973    async fn test_open_entry(cx: &mut TestAppContext) {
 974        let app_state = init_test(cx);
 975        app_state
 976            .fs
 977            .as_fake()
 978            .insert_tree(
 979                "/root",
 980                json!({
 981                    "a": {
 982                        "file1": "contents 1",
 983                        "file2": "contents 2",
 984                        "file3": "contents 3",
 985                    },
 986                }),
 987            )
 988            .await;
 989
 990        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 991        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 992
 993        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 994        let file1 = entries[0].clone();
 995        let file2 = entries[1].clone();
 996        let file3 = entries[2].clone();
 997
 998        // Open the first entry
 999        let entry_1 = workspace
1000            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1001            .await
1002            .unwrap();
1003        cx.read(|cx| {
1004            let pane = workspace.read(cx).active_pane().read(cx);
1005            assert_eq!(
1006                pane.active_item().unwrap().project_path(cx),
1007                Some(file1.clone())
1008            );
1009            assert_eq!(pane.items_len(), 1);
1010        });
1011
1012        // Open the second entry
1013        workspace
1014            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1015            .await
1016            .unwrap();
1017        cx.read(|cx| {
1018            let pane = workspace.read(cx).active_pane().read(cx);
1019            assert_eq!(
1020                pane.active_item().unwrap().project_path(cx),
1021                Some(file2.clone())
1022            );
1023            assert_eq!(pane.items_len(), 2);
1024        });
1025
1026        // Open the first entry again. The existing pane item is activated.
1027        let entry_1b = workspace
1028            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1029            .await
1030            .unwrap();
1031        assert_eq!(entry_1.id(), entry_1b.id());
1032
1033        cx.read(|cx| {
1034            let pane = workspace.read(cx).active_pane().read(cx);
1035            assert_eq!(
1036                pane.active_item().unwrap().project_path(cx),
1037                Some(file1.clone())
1038            );
1039            assert_eq!(pane.items_len(), 2);
1040        });
1041
1042        // Split the pane with the first entry, then open the second entry again.
1043        workspace
1044            .update(cx, |w, cx| {
1045                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1046                w.open_path(file2.clone(), None, true, cx)
1047            })
1048            .await
1049            .unwrap();
1050
1051        workspace.read_with(cx, |w, cx| {
1052            assert_eq!(
1053                w.active_pane()
1054                    .read(cx)
1055                    .active_item()
1056                    .unwrap()
1057                    .project_path(cx),
1058                Some(file2.clone())
1059            );
1060        });
1061
1062        // Open the third entry twice concurrently. Only one pane item is added.
1063        let (t1, t2) = workspace.update(cx, |w, cx| {
1064            (
1065                w.open_path(file3.clone(), None, true, cx),
1066                w.open_path(file3.clone(), None, true, cx),
1067            )
1068        });
1069        t1.await.unwrap();
1070        t2.await.unwrap();
1071        cx.read(|cx| {
1072            let pane = workspace.read(cx).active_pane().read(cx);
1073            assert_eq!(
1074                pane.active_item().unwrap().project_path(cx),
1075                Some(file3.clone())
1076            );
1077            let pane_entries = pane
1078                .items()
1079                .map(|i| i.project_path(cx).unwrap())
1080                .collect::<Vec<_>>();
1081            assert_eq!(pane_entries, &[file1, file2, file3]);
1082        });
1083    }
1084
1085    #[gpui::test]
1086    async fn test_open_paths(cx: &mut TestAppContext) {
1087        let app_state = init_test(cx);
1088
1089        app_state
1090            .fs
1091            .as_fake()
1092            .insert_tree(
1093                "/",
1094                json!({
1095                    "dir1": {
1096                        "a.txt": ""
1097                    },
1098                    "dir2": {
1099                        "b.txt": ""
1100                    },
1101                    "dir3": {
1102                        "c.txt": ""
1103                    },
1104                    "d.txt": ""
1105                }),
1106            )
1107            .await;
1108
1109        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
1110        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1111
1112        // Open a file within an existing worktree.
1113        workspace
1114            .update(cx, |view, cx| {
1115                view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1116            })
1117            .await;
1118        cx.read(|cx| {
1119            assert_eq!(
1120                workspace
1121                    .read(cx)
1122                    .active_pane()
1123                    .read(cx)
1124                    .active_item()
1125                    .unwrap()
1126                    .as_any()
1127                    .downcast_ref::<Editor>()
1128                    .unwrap()
1129                    .read(cx)
1130                    .title(cx),
1131                "a.txt"
1132            );
1133        });
1134
1135        // Open a file outside of any existing worktree.
1136        workspace
1137            .update(cx, |view, cx| {
1138                view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1139            })
1140            .await;
1141        cx.read(|cx| {
1142            let worktree_roots = workspace
1143                .read(cx)
1144                .worktrees(cx)
1145                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1146                .collect::<HashSet<_>>();
1147            assert_eq!(
1148                worktree_roots,
1149                vec!["/dir1", "/dir2/b.txt"]
1150                    .into_iter()
1151                    .map(Path::new)
1152                    .collect(),
1153            );
1154            assert_eq!(
1155                workspace
1156                    .read(cx)
1157                    .active_pane()
1158                    .read(cx)
1159                    .active_item()
1160                    .unwrap()
1161                    .as_any()
1162                    .downcast_ref::<Editor>()
1163                    .unwrap()
1164                    .read(cx)
1165                    .title(cx),
1166                "b.txt"
1167            );
1168        });
1169
1170        // Ensure opening a directory and one of its children only adds one worktree.
1171        workspace
1172            .update(cx, |view, cx| {
1173                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1174            })
1175            .await;
1176        cx.read(|cx| {
1177            let worktree_roots = workspace
1178                .read(cx)
1179                .worktrees(cx)
1180                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1181                .collect::<HashSet<_>>();
1182            assert_eq!(
1183                worktree_roots,
1184                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1185                    .into_iter()
1186                    .map(Path::new)
1187                    .collect(),
1188            );
1189            assert_eq!(
1190                workspace
1191                    .read(cx)
1192                    .active_pane()
1193                    .read(cx)
1194                    .active_item()
1195                    .unwrap()
1196                    .as_any()
1197                    .downcast_ref::<Editor>()
1198                    .unwrap()
1199                    .read(cx)
1200                    .title(cx),
1201                "c.txt"
1202            );
1203        });
1204
1205        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1206        workspace
1207            .update(cx, |view, cx| {
1208                view.open_paths(vec!["/d.txt".into()], false, cx)
1209            })
1210            .await;
1211        cx.read(|cx| {
1212            let worktree_roots = workspace
1213                .read(cx)
1214                .worktrees(cx)
1215                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1216                .collect::<HashSet<_>>();
1217            assert_eq!(
1218                worktree_roots,
1219                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1220                    .into_iter()
1221                    .map(Path::new)
1222                    .collect(),
1223            );
1224
1225            let visible_worktree_roots = workspace
1226                .read(cx)
1227                .visible_worktrees(cx)
1228                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1229                .collect::<HashSet<_>>();
1230            assert_eq!(
1231                visible_worktree_roots,
1232                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1233                    .into_iter()
1234                    .map(Path::new)
1235                    .collect(),
1236            );
1237
1238            assert_eq!(
1239                workspace
1240                    .read(cx)
1241                    .active_pane()
1242                    .read(cx)
1243                    .active_item()
1244                    .unwrap()
1245                    .as_any()
1246                    .downcast_ref::<Editor>()
1247                    .unwrap()
1248                    .read(cx)
1249                    .title(cx),
1250                "d.txt"
1251            );
1252        });
1253    }
1254
1255    #[gpui::test]
1256    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1257        let app_state = init_test(cx);
1258        app_state
1259            .fs
1260            .as_fake()
1261            .insert_tree("/root", json!({ "a.txt": "" }))
1262            .await;
1263
1264        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1265        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1266
1267        // Open a file within an existing worktree.
1268        workspace
1269            .update(cx, |view, cx| {
1270                view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1271            })
1272            .await;
1273        let editor = cx.read(|cx| {
1274            let pane = workspace.read(cx).active_pane().read(cx);
1275            let item = pane.active_item().unwrap();
1276            item.downcast::<Editor>().unwrap()
1277        });
1278
1279        editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1280        app_state
1281            .fs
1282            .as_fake()
1283            .insert_file("/root/a.txt", "changed".to_string())
1284            .await;
1285        editor
1286            .condition(cx, |editor, cx| editor.has_conflict(cx))
1287            .await;
1288        cx.read(|cx| assert!(editor.is_dirty(cx)));
1289
1290        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1291        cx.simulate_prompt_answer(window_id, 0);
1292        save_task.await.unwrap();
1293        editor.read_with(cx, |editor, cx| {
1294            assert!(!editor.is_dirty(cx));
1295            assert!(!editor.has_conflict(cx));
1296        });
1297    }
1298
1299    #[gpui::test]
1300    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1301        let app_state = init_test(cx);
1302        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1303
1304        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1305        project.update(cx, |project, _| project.languages().add(rust_lang()));
1306        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1307        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1308
1309        // Create a new untitled buffer
1310        cx.dispatch_action(window_id, NewFile);
1311        let editor = workspace.read_with(cx, |workspace, cx| {
1312            workspace
1313                .active_item(cx)
1314                .unwrap()
1315                .downcast::<Editor>()
1316                .unwrap()
1317        });
1318
1319        editor.update(cx, |editor, cx| {
1320            assert!(!editor.is_dirty(cx));
1321            assert_eq!(editor.title(cx), "untitled");
1322            assert!(Arc::ptr_eq(
1323                &editor.language_at(0, cx).unwrap(),
1324                &languages::PLAIN_TEXT
1325            ));
1326            editor.handle_input("hi", cx);
1327            assert!(editor.is_dirty(cx));
1328        });
1329
1330        // Save the buffer. This prompts for a filename.
1331        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1332        cx.simulate_new_path_selection(|parent_dir| {
1333            assert_eq!(parent_dir, Path::new("/root"));
1334            Some(parent_dir.join("the-new-name.rs"))
1335        });
1336        cx.read(|cx| {
1337            assert!(editor.is_dirty(cx));
1338            assert_eq!(editor.read(cx).title(cx), "untitled");
1339        });
1340
1341        // When the save completes, the buffer's title is updated and the language is assigned based
1342        // on the path.
1343        save_task.await.unwrap();
1344        editor.read_with(cx, |editor, cx| {
1345            assert!(!editor.is_dirty(cx));
1346            assert_eq!(editor.title(cx), "the-new-name.rs");
1347            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1348        });
1349
1350        // Edit the file and save it again. This time, there is no filename prompt.
1351        editor.update(cx, |editor, cx| {
1352            editor.handle_input(" there", cx);
1353            assert!(editor.is_dirty(cx));
1354        });
1355        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1356        save_task.await.unwrap();
1357        assert!(!cx.did_prompt_for_new_path());
1358        editor.read_with(cx, |editor, cx| {
1359            assert!(!editor.is_dirty(cx));
1360            assert_eq!(editor.title(cx), "the-new-name.rs")
1361        });
1362
1363        // Open the same newly-created file in another pane item. The new editor should reuse
1364        // the same buffer.
1365        cx.dispatch_action(window_id, NewFile);
1366        workspace
1367            .update(cx, |workspace, cx| {
1368                workspace.split_and_clone(
1369                    workspace.active_pane().clone(),
1370                    SplitDirection::Right,
1371                    cx,
1372                );
1373                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1374            })
1375            .await
1376            .unwrap();
1377        let editor2 = workspace.update(cx, |workspace, cx| {
1378            workspace
1379                .active_item(cx)
1380                .unwrap()
1381                .downcast::<Editor>()
1382                .unwrap()
1383        });
1384        cx.read(|cx| {
1385            assert_eq!(
1386                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1387                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1388            );
1389        })
1390    }
1391
1392    #[gpui::test]
1393    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1394        let app_state = init_test(cx);
1395        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1396
1397        let project = Project::test(app_state.fs.clone(), [], cx).await;
1398        project.update(cx, |project, _| project.languages().add(rust_lang()));
1399        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1400
1401        // Create a new untitled buffer
1402        cx.dispatch_action(window_id, NewFile);
1403        let editor = workspace.read_with(cx, |workspace, cx| {
1404            workspace
1405                .active_item(cx)
1406                .unwrap()
1407                .downcast::<Editor>()
1408                .unwrap()
1409        });
1410
1411        editor.update(cx, |editor, cx| {
1412            assert!(Arc::ptr_eq(
1413                &editor.language_at(0, cx).unwrap(),
1414                &languages::PLAIN_TEXT
1415            ));
1416            editor.handle_input("hi", cx);
1417            assert!(editor.is_dirty(cx));
1418        });
1419
1420        // Save the buffer. This prompts for a filename.
1421        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1422        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1423        save_task.await.unwrap();
1424        // The buffer is not dirty anymore and the language is assigned based on the path.
1425        editor.read_with(cx, |editor, cx| {
1426            assert!(!editor.is_dirty(cx));
1427            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1428        });
1429    }
1430
1431    #[gpui::test]
1432    async fn test_pane_actions(cx: &mut TestAppContext) {
1433        let app_state = init_test(cx);
1434        app_state
1435            .fs
1436            .as_fake()
1437            .insert_tree(
1438                "/root",
1439                json!({
1440                    "a": {
1441                        "file1": "contents 1",
1442                        "file2": "contents 2",
1443                        "file3": "contents 3",
1444                    },
1445                }),
1446            )
1447            .await;
1448
1449        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1450        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1451
1452        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1453        let file1 = entries[0].clone();
1454
1455        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1456
1457        workspace
1458            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1459            .await
1460            .unwrap();
1461
1462        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1463            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1464            assert_eq!(editor.project_path(cx), Some(file1.clone()));
1465            let buffer = editor.update(cx, |editor, cx| {
1466                editor.insert("dirt", cx);
1467                editor.buffer().downgrade()
1468            });
1469            (editor.downgrade(), buffer)
1470        });
1471
1472        cx.dispatch_action(window_id, pane::SplitRight);
1473        let editor_2 = cx.update(|cx| {
1474            let pane_2 = workspace.read(cx).active_pane().clone();
1475            assert_ne!(pane_1, pane_2);
1476
1477            let pane2_item = pane_2.read(cx).active_item().unwrap();
1478            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1479
1480            pane2_item.downcast::<Editor>().unwrap().downgrade()
1481        });
1482        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1483
1484        cx.foreground().run_until_parked();
1485        workspace.read_with(cx, |workspace, _| {
1486            assert_eq!(workspace.panes().len(), 1);
1487            assert_eq!(workspace.active_pane(), &pane_1);
1488        });
1489
1490        cx.dispatch_action(window_id, workspace::CloseActiveItem);
1491        cx.foreground().run_until_parked();
1492        cx.simulate_prompt_answer(window_id, 1);
1493        cx.foreground().run_until_parked();
1494
1495        workspace.read_with(cx, |workspace, cx| {
1496            assert_eq!(workspace.panes().len(), 1);
1497            assert!(workspace.active_item(cx).is_none());
1498        });
1499
1500        cx.assert_dropped(editor_1);
1501        cx.assert_dropped(editor_2);
1502        cx.assert_dropped(buffer);
1503    }
1504
1505    #[gpui::test]
1506    async fn test_navigation(cx: &mut TestAppContext) {
1507        let app_state = init_test(cx);
1508        app_state
1509            .fs
1510            .as_fake()
1511            .insert_tree(
1512                "/root",
1513                json!({
1514                    "a": {
1515                        "file1": "contents 1\n".repeat(20),
1516                        "file2": "contents 2\n".repeat(20),
1517                        "file3": "contents 3\n".repeat(20),
1518                    },
1519                }),
1520            )
1521            .await;
1522
1523        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1524        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1525        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1526
1527        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1528        let file1 = entries[0].clone();
1529        let file2 = entries[1].clone();
1530        let file3 = entries[2].clone();
1531
1532        let editor1 = workspace
1533            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1534            .await
1535            .unwrap()
1536            .downcast::<Editor>()
1537            .unwrap();
1538        editor1.update(cx, |editor, cx| {
1539            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1540                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1541            });
1542        });
1543        let editor2 = workspace
1544            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1545            .await
1546            .unwrap()
1547            .downcast::<Editor>()
1548            .unwrap();
1549        let editor3 = workspace
1550            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1551            .await
1552            .unwrap()
1553            .downcast::<Editor>()
1554            .unwrap();
1555
1556        editor3
1557            .update(cx, |editor, cx| {
1558                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1559                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1560                });
1561                editor.newline(&Default::default(), cx);
1562                editor.newline(&Default::default(), cx);
1563                editor.move_down(&Default::default(), cx);
1564                editor.move_down(&Default::default(), cx);
1565                editor.save(project.clone(), cx)
1566            })
1567            .await
1568            .unwrap();
1569        editor3.update(cx, |editor, cx| {
1570            editor.set_scroll_position(vec2f(0., 12.5), cx)
1571        });
1572        assert_eq!(
1573            active_location(&workspace, cx),
1574            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1575        );
1576
1577        workspace
1578            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1579            .await
1580            .unwrap();
1581        assert_eq!(
1582            active_location(&workspace, cx),
1583            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1584        );
1585
1586        workspace
1587            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1588            .await
1589            .unwrap();
1590        assert_eq!(
1591            active_location(&workspace, cx),
1592            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1593        );
1594
1595        workspace
1596            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1597            .await
1598            .unwrap();
1599        assert_eq!(
1600            active_location(&workspace, cx),
1601            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1602        );
1603
1604        workspace
1605            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1606            .await
1607            .unwrap();
1608        assert_eq!(
1609            active_location(&workspace, cx),
1610            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1611        );
1612
1613        // Go back one more time and ensure we don't navigate past the first item in the history.
1614        workspace
1615            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1616            .await
1617            .unwrap();
1618        assert_eq!(
1619            active_location(&workspace, cx),
1620            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1621        );
1622
1623        workspace
1624            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1625            .await
1626            .unwrap();
1627        assert_eq!(
1628            active_location(&workspace, cx),
1629            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1630        );
1631
1632        workspace
1633            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1634            .await
1635            .unwrap();
1636        assert_eq!(
1637            active_location(&workspace, cx),
1638            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1639        );
1640
1641        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1642        // location.
1643        pane.update(cx, |pane, cx| {
1644            let editor3_id = editor3.id();
1645            drop(editor3);
1646            pane.close_item_by_id(editor3_id, cx)
1647        })
1648        .await
1649        .unwrap();
1650        workspace
1651            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1652            .await
1653            .unwrap();
1654        assert_eq!(
1655            active_location(&workspace, cx),
1656            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1657        );
1658
1659        workspace
1660            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1661            .await
1662            .unwrap();
1663        assert_eq!(
1664            active_location(&workspace, cx),
1665            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1666        );
1667
1668        workspace
1669            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1670            .await
1671            .unwrap();
1672        assert_eq!(
1673            active_location(&workspace, cx),
1674            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1675        );
1676
1677        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1678        pane.update(cx, |pane, cx| {
1679            let editor2_id = editor2.id();
1680            drop(editor2);
1681            pane.close_item_by_id(editor2_id, cx)
1682        })
1683        .await
1684        .unwrap();
1685        app_state
1686            .fs
1687            .remove_file(Path::new("/root/a/file2"), Default::default())
1688            .await
1689            .unwrap();
1690        workspace
1691            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1692            .await
1693            .unwrap();
1694        assert_eq!(
1695            active_location(&workspace, cx),
1696            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1697        );
1698        workspace
1699            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1700            .await
1701            .unwrap();
1702        assert_eq!(
1703            active_location(&workspace, cx),
1704            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1705        );
1706
1707        // Modify file to collapse multiple nav history entries into the same location.
1708        // Ensure we don't visit the same location twice when navigating.
1709        editor1.update(cx, |editor, cx| {
1710            editor.change_selections(None, cx, |s| {
1711                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1712            })
1713        });
1714
1715        for _ in 0..5 {
1716            editor1.update(cx, |editor, cx| {
1717                editor.change_selections(None, cx, |s| {
1718                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1719                });
1720            });
1721            editor1.update(cx, |editor, cx| {
1722                editor.change_selections(None, cx, |s| {
1723                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1724                })
1725            });
1726        }
1727
1728        editor1.update(cx, |editor, cx| {
1729            editor.transact(cx, |editor, cx| {
1730                editor.change_selections(None, cx, |s| {
1731                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1732                });
1733                editor.insert("", cx);
1734            })
1735        });
1736
1737        editor1.update(cx, |editor, cx| {
1738            editor.change_selections(None, cx, |s| {
1739                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1740            })
1741        });
1742        workspace
1743            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1744            .await
1745            .unwrap();
1746        assert_eq!(
1747            active_location(&workspace, cx),
1748            (file1.clone(), DisplayPoint::new(2, 0), 0.)
1749        );
1750        workspace
1751            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1752            .await
1753            .unwrap();
1754        assert_eq!(
1755            active_location(&workspace, cx),
1756            (file1.clone(), DisplayPoint::new(3, 0), 0.)
1757        );
1758
1759        fn active_location(
1760            workspace: &ViewHandle<Workspace>,
1761            cx: &mut TestAppContext,
1762        ) -> (ProjectPath, DisplayPoint, f32) {
1763            workspace.update(cx, |workspace, cx| {
1764                let item = workspace.active_item(cx).unwrap();
1765                let editor = item.downcast::<Editor>().unwrap();
1766                let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1767                    (
1768                        editor.selections.display_ranges(cx),
1769                        editor.scroll_position(cx),
1770                    )
1771                });
1772                (
1773                    item.project_path(cx).unwrap(),
1774                    selections[0].start,
1775                    scroll_position.y(),
1776                )
1777            })
1778        }
1779    }
1780
1781    #[gpui::test]
1782    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1783        let app_state = init_test(cx);
1784        app_state
1785            .fs
1786            .as_fake()
1787            .insert_tree(
1788                "/root",
1789                json!({
1790                    "a": {
1791                        "file1": "",
1792                        "file2": "",
1793                        "file3": "",
1794                        "file4": "",
1795                    },
1796                }),
1797            )
1798            .await;
1799
1800        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1801        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1802        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1803
1804        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1805        let file1 = entries[0].clone();
1806        let file2 = entries[1].clone();
1807        let file3 = entries[2].clone();
1808        let file4 = entries[3].clone();
1809
1810        let file1_item_id = workspace
1811            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1812            .await
1813            .unwrap()
1814            .id();
1815        let file2_item_id = workspace
1816            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1817            .await
1818            .unwrap()
1819            .id();
1820        let file3_item_id = workspace
1821            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1822            .await
1823            .unwrap()
1824            .id();
1825        let file4_item_id = workspace
1826            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1827            .await
1828            .unwrap()
1829            .id();
1830        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1831
1832        // Close all the pane items in some arbitrary order.
1833        pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
1834            .await
1835            .unwrap();
1836        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1837
1838        pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
1839            .await
1840            .unwrap();
1841        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1842
1843        pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
1844            .await
1845            .unwrap();
1846        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1847
1848        pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
1849            .await
1850            .unwrap();
1851        assert_eq!(active_path(&workspace, cx), None);
1852
1853        // Reopen all the closed items, ensuring they are reopened in the same order
1854        // in which they were closed.
1855        workspace
1856            .update(cx, Workspace::reopen_closed_item)
1857            .await
1858            .unwrap();
1859        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1860
1861        workspace
1862            .update(cx, Workspace::reopen_closed_item)
1863            .await
1864            .unwrap();
1865        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1866
1867        workspace
1868            .update(cx, Workspace::reopen_closed_item)
1869            .await
1870            .unwrap();
1871        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1872
1873        workspace
1874            .update(cx, Workspace::reopen_closed_item)
1875            .await
1876            .unwrap();
1877        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1878
1879        // Reopening past the last closed item is a no-op.
1880        workspace
1881            .update(cx, Workspace::reopen_closed_item)
1882            .await
1883            .unwrap();
1884        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1885
1886        // Reopening closed items doesn't interfere with navigation history.
1887        workspace
1888            .update(cx, |workspace, cx| {
1889                workspace.go_back(workspace.active_pane().downgrade(), cx)
1890            })
1891            .await
1892            .unwrap();
1893        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1894
1895        workspace
1896            .update(cx, |workspace, cx| {
1897                workspace.go_back(workspace.active_pane().downgrade(), cx)
1898            })
1899            .await
1900            .unwrap();
1901        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1902
1903        workspace
1904            .update(cx, |workspace, cx| {
1905                workspace.go_back(workspace.active_pane().downgrade(), cx)
1906            })
1907            .await
1908            .unwrap();
1909        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1910
1911        workspace
1912            .update(cx, |workspace, cx| {
1913                workspace.go_back(workspace.active_pane().downgrade(), cx)
1914            })
1915            .await
1916            .unwrap();
1917        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1918
1919        workspace
1920            .update(cx, |workspace, cx| {
1921                workspace.go_back(workspace.active_pane().downgrade(), cx)
1922            })
1923            .await
1924            .unwrap();
1925        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1926
1927        workspace
1928            .update(cx, |workspace, cx| {
1929                workspace.go_back(workspace.active_pane().downgrade(), cx)
1930            })
1931            .await
1932            .unwrap();
1933        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1934
1935        workspace
1936            .update(cx, |workspace, cx| {
1937                workspace.go_back(workspace.active_pane().downgrade(), cx)
1938            })
1939            .await
1940            .unwrap();
1941        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1942
1943        workspace
1944            .update(cx, |workspace, cx| {
1945                workspace.go_back(workspace.active_pane().downgrade(), cx)
1946            })
1947            .await
1948            .unwrap();
1949        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1950
1951        fn active_path(
1952            workspace: &ViewHandle<Workspace>,
1953            cx: &TestAppContext,
1954        ) -> Option<ProjectPath> {
1955            workspace.read_with(cx, |workspace, cx| {
1956                let item = workspace.active_item(cx)?;
1957                item.project_path(cx)
1958            })
1959        }
1960    }
1961
1962    #[gpui::test]
1963    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
1964        struct TestView;
1965
1966        impl Entity for TestView {
1967            type Event = ();
1968        }
1969
1970        impl View for TestView {
1971            fn ui_name() -> &'static str {
1972                "TestView"
1973            }
1974
1975            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
1976                Empty::new().into_any()
1977            }
1978        }
1979
1980        let executor = cx.background();
1981        let fs = FakeFs::new(executor.clone());
1982
1983        actions!(test, [A, B]);
1984        // From the Atom keymap
1985        actions!(workspace, [ActivatePreviousPane]);
1986        // From the JetBrains keymap
1987        actions!(pane, [ActivatePrevItem]);
1988
1989        fs.save(
1990            "/settings.json".as_ref(),
1991            &r#"
1992            {
1993                "base_keymap": "Atom"
1994            }
1995            "#
1996            .into(),
1997            Default::default(),
1998        )
1999        .await
2000        .unwrap();
2001
2002        fs.save(
2003            "/keymap.json".as_ref(),
2004            &r#"
2005            [
2006                {
2007                    "bindings": {
2008                        "backspace": "test::A"
2009                    }
2010                }
2011            ]
2012            "#
2013            .into(),
2014            Default::default(),
2015        )
2016        .await
2017        .unwrap();
2018
2019        cx.update(|cx| {
2020            cx.set_global(SettingsStore::test(cx));
2021            theme::init(Assets, cx);
2022            welcome::init(cx);
2023
2024            cx.add_global_action(|_: &A, _cx| {});
2025            cx.add_global_action(|_: &B, _cx| {});
2026            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2027            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2028
2029            let settings_rx = watch_config_file(
2030                executor.clone(),
2031                fs.clone(),
2032                PathBuf::from("/settings.json"),
2033            );
2034            let keymap_rx =
2035                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2036
2037            handle_keymap_file_changes(keymap_rx, cx);
2038            handle_settings_file_changes(settings_rx, cx);
2039        });
2040
2041        cx.foreground().run_until_parked();
2042
2043        let (window_id, _view) = cx.add_window(|_| TestView);
2044
2045        // Test loading the keymap base at all
2046        assert_key_bindings_for(
2047            window_id,
2048            cx,
2049            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2050            line!(),
2051        );
2052
2053        // Test modifying the users keymap, while retaining the base keymap
2054        fs.save(
2055            "/keymap.json".as_ref(),
2056            &r#"
2057            [
2058                {
2059                    "bindings": {
2060                        "backspace": "test::B"
2061                    }
2062                }
2063            ]
2064            "#
2065            .into(),
2066            Default::default(),
2067        )
2068        .await
2069        .unwrap();
2070
2071        cx.foreground().run_until_parked();
2072
2073        assert_key_bindings_for(
2074            window_id,
2075            cx,
2076            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
2077            line!(),
2078        );
2079
2080        // Test modifying the base, while retaining the users keymap
2081        fs.save(
2082            "/settings.json".as_ref(),
2083            &r#"
2084            {
2085                "base_keymap": "JetBrains"
2086            }
2087            "#
2088            .into(),
2089            Default::default(),
2090        )
2091        .await
2092        .unwrap();
2093
2094        cx.foreground().run_until_parked();
2095
2096        assert_key_bindings_for(
2097            window_id,
2098            cx,
2099            vec![("backspace", &B), ("[", &ActivatePrevItem)],
2100            line!(),
2101        );
2102
2103        #[track_caller]
2104        fn assert_key_bindings_for<'a>(
2105            window_id: usize,
2106            cx: &TestAppContext,
2107            actions: Vec<(&'static str, &'a dyn Action)>,
2108            line: u32,
2109        ) {
2110            for (key, action) in actions {
2111                // assert that...
2112                assert!(
2113                    cx.available_actions(window_id, 0)
2114                        .into_iter()
2115                        .any(|(_, bound_action, b)| {
2116                            // action names match...
2117                            bound_action.name() == action.name()
2118                        && bound_action.namespace() == action.namespace()
2119                        // and key strokes contain the given key
2120                        && b.iter()
2121                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2122                        }),
2123                    "On {} Failed to find {} with key binding {}",
2124                    line,
2125                    action.name(),
2126                    key
2127                );
2128            }
2129        }
2130    }
2131
2132    #[gpui::test]
2133    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
2134        struct TestView;
2135
2136        impl Entity for TestView {
2137            type Event = ();
2138        }
2139
2140        impl View for TestView {
2141            fn ui_name() -> &'static str {
2142                "TestView"
2143            }
2144
2145            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2146                Empty::new().into_any()
2147            }
2148        }
2149
2150        let executor = cx.background();
2151        let fs = FakeFs::new(executor.clone());
2152
2153        actions!(test, [A, B]);
2154        // From the Atom keymap
2155        actions!(workspace, [ActivatePreviousPane]);
2156        // From the JetBrains keymap
2157        actions!(pane, [ActivatePrevItem]);
2158
2159        fs.save(
2160            "/settings.json".as_ref(),
2161            &r#"
2162            {
2163                "base_keymap": "Atom"
2164            }
2165            "#
2166            .into(),
2167            Default::default(),
2168        )
2169        .await
2170        .unwrap();
2171
2172        fs.save(
2173            "/keymap.json".as_ref(),
2174            &r#"
2175            [
2176                {
2177                    "bindings": {
2178                        "backspace": "test::A"
2179                    }
2180                }
2181            ]
2182            "#
2183            .into(),
2184            Default::default(),
2185        )
2186        .await
2187        .unwrap();
2188
2189        cx.update(|cx| {
2190            cx.set_global(SettingsStore::test(cx));
2191            theme::init(Assets, cx);
2192            welcome::init(cx);
2193
2194            cx.add_global_action(|_: &A, _cx| {});
2195            cx.add_global_action(|_: &B, _cx| {});
2196            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2197            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2198
2199            let settings_rx = watch_config_file(
2200                executor.clone(),
2201                fs.clone(),
2202                PathBuf::from("/settings.json"),
2203            );
2204            let keymap_rx =
2205                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2206
2207            handle_keymap_file_changes(keymap_rx, cx);
2208            handle_settings_file_changes(settings_rx, cx);
2209        });
2210
2211        cx.foreground().run_until_parked();
2212
2213        let (window_id, _view) = cx.add_window(|_| TestView);
2214
2215        // Test loading the keymap base at all
2216        assert_key_bindings_for(
2217            window_id,
2218            cx,
2219            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2220            line!(),
2221        );
2222
2223        // Test disabling the key binding for the base keymap
2224        fs.save(
2225            "/keymap.json".as_ref(),
2226            &r#"
2227            [
2228                {
2229                    "bindings": {
2230                        "backspace": null
2231                    }
2232                }
2233            ]
2234            "#
2235            .into(),
2236            Default::default(),
2237        )
2238        .await
2239        .unwrap();
2240
2241        cx.foreground().run_until_parked();
2242
2243        assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
2244
2245        // Test modifying the base, while retaining the users keymap
2246        fs.save(
2247            "/settings.json".as_ref(),
2248            &r#"
2249            {
2250                "base_keymap": "JetBrains"
2251            }
2252            "#
2253            .into(),
2254            Default::default(),
2255        )
2256        .await
2257        .unwrap();
2258
2259        cx.foreground().run_until_parked();
2260
2261        assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
2262
2263        #[track_caller]
2264        fn assert_key_bindings_for<'a>(
2265            window_id: usize,
2266            cx: &TestAppContext,
2267            actions: Vec<(&'static str, &'a dyn Action)>,
2268            line: u32,
2269        ) {
2270            for (key, action) in actions {
2271                // assert that...
2272                assert!(
2273                    cx.available_actions(window_id, 0)
2274                        .into_iter()
2275                        .any(|(_, bound_action, b)| {
2276                            // action names match...
2277                            bound_action.name() == action.name()
2278                        && bound_action.namespace() == action.namespace()
2279                        // and key strokes contain the given key
2280                        && b.iter()
2281                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2282                        }),
2283                    "On {} Failed to find {} with key binding {}",
2284                    line,
2285                    action.name(),
2286                    key
2287                );
2288            }
2289        }
2290    }
2291
2292    #[gpui::test]
2293    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
2294        cx.platform()
2295            .fonts()
2296            .add_fonts(&[
2297                Assets
2298                    .load("fonts/zed-sans/zed-sans-extended.ttf")
2299                    .unwrap()
2300                    .to_vec()
2301                    .into(),
2302                Assets
2303                    .load("fonts/zed-mono/zed-mono-extended.ttf")
2304                    .unwrap()
2305                    .to_vec()
2306                    .into(),
2307            ])
2308            .unwrap();
2309        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
2310        let mut settings = SettingsStore::default();
2311        settings
2312            .set_default_settings(&settings::default_settings(), cx)
2313            .unwrap();
2314        cx.set_global(settings);
2315        theme::init(Assets, cx);
2316
2317        let mut has_default_theme = false;
2318        for theme_name in themes.list(false).map(|meta| meta.name) {
2319            let theme = themes.get(&theme_name).unwrap();
2320            assert_eq!(theme.meta.name, theme_name);
2321            if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
2322                has_default_theme = true;
2323            }
2324        }
2325        assert!(has_default_theme);
2326    }
2327
2328    #[gpui::test]
2329    fn test_bundled_languages(cx: &mut AppContext) {
2330        let mut languages = LanguageRegistry::test();
2331        languages.set_executor(cx.background().clone());
2332        let languages = Arc::new(languages);
2333        let http = FakeHttpClient::with_404_response();
2334        let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
2335        languages::init(languages.clone(), node_runtime);
2336        for name in languages.language_names() {
2337            languages.language_for_name(&name);
2338        }
2339        cx.foreground().run_until_parked();
2340    }
2341
2342    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2343        cx.foreground().forbid_parking();
2344        cx.update(|cx| {
2345            let mut app_state = AppState::test(cx);
2346            let state = Arc::get_mut(&mut app_state).unwrap();
2347            state.initialize_workspace = initialize_workspace;
2348            state.build_window_options = build_window_options;
2349            theme::init((), cx);
2350            audio::init((), cx);
2351            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
2352            workspace::init(app_state.clone(), cx);
2353            Project::init_settings(cx);
2354            language::init(cx);
2355            editor::init(cx);
2356            project_panel::init_settings(cx);
2357            pane::init(cx);
2358            project_panel::init((), cx);
2359            terminal_view::init(cx);
2360            ai::init(cx);
2361            app_state
2362        })
2363    }
2364
2365    fn rust_lang() -> Arc<language::Language> {
2366        Arc::new(language::Language::new(
2367            language::LanguageConfig {
2368                name: "Rust".into(),
2369                path_suffixes: vec!["rs".to_string()],
2370                ..Default::default()
2371            },
2372            Some(tree_sitter_rust::language()),
2373        ))
2374    }
2375}