zed.rs

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