zed.rs

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