project_tests.rs

    1#![allow(clippy::format_collect)]
    2
    3use crate::{
    4    Event,
    5    git_store::{GitStoreEvent, RepositoryEvent, StatusEntry, pending_op},
    6    task_inventory::TaskContexts,
    7    task_store::TaskSettingsLocation,
    8    *,
    9};
   10use async_trait::async_trait;
   11use buffer_diff::{
   12    BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
   13    DiffHunkStatusKind, assert_hunks,
   14};
   15use fs::FakeFs;
   16use futures::{StreamExt, future};
   17use git::{
   18    GitHostingProviderRegistry,
   19    repository::{RepoPath, repo_path},
   20    status::{StatusCode, TrackedStatus},
   21};
   22use git2::RepositoryInitOptions;
   23use gpui::{App, BackgroundExecutor, FutureExt, SemanticVersion, UpdateGlobal};
   24use itertools::Itertools;
   25use language::{
   26    Diagnostic, DiagnosticEntry, DiagnosticEntryRef, DiagnosticSet, DiagnosticSourceKind,
   27    DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding,
   28    ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList,
   29    ToolchainLister,
   30    language_settings::{LanguageSettingsContent, language_settings},
   31    tree_sitter_rust, tree_sitter_typescript,
   32};
   33use lsp::{
   34    DiagnosticSeverity, DocumentChanges, FileOperationFilter, NumberOrString, TextDocumentEdit,
   35    Uri, WillRenameFiles, notification::DidRenameFiles,
   36};
   37use parking_lot::Mutex;
   38use paths::{config_dir, global_gitignore_path, tasks_file};
   39use postage::stream::Stream as _;
   40use pretty_assertions::{assert_eq, assert_matches};
   41use rand::{Rng as _, rngs::StdRng};
   42use serde_json::json;
   43#[cfg(not(windows))]
   44use std::os;
   45use std::{
   46    env, mem,
   47    num::NonZeroU32,
   48    ops::Range,
   49    str::FromStr,
   50    sync::{Arc, OnceLock},
   51    task::Poll,
   52};
   53use sum_tree::SumTree;
   54use task::{ResolvedTask, ShellKind, TaskContext};
   55use unindent::Unindent as _;
   56use util::{
   57    TryFutureExt as _, assert_set_eq, maybe, path,
   58    paths::PathMatcher,
   59    rel_path::rel_path,
   60    test::{TempTree, marked_text_offsets},
   61    uri,
   62};
   63use worktree::WorktreeModelHandle as _;
   64
   65#[gpui::test]
   66async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
   67    cx.executor().allow_parking();
   68
   69    let (tx, mut rx) = futures::channel::mpsc::unbounded();
   70    let _thread = std::thread::spawn(move || {
   71        #[cfg(not(target_os = "windows"))]
   72        std::fs::metadata("/tmp").unwrap();
   73        #[cfg(target_os = "windows")]
   74        std::fs::metadata("C:/Windows").unwrap();
   75        std::thread::sleep(Duration::from_millis(1000));
   76        tx.unbounded_send(1).unwrap();
   77    });
   78    rx.next().await.unwrap();
   79}
   80
   81#[gpui::test]
   82async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
   83    cx.executor().allow_parking();
   84
   85    let io_task = smol::unblock(move || {
   86        println!("sleeping on thread {:?}", std::thread::current().id());
   87        std::thread::sleep(Duration::from_millis(10));
   88        1
   89    });
   90
   91    let task = cx.foreground_executor().spawn(async move {
   92        io_task.await;
   93    });
   94
   95    task.await;
   96}
   97
   98// NOTE:
   99// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
  100// we assume that they are not supported out of the box.
  101#[cfg(not(windows))]
  102#[gpui::test]
  103async fn test_symlinks(cx: &mut gpui::TestAppContext) {
  104    init_test(cx);
  105    cx.executor().allow_parking();
  106
  107    let dir = TempTree::new(json!({
  108        "root": {
  109            "apple": "",
  110            "banana": {
  111                "carrot": {
  112                    "date": "",
  113                    "endive": "",
  114                }
  115            },
  116            "fennel": {
  117                "grape": "",
  118            }
  119        }
  120    }));
  121
  122    let root_link_path = dir.path().join("root_link");
  123    os::unix::fs::symlink(dir.path().join("root"), &root_link_path).unwrap();
  124    os::unix::fs::symlink(
  125        dir.path().join("root/fennel"),
  126        dir.path().join("root/finnochio"),
  127    )
  128    .unwrap();
  129
  130    let project = Project::test(
  131        Arc::new(RealFs::new(None, cx.executor())),
  132        [root_link_path.as_ref()],
  133        cx,
  134    )
  135    .await;
  136
  137    project.update(cx, |project, cx| {
  138        let tree = project.worktrees(cx).next().unwrap().read(cx);
  139        assert_eq!(tree.file_count(), 5);
  140        assert_eq!(
  141            tree.entry_for_path(rel_path("fennel/grape")).unwrap().inode,
  142            tree.entry_for_path(rel_path("finnochio/grape"))
  143                .unwrap()
  144                .inode
  145        );
  146    });
  147}
  148
  149#[gpui::test]
  150async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
  151    init_test(cx);
  152
  153    let dir = TempTree::new(json!({
  154        ".editorconfig": r#"
  155        root = true
  156        [*.rs]
  157            indent_style = tab
  158            indent_size = 3
  159            end_of_line = lf
  160            insert_final_newline = true
  161            trim_trailing_whitespace = true
  162            max_line_length = 120
  163        [*.js]
  164            tab_width = 10
  165            max_line_length = off
  166        "#,
  167        ".zed": {
  168            "settings.json": r#"{
  169                "tab_size": 8,
  170                "hard_tabs": false,
  171                "ensure_final_newline_on_save": false,
  172                "remove_trailing_whitespace_on_save": false,
  173                "preferred_line_length": 64,
  174                "soft_wrap": "editor_width",
  175            }"#,
  176        },
  177        "a.rs": "fn a() {\n    A\n}",
  178        "b": {
  179            ".editorconfig": r#"
  180            [*.rs]
  181                indent_size = 2
  182                max_line_length = off,
  183            "#,
  184            "b.rs": "fn b() {\n    B\n}",
  185        },
  186        "c.js": "def c\n  C\nend",
  187        "README.json": "tabs are better\n",
  188    }));
  189
  190    let path = dir.path();
  191    let fs = FakeFs::new(cx.executor());
  192    fs.insert_tree_from_real_fs(path, path).await;
  193    let project = Project::test(fs, [path], cx).await;
  194
  195    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
  196    language_registry.add(js_lang());
  197    language_registry.add(json_lang());
  198    language_registry.add(rust_lang());
  199
  200    let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
  201
  202    cx.executor().run_until_parked();
  203
  204    cx.update(|cx| {
  205        let tree = worktree.read(cx);
  206        let settings_for = |path: &str| {
  207            let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone();
  208            let file = File::for_entry(file_entry, worktree.clone());
  209            let file_language = project
  210                .read(cx)
  211                .languages()
  212                .load_language_for_file_path(file.path.as_std_path());
  213            let file_language = cx
  214                .background_executor()
  215                .block(file_language)
  216                .expect("Failed to get file language");
  217            let file = file as _;
  218            language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
  219        };
  220
  221        let settings_a = settings_for("a.rs");
  222        let settings_b = settings_for("b/b.rs");
  223        let settings_c = settings_for("c.js");
  224        let settings_readme = settings_for("README.json");
  225
  226        // .editorconfig overrides .zed/settings
  227        assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
  228        assert_eq!(settings_a.hard_tabs, true);
  229        assert_eq!(settings_a.ensure_final_newline_on_save, true);
  230        assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
  231        assert_eq!(settings_a.preferred_line_length, 120);
  232
  233        // .editorconfig in b/ overrides .editorconfig in root
  234        assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
  235
  236        // "indent_size" is not set, so "tab_width" is used
  237        assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
  238
  239        // When max_line_length is "off", default to .zed/settings.json
  240        assert_eq!(settings_b.preferred_line_length, 64);
  241        assert_eq!(settings_c.preferred_line_length, 64);
  242
  243        // README.md should not be affected by .editorconfig's globe "*.rs"
  244        assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
  245    });
  246}
  247
  248#[gpui::test]
  249async fn test_git_provider_project_setting(cx: &mut gpui::TestAppContext) {
  250    init_test(cx);
  251    cx.update(|cx| {
  252        GitHostingProviderRegistry::default_global(cx);
  253        git_hosting_providers::init(cx);
  254    });
  255
  256    let fs = FakeFs::new(cx.executor());
  257    let str_path = path!("/dir");
  258    let path = Path::new(str_path);
  259
  260    fs.insert_tree(
  261        path!("/dir"),
  262        json!({
  263            ".zed": {
  264                "settings.json": r#"{
  265                    "git_hosting_providers": [
  266                        {
  267                            "provider": "gitlab",
  268                            "base_url": "https://google.com",
  269                            "name": "foo"
  270                        }
  271                    ]
  272                }"#
  273            },
  274        }),
  275    )
  276    .await;
  277
  278    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
  279    let (_worktree, _) =
  280        project.read_with(cx, |project, cx| project.find_worktree(path, cx).unwrap());
  281    cx.executor().run_until_parked();
  282
  283    cx.update(|cx| {
  284        let provider = GitHostingProviderRegistry::global(cx);
  285        assert!(
  286            provider
  287                .list_hosting_providers()
  288                .into_iter()
  289                .any(|provider| provider.name() == "foo")
  290        );
  291    });
  292
  293    fs.atomic_write(
  294        Path::new(path!("/dir/.zed/settings.json")).to_owned(),
  295        "{}".into(),
  296    )
  297    .await
  298    .unwrap();
  299
  300    cx.run_until_parked();
  301
  302    cx.update(|cx| {
  303        let provider = GitHostingProviderRegistry::global(cx);
  304        assert!(
  305            !provider
  306                .list_hosting_providers()
  307                .into_iter()
  308                .any(|provider| provider.name() == "foo")
  309        );
  310    });
  311}
  312
  313#[gpui::test]
  314async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
  315    init_test(cx);
  316    TaskStore::init(None);
  317
  318    let fs = FakeFs::new(cx.executor());
  319    fs.insert_tree(
  320        path!("/dir"),
  321        json!({
  322            ".zed": {
  323                "settings.json": r#"{ "tab_size": 8 }"#,
  324                "tasks.json": r#"[{
  325                    "label": "cargo check all",
  326                    "command": "cargo",
  327                    "args": ["check", "--all"]
  328                },]"#,
  329            },
  330            "a": {
  331                "a.rs": "fn a() {\n    A\n}"
  332            },
  333            "b": {
  334                ".zed": {
  335                    "settings.json": r#"{ "tab_size": 2 }"#,
  336                    "tasks.json": r#"[{
  337                        "label": "cargo check",
  338                        "command": "cargo",
  339                        "args": ["check"]
  340                    },]"#,
  341                },
  342                "b.rs": "fn b() {\n  B\n}"
  343            }
  344        }),
  345    )
  346    .await;
  347
  348    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
  349    let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
  350
  351    cx.executor().run_until_parked();
  352    let worktree_id = cx.update(|cx| {
  353        project.update(cx, |project, cx| {
  354            project.worktrees(cx).next().unwrap().read(cx).id()
  355        })
  356    });
  357
  358    let mut task_contexts = TaskContexts::default();
  359    task_contexts.active_worktree_context = Some((worktree_id, TaskContext::default()));
  360    let task_contexts = Arc::new(task_contexts);
  361
  362    let topmost_local_task_source_kind = TaskSourceKind::Worktree {
  363        id: worktree_id,
  364        directory_in_worktree: rel_path(".zed").into(),
  365        id_base: "local worktree tasks from directory \".zed\"".into(),
  366    };
  367
  368    let all_tasks = cx
  369        .update(|cx| {
  370            let tree = worktree.read(cx);
  371
  372            let file_a = File::for_entry(
  373                tree.entry_for_path(rel_path("a/a.rs")).unwrap().clone(),
  374                worktree.clone(),
  375            ) as _;
  376            let settings_a = language_settings(None, Some(&file_a), cx);
  377            let file_b = File::for_entry(
  378                tree.entry_for_path(rel_path("b/b.rs")).unwrap().clone(),
  379                worktree.clone(),
  380            ) as _;
  381            let settings_b = language_settings(None, Some(&file_b), cx);
  382
  383            assert_eq!(settings_a.tab_size.get(), 8);
  384            assert_eq!(settings_b.tab_size.get(), 2);
  385
  386            get_all_tasks(&project, task_contexts.clone(), cx)
  387        })
  388        .await
  389        .into_iter()
  390        .map(|(source_kind, task)| {
  391            let resolved = task.resolved;
  392            (
  393                source_kind,
  394                task.resolved_label,
  395                resolved.args,
  396                resolved.env,
  397            )
  398        })
  399        .collect::<Vec<_>>();
  400    assert_eq!(
  401        all_tasks,
  402        vec![
  403            (
  404                TaskSourceKind::Worktree {
  405                    id: worktree_id,
  406                    directory_in_worktree: rel_path("b/.zed").into(),
  407                    id_base: "local worktree tasks from directory \"b/.zed\"".into()
  408                },
  409                "cargo check".to_string(),
  410                vec!["check".to_string()],
  411                HashMap::default(),
  412            ),
  413            (
  414                topmost_local_task_source_kind.clone(),
  415                "cargo check all".to_string(),
  416                vec!["check".to_string(), "--all".to_string()],
  417                HashMap::default(),
  418            ),
  419        ]
  420    );
  421
  422    let (_, resolved_task) = cx
  423        .update(|cx| get_all_tasks(&project, task_contexts.clone(), cx))
  424        .await
  425        .into_iter()
  426        .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind)
  427        .expect("should have one global task");
  428    project.update(cx, |project, cx| {
  429        let task_inventory = project
  430            .task_store
  431            .read(cx)
  432            .task_inventory()
  433            .cloned()
  434            .unwrap();
  435        task_inventory.update(cx, |inventory, _| {
  436            inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task);
  437            inventory
  438                .update_file_based_tasks(
  439                    TaskSettingsLocation::Global(tasks_file()),
  440                    Some(
  441                        &json!([{
  442                            "label": "cargo check unstable",
  443                            "command": "cargo",
  444                            "args": [
  445                                "check",
  446                                "--all",
  447                                "--all-targets"
  448                            ],
  449                            "env": {
  450                                "RUSTFLAGS": "-Zunstable-options"
  451                            }
  452                        }])
  453                        .to_string(),
  454                    ),
  455                )
  456                .unwrap();
  457        });
  458    });
  459    cx.run_until_parked();
  460
  461    let all_tasks = cx
  462        .update(|cx| get_all_tasks(&project, task_contexts.clone(), cx))
  463        .await
  464        .into_iter()
  465        .map(|(source_kind, task)| {
  466            let resolved = task.resolved;
  467            (
  468                source_kind,
  469                task.resolved_label,
  470                resolved.args,
  471                resolved.env,
  472            )
  473        })
  474        .collect::<Vec<_>>();
  475    assert_eq!(
  476        all_tasks,
  477        vec![
  478            (
  479                topmost_local_task_source_kind.clone(),
  480                "cargo check all".to_string(),
  481                vec!["check".to_string(), "--all".to_string()],
  482                HashMap::default(),
  483            ),
  484            (
  485                TaskSourceKind::Worktree {
  486                    id: worktree_id,
  487                    directory_in_worktree: rel_path("b/.zed").into(),
  488                    id_base: "local worktree tasks from directory \"b/.zed\"".into()
  489                },
  490                "cargo check".to_string(),
  491                vec!["check".to_string()],
  492                HashMap::default(),
  493            ),
  494            (
  495                TaskSourceKind::AbsPath {
  496                    abs_path: paths::tasks_file().clone(),
  497                    id_base: "global tasks.json".into(),
  498                },
  499                "cargo check unstable".to_string(),
  500                vec![
  501                    "check".to_string(),
  502                    "--all".to_string(),
  503                    "--all-targets".to_string(),
  504                ],
  505                HashMap::from_iter(Some((
  506                    "RUSTFLAGS".to_string(),
  507                    "-Zunstable-options".to_string()
  508                ))),
  509            ),
  510        ]
  511    );
  512}
  513
  514#[gpui::test]
  515async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
  516    init_test(cx);
  517    TaskStore::init(None);
  518
  519    let fs = FakeFs::new(cx.executor());
  520    fs.insert_tree(
  521        path!("/dir"),
  522        json!({
  523            ".zed": {
  524                "tasks.json": r#"[{
  525                    "label": "test worktree root",
  526                    "command": "echo $ZED_WORKTREE_ROOT"
  527                }]"#,
  528            },
  529            "a": {
  530                "a.rs": "fn a() {\n    A\n}"
  531            },
  532        }),
  533    )
  534    .await;
  535
  536    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
  537    let _worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
  538
  539    cx.executor().run_until_parked();
  540    let worktree_id = cx.update(|cx| {
  541        project.update(cx, |project, cx| {
  542            project.worktrees(cx).next().unwrap().read(cx).id()
  543        })
  544    });
  545
  546    let active_non_worktree_item_tasks = cx
  547        .update(|cx| {
  548            get_all_tasks(
  549                &project,
  550                Arc::new(TaskContexts {
  551                    active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
  552                    active_worktree_context: None,
  553                    other_worktree_contexts: Vec::new(),
  554                    lsp_task_sources: HashMap::default(),
  555                    latest_selection: None,
  556                }),
  557                cx,
  558            )
  559        })
  560        .await;
  561    assert!(
  562        active_non_worktree_item_tasks.is_empty(),
  563        "A task can not be resolved with context with no ZED_WORKTREE_ROOT data"
  564    );
  565
  566    let active_worktree_tasks = cx
  567        .update(|cx| {
  568            get_all_tasks(
  569                &project,
  570                Arc::new(TaskContexts {
  571                    active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
  572                    active_worktree_context: Some((worktree_id, {
  573                        let mut worktree_context = TaskContext::default();
  574                        worktree_context
  575                            .task_variables
  576                            .insert(task::VariableName::WorktreeRoot, "/dir".to_string());
  577                        worktree_context
  578                    })),
  579                    other_worktree_contexts: Vec::new(),
  580                    lsp_task_sources: HashMap::default(),
  581                    latest_selection: None,
  582                }),
  583                cx,
  584            )
  585        })
  586        .await;
  587    assert_eq!(
  588        active_worktree_tasks
  589            .into_iter()
  590            .map(|(source_kind, task)| {
  591                let resolved = task.resolved;
  592                (source_kind, resolved.command.unwrap())
  593            })
  594            .collect::<Vec<_>>(),
  595        vec![(
  596            TaskSourceKind::Worktree {
  597                id: worktree_id,
  598                directory_in_worktree: rel_path(".zed").into(),
  599                id_base: "local worktree tasks from directory \".zed\"".into(),
  600            },
  601            "echo /dir".to_string(),
  602        )]
  603    );
  604}
  605
  606#[gpui::test]
  607async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
  608    cx: &mut gpui::TestAppContext,
  609) {
  610    pub(crate) struct PyprojectTomlManifestProvider;
  611
  612    impl ManifestProvider for PyprojectTomlManifestProvider {
  613        fn name(&self) -> ManifestName {
  614            SharedString::new_static("pyproject.toml").into()
  615        }
  616
  617        fn search(
  618            &self,
  619            ManifestQuery {
  620                path,
  621                depth,
  622                delegate,
  623            }: ManifestQuery,
  624        ) -> Option<Arc<RelPath>> {
  625            for path in path.ancestors().take(depth) {
  626                let p = path.join(rel_path("pyproject.toml"));
  627                if delegate.exists(&p, Some(false)) {
  628                    return Some(path.into());
  629                }
  630            }
  631
  632            None
  633        }
  634    }
  635
  636    init_test(cx);
  637    let fs = FakeFs::new(cx.executor());
  638
  639    fs.insert_tree(
  640        path!("/the-root"),
  641        json!({
  642            ".zed": {
  643                "settings.json": r#"
  644                {
  645                    "languages": {
  646                        "Python": {
  647                            "language_servers": ["ty"]
  648                        }
  649                    }
  650                }"#
  651            },
  652            "project-a": {
  653                ".venv": {},
  654                "file.py": "",
  655                "pyproject.toml": ""
  656            },
  657            "project-b": {
  658                ".venv": {},
  659                "source_file.py":"",
  660                "another_file.py": "",
  661                "pyproject.toml": ""
  662            }
  663        }),
  664    )
  665    .await;
  666    cx.update(|cx| {
  667        ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider))
  668    });
  669
  670    let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
  671    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
  672    let _fake_python_server = language_registry.register_fake_lsp(
  673        "Python",
  674        FakeLspAdapter {
  675            name: "ty",
  676            capabilities: lsp::ServerCapabilities {
  677                ..Default::default()
  678            },
  679            ..Default::default()
  680        },
  681    );
  682
  683    language_registry.add(python_lang(fs.clone()));
  684    let (first_buffer, _handle) = project
  685        .update(cx, |project, cx| {
  686            project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx)
  687        })
  688        .await
  689        .unwrap();
  690    cx.executor().run_until_parked();
  691    let servers = project.update(cx, |project, cx| {
  692        project.lsp_store.update(cx, |this, cx| {
  693            first_buffer.update(cx, |buffer, cx| {
  694                this.language_servers_for_local_buffer(buffer, cx)
  695                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
  696                    .collect::<Vec<_>>()
  697            })
  698        })
  699    });
  700    cx.executor().run_until_parked();
  701    assert_eq!(servers.len(), 1);
  702    let (adapter, server) = servers.into_iter().next().unwrap();
  703    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
  704    assert_eq!(server.server_id(), LanguageServerId(0));
  705    // `workspace_folders` are set to the rooting point.
  706    assert_eq!(
  707        server.workspace_folders(),
  708        BTreeSet::from_iter(
  709            [Uri::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter()
  710        )
  711    );
  712
  713    let (second_project_buffer, _other_handle) = project
  714        .update(cx, |project, cx| {
  715            project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx)
  716        })
  717        .await
  718        .unwrap();
  719    cx.executor().run_until_parked();
  720    let servers = project.update(cx, |project, cx| {
  721        project.lsp_store.update(cx, |this, cx| {
  722            second_project_buffer.update(cx, |buffer, cx| {
  723                this.language_servers_for_local_buffer(buffer, cx)
  724                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
  725                    .collect::<Vec<_>>()
  726            })
  727        })
  728    });
  729    cx.executor().run_until_parked();
  730    assert_eq!(servers.len(), 1);
  731    let (adapter, server) = servers.into_iter().next().unwrap();
  732    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
  733    // We're not using venvs at all here, so both folders should fall under the same root.
  734    assert_eq!(server.server_id(), LanguageServerId(0));
  735    // Now, let's select a different toolchain for one of subprojects.
  736
  737    let Toolchains {
  738        toolchains: available_toolchains_for_b,
  739        root_path,
  740        ..
  741    } = project
  742        .update(cx, |this, cx| {
  743            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
  744            this.available_toolchains(
  745                ProjectPath {
  746                    worktree_id,
  747                    path: rel_path("project-b/source_file.py").into(),
  748                },
  749                LanguageName::new("Python"),
  750                cx,
  751            )
  752        })
  753        .await
  754        .expect("A toolchain to be discovered");
  755    assert_eq!(root_path.as_ref(), rel_path("project-b"));
  756    assert_eq!(available_toolchains_for_b.toolchains().len(), 1);
  757    let currently_active_toolchain = project
  758        .update(cx, |this, cx| {
  759            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
  760            this.active_toolchain(
  761                ProjectPath {
  762                    worktree_id,
  763                    path: rel_path("project-b/source_file.py").into(),
  764                },
  765                LanguageName::new("Python"),
  766                cx,
  767            )
  768        })
  769        .await;
  770
  771    assert!(currently_active_toolchain.is_none());
  772    let _ = project
  773        .update(cx, |this, cx| {
  774            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
  775            this.activate_toolchain(
  776                ProjectPath {
  777                    worktree_id,
  778                    path: root_path,
  779                },
  780                available_toolchains_for_b
  781                    .toolchains
  782                    .into_iter()
  783                    .next()
  784                    .unwrap(),
  785                cx,
  786            )
  787        })
  788        .await
  789        .unwrap();
  790    cx.run_until_parked();
  791    let servers = project.update(cx, |project, cx| {
  792        project.lsp_store.update(cx, |this, cx| {
  793            second_project_buffer.update(cx, |buffer, cx| {
  794                this.language_servers_for_local_buffer(buffer, cx)
  795                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
  796                    .collect::<Vec<_>>()
  797            })
  798        })
  799    });
  800    cx.executor().run_until_parked();
  801    assert_eq!(servers.len(), 1);
  802    let (adapter, server) = servers.into_iter().next().unwrap();
  803    assert_eq!(adapter.name(), LanguageServerName::new_static("ty"));
  804    // There's a new language server in town.
  805    assert_eq!(server.server_id(), LanguageServerId(1));
  806}
  807
  808#[gpui::test]
  809async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
  810    init_test(cx);
  811
  812    let fs = FakeFs::new(cx.executor());
  813    fs.insert_tree(
  814        path!("/dir"),
  815        json!({
  816            "test.rs": "const A: i32 = 1;",
  817            "test2.rs": "",
  818            "Cargo.toml": "a = 1",
  819            "package.json": "{\"a\": 1}",
  820        }),
  821    )
  822    .await;
  823
  824    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
  825    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
  826
  827    let mut fake_rust_servers = language_registry.register_fake_lsp(
  828        "Rust",
  829        FakeLspAdapter {
  830            name: "the-rust-language-server",
  831            capabilities: lsp::ServerCapabilities {
  832                completion_provider: Some(lsp::CompletionOptions {
  833                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
  834                    ..Default::default()
  835                }),
  836                text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
  837                    lsp::TextDocumentSyncOptions {
  838                        save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
  839                        ..Default::default()
  840                    },
  841                )),
  842                ..Default::default()
  843            },
  844            ..Default::default()
  845        },
  846    );
  847    let mut fake_json_servers = language_registry.register_fake_lsp(
  848        "JSON",
  849        FakeLspAdapter {
  850            name: "the-json-language-server",
  851            capabilities: lsp::ServerCapabilities {
  852                completion_provider: Some(lsp::CompletionOptions {
  853                    trigger_characters: Some(vec![":".to_string()]),
  854                    ..Default::default()
  855                }),
  856                text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
  857                    lsp::TextDocumentSyncOptions {
  858                        save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
  859                        ..Default::default()
  860                    },
  861                )),
  862                ..Default::default()
  863            },
  864            ..Default::default()
  865        },
  866    );
  867
  868    // Open a buffer without an associated language server.
  869    let (toml_buffer, _handle) = project
  870        .update(cx, |project, cx| {
  871            project.open_local_buffer_with_lsp(path!("/dir/Cargo.toml"), cx)
  872        })
  873        .await
  874        .unwrap();
  875
  876    // Open a buffer with an associated language server before the language for it has been loaded.
  877    let (rust_buffer, _handle2) = project
  878        .update(cx, |project, cx| {
  879            project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx)
  880        })
  881        .await
  882        .unwrap();
  883    rust_buffer.update(cx, |buffer, _| {
  884        assert_eq!(buffer.language().map(|l| l.name()), None);
  885    });
  886
  887    // Now we add the languages to the project, and ensure they get assigned to all
  888    // the relevant open buffers.
  889    language_registry.add(json_lang());
  890    language_registry.add(rust_lang());
  891    cx.executor().run_until_parked();
  892    rust_buffer.update(cx, |buffer, _| {
  893        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
  894    });
  895
  896    // A server is started up, and it is notified about Rust files.
  897    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
  898    assert_eq!(
  899        fake_rust_server
  900            .receive_notification::<lsp::notification::DidOpenTextDocument>()
  901            .await
  902            .text_document,
  903        lsp::TextDocumentItem {
  904            uri: lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(),
  905            version: 0,
  906            text: "const A: i32 = 1;".to_string(),
  907            language_id: "rust".to_string(),
  908        }
  909    );
  910
  911    // The buffer is configured based on the language server's capabilities.
  912    rust_buffer.update(cx, |buffer, _| {
  913        assert_eq!(
  914            buffer
  915                .completion_triggers()
  916                .iter()
  917                .cloned()
  918                .collect::<Vec<_>>(),
  919            &[".".to_string(), "::".to_string()]
  920        );
  921    });
  922    toml_buffer.update(cx, |buffer, _| {
  923        assert!(buffer.completion_triggers().is_empty());
  924    });
  925
  926    // Edit a buffer. The changes are reported to the language server.
  927    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
  928    assert_eq!(
  929        fake_rust_server
  930            .receive_notification::<lsp::notification::DidChangeTextDocument>()
  931            .await
  932            .text_document,
  933        lsp::VersionedTextDocumentIdentifier::new(
  934            lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(),
  935            1
  936        )
  937    );
  938
  939    // Open a third buffer with a different associated language server.
  940    let (json_buffer, _json_handle) = project
  941        .update(cx, |project, cx| {
  942            project.open_local_buffer_with_lsp(path!("/dir/package.json"), cx)
  943        })
  944        .await
  945        .unwrap();
  946
  947    // A json language server is started up and is only notified about the json buffer.
  948    let mut fake_json_server = fake_json_servers.next().await.unwrap();
  949    assert_eq!(
  950        fake_json_server
  951            .receive_notification::<lsp::notification::DidOpenTextDocument>()
  952            .await
  953            .text_document,
  954        lsp::TextDocumentItem {
  955            uri: lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(),
  956            version: 0,
  957            text: "{\"a\": 1}".to_string(),
  958            language_id: "json".to_string(),
  959        }
  960    );
  961
  962    // This buffer is configured based on the second language server's
  963    // capabilities.
  964    json_buffer.update(cx, |buffer, _| {
  965        assert_eq!(
  966            buffer
  967                .completion_triggers()
  968                .iter()
  969                .cloned()
  970                .collect::<Vec<_>>(),
  971            &[":".to_string()]
  972        );
  973    });
  974
  975    // When opening another buffer whose language server is already running,
  976    // it is also configured based on the existing language server's capabilities.
  977    let (rust_buffer2, _handle4) = project
  978        .update(cx, |project, cx| {
  979            project.open_local_buffer_with_lsp(path!("/dir/test2.rs"), cx)
  980        })
  981        .await
  982        .unwrap();
  983    rust_buffer2.update(cx, |buffer, _| {
  984        assert_eq!(
  985            buffer
  986                .completion_triggers()
  987                .iter()
  988                .cloned()
  989                .collect::<Vec<_>>(),
  990            &[".".to_string(), "::".to_string()]
  991        );
  992    });
  993
  994    // Changes are reported only to servers matching the buffer's language.
  995    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
  996    rust_buffer2.update(cx, |buffer, cx| {
  997        buffer.edit([(0..0, "let x = 1;")], None, cx)
  998    });
  999    assert_eq!(
 1000        fake_rust_server
 1001            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 1002            .await
 1003            .text_document,
 1004        lsp::VersionedTextDocumentIdentifier::new(
 1005            lsp::Uri::from_file_path(path!("/dir/test2.rs")).unwrap(),
 1006            1
 1007        )
 1008    );
 1009
 1010    // Save notifications are reported to all servers.
 1011    project
 1012        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
 1013        .await
 1014        .unwrap();
 1015    assert_eq!(
 1016        fake_rust_server
 1017            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 1018            .await
 1019            .text_document,
 1020        lsp::TextDocumentIdentifier::new(
 1021            lsp::Uri::from_file_path(path!("/dir/Cargo.toml")).unwrap()
 1022        )
 1023    );
 1024    assert_eq!(
 1025        fake_json_server
 1026            .receive_notification::<lsp::notification::DidSaveTextDocument>()
 1027            .await
 1028            .text_document,
 1029        lsp::TextDocumentIdentifier::new(
 1030            lsp::Uri::from_file_path(path!("/dir/Cargo.toml")).unwrap()
 1031        )
 1032    );
 1033
 1034    // Renames are reported only to servers matching the buffer's language.
 1035    fs.rename(
 1036        Path::new(path!("/dir/test2.rs")),
 1037        Path::new(path!("/dir/test3.rs")),
 1038        Default::default(),
 1039    )
 1040    .await
 1041    .unwrap();
 1042    assert_eq!(
 1043        fake_rust_server
 1044            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 1045            .await
 1046            .text_document,
 1047        lsp::TextDocumentIdentifier::new(lsp::Uri::from_file_path(path!("/dir/test2.rs")).unwrap()),
 1048    );
 1049    assert_eq!(
 1050        fake_rust_server
 1051            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 1052            .await
 1053            .text_document,
 1054        lsp::TextDocumentItem {
 1055            uri: lsp::Uri::from_file_path(path!("/dir/test3.rs")).unwrap(),
 1056            version: 0,
 1057            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 1058            language_id: "rust".to_string(),
 1059        },
 1060    );
 1061
 1062    rust_buffer2.update(cx, |buffer, cx| {
 1063        buffer.update_diagnostics(
 1064            LanguageServerId(0),
 1065            DiagnosticSet::from_sorted_entries(
 1066                vec![DiagnosticEntry {
 1067                    diagnostic: Default::default(),
 1068                    range: Anchor::MIN..Anchor::MAX,
 1069                }],
 1070                &buffer.snapshot(),
 1071            ),
 1072            cx,
 1073        );
 1074        assert_eq!(
 1075            buffer
 1076                .snapshot()
 1077                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 1078                .count(),
 1079            1
 1080        );
 1081    });
 1082
 1083    // When the rename changes the extension of the file, the buffer gets closed on the old
 1084    // language server and gets opened on the new one.
 1085    fs.rename(
 1086        Path::new(path!("/dir/test3.rs")),
 1087        Path::new(path!("/dir/test3.json")),
 1088        Default::default(),
 1089    )
 1090    .await
 1091    .unwrap();
 1092    assert_eq!(
 1093        fake_rust_server
 1094            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 1095            .await
 1096            .text_document,
 1097        lsp::TextDocumentIdentifier::new(lsp::Uri::from_file_path(path!("/dir/test3.rs")).unwrap()),
 1098    );
 1099    assert_eq!(
 1100        fake_json_server
 1101            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 1102            .await
 1103            .text_document,
 1104        lsp::TextDocumentItem {
 1105            uri: lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(),
 1106            version: 0,
 1107            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 1108            language_id: "json".to_string(),
 1109        },
 1110    );
 1111
 1112    // We clear the diagnostics, since the language has changed.
 1113    rust_buffer2.update(cx, |buffer, _| {
 1114        assert_eq!(
 1115            buffer
 1116                .snapshot()
 1117                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
 1118                .count(),
 1119            0
 1120        );
 1121    });
 1122
 1123    // The renamed file's version resets after changing language server.
 1124    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
 1125    assert_eq!(
 1126        fake_json_server
 1127            .receive_notification::<lsp::notification::DidChangeTextDocument>()
 1128            .await
 1129            .text_document,
 1130        lsp::VersionedTextDocumentIdentifier::new(
 1131            lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(),
 1132            1
 1133        )
 1134    );
 1135
 1136    // Restart language servers
 1137    project.update(cx, |project, cx| {
 1138        project.restart_language_servers_for_buffers(
 1139            vec![rust_buffer.clone(), json_buffer.clone()],
 1140            HashSet::default(),
 1141            cx,
 1142        );
 1143    });
 1144
 1145    let mut rust_shutdown_requests = fake_rust_server
 1146        .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 1147    let mut json_shutdown_requests = fake_json_server
 1148        .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
 1149    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
 1150
 1151    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 1152    let mut fake_json_server = fake_json_servers.next().await.unwrap();
 1153
 1154    // Ensure rust document is reopened in new rust language server
 1155    assert_eq!(
 1156        fake_rust_server
 1157            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 1158            .await
 1159            .text_document,
 1160        lsp::TextDocumentItem {
 1161            uri: lsp::Uri::from_file_path(path!("/dir/test.rs")).unwrap(),
 1162            version: 0,
 1163            text: rust_buffer.update(cx, |buffer, _| buffer.text()),
 1164            language_id: "rust".to_string(),
 1165        }
 1166    );
 1167
 1168    // Ensure json documents are reopened in new json language server
 1169    assert_set_eq!(
 1170        [
 1171            fake_json_server
 1172                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 1173                .await
 1174                .text_document,
 1175            fake_json_server
 1176                .receive_notification::<lsp::notification::DidOpenTextDocument>()
 1177                .await
 1178                .text_document,
 1179        ],
 1180        [
 1181            lsp::TextDocumentItem {
 1182                uri: lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(),
 1183                version: 0,
 1184                text: json_buffer.update(cx, |buffer, _| buffer.text()),
 1185                language_id: "json".to_string(),
 1186            },
 1187            lsp::TextDocumentItem {
 1188                uri: lsp::Uri::from_file_path(path!("/dir/test3.json")).unwrap(),
 1189                version: 0,
 1190                text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
 1191                language_id: "json".to_string(),
 1192            }
 1193        ]
 1194    );
 1195
 1196    // Close notifications are reported only to servers matching the buffer's language.
 1197    cx.update(|_| drop(_json_handle));
 1198    let close_message = lsp::DidCloseTextDocumentParams {
 1199        text_document: lsp::TextDocumentIdentifier::new(
 1200            lsp::Uri::from_file_path(path!("/dir/package.json")).unwrap(),
 1201        ),
 1202    };
 1203    assert_eq!(
 1204        fake_json_server
 1205            .receive_notification::<lsp::notification::DidCloseTextDocument>()
 1206            .await,
 1207        close_message,
 1208    );
 1209}
 1210
 1211#[gpui::test]
 1212async fn test_language_server_relative_path(cx: &mut gpui::TestAppContext) {
 1213    init_test(cx);
 1214
 1215    let settings_json_contents = json!({
 1216        "languages": {
 1217            "Rust": {
 1218                "language_servers": ["my_fake_lsp", "lsp_on_path"]
 1219            }
 1220        },
 1221        "lsp": {
 1222            "my_fake_lsp": {
 1223                "binary": {
 1224                    // file exists, so this is treated as a relative path
 1225                    "path": path!(".relative_path/to/my_fake_lsp_binary.exe").to_string(),
 1226                }
 1227            },
 1228            "lsp_on_path": {
 1229                "binary": {
 1230                    // file doesn't exist, so it will fall back on PATH env var
 1231                    "path": path!("lsp_on_path.exe").to_string(),
 1232                }
 1233            }
 1234        },
 1235    });
 1236
 1237    let fs = FakeFs::new(cx.executor());
 1238    fs.insert_tree(
 1239        path!("/the-root"),
 1240        json!({
 1241            ".zed": {
 1242                "settings.json": settings_json_contents.to_string(),
 1243            },
 1244            ".relative_path": {
 1245                "to": {
 1246                    "my_fake_lsp.exe": "",
 1247                },
 1248            },
 1249            "src": {
 1250                "main.rs": "",
 1251            }
 1252        }),
 1253    )
 1254    .await;
 1255
 1256    let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
 1257    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 1258    language_registry.add(rust_lang());
 1259
 1260    let mut my_fake_lsp = language_registry.register_fake_lsp(
 1261        "Rust",
 1262        FakeLspAdapter {
 1263            name: "my_fake_lsp",
 1264            ..Default::default()
 1265        },
 1266    );
 1267    let mut lsp_on_path = language_registry.register_fake_lsp(
 1268        "Rust",
 1269        FakeLspAdapter {
 1270            name: "lsp_on_path",
 1271            ..Default::default()
 1272        },
 1273    );
 1274
 1275    cx.run_until_parked();
 1276
 1277    // Start the language server by opening a buffer with a compatible file extension.
 1278    project
 1279        .update(cx, |project, cx| {
 1280            project.open_local_buffer_with_lsp(path!("/the-root/src/main.rs"), cx)
 1281        })
 1282        .await
 1283        .unwrap();
 1284
 1285    let lsp_path = my_fake_lsp.next().await.unwrap().binary.path;
 1286    assert_eq!(
 1287        lsp_path.to_string_lossy(),
 1288        path!("/the-root/.relative_path/to/my_fake_lsp_binary.exe"),
 1289    );
 1290
 1291    let lsp_path = lsp_on_path.next().await.unwrap().binary.path;
 1292    assert_eq!(lsp_path.to_string_lossy(), path!("lsp_on_path.exe"));
 1293}
 1294
 1295#[gpui::test]
 1296async fn test_language_server_tilde_path(cx: &mut gpui::TestAppContext) {
 1297    init_test(cx);
 1298
 1299    let settings_json_contents = json!({
 1300        "languages": {
 1301            "Rust": {
 1302                "language_servers": ["tilde_lsp"]
 1303            }
 1304        },
 1305        "lsp": {
 1306            "tilde_lsp": {
 1307                "binary": {
 1308                    "path": "~/.local/bin/rust-analyzer",
 1309                }
 1310            }
 1311        },
 1312    });
 1313
 1314    let fs = FakeFs::new(cx.executor());
 1315    fs.insert_tree(
 1316        path!("/root"),
 1317        json!({
 1318            ".zed": {
 1319                "settings.json": settings_json_contents.to_string(),
 1320            },
 1321            "src": {
 1322                "main.rs": "fn main() {}",
 1323            }
 1324        }),
 1325    )
 1326    .await;
 1327
 1328    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 1329    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 1330    language_registry.add(rust_lang());
 1331
 1332    let mut tilde_lsp = language_registry.register_fake_lsp(
 1333        "Rust",
 1334        FakeLspAdapter {
 1335            name: "tilde_lsp",
 1336            ..Default::default()
 1337        },
 1338    );
 1339    cx.run_until_parked();
 1340
 1341    project
 1342        .update(cx, |project, cx| {
 1343            project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
 1344        })
 1345        .await
 1346        .unwrap();
 1347
 1348    let lsp_path = tilde_lsp.next().await.unwrap().binary.path;
 1349    let expected_path = paths::home_dir().join(".local/bin/rust-analyzer");
 1350    assert_eq!(
 1351        lsp_path, expected_path,
 1352        "Tilde path should expand to home directory"
 1353    );
 1354}
 1355
 1356#[gpui::test]
 1357async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
 1358    init_test(cx);
 1359
 1360    let fs = FakeFs::new(cx.executor());
 1361    fs.insert_tree(
 1362        path!("/the-root"),
 1363        json!({
 1364            ".gitignore": "target\n",
 1365            "Cargo.lock": "",
 1366            "src": {
 1367                "a.rs": "",
 1368                "b.rs": "",
 1369            },
 1370            "target": {
 1371                "x": {
 1372                    "out": {
 1373                        "x.rs": ""
 1374                    }
 1375                },
 1376                "y": {
 1377                    "out": {
 1378                        "y.rs": "",
 1379                    }
 1380                },
 1381                "z": {
 1382                    "out": {
 1383                        "z.rs": ""
 1384                    }
 1385                }
 1386            }
 1387        }),
 1388    )
 1389    .await;
 1390    fs.insert_tree(
 1391        path!("/the-registry"),
 1392        json!({
 1393            "dep1": {
 1394                "src": {
 1395                    "dep1.rs": "",
 1396                }
 1397            },
 1398            "dep2": {
 1399                "src": {
 1400                    "dep2.rs": "",
 1401                }
 1402            },
 1403        }),
 1404    )
 1405    .await;
 1406    fs.insert_tree(
 1407        path!("/the/stdlib"),
 1408        json!({
 1409            "LICENSE": "",
 1410            "src": {
 1411                "string.rs": "",
 1412            }
 1413        }),
 1414    )
 1415    .await;
 1416
 1417    let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
 1418    let (language_registry, lsp_store) = project.read_with(cx, |project, _| {
 1419        (project.languages().clone(), project.lsp_store())
 1420    });
 1421    language_registry.add(rust_lang());
 1422    let mut fake_servers = language_registry.register_fake_lsp(
 1423        "Rust",
 1424        FakeLspAdapter {
 1425            name: "the-language-server",
 1426            ..Default::default()
 1427        },
 1428    );
 1429
 1430    cx.executor().run_until_parked();
 1431
 1432    // Start the language server by opening a buffer with a compatible file extension.
 1433    project
 1434        .update(cx, |project, cx| {
 1435            project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
 1436        })
 1437        .await
 1438        .unwrap();
 1439
 1440    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
 1441    project.update(cx, |project, cx| {
 1442        let worktree = project.worktrees(cx).next().unwrap();
 1443        assert_eq!(
 1444            worktree
 1445                .read(cx)
 1446                .snapshot()
 1447                .entries(true, 0)
 1448                .map(|entry| (entry.path.as_unix_str(), entry.is_ignored))
 1449                .collect::<Vec<_>>(),
 1450            &[
 1451                ("", false),
 1452                (".gitignore", false),
 1453                ("Cargo.lock", false),
 1454                ("src", false),
 1455                ("src/a.rs", false),
 1456                ("src/b.rs", false),
 1457                ("target", true),
 1458            ]
 1459        );
 1460    });
 1461
 1462    let prev_read_dir_count = fs.read_dir_call_count();
 1463
 1464    let fake_server = fake_servers.next().await.unwrap();
 1465    let server_id = lsp_store.read_with(cx, |lsp_store, _| {
 1466        let (id, _) = lsp_store.language_server_statuses().next().unwrap();
 1467        id
 1468    });
 1469
 1470    // Simulate jumping to a definition in a dependency outside of the worktree.
 1471    let _out_of_worktree_buffer = project
 1472        .update(cx, |project, cx| {
 1473            project.open_local_buffer_via_lsp(
 1474                lsp::Uri::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(),
 1475                server_id,
 1476                cx,
 1477            )
 1478        })
 1479        .await
 1480        .unwrap();
 1481
 1482    // Keep track of the FS events reported to the language server.
 1483    let file_changes = Arc::new(Mutex::new(Vec::new()));
 1484    fake_server
 1485        .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
 1486            registrations: vec![lsp::Registration {
 1487                id: Default::default(),
 1488                method: "workspace/didChangeWatchedFiles".to_string(),
 1489                register_options: serde_json::to_value(
 1490                    lsp::DidChangeWatchedFilesRegistrationOptions {
 1491                        watchers: vec![
 1492                            lsp::FileSystemWatcher {
 1493                                glob_pattern: lsp::GlobPattern::String(
 1494                                    path!("/the-root/Cargo.toml").to_string(),
 1495                                ),
 1496                                kind: None,
 1497                            },
 1498                            lsp::FileSystemWatcher {
 1499                                glob_pattern: lsp::GlobPattern::String(
 1500                                    path!("/the-root/src/*.{rs,c}").to_string(),
 1501                                ),
 1502                                kind: None,
 1503                            },
 1504                            lsp::FileSystemWatcher {
 1505                                glob_pattern: lsp::GlobPattern::String(
 1506                                    path!("/the-root/target/y/**/*.rs").to_string(),
 1507                                ),
 1508                                kind: None,
 1509                            },
 1510                            lsp::FileSystemWatcher {
 1511                                glob_pattern: lsp::GlobPattern::String(
 1512                                    path!("/the/stdlib/src/**/*.rs").to_string(),
 1513                                ),
 1514                                kind: None,
 1515                            },
 1516                            lsp::FileSystemWatcher {
 1517                                glob_pattern: lsp::GlobPattern::String(
 1518                                    path!("**/Cargo.lock").to_string(),
 1519                                ),
 1520                                kind: None,
 1521                            },
 1522                        ],
 1523                    },
 1524                )
 1525                .ok(),
 1526            }],
 1527        })
 1528        .await
 1529        .into_response()
 1530        .unwrap();
 1531    fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
 1532        let file_changes = file_changes.clone();
 1533        move |params, _| {
 1534            let mut file_changes = file_changes.lock();
 1535            file_changes.extend(params.changes);
 1536            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
 1537        }
 1538    });
 1539
 1540    cx.executor().run_until_parked();
 1541    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
 1542    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
 1543
 1544    let mut new_watched_paths = fs.watched_paths();
 1545    new_watched_paths.retain(|path| {
 1546        !path.starts_with(config_dir()) && !path.starts_with(global_gitignore_path().unwrap())
 1547    });
 1548    assert_eq!(
 1549        &new_watched_paths,
 1550        &[
 1551            Path::new(path!("/the-root")),
 1552            Path::new(path!("/the-registry/dep1/src/dep1.rs")),
 1553            Path::new(path!("/the/stdlib/src"))
 1554        ]
 1555    );
 1556
 1557    // Now the language server has asked us to watch an ignored directory path,
 1558    // so we recursively load it.
 1559    project.update(cx, |project, cx| {
 1560        let worktree = project.visible_worktrees(cx).next().unwrap();
 1561        assert_eq!(
 1562            worktree
 1563                .read(cx)
 1564                .snapshot()
 1565                .entries(true, 0)
 1566                .map(|entry| (entry.path.as_unix_str(), entry.is_ignored))
 1567                .collect::<Vec<_>>(),
 1568            &[
 1569                ("", false),
 1570                (".gitignore", false),
 1571                ("Cargo.lock", false),
 1572                ("src", false),
 1573                ("src/a.rs", false),
 1574                ("src/b.rs", false),
 1575                ("target", true),
 1576                ("target/x", true),
 1577                ("target/y", true),
 1578                ("target/y/out", true),
 1579                ("target/y/out/y.rs", true),
 1580                ("target/z", true),
 1581            ]
 1582        );
 1583    });
 1584
 1585    // Perform some file system mutations, two of which match the watched patterns,
 1586    // and one of which does not.
 1587    fs.create_file(path!("/the-root/src/c.rs").as_ref(), Default::default())
 1588        .await
 1589        .unwrap();
 1590    fs.create_file(path!("/the-root/src/d.txt").as_ref(), Default::default())
 1591        .await
 1592        .unwrap();
 1593    fs.remove_file(path!("/the-root/src/b.rs").as_ref(), Default::default())
 1594        .await
 1595        .unwrap();
 1596    fs.create_file(
 1597        path!("/the-root/target/x/out/x2.rs").as_ref(),
 1598        Default::default(),
 1599    )
 1600    .await
 1601    .unwrap();
 1602    fs.create_file(
 1603        path!("/the-root/target/y/out/y2.rs").as_ref(),
 1604        Default::default(),
 1605    )
 1606    .await
 1607    .unwrap();
 1608    fs.save(
 1609        path!("/the-root/Cargo.lock").as_ref(),
 1610        &"".into(),
 1611        Default::default(),
 1612    )
 1613    .await
 1614    .unwrap();
 1615    fs.save(
 1616        path!("/the-stdlib/LICENSE").as_ref(),
 1617        &"".into(),
 1618        Default::default(),
 1619    )
 1620    .await
 1621    .unwrap();
 1622    fs.save(
 1623        path!("/the/stdlib/src/string.rs").as_ref(),
 1624        &"".into(),
 1625        Default::default(),
 1626    )
 1627    .await
 1628    .unwrap();
 1629
 1630    // The language server receives events for the FS mutations that match its watch patterns.
 1631    cx.executor().run_until_parked();
 1632    assert_eq!(
 1633        &*file_changes.lock(),
 1634        &[
 1635            lsp::FileEvent {
 1636                uri: lsp::Uri::from_file_path(path!("/the-root/Cargo.lock")).unwrap(),
 1637                typ: lsp::FileChangeType::CHANGED,
 1638            },
 1639            lsp::FileEvent {
 1640                uri: lsp::Uri::from_file_path(path!("/the-root/src/b.rs")).unwrap(),
 1641                typ: lsp::FileChangeType::DELETED,
 1642            },
 1643            lsp::FileEvent {
 1644                uri: lsp::Uri::from_file_path(path!("/the-root/src/c.rs")).unwrap(),
 1645                typ: lsp::FileChangeType::CREATED,
 1646            },
 1647            lsp::FileEvent {
 1648                uri: lsp::Uri::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(),
 1649                typ: lsp::FileChangeType::CREATED,
 1650            },
 1651            lsp::FileEvent {
 1652                uri: lsp::Uri::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(),
 1653                typ: lsp::FileChangeType::CHANGED,
 1654            },
 1655        ]
 1656    );
 1657}
 1658
 1659#[gpui::test]
 1660async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 1661    init_test(cx);
 1662
 1663    let fs = FakeFs::new(cx.executor());
 1664    fs.insert_tree(
 1665        path!("/dir"),
 1666        json!({
 1667            "a.rs": "let a = 1;",
 1668            "b.rs": "let b = 2;"
 1669        }),
 1670    )
 1671    .await;
 1672
 1673    let project = Project::test(
 1674        fs,
 1675        [path!("/dir/a.rs").as_ref(), path!("/dir/b.rs").as_ref()],
 1676        cx,
 1677    )
 1678    .await;
 1679    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 1680
 1681    let buffer_a = project
 1682        .update(cx, |project, cx| {
 1683            project.open_local_buffer(path!("/dir/a.rs"), cx)
 1684        })
 1685        .await
 1686        .unwrap();
 1687    let buffer_b = project
 1688        .update(cx, |project, cx| {
 1689            project.open_local_buffer(path!("/dir/b.rs"), cx)
 1690        })
 1691        .await
 1692        .unwrap();
 1693
 1694    lsp_store.update(cx, |lsp_store, cx| {
 1695        lsp_store
 1696            .update_diagnostics(
 1697                LanguageServerId(0),
 1698                lsp::PublishDiagnosticsParams {
 1699                    uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 1700                    version: None,
 1701                    diagnostics: vec![lsp::Diagnostic {
 1702                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 1703                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 1704                        message: "error 1".to_string(),
 1705                        ..Default::default()
 1706                    }],
 1707                },
 1708                None,
 1709                DiagnosticSourceKind::Pushed,
 1710                &[],
 1711                cx,
 1712            )
 1713            .unwrap();
 1714        lsp_store
 1715            .update_diagnostics(
 1716                LanguageServerId(0),
 1717                lsp::PublishDiagnosticsParams {
 1718                    uri: Uri::from_file_path(path!("/dir/b.rs")).unwrap(),
 1719                    version: None,
 1720                    diagnostics: vec![lsp::Diagnostic {
 1721                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 1722                        severity: Some(DiagnosticSeverity::WARNING),
 1723                        message: "error 2".to_string(),
 1724                        ..Default::default()
 1725                    }],
 1726                },
 1727                None,
 1728                DiagnosticSourceKind::Pushed,
 1729                &[],
 1730                cx,
 1731            )
 1732            .unwrap();
 1733    });
 1734
 1735    buffer_a.update(cx, |buffer, _| {
 1736        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 1737        assert_eq!(
 1738            chunks
 1739                .iter()
 1740                .map(|(s, d)| (s.as_str(), *d))
 1741                .collect::<Vec<_>>(),
 1742            &[
 1743                ("let ", None),
 1744                ("a", Some(DiagnosticSeverity::ERROR)),
 1745                (" = 1;", None),
 1746            ]
 1747        );
 1748    });
 1749    buffer_b.update(cx, |buffer, _| {
 1750        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 1751        assert_eq!(
 1752            chunks
 1753                .iter()
 1754                .map(|(s, d)| (s.as_str(), *d))
 1755                .collect::<Vec<_>>(),
 1756            &[
 1757                ("let ", None),
 1758                ("b", Some(DiagnosticSeverity::WARNING)),
 1759                (" = 2;", None),
 1760            ]
 1761        );
 1762    });
 1763}
 1764
 1765#[gpui::test]
 1766async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
 1767    init_test(cx);
 1768
 1769    let fs = FakeFs::new(cx.executor());
 1770    fs.insert_tree(
 1771        path!("/root"),
 1772        json!({
 1773            "dir": {
 1774                ".git": {
 1775                    "HEAD": "ref: refs/heads/main",
 1776                },
 1777                ".gitignore": "b.rs",
 1778                "a.rs": "let a = 1;",
 1779                "b.rs": "let b = 2;",
 1780            },
 1781            "other.rs": "let b = c;"
 1782        }),
 1783    )
 1784    .await;
 1785
 1786    let project = Project::test(fs, [path!("/root/dir").as_ref()], cx).await;
 1787    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 1788    let (worktree, _) = project
 1789        .update(cx, |project, cx| {
 1790            project.find_or_create_worktree(path!("/root/dir"), true, cx)
 1791        })
 1792        .await
 1793        .unwrap();
 1794    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 1795
 1796    let (worktree, _) = project
 1797        .update(cx, |project, cx| {
 1798            project.find_or_create_worktree(path!("/root/other.rs"), false, cx)
 1799        })
 1800        .await
 1801        .unwrap();
 1802    let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
 1803
 1804    let server_id = LanguageServerId(0);
 1805    lsp_store.update(cx, |lsp_store, cx| {
 1806        lsp_store
 1807            .update_diagnostics(
 1808                server_id,
 1809                lsp::PublishDiagnosticsParams {
 1810                    uri: Uri::from_file_path(path!("/root/dir/b.rs")).unwrap(),
 1811                    version: None,
 1812                    diagnostics: vec![lsp::Diagnostic {
 1813                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
 1814                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 1815                        message: "unused variable 'b'".to_string(),
 1816                        ..Default::default()
 1817                    }],
 1818                },
 1819                None,
 1820                DiagnosticSourceKind::Pushed,
 1821                &[],
 1822                cx,
 1823            )
 1824            .unwrap();
 1825        lsp_store
 1826            .update_diagnostics(
 1827                server_id,
 1828                lsp::PublishDiagnosticsParams {
 1829                    uri: Uri::from_file_path(path!("/root/other.rs")).unwrap(),
 1830                    version: None,
 1831                    diagnostics: vec![lsp::Diagnostic {
 1832                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
 1833                        severity: Some(lsp::DiagnosticSeverity::ERROR),
 1834                        message: "unknown variable 'c'".to_string(),
 1835                        ..Default::default()
 1836                    }],
 1837                },
 1838                None,
 1839                DiagnosticSourceKind::Pushed,
 1840                &[],
 1841                cx,
 1842            )
 1843            .unwrap();
 1844    });
 1845
 1846    let main_ignored_buffer = project
 1847        .update(cx, |project, cx| {
 1848            project.open_buffer((main_worktree_id, rel_path("b.rs")), cx)
 1849        })
 1850        .await
 1851        .unwrap();
 1852    main_ignored_buffer.update(cx, |buffer, _| {
 1853        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 1854        assert_eq!(
 1855            chunks
 1856                .iter()
 1857                .map(|(s, d)| (s.as_str(), *d))
 1858                .collect::<Vec<_>>(),
 1859            &[
 1860                ("let ", None),
 1861                ("b", Some(DiagnosticSeverity::ERROR)),
 1862                (" = 2;", None),
 1863            ],
 1864            "Gigitnored buffers should still get in-buffer diagnostics",
 1865        );
 1866    });
 1867    let other_buffer = project
 1868        .update(cx, |project, cx| {
 1869            project.open_buffer((other_worktree_id, rel_path("")), cx)
 1870        })
 1871        .await
 1872        .unwrap();
 1873    other_buffer.update(cx, |buffer, _| {
 1874        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 1875        assert_eq!(
 1876            chunks
 1877                .iter()
 1878                .map(|(s, d)| (s.as_str(), *d))
 1879                .collect::<Vec<_>>(),
 1880            &[
 1881                ("let b = ", None),
 1882                ("c", Some(DiagnosticSeverity::ERROR)),
 1883                (";", None),
 1884            ],
 1885            "Buffers from hidden projects should still get in-buffer diagnostics"
 1886        );
 1887    });
 1888
 1889    project.update(cx, |project, cx| {
 1890        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
 1891        assert_eq!(
 1892            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
 1893            vec![(
 1894                ProjectPath {
 1895                    worktree_id: main_worktree_id,
 1896                    path: rel_path("b.rs").into(),
 1897                },
 1898                server_id,
 1899                DiagnosticSummary {
 1900                    error_count: 1,
 1901                    warning_count: 0,
 1902                }
 1903            )]
 1904        );
 1905        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
 1906        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
 1907    });
 1908}
 1909
 1910#[gpui::test]
 1911async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
 1912    init_test(cx);
 1913
 1914    let progress_token = "the-progress-token";
 1915
 1916    let fs = FakeFs::new(cx.executor());
 1917    fs.insert_tree(
 1918        path!("/dir"),
 1919        json!({
 1920            "a.rs": "fn a() { A }",
 1921            "b.rs": "const y: i32 = 1",
 1922        }),
 1923    )
 1924    .await;
 1925
 1926    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 1927    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 1928
 1929    language_registry.add(rust_lang());
 1930    let mut fake_servers = language_registry.register_fake_lsp(
 1931        "Rust",
 1932        FakeLspAdapter {
 1933            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 1934            disk_based_diagnostics_sources: vec!["disk".into()],
 1935            ..Default::default()
 1936        },
 1937    );
 1938
 1939    let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
 1940
 1941    // Cause worktree to start the fake language server
 1942    let _ = project
 1943        .update(cx, |project, cx| {
 1944            project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
 1945        })
 1946        .await
 1947        .unwrap();
 1948
 1949    let mut events = cx.events(&project);
 1950
 1951    let fake_server = fake_servers.next().await.unwrap();
 1952    assert_eq!(
 1953        events.next().await.unwrap(),
 1954        Event::LanguageServerAdded(
 1955            LanguageServerId(0),
 1956            fake_server.server.name(),
 1957            Some(worktree_id)
 1958        ),
 1959    );
 1960
 1961    fake_server
 1962        .start_progress(format!("{}/0", progress_token))
 1963        .await;
 1964    assert_eq!(
 1965        events.next().await.unwrap(),
 1966        Event::DiskBasedDiagnosticsStarted {
 1967            language_server_id: LanguageServerId(0),
 1968        }
 1969    );
 1970
 1971    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 1972        uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 1973        version: None,
 1974        diagnostics: vec![lsp::Diagnostic {
 1975            range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 1976            severity: Some(lsp::DiagnosticSeverity::ERROR),
 1977            message: "undefined variable 'A'".to_string(),
 1978            ..Default::default()
 1979        }],
 1980    });
 1981    assert_eq!(
 1982        events.next().await.unwrap(),
 1983        Event::DiagnosticsUpdated {
 1984            language_server_id: LanguageServerId(0),
 1985            paths: vec![(worktree_id, rel_path("a.rs")).into()],
 1986        }
 1987    );
 1988
 1989    fake_server.end_progress(format!("{}/0", progress_token));
 1990    assert_eq!(
 1991        events.next().await.unwrap(),
 1992        Event::DiskBasedDiagnosticsFinished {
 1993            language_server_id: LanguageServerId(0)
 1994        }
 1995    );
 1996
 1997    let buffer = project
 1998        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
 1999        .await
 2000        .unwrap();
 2001
 2002    buffer.update(cx, |buffer, _| {
 2003        let snapshot = buffer.snapshot();
 2004        let diagnostics = snapshot
 2005            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
 2006            .collect::<Vec<_>>();
 2007        assert_eq!(
 2008            diagnostics,
 2009            &[DiagnosticEntryRef {
 2010                range: Point::new(0, 9)..Point::new(0, 10),
 2011                diagnostic: &Diagnostic {
 2012                    severity: lsp::DiagnosticSeverity::ERROR,
 2013                    message: "undefined variable 'A'".to_string(),
 2014                    group_id: 0,
 2015                    is_primary: true,
 2016                    source_kind: DiagnosticSourceKind::Pushed,
 2017                    ..Diagnostic::default()
 2018                }
 2019            }]
 2020        )
 2021    });
 2022
 2023    // Ensure publishing empty diagnostics twice only results in one update event.
 2024    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2025        uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2026        version: None,
 2027        diagnostics: Default::default(),
 2028    });
 2029    assert_eq!(
 2030        events.next().await.unwrap(),
 2031        Event::DiagnosticsUpdated {
 2032            language_server_id: LanguageServerId(0),
 2033            paths: vec![(worktree_id, rel_path("a.rs")).into()],
 2034        }
 2035    );
 2036
 2037    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2038        uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2039        version: None,
 2040        diagnostics: Default::default(),
 2041    });
 2042    cx.executor().run_until_parked();
 2043    assert_eq!(futures::poll!(events.next()), Poll::Pending);
 2044}
 2045
 2046#[gpui::test]
 2047async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
 2048    init_test(cx);
 2049
 2050    let progress_token = "the-progress-token";
 2051
 2052    let fs = FakeFs::new(cx.executor());
 2053    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
 2054
 2055    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2056
 2057    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2058    language_registry.add(rust_lang());
 2059    let mut fake_servers = language_registry.register_fake_lsp(
 2060        "Rust",
 2061        FakeLspAdapter {
 2062            name: "the-language-server",
 2063            disk_based_diagnostics_sources: vec!["disk".into()],
 2064            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 2065            ..FakeLspAdapter::default()
 2066        },
 2067    );
 2068
 2069    let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
 2070
 2071    let (buffer, _handle) = project
 2072        .update(cx, |project, cx| {
 2073            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2074        })
 2075        .await
 2076        .unwrap();
 2077    let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
 2078    // Simulate diagnostics starting to update.
 2079    let fake_server = fake_servers.next().await.unwrap();
 2080    fake_server.start_progress(progress_token).await;
 2081
 2082    // Restart the server before the diagnostics finish updating.
 2083    project.update(cx, |project, cx| {
 2084        project.restart_language_servers_for_buffers(vec![buffer], HashSet::default(), cx);
 2085    });
 2086    let mut events = cx.events(&project);
 2087
 2088    // Simulate the newly started server sending more diagnostics.
 2089    let fake_server = fake_servers.next().await.unwrap();
 2090    assert_eq!(
 2091        events.next().await.unwrap(),
 2092        Event::LanguageServerRemoved(LanguageServerId(0))
 2093    );
 2094    assert_eq!(
 2095        events.next().await.unwrap(),
 2096        Event::LanguageServerAdded(
 2097            LanguageServerId(1),
 2098            fake_server.server.name(),
 2099            Some(worktree_id)
 2100        )
 2101    );
 2102    fake_server.start_progress(progress_token).await;
 2103    assert_eq!(
 2104        events.next().await.unwrap(),
 2105        Event::LanguageServerBufferRegistered {
 2106            server_id: LanguageServerId(1),
 2107            buffer_id,
 2108            buffer_abs_path: PathBuf::from(path!("/dir/a.rs")),
 2109            name: Some(fake_server.server.name())
 2110        }
 2111    );
 2112    assert_eq!(
 2113        events.next().await.unwrap(),
 2114        Event::DiskBasedDiagnosticsStarted {
 2115            language_server_id: LanguageServerId(1)
 2116        }
 2117    );
 2118    project.update(cx, |project, cx| {
 2119        assert_eq!(
 2120            project
 2121                .language_servers_running_disk_based_diagnostics(cx)
 2122                .collect::<Vec<_>>(),
 2123            [LanguageServerId(1)]
 2124        );
 2125    });
 2126
 2127    // All diagnostics are considered done, despite the old server's diagnostic
 2128    // task never completing.
 2129    fake_server.end_progress(progress_token);
 2130    assert_eq!(
 2131        events.next().await.unwrap(),
 2132        Event::DiskBasedDiagnosticsFinished {
 2133            language_server_id: LanguageServerId(1)
 2134        }
 2135    );
 2136    project.update(cx, |project, cx| {
 2137        assert_eq!(
 2138            project
 2139                .language_servers_running_disk_based_diagnostics(cx)
 2140                .collect::<Vec<_>>(),
 2141            [] as [language::LanguageServerId; 0]
 2142        );
 2143    });
 2144}
 2145
 2146#[gpui::test]
 2147async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
 2148    init_test(cx);
 2149
 2150    let fs = FakeFs::new(cx.executor());
 2151    fs.insert_tree(path!("/dir"), json!({ "a.rs": "x" })).await;
 2152
 2153    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2154
 2155    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2156    language_registry.add(rust_lang());
 2157    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
 2158
 2159    let (buffer, _) = project
 2160        .update(cx, |project, cx| {
 2161            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2162        })
 2163        .await
 2164        .unwrap();
 2165
 2166    // Publish diagnostics
 2167    let fake_server = fake_servers.next().await.unwrap();
 2168    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2169        uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2170        version: None,
 2171        diagnostics: vec![lsp::Diagnostic {
 2172            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 2173            severity: Some(lsp::DiagnosticSeverity::ERROR),
 2174            message: "the message".to_string(),
 2175            ..Default::default()
 2176        }],
 2177    });
 2178
 2179    cx.executor().run_until_parked();
 2180    buffer.update(cx, |buffer, _| {
 2181        assert_eq!(
 2182            buffer
 2183                .snapshot()
 2184                .diagnostics_in_range::<_, usize>(0..1, false)
 2185                .map(|entry| entry.diagnostic.message.clone())
 2186                .collect::<Vec<_>>(),
 2187            ["the message".to_string()]
 2188        );
 2189    });
 2190    project.update(cx, |project, cx| {
 2191        assert_eq!(
 2192            project.diagnostic_summary(false, cx),
 2193            DiagnosticSummary {
 2194                error_count: 1,
 2195                warning_count: 0,
 2196            }
 2197        );
 2198    });
 2199
 2200    project.update(cx, |project, cx| {
 2201        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
 2202    });
 2203
 2204    // The diagnostics are cleared.
 2205    cx.executor().run_until_parked();
 2206    buffer.update(cx, |buffer, _| {
 2207        assert_eq!(
 2208            buffer
 2209                .snapshot()
 2210                .diagnostics_in_range::<_, usize>(0..1, false)
 2211                .map(|entry| entry.diagnostic.message.clone())
 2212                .collect::<Vec<_>>(),
 2213            Vec::<String>::new(),
 2214        );
 2215    });
 2216    project.update(cx, |project, cx| {
 2217        assert_eq!(
 2218            project.diagnostic_summary(false, cx),
 2219            DiagnosticSummary {
 2220                error_count: 0,
 2221                warning_count: 0,
 2222            }
 2223        );
 2224    });
 2225}
 2226
 2227#[gpui::test]
 2228async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
 2229    init_test(cx);
 2230
 2231    let fs = FakeFs::new(cx.executor());
 2232    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
 2233
 2234    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2235    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2236
 2237    language_registry.add(rust_lang());
 2238    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
 2239
 2240    let (buffer, _handle) = project
 2241        .update(cx, |project, cx| {
 2242            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2243        })
 2244        .await
 2245        .unwrap();
 2246
 2247    // Before restarting the server, report diagnostics with an unknown buffer version.
 2248    let fake_server = fake_servers.next().await.unwrap();
 2249    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2250        uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2251        version: Some(10000),
 2252        diagnostics: Vec::new(),
 2253    });
 2254    cx.executor().run_until_parked();
 2255    project.update(cx, |project, cx| {
 2256        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx);
 2257    });
 2258
 2259    let mut fake_server = fake_servers.next().await.unwrap();
 2260    let notification = fake_server
 2261        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2262        .await
 2263        .text_document;
 2264    assert_eq!(notification.version, 0);
 2265}
 2266
 2267#[gpui::test]
 2268async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
 2269    init_test(cx);
 2270
 2271    let progress_token = "the-progress-token";
 2272
 2273    let fs = FakeFs::new(cx.executor());
 2274    fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
 2275
 2276    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2277
 2278    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2279    language_registry.add(rust_lang());
 2280    let mut fake_servers = language_registry.register_fake_lsp(
 2281        "Rust",
 2282        FakeLspAdapter {
 2283            name: "the-language-server",
 2284            disk_based_diagnostics_sources: vec!["disk".into()],
 2285            disk_based_diagnostics_progress_token: Some(progress_token.into()),
 2286            ..Default::default()
 2287        },
 2288    );
 2289
 2290    let (buffer, _handle) = project
 2291        .update(cx, |project, cx| {
 2292            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2293        })
 2294        .await
 2295        .unwrap();
 2296
 2297    // Simulate diagnostics starting to update.
 2298    let mut fake_server = fake_servers.next().await.unwrap();
 2299    fake_server
 2300        .start_progress_with(
 2301            "another-token",
 2302            lsp::WorkDoneProgressBegin {
 2303                cancellable: Some(false),
 2304                ..Default::default()
 2305            },
 2306        )
 2307        .await;
 2308    fake_server
 2309        .start_progress_with(
 2310            progress_token,
 2311            lsp::WorkDoneProgressBegin {
 2312                cancellable: Some(true),
 2313                ..Default::default()
 2314            },
 2315        )
 2316        .await;
 2317    cx.executor().run_until_parked();
 2318
 2319    project.update(cx, |project, cx| {
 2320        project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
 2321    });
 2322
 2323    let cancel_notification = fake_server
 2324        .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
 2325        .await;
 2326    assert_eq!(
 2327        cancel_notification.token,
 2328        NumberOrString::String(progress_token.into())
 2329    );
 2330}
 2331
 2332#[gpui::test]
 2333async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
 2334    init_test(cx);
 2335
 2336    let fs = FakeFs::new(cx.executor());
 2337    fs.insert_tree(path!("/dir"), json!({ "a.rs": "", "b.js": "" }))
 2338        .await;
 2339
 2340    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2341    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2342
 2343    let mut fake_rust_servers = language_registry.register_fake_lsp(
 2344        "Rust",
 2345        FakeLspAdapter {
 2346            name: "rust-lsp",
 2347            ..Default::default()
 2348        },
 2349    );
 2350    let mut fake_js_servers = language_registry.register_fake_lsp(
 2351        "JavaScript",
 2352        FakeLspAdapter {
 2353            name: "js-lsp",
 2354            ..Default::default()
 2355        },
 2356    );
 2357    language_registry.add(rust_lang());
 2358    language_registry.add(js_lang());
 2359
 2360    let _rs_buffer = project
 2361        .update(cx, |project, cx| {
 2362            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2363        })
 2364        .await
 2365        .unwrap();
 2366    let _js_buffer = project
 2367        .update(cx, |project, cx| {
 2368            project.open_local_buffer_with_lsp(path!("/dir/b.js"), cx)
 2369        })
 2370        .await
 2371        .unwrap();
 2372
 2373    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
 2374    assert_eq!(
 2375        fake_rust_server_1
 2376            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2377            .await
 2378            .text_document
 2379            .uri
 2380            .as_str(),
 2381        uri!("file:///dir/a.rs")
 2382    );
 2383
 2384    let mut fake_js_server = fake_js_servers.next().await.unwrap();
 2385    assert_eq!(
 2386        fake_js_server
 2387            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2388            .await
 2389            .text_document
 2390            .uri
 2391            .as_str(),
 2392        uri!("file:///dir/b.js")
 2393    );
 2394
 2395    // Disable Rust language server, ensuring only that server gets stopped.
 2396    cx.update(|cx| {
 2397        SettingsStore::update_global(cx, |settings, cx| {
 2398            settings.update_user_settings(cx, |settings| {
 2399                settings.languages_mut().insert(
 2400                    "Rust".into(),
 2401                    LanguageSettingsContent {
 2402                        enable_language_server: Some(false),
 2403                        ..Default::default()
 2404                    },
 2405                );
 2406            });
 2407        })
 2408    });
 2409    fake_rust_server_1
 2410        .receive_notification::<lsp::notification::Exit>()
 2411        .await;
 2412
 2413    // Enable Rust and disable JavaScript language servers, ensuring that the
 2414    // former gets started again and that the latter stops.
 2415    cx.update(|cx| {
 2416        SettingsStore::update_global(cx, |settings, cx| {
 2417            settings.update_user_settings(cx, |settings| {
 2418                settings.languages_mut().insert(
 2419                    "Rust".into(),
 2420                    LanguageSettingsContent {
 2421                        enable_language_server: Some(true),
 2422                        ..Default::default()
 2423                    },
 2424                );
 2425                settings.languages_mut().insert(
 2426                    "JavaScript".into(),
 2427                    LanguageSettingsContent {
 2428                        enable_language_server: Some(false),
 2429                        ..Default::default()
 2430                    },
 2431                );
 2432            });
 2433        })
 2434    });
 2435    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
 2436    assert_eq!(
 2437        fake_rust_server_2
 2438            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2439            .await
 2440            .text_document
 2441            .uri
 2442            .as_str(),
 2443        uri!("file:///dir/a.rs")
 2444    );
 2445    fake_js_server
 2446        .receive_notification::<lsp::notification::Exit>()
 2447        .await;
 2448}
 2449
 2450#[gpui::test(iterations = 3)]
 2451async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
 2452    init_test(cx);
 2453
 2454    let text = "
 2455        fn a() { A }
 2456        fn b() { BB }
 2457        fn c() { CCC }
 2458    "
 2459    .unindent();
 2460
 2461    let fs = FakeFs::new(cx.executor());
 2462    fs.insert_tree(path!("/dir"), json!({ "a.rs": text })).await;
 2463
 2464    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2465    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2466
 2467    language_registry.add(rust_lang());
 2468    let mut fake_servers = language_registry.register_fake_lsp(
 2469        "Rust",
 2470        FakeLspAdapter {
 2471            disk_based_diagnostics_sources: vec!["disk".into()],
 2472            ..Default::default()
 2473        },
 2474    );
 2475
 2476    let buffer = project
 2477        .update(cx, |project, cx| {
 2478            project.open_local_buffer(path!("/dir/a.rs"), cx)
 2479        })
 2480        .await
 2481        .unwrap();
 2482
 2483    let _handle = project.update(cx, |project, cx| {
 2484        project.register_buffer_with_language_servers(&buffer, cx)
 2485    });
 2486
 2487    let mut fake_server = fake_servers.next().await.unwrap();
 2488    let open_notification = fake_server
 2489        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2490        .await;
 2491
 2492    // Edit the buffer, moving the content down
 2493    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
 2494    let change_notification_1 = fake_server
 2495        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 2496        .await;
 2497    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
 2498
 2499    // Report some diagnostics for the initial version of the buffer
 2500    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2501        uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2502        version: Some(open_notification.text_document.version),
 2503        diagnostics: vec![
 2504            lsp::Diagnostic {
 2505                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 2506                severity: Some(DiagnosticSeverity::ERROR),
 2507                message: "undefined variable 'A'".to_string(),
 2508                source: Some("disk".to_string()),
 2509                ..Default::default()
 2510            },
 2511            lsp::Diagnostic {
 2512                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
 2513                severity: Some(DiagnosticSeverity::ERROR),
 2514                message: "undefined variable 'BB'".to_string(),
 2515                source: Some("disk".to_string()),
 2516                ..Default::default()
 2517            },
 2518            lsp::Diagnostic {
 2519                range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
 2520                severity: Some(DiagnosticSeverity::ERROR),
 2521                source: Some("disk".to_string()),
 2522                message: "undefined variable 'CCC'".to_string(),
 2523                ..Default::default()
 2524            },
 2525        ],
 2526    });
 2527
 2528    // The diagnostics have moved down since they were created.
 2529    cx.executor().run_until_parked();
 2530    buffer.update(cx, |buffer, _| {
 2531        assert_eq!(
 2532            buffer
 2533                .snapshot()
 2534                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
 2535                .collect::<Vec<_>>(),
 2536            &[
 2537                DiagnosticEntry {
 2538                    range: Point::new(3, 9)..Point::new(3, 11),
 2539                    diagnostic: Diagnostic {
 2540                        source: Some("disk".into()),
 2541                        severity: DiagnosticSeverity::ERROR,
 2542                        message: "undefined variable 'BB'".to_string(),
 2543                        is_disk_based: true,
 2544                        group_id: 1,
 2545                        is_primary: true,
 2546                        source_kind: DiagnosticSourceKind::Pushed,
 2547                        ..Diagnostic::default()
 2548                    },
 2549                },
 2550                DiagnosticEntry {
 2551                    range: Point::new(4, 9)..Point::new(4, 12),
 2552                    diagnostic: Diagnostic {
 2553                        source: Some("disk".into()),
 2554                        severity: DiagnosticSeverity::ERROR,
 2555                        message: "undefined variable 'CCC'".to_string(),
 2556                        is_disk_based: true,
 2557                        group_id: 2,
 2558                        is_primary: true,
 2559                        source_kind: DiagnosticSourceKind::Pushed,
 2560                        ..Diagnostic::default()
 2561                    }
 2562                }
 2563            ]
 2564        );
 2565        assert_eq!(
 2566            chunks_with_diagnostics(buffer, 0..buffer.len()),
 2567            [
 2568                ("\n\nfn a() { ".to_string(), None),
 2569                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 2570                (" }\nfn b() { ".to_string(), None),
 2571                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
 2572                (" }\nfn c() { ".to_string(), None),
 2573                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
 2574                (" }\n".to_string(), None),
 2575            ]
 2576        );
 2577        assert_eq!(
 2578            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
 2579            [
 2580                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
 2581                (" }\nfn c() { ".to_string(), None),
 2582                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
 2583            ]
 2584        );
 2585    });
 2586
 2587    // Ensure overlapping diagnostics are highlighted correctly.
 2588    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2589        uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2590        version: Some(open_notification.text_document.version),
 2591        diagnostics: vec![
 2592            lsp::Diagnostic {
 2593                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 2594                severity: Some(DiagnosticSeverity::ERROR),
 2595                message: "undefined variable 'A'".to_string(),
 2596                source: Some("disk".to_string()),
 2597                ..Default::default()
 2598            },
 2599            lsp::Diagnostic {
 2600                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
 2601                severity: Some(DiagnosticSeverity::WARNING),
 2602                message: "unreachable statement".to_string(),
 2603                source: Some("disk".to_string()),
 2604                ..Default::default()
 2605            },
 2606        ],
 2607    });
 2608
 2609    cx.executor().run_until_parked();
 2610    buffer.update(cx, |buffer, _| {
 2611        assert_eq!(
 2612            buffer
 2613                .snapshot()
 2614                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
 2615                .collect::<Vec<_>>(),
 2616            &[
 2617                DiagnosticEntry {
 2618                    range: Point::new(2, 9)..Point::new(2, 12),
 2619                    diagnostic: Diagnostic {
 2620                        source: Some("disk".into()),
 2621                        severity: DiagnosticSeverity::WARNING,
 2622                        message: "unreachable statement".to_string(),
 2623                        is_disk_based: true,
 2624                        group_id: 4,
 2625                        is_primary: true,
 2626                        source_kind: DiagnosticSourceKind::Pushed,
 2627                        ..Diagnostic::default()
 2628                    }
 2629                },
 2630                DiagnosticEntry {
 2631                    range: Point::new(2, 9)..Point::new(2, 10),
 2632                    diagnostic: Diagnostic {
 2633                        source: Some("disk".into()),
 2634                        severity: DiagnosticSeverity::ERROR,
 2635                        message: "undefined variable 'A'".to_string(),
 2636                        is_disk_based: true,
 2637                        group_id: 3,
 2638                        is_primary: true,
 2639                        source_kind: DiagnosticSourceKind::Pushed,
 2640                        ..Diagnostic::default()
 2641                    },
 2642                }
 2643            ]
 2644        );
 2645        assert_eq!(
 2646            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
 2647            [
 2648                ("fn a() { ".to_string(), None),
 2649                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
 2650                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 2651                ("\n".to_string(), None),
 2652            ]
 2653        );
 2654        assert_eq!(
 2655            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
 2656            [
 2657                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
 2658                ("\n".to_string(), None),
 2659            ]
 2660        );
 2661    });
 2662
 2663    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
 2664    // changes since the last save.
 2665    buffer.update(cx, |buffer, cx| {
 2666        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
 2667        buffer.edit(
 2668            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
 2669            None,
 2670            cx,
 2671        );
 2672        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
 2673    });
 2674    let change_notification_2 = fake_server
 2675        .receive_notification::<lsp::notification::DidChangeTextDocument>()
 2676        .await;
 2677    assert!(
 2678        change_notification_2.text_document.version > change_notification_1.text_document.version
 2679    );
 2680
 2681    // Handle out-of-order diagnostics
 2682    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
 2683        uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 2684        version: Some(change_notification_2.text_document.version),
 2685        diagnostics: vec![
 2686            lsp::Diagnostic {
 2687                range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
 2688                severity: Some(DiagnosticSeverity::ERROR),
 2689                message: "undefined variable 'BB'".to_string(),
 2690                source: Some("disk".to_string()),
 2691                ..Default::default()
 2692            },
 2693            lsp::Diagnostic {
 2694                range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 2695                severity: Some(DiagnosticSeverity::WARNING),
 2696                message: "undefined variable 'A'".to_string(),
 2697                source: Some("disk".to_string()),
 2698                ..Default::default()
 2699            },
 2700        ],
 2701    });
 2702
 2703    cx.executor().run_until_parked();
 2704    buffer.update(cx, |buffer, _| {
 2705        assert_eq!(
 2706            buffer
 2707                .snapshot()
 2708                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
 2709                .collect::<Vec<_>>(),
 2710            &[
 2711                DiagnosticEntry {
 2712                    range: Point::new(2, 21)..Point::new(2, 22),
 2713                    diagnostic: Diagnostic {
 2714                        source: Some("disk".into()),
 2715                        severity: DiagnosticSeverity::WARNING,
 2716                        message: "undefined variable 'A'".to_string(),
 2717                        is_disk_based: true,
 2718                        group_id: 6,
 2719                        is_primary: true,
 2720                        source_kind: DiagnosticSourceKind::Pushed,
 2721                        ..Diagnostic::default()
 2722                    }
 2723                },
 2724                DiagnosticEntry {
 2725                    range: Point::new(3, 9)..Point::new(3, 14),
 2726                    diagnostic: Diagnostic {
 2727                        source: Some("disk".into()),
 2728                        severity: DiagnosticSeverity::ERROR,
 2729                        message: "undefined variable 'BB'".to_string(),
 2730                        is_disk_based: true,
 2731                        group_id: 5,
 2732                        is_primary: true,
 2733                        source_kind: DiagnosticSourceKind::Pushed,
 2734                        ..Diagnostic::default()
 2735                    },
 2736                }
 2737            ]
 2738        );
 2739    });
 2740}
 2741
 2742#[gpui::test]
 2743async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
 2744    init_test(cx);
 2745
 2746    let text = concat!(
 2747        "let one = ;\n", //
 2748        "let two = \n",
 2749        "let three = 3;\n",
 2750    );
 2751
 2752    let fs = FakeFs::new(cx.executor());
 2753    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 2754
 2755    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 2756    let buffer = project
 2757        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
 2758        .await
 2759        .unwrap();
 2760
 2761    project.update(cx, |project, cx| {
 2762        project.lsp_store.update(cx, |lsp_store, cx| {
 2763            lsp_store
 2764                .update_diagnostic_entries(
 2765                    LanguageServerId(0),
 2766                    PathBuf::from("/dir/a.rs"),
 2767                    None,
 2768                    None,
 2769                    vec![
 2770                        DiagnosticEntry {
 2771                            range: Unclipped(PointUtf16::new(0, 10))
 2772                                ..Unclipped(PointUtf16::new(0, 10)),
 2773                            diagnostic: Diagnostic {
 2774                                severity: DiagnosticSeverity::ERROR,
 2775                                message: "syntax error 1".to_string(),
 2776                                source_kind: DiagnosticSourceKind::Pushed,
 2777                                ..Diagnostic::default()
 2778                            },
 2779                        },
 2780                        DiagnosticEntry {
 2781                            range: Unclipped(PointUtf16::new(1, 10))
 2782                                ..Unclipped(PointUtf16::new(1, 10)),
 2783                            diagnostic: Diagnostic {
 2784                                severity: DiagnosticSeverity::ERROR,
 2785                                message: "syntax error 2".to_string(),
 2786                                source_kind: DiagnosticSourceKind::Pushed,
 2787                                ..Diagnostic::default()
 2788                            },
 2789                        },
 2790                    ],
 2791                    cx,
 2792                )
 2793                .unwrap();
 2794        })
 2795    });
 2796
 2797    // An empty range is extended forward to include the following character.
 2798    // At the end of a line, an empty range is extended backward to include
 2799    // the preceding character.
 2800    buffer.update(cx, |buffer, _| {
 2801        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
 2802        assert_eq!(
 2803            chunks
 2804                .iter()
 2805                .map(|(s, d)| (s.as_str(), *d))
 2806                .collect::<Vec<_>>(),
 2807            &[
 2808                ("let one = ", None),
 2809                (";", Some(DiagnosticSeverity::ERROR)),
 2810                ("\nlet two =", None),
 2811                (" ", Some(DiagnosticSeverity::ERROR)),
 2812                ("\nlet three = 3;\n", None)
 2813            ]
 2814        );
 2815    });
 2816}
 2817
 2818#[gpui::test]
 2819async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
 2820    init_test(cx);
 2821
 2822    let fs = FakeFs::new(cx.executor());
 2823    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
 2824        .await;
 2825
 2826    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
 2827    let lsp_store = project.read_with(cx, |project, _| project.lsp_store.clone());
 2828
 2829    lsp_store.update(cx, |lsp_store, cx| {
 2830        lsp_store
 2831            .update_diagnostic_entries(
 2832                LanguageServerId(0),
 2833                Path::new("/dir/a.rs").to_owned(),
 2834                None,
 2835                None,
 2836                vec![DiagnosticEntry {
 2837                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
 2838                    diagnostic: Diagnostic {
 2839                        severity: DiagnosticSeverity::ERROR,
 2840                        is_primary: true,
 2841                        message: "syntax error a1".to_string(),
 2842                        source_kind: DiagnosticSourceKind::Pushed,
 2843                        ..Diagnostic::default()
 2844                    },
 2845                }],
 2846                cx,
 2847            )
 2848            .unwrap();
 2849        lsp_store
 2850            .update_diagnostic_entries(
 2851                LanguageServerId(1),
 2852                Path::new("/dir/a.rs").to_owned(),
 2853                None,
 2854                None,
 2855                vec![DiagnosticEntry {
 2856                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
 2857                    diagnostic: Diagnostic {
 2858                        severity: DiagnosticSeverity::ERROR,
 2859                        is_primary: true,
 2860                        message: "syntax error b1".to_string(),
 2861                        source_kind: DiagnosticSourceKind::Pushed,
 2862                        ..Diagnostic::default()
 2863                    },
 2864                }],
 2865                cx,
 2866            )
 2867            .unwrap();
 2868
 2869        assert_eq!(
 2870            lsp_store.diagnostic_summary(false, cx),
 2871            DiagnosticSummary {
 2872                error_count: 2,
 2873                warning_count: 0,
 2874            }
 2875        );
 2876    });
 2877}
 2878
 2879#[gpui::test]
 2880async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
 2881    init_test(cx);
 2882
 2883    let text = "
 2884        fn a() {
 2885            f1();
 2886        }
 2887        fn b() {
 2888            f2();
 2889        }
 2890        fn c() {
 2891            f3();
 2892        }
 2893    "
 2894    .unindent();
 2895
 2896    let fs = FakeFs::new(cx.executor());
 2897    fs.insert_tree(
 2898        path!("/dir"),
 2899        json!({
 2900            "a.rs": text.clone(),
 2901        }),
 2902    )
 2903    .await;
 2904
 2905    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 2906    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 2907
 2908    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 2909    language_registry.add(rust_lang());
 2910    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
 2911
 2912    let (buffer, _handle) = project
 2913        .update(cx, |project, cx| {
 2914            project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
 2915        })
 2916        .await
 2917        .unwrap();
 2918
 2919    let mut fake_server = fake_servers.next().await.unwrap();
 2920    let lsp_document_version = fake_server
 2921        .receive_notification::<lsp::notification::DidOpenTextDocument>()
 2922        .await
 2923        .text_document
 2924        .version;
 2925
 2926    // Simulate editing the buffer after the language server computes some edits.
 2927    buffer.update(cx, |buffer, cx| {
 2928        buffer.edit(
 2929            [(
 2930                Point::new(0, 0)..Point::new(0, 0),
 2931                "// above first function\n",
 2932            )],
 2933            None,
 2934            cx,
 2935        );
 2936        buffer.edit(
 2937            [(
 2938                Point::new(2, 0)..Point::new(2, 0),
 2939                "    // inside first function\n",
 2940            )],
 2941            None,
 2942            cx,
 2943        );
 2944        buffer.edit(
 2945            [(
 2946                Point::new(6, 4)..Point::new(6, 4),
 2947                "// inside second function ",
 2948            )],
 2949            None,
 2950            cx,
 2951        );
 2952
 2953        assert_eq!(
 2954            buffer.text(),
 2955            "
 2956                // above first function
 2957                fn a() {
 2958                    // inside first function
 2959                    f1();
 2960                }
 2961                fn b() {
 2962                    // inside second function f2();
 2963                }
 2964                fn c() {
 2965                    f3();
 2966                }
 2967            "
 2968            .unindent()
 2969        );
 2970    });
 2971
 2972    let edits = lsp_store
 2973        .update(cx, |lsp_store, cx| {
 2974            lsp_store.as_local_mut().unwrap().edits_from_lsp(
 2975                &buffer,
 2976                vec![
 2977                    // replace body of first function
 2978                    lsp::TextEdit {
 2979                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
 2980                        new_text: "
 2981                            fn a() {
 2982                                f10();
 2983                            }
 2984                            "
 2985                        .unindent(),
 2986                    },
 2987                    // edit inside second function
 2988                    lsp::TextEdit {
 2989                        range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
 2990                        new_text: "00".into(),
 2991                    },
 2992                    // edit inside third function via two distinct edits
 2993                    lsp::TextEdit {
 2994                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
 2995                        new_text: "4000".into(),
 2996                    },
 2997                    lsp::TextEdit {
 2998                        range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
 2999                        new_text: "".into(),
 3000                    },
 3001                ],
 3002                LanguageServerId(0),
 3003                Some(lsp_document_version),
 3004                cx,
 3005            )
 3006        })
 3007        .await
 3008        .unwrap();
 3009
 3010    buffer.update(cx, |buffer, cx| {
 3011        for (range, new_text) in edits {
 3012            buffer.edit([(range, new_text)], None, cx);
 3013        }
 3014        assert_eq!(
 3015            buffer.text(),
 3016            "
 3017                // above first function
 3018                fn a() {
 3019                    // inside first function
 3020                    f10();
 3021                }
 3022                fn b() {
 3023                    // inside second function f200();
 3024                }
 3025                fn c() {
 3026                    f4000();
 3027                }
 3028                "
 3029            .unindent()
 3030        );
 3031    });
 3032}
 3033
 3034#[gpui::test]
 3035async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
 3036    init_test(cx);
 3037
 3038    let text = "
 3039        use a::b;
 3040        use a::c;
 3041
 3042        fn f() {
 3043            b();
 3044            c();
 3045        }
 3046    "
 3047    .unindent();
 3048
 3049    let fs = FakeFs::new(cx.executor());
 3050    fs.insert_tree(
 3051        path!("/dir"),
 3052        json!({
 3053            "a.rs": text.clone(),
 3054        }),
 3055    )
 3056    .await;
 3057
 3058    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3059    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 3060    let buffer = project
 3061        .update(cx, |project, cx| {
 3062            project.open_local_buffer(path!("/dir/a.rs"), cx)
 3063        })
 3064        .await
 3065        .unwrap();
 3066
 3067    // Simulate the language server sending us a small edit in the form of a very large diff.
 3068    // Rust-analyzer does this when performing a merge-imports code action.
 3069    let edits = lsp_store
 3070        .update(cx, |lsp_store, cx| {
 3071            lsp_store.as_local_mut().unwrap().edits_from_lsp(
 3072                &buffer,
 3073                [
 3074                    // Replace the first use statement without editing the semicolon.
 3075                    lsp::TextEdit {
 3076                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
 3077                        new_text: "a::{b, c}".into(),
 3078                    },
 3079                    // Reinsert the remainder of the file between the semicolon and the final
 3080                    // newline of the file.
 3081                    lsp::TextEdit {
 3082                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
 3083                        new_text: "\n\n".into(),
 3084                    },
 3085                    lsp::TextEdit {
 3086                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
 3087                        new_text: "
 3088                            fn f() {
 3089                                b();
 3090                                c();
 3091                            }"
 3092                        .unindent(),
 3093                    },
 3094                    // Delete everything after the first newline of the file.
 3095                    lsp::TextEdit {
 3096                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
 3097                        new_text: "".into(),
 3098                    },
 3099                ],
 3100                LanguageServerId(0),
 3101                None,
 3102                cx,
 3103            )
 3104        })
 3105        .await
 3106        .unwrap();
 3107
 3108    buffer.update(cx, |buffer, cx| {
 3109        let edits = edits
 3110            .into_iter()
 3111            .map(|(range, text)| {
 3112                (
 3113                    range.start.to_point(buffer)..range.end.to_point(buffer),
 3114                    text,
 3115                )
 3116            })
 3117            .collect::<Vec<_>>();
 3118
 3119        assert_eq!(
 3120            edits,
 3121            [
 3122                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
 3123                (Point::new(1, 0)..Point::new(2, 0), "".into())
 3124            ]
 3125        );
 3126
 3127        for (range, new_text) in edits {
 3128            buffer.edit([(range, new_text)], None, cx);
 3129        }
 3130        assert_eq!(
 3131            buffer.text(),
 3132            "
 3133                use a::{b, c};
 3134
 3135                fn f() {
 3136                    b();
 3137                    c();
 3138                }
 3139            "
 3140            .unindent()
 3141        );
 3142    });
 3143}
 3144
 3145#[gpui::test]
 3146async fn test_edits_from_lsp_with_replacement_followed_by_adjacent_insertion(
 3147    cx: &mut gpui::TestAppContext,
 3148) {
 3149    init_test(cx);
 3150
 3151    let text = "Path()";
 3152
 3153    let fs = FakeFs::new(cx.executor());
 3154    fs.insert_tree(
 3155        path!("/dir"),
 3156        json!({
 3157            "a.rs": text
 3158        }),
 3159    )
 3160    .await;
 3161
 3162    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3163    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 3164    let buffer = project
 3165        .update(cx, |project, cx| {
 3166            project.open_local_buffer(path!("/dir/a.rs"), cx)
 3167        })
 3168        .await
 3169        .unwrap();
 3170
 3171    // Simulate the language server sending us a pair of edits at the same location,
 3172    // with an insertion following a replacement (which violates the LSP spec).
 3173    let edits = lsp_store
 3174        .update(cx, |lsp_store, cx| {
 3175            lsp_store.as_local_mut().unwrap().edits_from_lsp(
 3176                &buffer,
 3177                [
 3178                    lsp::TextEdit {
 3179                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
 3180                        new_text: "Path".into(),
 3181                    },
 3182                    lsp::TextEdit {
 3183                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
 3184                        new_text: "from path import Path\n\n\n".into(),
 3185                    },
 3186                ],
 3187                LanguageServerId(0),
 3188                None,
 3189                cx,
 3190            )
 3191        })
 3192        .await
 3193        .unwrap();
 3194
 3195    buffer.update(cx, |buffer, cx| {
 3196        buffer.edit(edits, None, cx);
 3197        assert_eq!(buffer.text(), "from path import Path\n\n\nPath()")
 3198    });
 3199}
 3200
 3201#[gpui::test]
 3202async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
 3203    init_test(cx);
 3204
 3205    let text = "
 3206        use a::b;
 3207        use a::c;
 3208
 3209        fn f() {
 3210            b();
 3211            c();
 3212        }
 3213    "
 3214    .unindent();
 3215
 3216    let fs = FakeFs::new(cx.executor());
 3217    fs.insert_tree(
 3218        path!("/dir"),
 3219        json!({
 3220            "a.rs": text.clone(),
 3221        }),
 3222    )
 3223    .await;
 3224
 3225    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3226    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 3227    let buffer = project
 3228        .update(cx, |project, cx| {
 3229            project.open_local_buffer(path!("/dir/a.rs"), cx)
 3230        })
 3231        .await
 3232        .unwrap();
 3233
 3234    // Simulate the language server sending us edits in a non-ordered fashion,
 3235    // with ranges sometimes being inverted or pointing to invalid locations.
 3236    let edits = lsp_store
 3237        .update(cx, |lsp_store, cx| {
 3238            lsp_store.as_local_mut().unwrap().edits_from_lsp(
 3239                &buffer,
 3240                [
 3241                    lsp::TextEdit {
 3242                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
 3243                        new_text: "\n\n".into(),
 3244                    },
 3245                    lsp::TextEdit {
 3246                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
 3247                        new_text: "a::{b, c}".into(),
 3248                    },
 3249                    lsp::TextEdit {
 3250                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
 3251                        new_text: "".into(),
 3252                    },
 3253                    lsp::TextEdit {
 3254                        range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
 3255                        new_text: "
 3256                            fn f() {
 3257                                b();
 3258                                c();
 3259                            }"
 3260                        .unindent(),
 3261                    },
 3262                ],
 3263                LanguageServerId(0),
 3264                None,
 3265                cx,
 3266            )
 3267        })
 3268        .await
 3269        .unwrap();
 3270
 3271    buffer.update(cx, |buffer, cx| {
 3272        let edits = edits
 3273            .into_iter()
 3274            .map(|(range, text)| {
 3275                (
 3276                    range.start.to_point(buffer)..range.end.to_point(buffer),
 3277                    text,
 3278                )
 3279            })
 3280            .collect::<Vec<_>>();
 3281
 3282        assert_eq!(
 3283            edits,
 3284            [
 3285                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
 3286                (Point::new(1, 0)..Point::new(2, 0), "".into())
 3287            ]
 3288        );
 3289
 3290        for (range, new_text) in edits {
 3291            buffer.edit([(range, new_text)], None, cx);
 3292        }
 3293        assert_eq!(
 3294            buffer.text(),
 3295            "
 3296                use a::{b, c};
 3297
 3298                fn f() {
 3299                    b();
 3300                    c();
 3301                }
 3302            "
 3303            .unindent()
 3304        );
 3305    });
 3306}
 3307
 3308fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 3309    buffer: &Buffer,
 3310    range: Range<T>,
 3311) -> Vec<(String, Option<DiagnosticSeverity>)> {
 3312    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
 3313    for chunk in buffer.snapshot().chunks(range, true) {
 3314        if chunks
 3315            .last()
 3316            .is_some_and(|prev_chunk| prev_chunk.1 == chunk.diagnostic_severity)
 3317        {
 3318            chunks.last_mut().unwrap().0.push_str(chunk.text);
 3319        } else {
 3320            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
 3321        }
 3322    }
 3323    chunks
 3324}
 3325
 3326#[gpui::test(iterations = 10)]
 3327async fn test_definition(cx: &mut gpui::TestAppContext) {
 3328    init_test(cx);
 3329
 3330    let fs = FakeFs::new(cx.executor());
 3331    fs.insert_tree(
 3332        path!("/dir"),
 3333        json!({
 3334            "a.rs": "const fn a() { A }",
 3335            "b.rs": "const y: i32 = crate::a()",
 3336        }),
 3337    )
 3338    .await;
 3339
 3340    let project = Project::test(fs, [path!("/dir/b.rs").as_ref()], cx).await;
 3341
 3342    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3343    language_registry.add(rust_lang());
 3344    let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
 3345
 3346    let (buffer, _handle) = project
 3347        .update(cx, |project, cx| {
 3348            project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
 3349        })
 3350        .await
 3351        .unwrap();
 3352
 3353    let fake_server = fake_servers.next().await.unwrap();
 3354    fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
 3355        let params = params.text_document_position_params;
 3356        assert_eq!(
 3357            params.text_document.uri.to_file_path().unwrap(),
 3358            Path::new(path!("/dir/b.rs")),
 3359        );
 3360        assert_eq!(params.position, lsp::Position::new(0, 22));
 3361
 3362        Ok(Some(lsp::GotoDefinitionResponse::Scalar(
 3363            lsp::Location::new(
 3364                lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
 3365                lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
 3366            ),
 3367        )))
 3368    });
 3369    let mut definitions = project
 3370        .update(cx, |project, cx| project.definitions(&buffer, 22, cx))
 3371        .await
 3372        .unwrap()
 3373        .unwrap();
 3374
 3375    // Assert no new language server started
 3376    cx.executor().run_until_parked();
 3377    assert!(fake_servers.try_next().is_err());
 3378
 3379    assert_eq!(definitions.len(), 1);
 3380    let definition = definitions.pop().unwrap();
 3381    cx.update(|cx| {
 3382        let target_buffer = definition.target.buffer.read(cx);
 3383        assert_eq!(
 3384            target_buffer
 3385                .file()
 3386                .unwrap()
 3387                .as_local()
 3388                .unwrap()
 3389                .abs_path(cx),
 3390            Path::new(path!("/dir/a.rs")),
 3391        );
 3392        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
 3393        assert_eq!(
 3394            list_worktrees(&project, cx),
 3395            [
 3396                (path!("/dir/a.rs").as_ref(), false),
 3397                (path!("/dir/b.rs").as_ref(), true)
 3398            ],
 3399        );
 3400
 3401        drop(definition);
 3402    });
 3403    cx.update(|cx| {
 3404        assert_eq!(
 3405            list_worktrees(&project, cx),
 3406            [(path!("/dir/b.rs").as_ref(), true)]
 3407        );
 3408    });
 3409
 3410    fn list_worktrees<'a>(project: &'a Entity<Project>, cx: &'a App) -> Vec<(&'a Path, bool)> {
 3411        project
 3412            .read(cx)
 3413            .worktrees(cx)
 3414            .map(|worktree| {
 3415                let worktree = worktree.read(cx);
 3416                (
 3417                    worktree.as_local().unwrap().abs_path().as_ref(),
 3418                    worktree.is_visible(),
 3419                )
 3420            })
 3421            .collect::<Vec<_>>()
 3422    }
 3423}
 3424
 3425#[gpui::test]
 3426async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
 3427    init_test(cx);
 3428
 3429    let fs = FakeFs::new(cx.executor());
 3430    fs.insert_tree(
 3431        path!("/dir"),
 3432        json!({
 3433            "a.ts": "",
 3434        }),
 3435    )
 3436    .await;
 3437
 3438    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3439
 3440    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3441    language_registry.add(typescript_lang());
 3442    let mut fake_language_servers = language_registry.register_fake_lsp(
 3443        "TypeScript",
 3444        FakeLspAdapter {
 3445            capabilities: lsp::ServerCapabilities {
 3446                completion_provider: Some(lsp::CompletionOptions {
 3447                    trigger_characters: Some(vec![".".to_string()]),
 3448                    ..Default::default()
 3449                }),
 3450                ..Default::default()
 3451            },
 3452            ..Default::default()
 3453        },
 3454    );
 3455
 3456    let (buffer, _handle) = project
 3457        .update(cx, |p, cx| {
 3458            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 3459        })
 3460        .await
 3461        .unwrap();
 3462
 3463    let fake_server = fake_language_servers.next().await.unwrap();
 3464
 3465    // When text_edit exists, it takes precedence over insert_text and label
 3466    let text = "let a = obj.fqn";
 3467    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3468    let completions = project.update(cx, |project, cx| {
 3469        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
 3470    });
 3471
 3472    fake_server
 3473        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
 3474            Ok(Some(lsp::CompletionResponse::Array(vec![
 3475                lsp::CompletionItem {
 3476                    label: "labelText".into(),
 3477                    insert_text: Some("insertText".into()),
 3478                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
 3479                        range: lsp::Range::new(
 3480                            lsp::Position::new(0, text.len() as u32 - 3),
 3481                            lsp::Position::new(0, text.len() as u32),
 3482                        ),
 3483                        new_text: "textEditText".into(),
 3484                    })),
 3485                    ..Default::default()
 3486                },
 3487            ])))
 3488        })
 3489        .next()
 3490        .await;
 3491
 3492    let completions = completions
 3493        .await
 3494        .unwrap()
 3495        .into_iter()
 3496        .flat_map(|response| response.completions)
 3497        .collect::<Vec<_>>();
 3498    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 3499
 3500    assert_eq!(completions.len(), 1);
 3501    assert_eq!(completions[0].new_text, "textEditText");
 3502    assert_eq!(
 3503        completions[0].replace_range.to_offset(&snapshot),
 3504        text.len() - 3..text.len()
 3505    );
 3506}
 3507
 3508#[gpui::test]
 3509async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
 3510    init_test(cx);
 3511
 3512    let fs = FakeFs::new(cx.executor());
 3513    fs.insert_tree(
 3514        path!("/dir"),
 3515        json!({
 3516            "a.ts": "",
 3517        }),
 3518    )
 3519    .await;
 3520
 3521    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3522
 3523    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3524    language_registry.add(typescript_lang());
 3525    let mut fake_language_servers = language_registry.register_fake_lsp(
 3526        "TypeScript",
 3527        FakeLspAdapter {
 3528            capabilities: lsp::ServerCapabilities {
 3529                completion_provider: Some(lsp::CompletionOptions {
 3530                    trigger_characters: Some(vec![".".to_string()]),
 3531                    ..Default::default()
 3532                }),
 3533                ..Default::default()
 3534            },
 3535            ..Default::default()
 3536        },
 3537    );
 3538
 3539    let (buffer, _handle) = project
 3540        .update(cx, |p, cx| {
 3541            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 3542        })
 3543        .await
 3544        .unwrap();
 3545
 3546    let fake_server = fake_language_servers.next().await.unwrap();
 3547    let text = "let a = obj.fqn";
 3548
 3549    // Test 1: When text_edit is None but text_edit_text exists with default edit_range
 3550    {
 3551        buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3552        let completions = project.update(cx, |project, cx| {
 3553            project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
 3554        });
 3555
 3556        fake_server
 3557            .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
 3558                Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
 3559                    is_incomplete: false,
 3560                    item_defaults: Some(lsp::CompletionListItemDefaults {
 3561                        edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
 3562                            lsp::Range::new(
 3563                                lsp::Position::new(0, text.len() as u32 - 3),
 3564                                lsp::Position::new(0, text.len() as u32),
 3565                            ),
 3566                        )),
 3567                        ..Default::default()
 3568                    }),
 3569                    items: vec![lsp::CompletionItem {
 3570                        label: "labelText".into(),
 3571                        text_edit_text: Some("textEditText".into()),
 3572                        text_edit: None,
 3573                        ..Default::default()
 3574                    }],
 3575                })))
 3576            })
 3577            .next()
 3578            .await;
 3579
 3580        let completions = completions
 3581            .await
 3582            .unwrap()
 3583            .into_iter()
 3584            .flat_map(|response| response.completions)
 3585            .collect::<Vec<_>>();
 3586        let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 3587
 3588        assert_eq!(completions.len(), 1);
 3589        assert_eq!(completions[0].new_text, "textEditText");
 3590        assert_eq!(
 3591            completions[0].replace_range.to_offset(&snapshot),
 3592            text.len() - 3..text.len()
 3593        );
 3594    }
 3595
 3596    // Test 2: When both text_edit and text_edit_text are None with default edit_range
 3597    {
 3598        buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3599        let completions = project.update(cx, |project, cx| {
 3600            project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
 3601        });
 3602
 3603        fake_server
 3604            .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
 3605                Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
 3606                    is_incomplete: false,
 3607                    item_defaults: Some(lsp::CompletionListItemDefaults {
 3608                        edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
 3609                            lsp::Range::new(
 3610                                lsp::Position::new(0, text.len() as u32 - 3),
 3611                                lsp::Position::new(0, text.len() as u32),
 3612                            ),
 3613                        )),
 3614                        ..Default::default()
 3615                    }),
 3616                    items: vec![lsp::CompletionItem {
 3617                        label: "labelText".into(),
 3618                        text_edit_text: None,
 3619                        insert_text: Some("irrelevant".into()),
 3620                        text_edit: None,
 3621                        ..Default::default()
 3622                    }],
 3623                })))
 3624            })
 3625            .next()
 3626            .await;
 3627
 3628        let completions = completions
 3629            .await
 3630            .unwrap()
 3631            .into_iter()
 3632            .flat_map(|response| response.completions)
 3633            .collect::<Vec<_>>();
 3634        let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 3635
 3636        assert_eq!(completions.len(), 1);
 3637        assert_eq!(completions[0].new_text, "labelText");
 3638        assert_eq!(
 3639            completions[0].replace_range.to_offset(&snapshot),
 3640            text.len() - 3..text.len()
 3641        );
 3642    }
 3643}
 3644
 3645#[gpui::test]
 3646async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
 3647    init_test(cx);
 3648
 3649    let fs = FakeFs::new(cx.executor());
 3650    fs.insert_tree(
 3651        path!("/dir"),
 3652        json!({
 3653            "a.ts": "",
 3654        }),
 3655    )
 3656    .await;
 3657
 3658    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3659
 3660    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3661    language_registry.add(typescript_lang());
 3662    let mut fake_language_servers = language_registry.register_fake_lsp(
 3663        "TypeScript",
 3664        FakeLspAdapter {
 3665            capabilities: lsp::ServerCapabilities {
 3666                completion_provider: Some(lsp::CompletionOptions {
 3667                    trigger_characters: Some(vec![":".to_string()]),
 3668                    ..Default::default()
 3669                }),
 3670                ..Default::default()
 3671            },
 3672            ..Default::default()
 3673        },
 3674    );
 3675
 3676    let (buffer, _handle) = project
 3677        .update(cx, |p, cx| {
 3678            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 3679        })
 3680        .await
 3681        .unwrap();
 3682
 3683    let fake_server = fake_language_servers.next().await.unwrap();
 3684
 3685    // Test 1: When text_edit is None but insert_text exists (no edit_range in defaults)
 3686    let text = "let a = b.fqn";
 3687    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3688    let completions = project.update(cx, |project, cx| {
 3689        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
 3690    });
 3691
 3692    fake_server
 3693        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 3694            Ok(Some(lsp::CompletionResponse::Array(vec![
 3695                lsp::CompletionItem {
 3696                    label: "fullyQualifiedName?".into(),
 3697                    insert_text: Some("fullyQualifiedName".into()),
 3698                    ..Default::default()
 3699                },
 3700            ])))
 3701        })
 3702        .next()
 3703        .await;
 3704    let completions = completions
 3705        .await
 3706        .unwrap()
 3707        .into_iter()
 3708        .flat_map(|response| response.completions)
 3709        .collect::<Vec<_>>();
 3710    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 3711    assert_eq!(completions.len(), 1);
 3712    assert_eq!(completions[0].new_text, "fullyQualifiedName");
 3713    assert_eq!(
 3714        completions[0].replace_range.to_offset(&snapshot),
 3715        text.len() - 3..text.len()
 3716    );
 3717
 3718    // Test 2: When both text_edit and insert_text are None (no edit_range in defaults)
 3719    let text = "let a = \"atoms/cmp\"";
 3720    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3721    let completions = project.update(cx, |project, cx| {
 3722        project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
 3723    });
 3724
 3725    fake_server
 3726        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 3727            Ok(Some(lsp::CompletionResponse::Array(vec![
 3728                lsp::CompletionItem {
 3729                    label: "component".into(),
 3730                    ..Default::default()
 3731                },
 3732            ])))
 3733        })
 3734        .next()
 3735        .await;
 3736    let completions = completions
 3737        .await
 3738        .unwrap()
 3739        .into_iter()
 3740        .flat_map(|response| response.completions)
 3741        .collect::<Vec<_>>();
 3742    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
 3743    assert_eq!(completions.len(), 1);
 3744    assert_eq!(completions[0].new_text, "component");
 3745    assert_eq!(
 3746        completions[0].replace_range.to_offset(&snapshot),
 3747        text.len() - 4..text.len() - 1
 3748    );
 3749}
 3750
 3751#[gpui::test]
 3752async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
 3753    init_test(cx);
 3754
 3755    let fs = FakeFs::new(cx.executor());
 3756    fs.insert_tree(
 3757        path!("/dir"),
 3758        json!({
 3759            "a.ts": "",
 3760        }),
 3761    )
 3762    .await;
 3763
 3764    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3765
 3766    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3767    language_registry.add(typescript_lang());
 3768    let mut fake_language_servers = language_registry.register_fake_lsp(
 3769        "TypeScript",
 3770        FakeLspAdapter {
 3771            capabilities: lsp::ServerCapabilities {
 3772                completion_provider: Some(lsp::CompletionOptions {
 3773                    trigger_characters: Some(vec![":".to_string()]),
 3774                    ..Default::default()
 3775                }),
 3776                ..Default::default()
 3777            },
 3778            ..Default::default()
 3779        },
 3780    );
 3781
 3782    let (buffer, _handle) = project
 3783        .update(cx, |p, cx| {
 3784            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 3785        })
 3786        .await
 3787        .unwrap();
 3788
 3789    let fake_server = fake_language_servers.next().await.unwrap();
 3790
 3791    let text = "let a = b.fqn";
 3792    buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
 3793    let completions = project.update(cx, |project, cx| {
 3794        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
 3795    });
 3796
 3797    fake_server
 3798        .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
 3799            Ok(Some(lsp::CompletionResponse::Array(vec![
 3800                lsp::CompletionItem {
 3801                    label: "fullyQualifiedName?".into(),
 3802                    insert_text: Some("fully\rQualified\r\nName".into()),
 3803                    ..Default::default()
 3804                },
 3805            ])))
 3806        })
 3807        .next()
 3808        .await;
 3809    let completions = completions
 3810        .await
 3811        .unwrap()
 3812        .into_iter()
 3813        .flat_map(|response| response.completions)
 3814        .collect::<Vec<_>>();
 3815    assert_eq!(completions.len(), 1);
 3816    assert_eq!(completions[0].new_text, "fully\nQualified\nName");
 3817}
 3818
 3819#[gpui::test(iterations = 10)]
 3820async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
 3821    init_test(cx);
 3822
 3823    let fs = FakeFs::new(cx.executor());
 3824    fs.insert_tree(
 3825        path!("/dir"),
 3826        json!({
 3827            "a.ts": "a",
 3828        }),
 3829    )
 3830    .await;
 3831
 3832    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 3833
 3834    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 3835    language_registry.add(typescript_lang());
 3836    let mut fake_language_servers = language_registry.register_fake_lsp(
 3837        "TypeScript",
 3838        FakeLspAdapter {
 3839            capabilities: lsp::ServerCapabilities {
 3840                code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
 3841                    lsp::CodeActionOptions {
 3842                        resolve_provider: Some(true),
 3843                        ..lsp::CodeActionOptions::default()
 3844                    },
 3845                )),
 3846                execute_command_provider: Some(lsp::ExecuteCommandOptions {
 3847                    commands: vec!["_the/command".to_string()],
 3848                    ..lsp::ExecuteCommandOptions::default()
 3849                }),
 3850                ..lsp::ServerCapabilities::default()
 3851            },
 3852            ..FakeLspAdapter::default()
 3853        },
 3854    );
 3855
 3856    let (buffer, _handle) = project
 3857        .update(cx, |p, cx| {
 3858            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 3859        })
 3860        .await
 3861        .unwrap();
 3862
 3863    let fake_server = fake_language_servers.next().await.unwrap();
 3864
 3865    // Language server returns code actions that contain commands, and not edits.
 3866    let actions = project.update(cx, |project, cx| {
 3867        project.code_actions(&buffer, 0..0, None, cx)
 3868    });
 3869    fake_server
 3870        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
 3871            Ok(Some(vec![
 3872                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
 3873                    title: "The code action".into(),
 3874                    data: Some(serde_json::json!({
 3875                        "command": "_the/command",
 3876                    })),
 3877                    ..lsp::CodeAction::default()
 3878                }),
 3879                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
 3880                    title: "two".into(),
 3881                    ..lsp::CodeAction::default()
 3882                }),
 3883            ]))
 3884        })
 3885        .next()
 3886        .await;
 3887
 3888    let action = actions.await.unwrap().unwrap()[0].clone();
 3889    let apply = project.update(cx, |project, cx| {
 3890        project.apply_code_action(buffer.clone(), action, true, cx)
 3891    });
 3892
 3893    // Resolving the code action does not populate its edits. In absence of
 3894    // edits, we must execute the given command.
 3895    fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
 3896        |mut action, _| async move {
 3897            if action.data.is_some() {
 3898                action.command = Some(lsp::Command {
 3899                    title: "The command".into(),
 3900                    command: "_the/command".into(),
 3901                    arguments: Some(vec![json!("the-argument")]),
 3902                });
 3903            }
 3904            Ok(action)
 3905        },
 3906    );
 3907
 3908    // While executing the command, the language server sends the editor
 3909    // a `workspaceEdit` request.
 3910    fake_server
 3911        .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
 3912            let fake = fake_server.clone();
 3913            move |params, _| {
 3914                assert_eq!(params.command, "_the/command");
 3915                let fake = fake.clone();
 3916                async move {
 3917                    fake.server
 3918                        .request::<lsp::request::ApplyWorkspaceEdit>(
 3919                            lsp::ApplyWorkspaceEditParams {
 3920                                label: None,
 3921                                edit: lsp::WorkspaceEdit {
 3922                                    changes: Some(
 3923                                        [(
 3924                                            lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
 3925                                            vec![lsp::TextEdit {
 3926                                                range: lsp::Range::new(
 3927                                                    lsp::Position::new(0, 0),
 3928                                                    lsp::Position::new(0, 0),
 3929                                                ),
 3930                                                new_text: "X".into(),
 3931                                            }],
 3932                                        )]
 3933                                        .into_iter()
 3934                                        .collect(),
 3935                                    ),
 3936                                    ..Default::default()
 3937                                },
 3938                            },
 3939                        )
 3940                        .await
 3941                        .into_response()
 3942                        .unwrap();
 3943                    Ok(Some(json!(null)))
 3944                }
 3945            }
 3946        })
 3947        .next()
 3948        .await;
 3949
 3950    // Applying the code action returns a project transaction containing the edits
 3951    // sent by the language server in its `workspaceEdit` request.
 3952    let transaction = apply.await.unwrap();
 3953    assert!(transaction.0.contains_key(&buffer));
 3954    buffer.update(cx, |buffer, cx| {
 3955        assert_eq!(buffer.text(), "Xa");
 3956        buffer.undo(cx);
 3957        assert_eq!(buffer.text(), "a");
 3958    });
 3959}
 3960
 3961#[gpui::test]
 3962async fn test_rename_file_to_new_directory(cx: &mut gpui::TestAppContext) {
 3963    init_test(cx);
 3964    let fs = FakeFs::new(cx.background_executor.clone());
 3965    let expected_contents = "content";
 3966    fs.as_fake()
 3967        .insert_tree(
 3968            "/root",
 3969            json!({
 3970                "test.txt": expected_contents
 3971            }),
 3972        )
 3973        .await;
 3974
 3975    let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
 3976
 3977    let (worktree, entry_id) = project.read_with(cx, |project, cx| {
 3978        let worktree = project.worktrees(cx).next().unwrap();
 3979        let entry_id = worktree
 3980            .read(cx)
 3981            .entry_for_path(rel_path("test.txt"))
 3982            .unwrap()
 3983            .id;
 3984        (worktree, entry_id)
 3985    });
 3986    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 3987    let _result = project
 3988        .update(cx, |project, cx| {
 3989            project.rename_entry(
 3990                entry_id,
 3991                (worktree_id, rel_path("dir1/dir2/dir3/test.txt")).into(),
 3992                cx,
 3993            )
 3994        })
 3995        .await
 3996        .unwrap();
 3997    worktree.read_with(cx, |worktree, _| {
 3998        assert!(
 3999            worktree.entry_for_path(rel_path("test.txt")).is_none(),
 4000            "Old file should have been removed"
 4001        );
 4002        assert!(
 4003            worktree
 4004                .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
 4005                .is_some(),
 4006            "Whole directory hierarchy and the new file should have been created"
 4007        );
 4008    });
 4009    assert_eq!(
 4010        worktree
 4011            .update(cx, |worktree, cx| {
 4012                worktree.load_file(rel_path("dir1/dir2/dir3/test.txt"), cx)
 4013            })
 4014            .await
 4015            .unwrap()
 4016            .text,
 4017        expected_contents,
 4018        "Moved file's contents should be preserved"
 4019    );
 4020
 4021    let entry_id = worktree.read_with(cx, |worktree, _| {
 4022        worktree
 4023            .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
 4024            .unwrap()
 4025            .id
 4026    });
 4027
 4028    let _result = project
 4029        .update(cx, |project, cx| {
 4030            project.rename_entry(
 4031                entry_id,
 4032                (worktree_id, rel_path("dir1/dir2/test.txt")).into(),
 4033                cx,
 4034            )
 4035        })
 4036        .await
 4037        .unwrap();
 4038    worktree.read_with(cx, |worktree, _| {
 4039        assert!(
 4040            worktree.entry_for_path(rel_path("test.txt")).is_none(),
 4041            "First file should not reappear"
 4042        );
 4043        assert!(
 4044            worktree
 4045                .entry_for_path(rel_path("dir1/dir2/dir3/test.txt"))
 4046                .is_none(),
 4047            "Old file should have been removed"
 4048        );
 4049        assert!(
 4050            worktree
 4051                .entry_for_path(rel_path("dir1/dir2/test.txt"))
 4052                .is_some(),
 4053            "No error should have occurred after moving into existing directory"
 4054        );
 4055    });
 4056    assert_eq!(
 4057        worktree
 4058            .update(cx, |worktree, cx| {
 4059                worktree.load_file(rel_path("dir1/dir2/test.txt"), cx)
 4060            })
 4061            .await
 4062            .unwrap()
 4063            .text,
 4064        expected_contents,
 4065        "Moved file's contents should be preserved"
 4066    );
 4067}
 4068
 4069#[gpui::test(iterations = 10)]
 4070async fn test_save_file(cx: &mut gpui::TestAppContext) {
 4071    init_test(cx);
 4072
 4073    let fs = FakeFs::new(cx.executor());
 4074    fs.insert_tree(
 4075        path!("/dir"),
 4076        json!({
 4077            "file1": "the old contents",
 4078        }),
 4079    )
 4080    .await;
 4081
 4082    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4083    let buffer = project
 4084        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 4085        .await
 4086        .unwrap();
 4087    buffer.update(cx, |buffer, cx| {
 4088        assert_eq!(buffer.text(), "the old contents");
 4089        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
 4090    });
 4091
 4092    project
 4093        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 4094        .await
 4095        .unwrap();
 4096
 4097    let new_text = fs
 4098        .load(Path::new(path!("/dir/file1")))
 4099        .await
 4100        .unwrap()
 4101        .replace("\r\n", "\n");
 4102    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
 4103}
 4104
 4105#[gpui::test(iterations = 10)]
 4106async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) {
 4107    // Issue: #24349
 4108    init_test(cx);
 4109
 4110    let fs = FakeFs::new(cx.executor());
 4111    fs.insert_tree(path!("/dir"), json!({})).await;
 4112
 4113    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4114    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 4115
 4116    language_registry.add(rust_lang());
 4117    let mut fake_rust_servers = language_registry.register_fake_lsp(
 4118        "Rust",
 4119        FakeLspAdapter {
 4120            name: "the-rust-language-server",
 4121            capabilities: lsp::ServerCapabilities {
 4122                completion_provider: Some(lsp::CompletionOptions {
 4123                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
 4124                    ..Default::default()
 4125                }),
 4126                text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
 4127                    lsp::TextDocumentSyncOptions {
 4128                        save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
 4129                        ..Default::default()
 4130                    },
 4131                )),
 4132                ..Default::default()
 4133            },
 4134            ..Default::default()
 4135        },
 4136    );
 4137
 4138    let buffer = project
 4139        .update(cx, |this, cx| this.create_buffer(false, cx))
 4140        .unwrap()
 4141        .await;
 4142    project.update(cx, |this, cx| {
 4143        this.register_buffer_with_language_servers(&buffer, cx);
 4144        buffer.update(cx, |buffer, cx| {
 4145            assert!(!this.has_language_servers_for(buffer, cx));
 4146        })
 4147    });
 4148
 4149    project
 4150        .update(cx, |this, cx| {
 4151            let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
 4152            this.save_buffer_as(
 4153                buffer.clone(),
 4154                ProjectPath {
 4155                    worktree_id,
 4156                    path: rel_path("file.rs").into(),
 4157                },
 4158                cx,
 4159            )
 4160        })
 4161        .await
 4162        .unwrap();
 4163    // A server is started up, and it is notified about Rust files.
 4164    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
 4165    assert_eq!(
 4166        fake_rust_server
 4167            .receive_notification::<lsp::notification::DidOpenTextDocument>()
 4168            .await
 4169            .text_document,
 4170        lsp::TextDocumentItem {
 4171            uri: lsp::Uri::from_file_path(path!("/dir/file.rs")).unwrap(),
 4172            version: 0,
 4173            text: "".to_string(),
 4174            language_id: "rust".to_string(),
 4175        }
 4176    );
 4177
 4178    project.update(cx, |this, cx| {
 4179        buffer.update(cx, |buffer, cx| {
 4180            assert!(this.has_language_servers_for(buffer, cx));
 4181        })
 4182    });
 4183}
 4184
 4185#[gpui::test(iterations = 30)]
 4186async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
 4187    init_test(cx);
 4188
 4189    let fs = FakeFs::new(cx.executor());
 4190    fs.insert_tree(
 4191        path!("/dir"),
 4192        json!({
 4193            "file1": "the original contents",
 4194        }),
 4195    )
 4196    .await;
 4197
 4198    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4199    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 4200    let buffer = project
 4201        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 4202        .await
 4203        .unwrap();
 4204
 4205    // Simulate buffer diffs being slow, so that they don't complete before
 4206    // the next file change occurs.
 4207    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
 4208
 4209    // Change the buffer's file on disk, and then wait for the file change
 4210    // to be detected by the worktree, so that the buffer starts reloading.
 4211    fs.save(
 4212        path!("/dir/file1").as_ref(),
 4213        &"the first contents".into(),
 4214        Default::default(),
 4215    )
 4216    .await
 4217    .unwrap();
 4218    worktree.next_event(cx).await;
 4219
 4220    // Change the buffer's file again. Depending on the random seed, the
 4221    // previous file change may still be in progress.
 4222    fs.save(
 4223        path!("/dir/file1").as_ref(),
 4224        &"the second contents".into(),
 4225        Default::default(),
 4226    )
 4227    .await
 4228    .unwrap();
 4229    worktree.next_event(cx).await;
 4230
 4231    cx.executor().run_until_parked();
 4232    let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
 4233    buffer.read_with(cx, |buffer, _| {
 4234        assert_eq!(buffer.text(), on_disk_text);
 4235        assert!(!buffer.is_dirty(), "buffer should not be dirty");
 4236        assert!(!buffer.has_conflict(), "buffer should not be dirty");
 4237    });
 4238}
 4239
 4240#[gpui::test(iterations = 30)]
 4241async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
 4242    init_test(cx);
 4243
 4244    let fs = FakeFs::new(cx.executor());
 4245    fs.insert_tree(
 4246        path!("/dir"),
 4247        json!({
 4248            "file1": "the original contents",
 4249        }),
 4250    )
 4251    .await;
 4252
 4253    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4254    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 4255    let buffer = project
 4256        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 4257        .await
 4258        .unwrap();
 4259
 4260    // Simulate buffer diffs being slow, so that they don't complete before
 4261    // the next file change occurs.
 4262    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
 4263
 4264    // Change the buffer's file on disk, and then wait for the file change
 4265    // to be detected by the worktree, so that the buffer starts reloading.
 4266    fs.save(
 4267        path!("/dir/file1").as_ref(),
 4268        &"the first contents".into(),
 4269        Default::default(),
 4270    )
 4271    .await
 4272    .unwrap();
 4273    worktree.next_event(cx).await;
 4274
 4275    cx.executor()
 4276        .spawn(cx.executor().simulate_random_delay())
 4277        .await;
 4278
 4279    // Perform a noop edit, causing the buffer's version to increase.
 4280    buffer.update(cx, |buffer, cx| {
 4281        buffer.edit([(0..0, " ")], None, cx);
 4282        buffer.undo(cx);
 4283    });
 4284
 4285    cx.executor().run_until_parked();
 4286    let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
 4287    buffer.read_with(cx, |buffer, _| {
 4288        let buffer_text = buffer.text();
 4289        if buffer_text == on_disk_text {
 4290            assert!(
 4291                !buffer.is_dirty() && !buffer.has_conflict(),
 4292                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
 4293            );
 4294        }
 4295        // If the file change occurred while the buffer was processing the first
 4296        // change, the buffer will be in a conflicting state.
 4297        else {
 4298            assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
 4299            assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
 4300        }
 4301    });
 4302}
 4303
 4304#[gpui::test]
 4305async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
 4306    init_test(cx);
 4307
 4308    let fs = FakeFs::new(cx.executor());
 4309    fs.insert_tree(
 4310        path!("/dir"),
 4311        json!({
 4312            "file1": "the old contents",
 4313        }),
 4314    )
 4315    .await;
 4316
 4317    let project = Project::test(fs.clone(), [path!("/dir/file1").as_ref()], cx).await;
 4318    let buffer = project
 4319        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 4320        .await
 4321        .unwrap();
 4322    buffer.update(cx, |buffer, cx| {
 4323        buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
 4324    });
 4325
 4326    project
 4327        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 4328        .await
 4329        .unwrap();
 4330
 4331    let new_text = fs
 4332        .load(Path::new(path!("/dir/file1")))
 4333        .await
 4334        .unwrap()
 4335        .replace("\r\n", "\n");
 4336    assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
 4337}
 4338
 4339#[gpui::test]
 4340async fn test_save_as(cx: &mut gpui::TestAppContext) {
 4341    init_test(cx);
 4342
 4343    let fs = FakeFs::new(cx.executor());
 4344    fs.insert_tree("/dir", json!({})).await;
 4345
 4346    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 4347
 4348    let languages = project.update(cx, |project, _| project.languages().clone());
 4349    languages.add(rust_lang());
 4350
 4351    let buffer = project.update(cx, |project, cx| {
 4352        project.create_local_buffer("", None, false, cx)
 4353    });
 4354    buffer.update(cx, |buffer, cx| {
 4355        buffer.edit([(0..0, "abc")], None, cx);
 4356        assert!(buffer.is_dirty());
 4357        assert!(!buffer.has_conflict());
 4358        assert_eq!(buffer.language().unwrap().name(), "Plain Text".into());
 4359    });
 4360    project
 4361        .update(cx, |project, cx| {
 4362            let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
 4363            let path = ProjectPath {
 4364                worktree_id,
 4365                path: rel_path("file1.rs").into(),
 4366            };
 4367            project.save_buffer_as(buffer.clone(), path, cx)
 4368        })
 4369        .await
 4370        .unwrap();
 4371    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
 4372
 4373    cx.executor().run_until_parked();
 4374    buffer.update(cx, |buffer, cx| {
 4375        assert_eq!(
 4376            buffer.file().unwrap().full_path(cx),
 4377            Path::new("dir/file1.rs")
 4378        );
 4379        assert!(!buffer.is_dirty());
 4380        assert!(!buffer.has_conflict());
 4381        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
 4382    });
 4383
 4384    let opened_buffer = project
 4385        .update(cx, |project, cx| {
 4386            project.open_local_buffer("/dir/file1.rs", cx)
 4387        })
 4388        .await
 4389        .unwrap();
 4390    assert_eq!(opened_buffer, buffer);
 4391}
 4392
 4393#[gpui::test]
 4394async fn test_save_as_existing_file(cx: &mut gpui::TestAppContext) {
 4395    init_test(cx);
 4396
 4397    let fs = FakeFs::new(cx.executor());
 4398    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4399
 4400    fs.insert_tree(
 4401        path!("/dir"),
 4402        json!({
 4403                "data_a.txt": "data about a"
 4404        }),
 4405    )
 4406    .await;
 4407
 4408    let buffer = project
 4409        .update(cx, |project, cx| {
 4410            project.open_local_buffer(path!("/dir/data_a.txt"), cx)
 4411        })
 4412        .await
 4413        .unwrap();
 4414
 4415    buffer.update(cx, |buffer, cx| {
 4416        buffer.edit([(11..12, "b")], None, cx);
 4417    });
 4418
 4419    // Save buffer's contents as a new file and confirm that the buffer's now
 4420    // associated with `data_b.txt` instead of `data_a.txt`, confirming that the
 4421    // file associated with the buffer has now been updated to `data_b.txt`
 4422    project
 4423        .update(cx, |project, cx| {
 4424            let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
 4425            let new_path = ProjectPath {
 4426                worktree_id,
 4427                path: rel_path("data_b.txt").into(),
 4428            };
 4429
 4430            project.save_buffer_as(buffer.clone(), new_path, cx)
 4431        })
 4432        .await
 4433        .unwrap();
 4434
 4435    buffer.update(cx, |buffer, cx| {
 4436        assert_eq!(
 4437            buffer.file().unwrap().full_path(cx),
 4438            Path::new("dir/data_b.txt")
 4439        )
 4440    });
 4441
 4442    // Open the original `data_a.txt` file, confirming that its contents are
 4443    // unchanged and the resulting buffer's associated file is `data_a.txt`.
 4444    let original_buffer = project
 4445        .update(cx, |project, cx| {
 4446            project.open_local_buffer(path!("/dir/data_a.txt"), cx)
 4447        })
 4448        .await
 4449        .unwrap();
 4450
 4451    original_buffer.update(cx, |buffer, cx| {
 4452        assert_eq!(buffer.text(), "data about a");
 4453        assert_eq!(
 4454            buffer.file().unwrap().full_path(cx),
 4455            Path::new("dir/data_a.txt")
 4456        )
 4457    });
 4458}
 4459
 4460#[gpui::test(retries = 5)]
 4461async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
 4462    use worktree::WorktreeModelHandle as _;
 4463
 4464    init_test(cx);
 4465    cx.executor().allow_parking();
 4466
 4467    let dir = TempTree::new(json!({
 4468        "a": {
 4469            "file1": "",
 4470            "file2": "",
 4471            "file3": "",
 4472        },
 4473        "b": {
 4474            "c": {
 4475                "file4": "",
 4476                "file5": "",
 4477            }
 4478        }
 4479    }));
 4480
 4481    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
 4482
 4483    let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
 4484        let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
 4485        async move { buffer.await.unwrap() }
 4486    };
 4487    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
 4488        project.update(cx, |project, cx| {
 4489            let tree = project.worktrees(cx).next().unwrap();
 4490            tree.read(cx)
 4491                .entry_for_path(rel_path(path))
 4492                .unwrap_or_else(|| panic!("no entry for path {}", path))
 4493                .id
 4494        })
 4495    };
 4496
 4497    let buffer2 = buffer_for_path("a/file2", cx).await;
 4498    let buffer3 = buffer_for_path("a/file3", cx).await;
 4499    let buffer4 = buffer_for_path("b/c/file4", cx).await;
 4500    let buffer5 = buffer_for_path("b/c/file5", cx).await;
 4501
 4502    let file2_id = id_for_path("a/file2", cx);
 4503    let file3_id = id_for_path("a/file3", cx);
 4504    let file4_id = id_for_path("b/c/file4", cx);
 4505
 4506    // Create a remote copy of this worktree.
 4507    let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
 4508    let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
 4509
 4510    let updates = Arc::new(Mutex::new(Vec::new()));
 4511    tree.update(cx, |tree, cx| {
 4512        let updates = updates.clone();
 4513        tree.observe_updates(0, cx, move |update| {
 4514            updates.lock().push(update);
 4515            async { true }
 4516        });
 4517    });
 4518
 4519    let remote = cx.update(|cx| {
 4520        Worktree::remote(
 4521            0,
 4522            ReplicaId::REMOTE_SERVER,
 4523            metadata,
 4524            project.read(cx).client().into(),
 4525            project.read(cx).path_style(cx),
 4526            cx,
 4527        )
 4528    });
 4529
 4530    cx.executor().run_until_parked();
 4531
 4532    cx.update(|cx| {
 4533        assert!(!buffer2.read(cx).is_dirty());
 4534        assert!(!buffer3.read(cx).is_dirty());
 4535        assert!(!buffer4.read(cx).is_dirty());
 4536        assert!(!buffer5.read(cx).is_dirty());
 4537    });
 4538
 4539    // Rename and delete files and directories.
 4540    tree.flush_fs_events(cx).await;
 4541    std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
 4542    std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
 4543    std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
 4544    std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
 4545    tree.flush_fs_events(cx).await;
 4546
 4547    cx.update(|app| {
 4548        assert_eq!(
 4549            tree.read(app).paths().collect::<Vec<_>>(),
 4550            vec![
 4551                rel_path("a"),
 4552                rel_path("a/file1"),
 4553                rel_path("a/file2.new"),
 4554                rel_path("b"),
 4555                rel_path("d"),
 4556                rel_path("d/file3"),
 4557                rel_path("d/file4"),
 4558            ]
 4559        );
 4560    });
 4561
 4562    assert_eq!(id_for_path("a/file2.new", cx), file2_id);
 4563    assert_eq!(id_for_path("d/file3", cx), file3_id);
 4564    assert_eq!(id_for_path("d/file4", cx), file4_id);
 4565
 4566    cx.update(|cx| {
 4567        assert_eq!(
 4568            buffer2.read(cx).file().unwrap().path().as_ref(),
 4569            rel_path("a/file2.new")
 4570        );
 4571        assert_eq!(
 4572            buffer3.read(cx).file().unwrap().path().as_ref(),
 4573            rel_path("d/file3")
 4574        );
 4575        assert_eq!(
 4576            buffer4.read(cx).file().unwrap().path().as_ref(),
 4577            rel_path("d/file4")
 4578        );
 4579        assert_eq!(
 4580            buffer5.read(cx).file().unwrap().path().as_ref(),
 4581            rel_path("b/c/file5")
 4582        );
 4583
 4584        assert_matches!(
 4585            buffer2.read(cx).file().unwrap().disk_state(),
 4586            DiskState::Present { .. }
 4587        );
 4588        assert_matches!(
 4589            buffer3.read(cx).file().unwrap().disk_state(),
 4590            DiskState::Present { .. }
 4591        );
 4592        assert_matches!(
 4593            buffer4.read(cx).file().unwrap().disk_state(),
 4594            DiskState::Present { .. }
 4595        );
 4596        assert_eq!(
 4597            buffer5.read(cx).file().unwrap().disk_state(),
 4598            DiskState::Deleted
 4599        );
 4600    });
 4601
 4602    // Update the remote worktree. Check that it becomes consistent with the
 4603    // local worktree.
 4604    cx.executor().run_until_parked();
 4605
 4606    remote.update(cx, |remote, _| {
 4607        for update in updates.lock().drain(..) {
 4608            remote.as_remote_mut().unwrap().update_from_remote(update);
 4609        }
 4610    });
 4611    cx.executor().run_until_parked();
 4612    remote.update(cx, |remote, _| {
 4613        assert_eq!(
 4614            remote.paths().collect::<Vec<_>>(),
 4615            vec![
 4616                rel_path("a"),
 4617                rel_path("a/file1"),
 4618                rel_path("a/file2.new"),
 4619                rel_path("b"),
 4620                rel_path("d"),
 4621                rel_path("d/file3"),
 4622                rel_path("d/file4"),
 4623            ]
 4624        );
 4625    });
 4626}
 4627
 4628#[gpui::test(iterations = 10)]
 4629async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
 4630    init_test(cx);
 4631
 4632    let fs = FakeFs::new(cx.executor());
 4633    fs.insert_tree(
 4634        path!("/dir"),
 4635        json!({
 4636            "a": {
 4637                "file1": "",
 4638            }
 4639        }),
 4640    )
 4641    .await;
 4642
 4643    let project = Project::test(fs, [Path::new(path!("/dir"))], cx).await;
 4644    let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
 4645    let tree_id = tree.update(cx, |tree, _| tree.id());
 4646
 4647    let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
 4648        project.update(cx, |project, cx| {
 4649            let tree = project.worktrees(cx).next().unwrap();
 4650            tree.read(cx)
 4651                .entry_for_path(rel_path(path))
 4652                .unwrap_or_else(|| panic!("no entry for path {}", path))
 4653                .id
 4654        })
 4655    };
 4656
 4657    let dir_id = id_for_path("a", cx);
 4658    let file_id = id_for_path("a/file1", cx);
 4659    let buffer = project
 4660        .update(cx, |p, cx| {
 4661            p.open_buffer((tree_id, rel_path("a/file1")), cx)
 4662        })
 4663        .await
 4664        .unwrap();
 4665    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
 4666
 4667    project
 4668        .update(cx, |project, cx| {
 4669            project.rename_entry(dir_id, (tree_id, rel_path("b")).into(), cx)
 4670        })
 4671        .unwrap()
 4672        .await
 4673        .into_included()
 4674        .unwrap();
 4675    cx.executor().run_until_parked();
 4676
 4677    assert_eq!(id_for_path("b", cx), dir_id);
 4678    assert_eq!(id_for_path("b/file1", cx), file_id);
 4679    buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
 4680}
 4681
 4682#[gpui::test]
 4683async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
 4684    init_test(cx);
 4685
 4686    let fs = FakeFs::new(cx.executor());
 4687    fs.insert_tree(
 4688        "/dir",
 4689        json!({
 4690            "a.txt": "a-contents",
 4691            "b.txt": "b-contents",
 4692        }),
 4693    )
 4694    .await;
 4695
 4696    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 4697
 4698    // Spawn multiple tasks to open paths, repeating some paths.
 4699    let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
 4700        (
 4701            p.open_local_buffer("/dir/a.txt", cx),
 4702            p.open_local_buffer("/dir/b.txt", cx),
 4703            p.open_local_buffer("/dir/a.txt", cx),
 4704        )
 4705    });
 4706
 4707    let buffer_a_1 = buffer_a_1.await.unwrap();
 4708    let buffer_a_2 = buffer_a_2.await.unwrap();
 4709    let buffer_b = buffer_b.await.unwrap();
 4710    assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
 4711    assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
 4712
 4713    // There is only one buffer per path.
 4714    let buffer_a_id = buffer_a_1.entity_id();
 4715    assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
 4716
 4717    // Open the same path again while it is still open.
 4718    drop(buffer_a_1);
 4719    let buffer_a_3 = project
 4720        .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
 4721        .await
 4722        .unwrap();
 4723
 4724    // There's still only one buffer per path.
 4725    assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
 4726}
 4727
 4728#[gpui::test]
 4729async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
 4730    init_test(cx);
 4731
 4732    let fs = FakeFs::new(cx.executor());
 4733    fs.insert_tree(
 4734        path!("/dir"),
 4735        json!({
 4736            "file1": "abc",
 4737            "file2": "def",
 4738            "file3": "ghi",
 4739        }),
 4740    )
 4741    .await;
 4742
 4743    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4744
 4745    let buffer1 = project
 4746        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 4747        .await
 4748        .unwrap();
 4749    let events = Arc::new(Mutex::new(Vec::new()));
 4750
 4751    // initially, the buffer isn't dirty.
 4752    buffer1.update(cx, |buffer, cx| {
 4753        cx.subscribe(&buffer1, {
 4754            let events = events.clone();
 4755            move |_, _, event, _| match event {
 4756                BufferEvent::Operation { .. } => {}
 4757                _ => events.lock().push(event.clone()),
 4758            }
 4759        })
 4760        .detach();
 4761
 4762        assert!(!buffer.is_dirty());
 4763        assert!(events.lock().is_empty());
 4764
 4765        buffer.edit([(1..2, "")], None, cx);
 4766    });
 4767
 4768    // after the first edit, the buffer is dirty, and emits a dirtied event.
 4769    buffer1.update(cx, |buffer, cx| {
 4770        assert!(buffer.text() == "ac");
 4771        assert!(buffer.is_dirty());
 4772        assert_eq!(
 4773            *events.lock(),
 4774            &[
 4775                language::BufferEvent::Edited,
 4776                language::BufferEvent::DirtyChanged
 4777            ]
 4778        );
 4779        events.lock().clear();
 4780        buffer.did_save(
 4781            buffer.version(),
 4782            buffer.file().unwrap().disk_state().mtime(),
 4783            cx,
 4784        );
 4785    });
 4786
 4787    // after saving, the buffer is not dirty, and emits a saved event.
 4788    buffer1.update(cx, |buffer, cx| {
 4789        assert!(!buffer.is_dirty());
 4790        assert_eq!(*events.lock(), &[language::BufferEvent::Saved]);
 4791        events.lock().clear();
 4792
 4793        buffer.edit([(1..1, "B")], None, cx);
 4794        buffer.edit([(2..2, "D")], None, cx);
 4795    });
 4796
 4797    // after editing again, the buffer is dirty, and emits another dirty event.
 4798    buffer1.update(cx, |buffer, cx| {
 4799        assert!(buffer.text() == "aBDc");
 4800        assert!(buffer.is_dirty());
 4801        assert_eq!(
 4802            *events.lock(),
 4803            &[
 4804                language::BufferEvent::Edited,
 4805                language::BufferEvent::DirtyChanged,
 4806                language::BufferEvent::Edited,
 4807            ],
 4808        );
 4809        events.lock().clear();
 4810
 4811        // After restoring the buffer to its previously-saved state,
 4812        // the buffer is not considered dirty anymore.
 4813        buffer.edit([(1..3, "")], None, cx);
 4814        assert!(buffer.text() == "ac");
 4815        assert!(!buffer.is_dirty());
 4816    });
 4817
 4818    assert_eq!(
 4819        *events.lock(),
 4820        &[
 4821            language::BufferEvent::Edited,
 4822            language::BufferEvent::DirtyChanged
 4823        ]
 4824    );
 4825
 4826    // When a file is deleted, it is not considered dirty.
 4827    let events = Arc::new(Mutex::new(Vec::new()));
 4828    let buffer2 = project
 4829        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
 4830        .await
 4831        .unwrap();
 4832    buffer2.update(cx, |_, cx| {
 4833        cx.subscribe(&buffer2, {
 4834            let events = events.clone();
 4835            move |_, _, event, _| match event {
 4836                BufferEvent::Operation { .. } => {}
 4837                _ => events.lock().push(event.clone()),
 4838            }
 4839        })
 4840        .detach();
 4841    });
 4842
 4843    fs.remove_file(path!("/dir/file2").as_ref(), Default::default())
 4844        .await
 4845        .unwrap();
 4846    cx.executor().run_until_parked();
 4847    buffer2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
 4848    assert_eq!(
 4849        mem::take(&mut *events.lock()),
 4850        &[language::BufferEvent::FileHandleChanged]
 4851    );
 4852
 4853    // Buffer becomes dirty when edited.
 4854    buffer2.update(cx, |buffer, cx| {
 4855        buffer.edit([(2..3, "")], None, cx);
 4856        assert_eq!(buffer.is_dirty(), true);
 4857    });
 4858    assert_eq!(
 4859        mem::take(&mut *events.lock()),
 4860        &[
 4861            language::BufferEvent::Edited,
 4862            language::BufferEvent::DirtyChanged
 4863        ]
 4864    );
 4865
 4866    // Buffer becomes clean again when all of its content is removed, because
 4867    // the file was deleted.
 4868    buffer2.update(cx, |buffer, cx| {
 4869        buffer.edit([(0..2, "")], None, cx);
 4870        assert_eq!(buffer.is_empty(), true);
 4871        assert_eq!(buffer.is_dirty(), false);
 4872    });
 4873    assert_eq!(
 4874        *events.lock(),
 4875        &[
 4876            language::BufferEvent::Edited,
 4877            language::BufferEvent::DirtyChanged
 4878        ]
 4879    );
 4880
 4881    // When a file is already dirty when deleted, we don't emit a Dirtied event.
 4882    let events = Arc::new(Mutex::new(Vec::new()));
 4883    let buffer3 = project
 4884        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file3"), cx))
 4885        .await
 4886        .unwrap();
 4887    buffer3.update(cx, |_, cx| {
 4888        cx.subscribe(&buffer3, {
 4889            let events = events.clone();
 4890            move |_, _, event, _| match event {
 4891                BufferEvent::Operation { .. } => {}
 4892                _ => events.lock().push(event.clone()),
 4893            }
 4894        })
 4895        .detach();
 4896    });
 4897
 4898    buffer3.update(cx, |buffer, cx| {
 4899        buffer.edit([(0..0, "x")], None, cx);
 4900    });
 4901    events.lock().clear();
 4902    fs.remove_file(path!("/dir/file3").as_ref(), Default::default())
 4903        .await
 4904        .unwrap();
 4905    cx.executor().run_until_parked();
 4906    assert_eq!(*events.lock(), &[language::BufferEvent::FileHandleChanged]);
 4907    cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
 4908}
 4909
 4910#[gpui::test]
 4911async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
 4912    init_test(cx);
 4913
 4914    let (initial_contents, initial_offsets) =
 4915        marked_text_offsets("one twoˇ\nthree ˇfourˇ five\nsixˇ seven\n");
 4916    let fs = FakeFs::new(cx.executor());
 4917    fs.insert_tree(
 4918        path!("/dir"),
 4919        json!({
 4920            "the-file": initial_contents,
 4921        }),
 4922    )
 4923    .await;
 4924    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 4925    let buffer = project
 4926        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/the-file"), cx))
 4927        .await
 4928        .unwrap();
 4929
 4930    let anchors = initial_offsets
 4931        .iter()
 4932        .map(|offset| buffer.update(cx, |b, _| b.anchor_before(offset)))
 4933        .collect::<Vec<_>>();
 4934
 4935    // Change the file on disk, adding two new lines of text, and removing
 4936    // one line.
 4937    buffer.update(cx, |buffer, _| {
 4938        assert!(!buffer.is_dirty());
 4939        assert!(!buffer.has_conflict());
 4940    });
 4941
 4942    let (new_contents, new_offsets) =
 4943        marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n");
 4944    fs.save(
 4945        path!("/dir/the-file").as_ref(),
 4946        &new_contents.as_str().into(),
 4947        LineEnding::Unix,
 4948    )
 4949    .await
 4950    .unwrap();
 4951
 4952    // Because the buffer was not modified, it is reloaded from disk. Its
 4953    // contents are edited according to the diff between the old and new
 4954    // file contents.
 4955    cx.executor().run_until_parked();
 4956    buffer.update(cx, |buffer, _| {
 4957        assert_eq!(buffer.text(), new_contents);
 4958        assert!(!buffer.is_dirty());
 4959        assert!(!buffer.has_conflict());
 4960
 4961        let anchor_offsets = anchors
 4962            .iter()
 4963            .map(|anchor| anchor.to_offset(&*buffer))
 4964            .collect::<Vec<_>>();
 4965        assert_eq!(anchor_offsets, new_offsets);
 4966    });
 4967
 4968    // Modify the buffer
 4969    buffer.update(cx, |buffer, cx| {
 4970        buffer.edit([(0..0, " ")], None, cx);
 4971        assert!(buffer.is_dirty());
 4972        assert!(!buffer.has_conflict());
 4973    });
 4974
 4975    // Change the file on disk again, adding blank lines to the beginning.
 4976    fs.save(
 4977        path!("/dir/the-file").as_ref(),
 4978        &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
 4979        LineEnding::Unix,
 4980    )
 4981    .await
 4982    .unwrap();
 4983
 4984    // Because the buffer is modified, it doesn't reload from disk, but is
 4985    // marked as having a conflict.
 4986    cx.executor().run_until_parked();
 4987    buffer.update(cx, |buffer, _| {
 4988        assert_eq!(buffer.text(), " ".to_string() + &new_contents);
 4989        assert!(buffer.has_conflict());
 4990    });
 4991}
 4992
 4993#[gpui::test]
 4994async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
 4995    init_test(cx);
 4996
 4997    let fs = FakeFs::new(cx.executor());
 4998    fs.insert_tree(
 4999        path!("/dir"),
 5000        json!({
 5001            "file1": "a\nb\nc\n",
 5002            "file2": "one\r\ntwo\r\nthree\r\n",
 5003        }),
 5004    )
 5005    .await;
 5006
 5007    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5008    let buffer1 = project
 5009        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
 5010        .await
 5011        .unwrap();
 5012    let buffer2 = project
 5013        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
 5014        .await
 5015        .unwrap();
 5016
 5017    buffer1.update(cx, |buffer, _| {
 5018        assert_eq!(buffer.text(), "a\nb\nc\n");
 5019        assert_eq!(buffer.line_ending(), LineEnding::Unix);
 5020    });
 5021    buffer2.update(cx, |buffer, _| {
 5022        assert_eq!(buffer.text(), "one\ntwo\nthree\n");
 5023        assert_eq!(buffer.line_ending(), LineEnding::Windows);
 5024    });
 5025
 5026    // Change a file's line endings on disk from unix to windows. The buffer's
 5027    // state updates correctly.
 5028    fs.save(
 5029        path!("/dir/file1").as_ref(),
 5030        &"aaa\nb\nc\n".into(),
 5031        LineEnding::Windows,
 5032    )
 5033    .await
 5034    .unwrap();
 5035    cx.executor().run_until_parked();
 5036    buffer1.update(cx, |buffer, _| {
 5037        assert_eq!(buffer.text(), "aaa\nb\nc\n");
 5038        assert_eq!(buffer.line_ending(), LineEnding::Windows);
 5039    });
 5040
 5041    // Save a file with windows line endings. The file is written correctly.
 5042    buffer2.update(cx, |buffer, cx| {
 5043        buffer.set_text("one\ntwo\nthree\nfour\n", cx);
 5044    });
 5045    project
 5046        .update(cx, |project, cx| project.save_buffer(buffer2, cx))
 5047        .await
 5048        .unwrap();
 5049    assert_eq!(
 5050        fs.load(path!("/dir/file2").as_ref()).await.unwrap(),
 5051        "one\r\ntwo\r\nthree\r\nfour\r\n",
 5052    );
 5053}
 5054
 5055#[gpui::test]
 5056async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
 5057    init_test(cx);
 5058
 5059    let fs = FakeFs::new(cx.executor());
 5060    fs.insert_tree(
 5061        path!("/dir"),
 5062        json!({
 5063            "a.rs": "
 5064                fn foo(mut v: Vec<usize>) {
 5065                    for x in &v {
 5066                        v.push(1);
 5067                    }
 5068                }
 5069            "
 5070            .unindent(),
 5071        }),
 5072    )
 5073    .await;
 5074
 5075    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5076    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
 5077    let buffer = project
 5078        .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
 5079        .await
 5080        .unwrap();
 5081
 5082    let buffer_uri = Uri::from_file_path(path!("/dir/a.rs")).unwrap();
 5083    let message = lsp::PublishDiagnosticsParams {
 5084        uri: buffer_uri.clone(),
 5085        diagnostics: vec![
 5086            lsp::Diagnostic {
 5087                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 5088                severity: Some(DiagnosticSeverity::WARNING),
 5089                message: "error 1".to_string(),
 5090                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 5091                    location: lsp::Location {
 5092                        uri: buffer_uri.clone(),
 5093                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 5094                    },
 5095                    message: "error 1 hint 1".to_string(),
 5096                }]),
 5097                ..Default::default()
 5098            },
 5099            lsp::Diagnostic {
 5100                range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 5101                severity: Some(DiagnosticSeverity::HINT),
 5102                message: "error 1 hint 1".to_string(),
 5103                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 5104                    location: lsp::Location {
 5105                        uri: buffer_uri.clone(),
 5106                        range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
 5107                    },
 5108                    message: "original diagnostic".to_string(),
 5109                }]),
 5110                ..Default::default()
 5111            },
 5112            lsp::Diagnostic {
 5113                range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 5114                severity: Some(DiagnosticSeverity::ERROR),
 5115                message: "error 2".to_string(),
 5116                related_information: Some(vec![
 5117                    lsp::DiagnosticRelatedInformation {
 5118                        location: lsp::Location {
 5119                            uri: buffer_uri.clone(),
 5120                            range: lsp::Range::new(
 5121                                lsp::Position::new(1, 13),
 5122                                lsp::Position::new(1, 15),
 5123                            ),
 5124                        },
 5125                        message: "error 2 hint 1".to_string(),
 5126                    },
 5127                    lsp::DiagnosticRelatedInformation {
 5128                        location: lsp::Location {
 5129                            uri: buffer_uri.clone(),
 5130                            range: lsp::Range::new(
 5131                                lsp::Position::new(1, 13),
 5132                                lsp::Position::new(1, 15),
 5133                            ),
 5134                        },
 5135                        message: "error 2 hint 2".to_string(),
 5136                    },
 5137                ]),
 5138                ..Default::default()
 5139            },
 5140            lsp::Diagnostic {
 5141                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 5142                severity: Some(DiagnosticSeverity::HINT),
 5143                message: "error 2 hint 1".to_string(),
 5144                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 5145                    location: lsp::Location {
 5146                        uri: buffer_uri.clone(),
 5147                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 5148                    },
 5149                    message: "original diagnostic".to_string(),
 5150                }]),
 5151                ..Default::default()
 5152            },
 5153            lsp::Diagnostic {
 5154                range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
 5155                severity: Some(DiagnosticSeverity::HINT),
 5156                message: "error 2 hint 2".to_string(),
 5157                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
 5158                    location: lsp::Location {
 5159                        uri: buffer_uri,
 5160                        range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
 5161                    },
 5162                    message: "original diagnostic".to_string(),
 5163                }]),
 5164                ..Default::default()
 5165            },
 5166        ],
 5167        version: None,
 5168    };
 5169
 5170    lsp_store
 5171        .update(cx, |lsp_store, cx| {
 5172            lsp_store.update_diagnostics(
 5173                LanguageServerId(0),
 5174                message,
 5175                None,
 5176                DiagnosticSourceKind::Pushed,
 5177                &[],
 5178                cx,
 5179            )
 5180        })
 5181        .unwrap();
 5182    let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
 5183
 5184    assert_eq!(
 5185        buffer
 5186            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
 5187            .collect::<Vec<_>>(),
 5188        &[
 5189            DiagnosticEntry {
 5190                range: Point::new(1, 8)..Point::new(1, 9),
 5191                diagnostic: Diagnostic {
 5192                    severity: DiagnosticSeverity::WARNING,
 5193                    message: "error 1".to_string(),
 5194                    group_id: 1,
 5195                    is_primary: true,
 5196                    source_kind: DiagnosticSourceKind::Pushed,
 5197                    ..Diagnostic::default()
 5198                }
 5199            },
 5200            DiagnosticEntry {
 5201                range: Point::new(1, 8)..Point::new(1, 9),
 5202                diagnostic: Diagnostic {
 5203                    severity: DiagnosticSeverity::HINT,
 5204                    message: "error 1 hint 1".to_string(),
 5205                    group_id: 1,
 5206                    is_primary: false,
 5207                    source_kind: DiagnosticSourceKind::Pushed,
 5208                    ..Diagnostic::default()
 5209                }
 5210            },
 5211            DiagnosticEntry {
 5212                range: Point::new(1, 13)..Point::new(1, 15),
 5213                diagnostic: Diagnostic {
 5214                    severity: DiagnosticSeverity::HINT,
 5215                    message: "error 2 hint 1".to_string(),
 5216                    group_id: 0,
 5217                    is_primary: false,
 5218                    source_kind: DiagnosticSourceKind::Pushed,
 5219                    ..Diagnostic::default()
 5220                }
 5221            },
 5222            DiagnosticEntry {
 5223                range: Point::new(1, 13)..Point::new(1, 15),
 5224                diagnostic: Diagnostic {
 5225                    severity: DiagnosticSeverity::HINT,
 5226                    message: "error 2 hint 2".to_string(),
 5227                    group_id: 0,
 5228                    is_primary: false,
 5229                    source_kind: DiagnosticSourceKind::Pushed,
 5230                    ..Diagnostic::default()
 5231                }
 5232            },
 5233            DiagnosticEntry {
 5234                range: Point::new(2, 8)..Point::new(2, 17),
 5235                diagnostic: Diagnostic {
 5236                    severity: DiagnosticSeverity::ERROR,
 5237                    message: "error 2".to_string(),
 5238                    group_id: 0,
 5239                    is_primary: true,
 5240                    source_kind: DiagnosticSourceKind::Pushed,
 5241                    ..Diagnostic::default()
 5242                }
 5243            }
 5244        ]
 5245    );
 5246
 5247    assert_eq!(
 5248        buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
 5249        &[
 5250            DiagnosticEntry {
 5251                range: Point::new(1, 13)..Point::new(1, 15),
 5252                diagnostic: Diagnostic {
 5253                    severity: DiagnosticSeverity::HINT,
 5254                    message: "error 2 hint 1".to_string(),
 5255                    group_id: 0,
 5256                    is_primary: false,
 5257                    source_kind: DiagnosticSourceKind::Pushed,
 5258                    ..Diagnostic::default()
 5259                }
 5260            },
 5261            DiagnosticEntry {
 5262                range: Point::new(1, 13)..Point::new(1, 15),
 5263                diagnostic: Diagnostic {
 5264                    severity: DiagnosticSeverity::HINT,
 5265                    message: "error 2 hint 2".to_string(),
 5266                    group_id: 0,
 5267                    is_primary: false,
 5268                    source_kind: DiagnosticSourceKind::Pushed,
 5269                    ..Diagnostic::default()
 5270                }
 5271            },
 5272            DiagnosticEntry {
 5273                range: Point::new(2, 8)..Point::new(2, 17),
 5274                diagnostic: Diagnostic {
 5275                    severity: DiagnosticSeverity::ERROR,
 5276                    message: "error 2".to_string(),
 5277                    group_id: 0,
 5278                    is_primary: true,
 5279                    source_kind: DiagnosticSourceKind::Pushed,
 5280                    ..Diagnostic::default()
 5281                }
 5282            }
 5283        ]
 5284    );
 5285
 5286    assert_eq!(
 5287        buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
 5288        &[
 5289            DiagnosticEntry {
 5290                range: Point::new(1, 8)..Point::new(1, 9),
 5291                diagnostic: Diagnostic {
 5292                    severity: DiagnosticSeverity::WARNING,
 5293                    message: "error 1".to_string(),
 5294                    group_id: 1,
 5295                    is_primary: true,
 5296                    source_kind: DiagnosticSourceKind::Pushed,
 5297                    ..Diagnostic::default()
 5298                }
 5299            },
 5300            DiagnosticEntry {
 5301                range: Point::new(1, 8)..Point::new(1, 9),
 5302                diagnostic: Diagnostic {
 5303                    severity: DiagnosticSeverity::HINT,
 5304                    message: "error 1 hint 1".to_string(),
 5305                    group_id: 1,
 5306                    is_primary: false,
 5307                    source_kind: DiagnosticSourceKind::Pushed,
 5308                    ..Diagnostic::default()
 5309                }
 5310            },
 5311        ]
 5312    );
 5313}
 5314
 5315#[gpui::test]
 5316async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
 5317    init_test(cx);
 5318
 5319    let fs = FakeFs::new(cx.executor());
 5320    fs.insert_tree(
 5321        path!("/dir"),
 5322        json!({
 5323            "one.rs": "const ONE: usize = 1;",
 5324            "two": {
 5325                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 5326            }
 5327
 5328        }),
 5329    )
 5330    .await;
 5331    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5332
 5333    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 5334    language_registry.add(rust_lang());
 5335    let watched_paths = lsp::FileOperationRegistrationOptions {
 5336        filters: vec![
 5337            FileOperationFilter {
 5338                scheme: Some("file".to_owned()),
 5339                pattern: lsp::FileOperationPattern {
 5340                    glob: "**/*.rs".to_owned(),
 5341                    matches: Some(lsp::FileOperationPatternKind::File),
 5342                    options: None,
 5343                },
 5344            },
 5345            FileOperationFilter {
 5346                scheme: Some("file".to_owned()),
 5347                pattern: lsp::FileOperationPattern {
 5348                    glob: "**/**".to_owned(),
 5349                    matches: Some(lsp::FileOperationPatternKind::Folder),
 5350                    options: None,
 5351                },
 5352            },
 5353        ],
 5354    };
 5355    let mut fake_servers = language_registry.register_fake_lsp(
 5356        "Rust",
 5357        FakeLspAdapter {
 5358            capabilities: lsp::ServerCapabilities {
 5359                workspace: Some(lsp::WorkspaceServerCapabilities {
 5360                    workspace_folders: None,
 5361                    file_operations: Some(lsp::WorkspaceFileOperationsServerCapabilities {
 5362                        did_rename: Some(watched_paths.clone()),
 5363                        will_rename: Some(watched_paths),
 5364                        ..Default::default()
 5365                    }),
 5366                }),
 5367                ..Default::default()
 5368            },
 5369            ..Default::default()
 5370        },
 5371    );
 5372
 5373    let _ = project
 5374        .update(cx, |project, cx| {
 5375            project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
 5376        })
 5377        .await
 5378        .unwrap();
 5379
 5380    let fake_server = fake_servers.next().await.unwrap();
 5381    let response = project.update(cx, |project, cx| {
 5382        let worktree = project.worktrees(cx).next().unwrap();
 5383        let entry = worktree
 5384            .read(cx)
 5385            .entry_for_path(rel_path("one.rs"))
 5386            .unwrap();
 5387        project.rename_entry(
 5388            entry.id,
 5389            (worktree.read(cx).id(), rel_path("three.rs")).into(),
 5390            cx,
 5391        )
 5392    });
 5393    let expected_edit = lsp::WorkspaceEdit {
 5394        changes: None,
 5395        document_changes: Some(DocumentChanges::Edits({
 5396            vec![TextDocumentEdit {
 5397                edits: vec![lsp::Edit::Plain(lsp::TextEdit {
 5398                    range: lsp::Range {
 5399                        start: lsp::Position {
 5400                            line: 0,
 5401                            character: 1,
 5402                        },
 5403                        end: lsp::Position {
 5404                            line: 0,
 5405                            character: 3,
 5406                        },
 5407                    },
 5408                    new_text: "This is not a drill".to_owned(),
 5409                })],
 5410                text_document: lsp::OptionalVersionedTextDocumentIdentifier {
 5411                    uri: Uri::from_str(uri!("file:///dir/two/two.rs")).unwrap(),
 5412                    version: Some(1337),
 5413                },
 5414            }]
 5415        })),
 5416        change_annotations: None,
 5417    };
 5418    let resolved_workspace_edit = Arc::new(OnceLock::new());
 5419    fake_server
 5420        .set_request_handler::<WillRenameFiles, _, _>({
 5421            let resolved_workspace_edit = resolved_workspace_edit.clone();
 5422            let expected_edit = expected_edit.clone();
 5423            move |params, _| {
 5424                let resolved_workspace_edit = resolved_workspace_edit.clone();
 5425                let expected_edit = expected_edit.clone();
 5426                async move {
 5427                    assert_eq!(params.files.len(), 1);
 5428                    assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
 5429                    assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
 5430                    resolved_workspace_edit.set(expected_edit.clone()).unwrap();
 5431                    Ok(Some(expected_edit))
 5432                }
 5433            }
 5434        })
 5435        .next()
 5436        .await
 5437        .unwrap();
 5438    let _ = response.await.unwrap();
 5439    fake_server
 5440        .handle_notification::<DidRenameFiles, _>(|params, _| {
 5441            assert_eq!(params.files.len(), 1);
 5442            assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
 5443            assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
 5444        })
 5445        .next()
 5446        .await
 5447        .unwrap();
 5448    assert_eq!(resolved_workspace_edit.get(), Some(&expected_edit));
 5449}
 5450
 5451#[gpui::test]
 5452async fn test_rename(cx: &mut gpui::TestAppContext) {
 5453    // hi
 5454    init_test(cx);
 5455
 5456    let fs = FakeFs::new(cx.executor());
 5457    fs.insert_tree(
 5458        path!("/dir"),
 5459        json!({
 5460            "one.rs": "const ONE: usize = 1;",
 5461            "two.rs": "const TWO: usize = one::ONE + one::ONE;"
 5462        }),
 5463    )
 5464    .await;
 5465
 5466    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5467
 5468    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 5469    language_registry.add(rust_lang());
 5470    let mut fake_servers = language_registry.register_fake_lsp(
 5471        "Rust",
 5472        FakeLspAdapter {
 5473            capabilities: lsp::ServerCapabilities {
 5474                rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
 5475                    prepare_provider: Some(true),
 5476                    work_done_progress_options: Default::default(),
 5477                })),
 5478                ..Default::default()
 5479            },
 5480            ..Default::default()
 5481        },
 5482    );
 5483
 5484    let (buffer, _handle) = project
 5485        .update(cx, |project, cx| {
 5486            project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
 5487        })
 5488        .await
 5489        .unwrap();
 5490
 5491    let fake_server = fake_servers.next().await.unwrap();
 5492
 5493    let response = project.update(cx, |project, cx| {
 5494        project.prepare_rename(buffer.clone(), 7, cx)
 5495    });
 5496    fake_server
 5497        .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
 5498            assert_eq!(
 5499                params.text_document.uri.as_str(),
 5500                uri!("file:///dir/one.rs")
 5501            );
 5502            assert_eq!(params.position, lsp::Position::new(0, 7));
 5503            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
 5504                lsp::Position::new(0, 6),
 5505                lsp::Position::new(0, 9),
 5506            ))))
 5507        })
 5508        .next()
 5509        .await
 5510        .unwrap();
 5511    let response = response.await.unwrap();
 5512    let PrepareRenameResponse::Success(range) = response else {
 5513        panic!("{:?}", response);
 5514    };
 5515    let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
 5516    assert_eq!(range, 6..9);
 5517
 5518    let response = project.update(cx, |project, cx| {
 5519        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), cx)
 5520    });
 5521    fake_server
 5522        .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
 5523            assert_eq!(
 5524                params.text_document_position.text_document.uri.as_str(),
 5525                uri!("file:///dir/one.rs")
 5526            );
 5527            assert_eq!(
 5528                params.text_document_position.position,
 5529                lsp::Position::new(0, 7)
 5530            );
 5531            assert_eq!(params.new_name, "THREE");
 5532            Ok(Some(lsp::WorkspaceEdit {
 5533                changes: Some(
 5534                    [
 5535                        (
 5536                            lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
 5537                            vec![lsp::TextEdit::new(
 5538                                lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
 5539                                "THREE".to_string(),
 5540                            )],
 5541                        ),
 5542                        (
 5543                            lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
 5544                            vec![
 5545                                lsp::TextEdit::new(
 5546                                    lsp::Range::new(
 5547                                        lsp::Position::new(0, 24),
 5548                                        lsp::Position::new(0, 27),
 5549                                    ),
 5550                                    "THREE".to_string(),
 5551                                ),
 5552                                lsp::TextEdit::new(
 5553                                    lsp::Range::new(
 5554                                        lsp::Position::new(0, 35),
 5555                                        lsp::Position::new(0, 38),
 5556                                    ),
 5557                                    "THREE".to_string(),
 5558                                ),
 5559                            ],
 5560                        ),
 5561                    ]
 5562                    .into_iter()
 5563                    .collect(),
 5564                ),
 5565                ..Default::default()
 5566            }))
 5567        })
 5568        .next()
 5569        .await
 5570        .unwrap();
 5571    let mut transaction = response.await.unwrap().0;
 5572    assert_eq!(transaction.len(), 2);
 5573    assert_eq!(
 5574        transaction
 5575            .remove_entry(&buffer)
 5576            .unwrap()
 5577            .0
 5578            .update(cx, |buffer, _| buffer.text()),
 5579        "const THREE: usize = 1;"
 5580    );
 5581    assert_eq!(
 5582        transaction
 5583            .into_keys()
 5584            .next()
 5585            .unwrap()
 5586            .update(cx, |buffer, _| buffer.text()),
 5587        "const TWO: usize = one::THREE + one::THREE;"
 5588    );
 5589}
 5590
 5591#[gpui::test]
 5592async fn test_search(cx: &mut gpui::TestAppContext) {
 5593    init_test(cx);
 5594
 5595    let fs = FakeFs::new(cx.executor());
 5596    fs.insert_tree(
 5597        path!("/dir"),
 5598        json!({
 5599            "one.rs": "const ONE: usize = 1;",
 5600            "two.rs": "const TWO: usize = one::ONE + one::ONE;",
 5601            "three.rs": "const THREE: usize = one::ONE + two::TWO;",
 5602            "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
 5603        }),
 5604    )
 5605    .await;
 5606    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5607    assert_eq!(
 5608        search(
 5609            &project,
 5610            SearchQuery::text(
 5611                "TWO",
 5612                false,
 5613                true,
 5614                false,
 5615                Default::default(),
 5616                Default::default(),
 5617                false,
 5618                None
 5619            )
 5620            .unwrap(),
 5621            cx
 5622        )
 5623        .await
 5624        .unwrap(),
 5625        HashMap::from_iter([
 5626            (path!("dir/two.rs").to_string(), vec![6..9]),
 5627            (path!("dir/three.rs").to_string(), vec![37..40])
 5628        ])
 5629    );
 5630
 5631    let buffer_4 = project
 5632        .update(cx, |project, cx| {
 5633            project.open_local_buffer(path!("/dir/four.rs"), cx)
 5634        })
 5635        .await
 5636        .unwrap();
 5637    buffer_4.update(cx, |buffer, cx| {
 5638        let text = "two::TWO";
 5639        buffer.edit([(20..28, text), (31..43, text)], None, cx);
 5640    });
 5641
 5642    assert_eq!(
 5643        search(
 5644            &project,
 5645            SearchQuery::text(
 5646                "TWO",
 5647                false,
 5648                true,
 5649                false,
 5650                Default::default(),
 5651                Default::default(),
 5652                false,
 5653                None,
 5654            )
 5655            .unwrap(),
 5656            cx
 5657        )
 5658        .await
 5659        .unwrap(),
 5660        HashMap::from_iter([
 5661            (path!("dir/two.rs").to_string(), vec![6..9]),
 5662            (path!("dir/three.rs").to_string(), vec![37..40]),
 5663            (path!("dir/four.rs").to_string(), vec![25..28, 36..39])
 5664        ])
 5665    );
 5666}
 5667
 5668#[gpui::test]
 5669async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
 5670    init_test(cx);
 5671
 5672    let search_query = "file";
 5673
 5674    let fs = FakeFs::new(cx.executor());
 5675    fs.insert_tree(
 5676        path!("/dir"),
 5677        json!({
 5678            "one.rs": r#"// Rust file one"#,
 5679            "one.ts": r#"// TypeScript file one"#,
 5680            "two.rs": r#"// Rust file two"#,
 5681            "two.ts": r#"// TypeScript file two"#,
 5682        }),
 5683    )
 5684    .await;
 5685    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5686
 5687    assert!(
 5688        search(
 5689            &project,
 5690            SearchQuery::text(
 5691                search_query,
 5692                false,
 5693                true,
 5694                false,
 5695                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
 5696                Default::default(),
 5697                false,
 5698                None
 5699            )
 5700            .unwrap(),
 5701            cx
 5702        )
 5703        .await
 5704        .unwrap()
 5705        .is_empty(),
 5706        "If no inclusions match, no files should be returned"
 5707    );
 5708
 5709    assert_eq!(
 5710        search(
 5711            &project,
 5712            SearchQuery::text(
 5713                search_query,
 5714                false,
 5715                true,
 5716                false,
 5717                PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
 5718                Default::default(),
 5719                false,
 5720                None
 5721            )
 5722            .unwrap(),
 5723            cx
 5724        )
 5725        .await
 5726        .unwrap(),
 5727        HashMap::from_iter([
 5728            (path!("dir/one.rs").to_string(), vec![8..12]),
 5729            (path!("dir/two.rs").to_string(), vec![8..12]),
 5730        ]),
 5731        "Rust only search should give only Rust files"
 5732    );
 5733
 5734    assert_eq!(
 5735        search(
 5736            &project,
 5737            SearchQuery::text(
 5738                search_query,
 5739                false,
 5740                true,
 5741                false,
 5742                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
 5743                    .unwrap(),
 5744                Default::default(),
 5745                false,
 5746                None,
 5747            )
 5748            .unwrap(),
 5749            cx
 5750        )
 5751        .await
 5752        .unwrap(),
 5753        HashMap::from_iter([
 5754            (path!("dir/one.ts").to_string(), vec![14..18]),
 5755            (path!("dir/two.ts").to_string(), vec![14..18]),
 5756        ]),
 5757        "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
 5758    );
 5759
 5760    assert_eq!(
 5761        search(
 5762            &project,
 5763            SearchQuery::text(
 5764                search_query,
 5765                false,
 5766                true,
 5767                false,
 5768                PathMatcher::new(
 5769                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
 5770                    PathStyle::local()
 5771                )
 5772                .unwrap(),
 5773                Default::default(),
 5774                false,
 5775                None,
 5776            )
 5777            .unwrap(),
 5778            cx
 5779        )
 5780        .await
 5781        .unwrap(),
 5782        HashMap::from_iter([
 5783            (path!("dir/two.ts").to_string(), vec![14..18]),
 5784            (path!("dir/one.rs").to_string(), vec![8..12]),
 5785            (path!("dir/one.ts").to_string(), vec![14..18]),
 5786            (path!("dir/two.rs").to_string(), vec![8..12]),
 5787        ]),
 5788        "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
 5789    );
 5790}
 5791
 5792#[gpui::test]
 5793async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
 5794    init_test(cx);
 5795
 5796    let search_query = "file";
 5797
 5798    let fs = FakeFs::new(cx.executor());
 5799    fs.insert_tree(
 5800        path!("/dir"),
 5801        json!({
 5802            "one.rs": r#"// Rust file one"#,
 5803            "one.ts": r#"// TypeScript file one"#,
 5804            "two.rs": r#"// Rust file two"#,
 5805            "two.ts": r#"// TypeScript file two"#,
 5806        }),
 5807    )
 5808    .await;
 5809    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5810
 5811    assert_eq!(
 5812        search(
 5813            &project,
 5814            SearchQuery::text(
 5815                search_query,
 5816                false,
 5817                true,
 5818                false,
 5819                Default::default(),
 5820                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
 5821                false,
 5822                None,
 5823            )
 5824            .unwrap(),
 5825            cx
 5826        )
 5827        .await
 5828        .unwrap(),
 5829        HashMap::from_iter([
 5830            (path!("dir/one.rs").to_string(), vec![8..12]),
 5831            (path!("dir/one.ts").to_string(), vec![14..18]),
 5832            (path!("dir/two.rs").to_string(), vec![8..12]),
 5833            (path!("dir/two.ts").to_string(), vec![14..18]),
 5834        ]),
 5835        "If no exclusions match, all files should be returned"
 5836    );
 5837
 5838    assert_eq!(
 5839        search(
 5840            &project,
 5841            SearchQuery::text(
 5842                search_query,
 5843                false,
 5844                true,
 5845                false,
 5846                Default::default(),
 5847                PathMatcher::new(&["*.rs".to_owned()], PathStyle::local()).unwrap(),
 5848                false,
 5849                None,
 5850            )
 5851            .unwrap(),
 5852            cx
 5853        )
 5854        .await
 5855        .unwrap(),
 5856        HashMap::from_iter([
 5857            (path!("dir/one.ts").to_string(), vec![14..18]),
 5858            (path!("dir/two.ts").to_string(), vec![14..18]),
 5859        ]),
 5860        "Rust exclusion search should give only TypeScript files"
 5861    );
 5862
 5863    assert_eq!(
 5864        search(
 5865            &project,
 5866            SearchQuery::text(
 5867                search_query,
 5868                false,
 5869                true,
 5870                false,
 5871                Default::default(),
 5872                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
 5873                    .unwrap(),
 5874                false,
 5875                None,
 5876            )
 5877            .unwrap(),
 5878            cx
 5879        )
 5880        .await
 5881        .unwrap(),
 5882        HashMap::from_iter([
 5883            (path!("dir/one.rs").to_string(), vec![8..12]),
 5884            (path!("dir/two.rs").to_string(), vec![8..12]),
 5885        ]),
 5886        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
 5887    );
 5888
 5889    assert!(
 5890        search(
 5891            &project,
 5892            SearchQuery::text(
 5893                search_query,
 5894                false,
 5895                true,
 5896                false,
 5897                Default::default(),
 5898                PathMatcher::new(
 5899                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
 5900                    PathStyle::local(),
 5901                )
 5902                .unwrap(),
 5903                false,
 5904                None,
 5905            )
 5906            .unwrap(),
 5907            cx
 5908        )
 5909        .await
 5910        .unwrap()
 5911        .is_empty(),
 5912        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
 5913    );
 5914}
 5915
 5916#[gpui::test]
 5917async fn test_search_with_buffer_exclusions(cx: &mut gpui::TestAppContext) {
 5918    init_test(cx);
 5919
 5920    let search_query = "file";
 5921
 5922    let fs = FakeFs::new(cx.executor());
 5923    fs.insert_tree(
 5924        path!("/dir"),
 5925        json!({
 5926            "one.rs": r#"// Rust file one"#,
 5927            "one.ts": r#"// TypeScript file one"#,
 5928            "two.rs": r#"// Rust file two"#,
 5929            "two.ts": r#"// TypeScript file two"#,
 5930        }),
 5931    )
 5932    .await;
 5933
 5934    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 5935    let path_style = PathStyle::local();
 5936    let _buffer = project.update(cx, |project, cx| {
 5937        project.create_local_buffer("file", None, false, cx)
 5938    });
 5939
 5940    assert_eq!(
 5941        search(
 5942            &project,
 5943            SearchQuery::text(
 5944                search_query,
 5945                false,
 5946                true,
 5947                false,
 5948                Default::default(),
 5949                PathMatcher::new(&["*.odd".to_owned()], path_style).unwrap(),
 5950                false,
 5951                None,
 5952            )
 5953            .unwrap(),
 5954            cx
 5955        )
 5956        .await
 5957        .unwrap(),
 5958        HashMap::from_iter([
 5959            (path!("dir/one.rs").to_string(), vec![8..12]),
 5960            (path!("dir/one.ts").to_string(), vec![14..18]),
 5961            (path!("dir/two.rs").to_string(), vec![8..12]),
 5962            (path!("dir/two.ts").to_string(), vec![14..18]),
 5963        ]),
 5964        "If no exclusions match, all files should be returned"
 5965    );
 5966
 5967    assert_eq!(
 5968        search(
 5969            &project,
 5970            SearchQuery::text(
 5971                search_query,
 5972                false,
 5973                true,
 5974                false,
 5975                Default::default(),
 5976                PathMatcher::new(&["*.rs".to_owned()], path_style).unwrap(),
 5977                false,
 5978                None,
 5979            )
 5980            .unwrap(),
 5981            cx
 5982        )
 5983        .await
 5984        .unwrap(),
 5985        HashMap::from_iter([
 5986            (path!("dir/one.ts").to_string(), vec![14..18]),
 5987            (path!("dir/two.ts").to_string(), vec![14..18]),
 5988        ]),
 5989        "Rust exclusion search should give only TypeScript files"
 5990    );
 5991
 5992    assert_eq!(
 5993        search(
 5994            &project,
 5995            SearchQuery::text(
 5996                search_query,
 5997                false,
 5998                true,
 5999                false,
 6000                Default::default(),
 6001                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], path_style).unwrap(),
 6002                false,
 6003                None,
 6004            )
 6005            .unwrap(),
 6006            cx
 6007        )
 6008        .await
 6009        .unwrap(),
 6010        HashMap::from_iter([
 6011            (path!("dir/one.rs").to_string(), vec![8..12]),
 6012            (path!("dir/two.rs").to_string(), vec![8..12]),
 6013        ]),
 6014        "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
 6015    );
 6016
 6017    assert!(
 6018        search(
 6019            &project,
 6020            SearchQuery::text(
 6021                search_query,
 6022                false,
 6023                true,
 6024                false,
 6025                Default::default(),
 6026                PathMatcher::new(
 6027                    &["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()],
 6028                    PathStyle::local(),
 6029                )
 6030                .unwrap(),
 6031                false,
 6032                None,
 6033            )
 6034            .unwrap(),
 6035            cx
 6036        )
 6037        .await
 6038        .unwrap()
 6039        .is_empty(),
 6040        "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
 6041    );
 6042}
 6043
 6044#[gpui::test]
 6045async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
 6046    init_test(cx);
 6047
 6048    let search_query = "file";
 6049
 6050    let fs = FakeFs::new(cx.executor());
 6051    fs.insert_tree(
 6052        path!("/dir"),
 6053        json!({
 6054            "one.rs": r#"// Rust file one"#,
 6055            "one.ts": r#"// TypeScript file one"#,
 6056            "two.rs": r#"// Rust file two"#,
 6057            "two.ts": r#"// TypeScript file two"#,
 6058        }),
 6059    )
 6060    .await;
 6061    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 6062    assert!(
 6063        search(
 6064            &project,
 6065            SearchQuery::text(
 6066                search_query,
 6067                false,
 6068                true,
 6069                false,
 6070                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
 6071                PathMatcher::new(&["*.odd".to_owned()], PathStyle::local()).unwrap(),
 6072                false,
 6073                None,
 6074            )
 6075            .unwrap(),
 6076            cx
 6077        )
 6078        .await
 6079        .unwrap()
 6080        .is_empty(),
 6081        "If both no exclusions and inclusions match, exclusions should win and return nothing"
 6082    );
 6083
 6084    assert!(
 6085        search(
 6086            &project,
 6087            SearchQuery::text(
 6088                search_query,
 6089                false,
 6090                true,
 6091                false,
 6092                PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
 6093                PathMatcher::new(&["*.ts".to_owned()], PathStyle::local()).unwrap(),
 6094                false,
 6095                None,
 6096            )
 6097            .unwrap(),
 6098            cx
 6099        )
 6100        .await
 6101        .unwrap()
 6102        .is_empty(),
 6103        "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
 6104    );
 6105
 6106    assert!(
 6107        search(
 6108            &project,
 6109            SearchQuery::text(
 6110                search_query,
 6111                false,
 6112                true,
 6113                false,
 6114                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
 6115                    .unwrap(),
 6116                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
 6117                    .unwrap(),
 6118                false,
 6119                None,
 6120            )
 6121            .unwrap(),
 6122            cx
 6123        )
 6124        .await
 6125        .unwrap()
 6126        .is_empty(),
 6127        "Non-matching inclusions and exclusions should not change that."
 6128    );
 6129
 6130    assert_eq!(
 6131        search(
 6132            &project,
 6133            SearchQuery::text(
 6134                search_query,
 6135                false,
 6136                true,
 6137                false,
 6138                PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()], PathStyle::local())
 6139                    .unwrap(),
 6140                PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()], PathStyle::local())
 6141                    .unwrap(),
 6142                false,
 6143                None,
 6144            )
 6145            .unwrap(),
 6146            cx
 6147        )
 6148        .await
 6149        .unwrap(),
 6150        HashMap::from_iter([
 6151            (path!("dir/one.ts").to_string(), vec![14..18]),
 6152            (path!("dir/two.ts").to_string(), vec![14..18]),
 6153        ]),
 6154        "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
 6155    );
 6156}
 6157
 6158#[gpui::test]
 6159async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
 6160    init_test(cx);
 6161
 6162    let fs = FakeFs::new(cx.executor());
 6163    fs.insert_tree(
 6164        path!("/worktree-a"),
 6165        json!({
 6166            "haystack.rs": r#"// NEEDLE"#,
 6167            "haystack.ts": r#"// NEEDLE"#,
 6168        }),
 6169    )
 6170    .await;
 6171    fs.insert_tree(
 6172        path!("/worktree-b"),
 6173        json!({
 6174            "haystack.rs": r#"// NEEDLE"#,
 6175            "haystack.ts": r#"// NEEDLE"#,
 6176        }),
 6177    )
 6178    .await;
 6179
 6180    let path_style = PathStyle::local();
 6181    let project = Project::test(
 6182        fs.clone(),
 6183        [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
 6184        cx,
 6185    )
 6186    .await;
 6187
 6188    assert_eq!(
 6189        search(
 6190            &project,
 6191            SearchQuery::text(
 6192                "NEEDLE",
 6193                false,
 6194                true,
 6195                false,
 6196                PathMatcher::new(&["worktree-a/*.rs".to_owned()], path_style).unwrap(),
 6197                Default::default(),
 6198                true,
 6199                None,
 6200            )
 6201            .unwrap(),
 6202            cx
 6203        )
 6204        .await
 6205        .unwrap(),
 6206        HashMap::from_iter([(path!("worktree-a/haystack.rs").to_string(), vec![3..9])]),
 6207        "should only return results from included worktree"
 6208    );
 6209    assert_eq!(
 6210        search(
 6211            &project,
 6212            SearchQuery::text(
 6213                "NEEDLE",
 6214                false,
 6215                true,
 6216                false,
 6217                PathMatcher::new(&["worktree-b/*.rs".to_owned()], path_style).unwrap(),
 6218                Default::default(),
 6219                true,
 6220                None,
 6221            )
 6222            .unwrap(),
 6223            cx
 6224        )
 6225        .await
 6226        .unwrap(),
 6227        HashMap::from_iter([(path!("worktree-b/haystack.rs").to_string(), vec![3..9])]),
 6228        "should only return results from included worktree"
 6229    );
 6230
 6231    assert_eq!(
 6232        search(
 6233            &project,
 6234            SearchQuery::text(
 6235                "NEEDLE",
 6236                false,
 6237                true,
 6238                false,
 6239                PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap(),
 6240                Default::default(),
 6241                false,
 6242                None,
 6243            )
 6244            .unwrap(),
 6245            cx
 6246        )
 6247        .await
 6248        .unwrap(),
 6249        HashMap::from_iter([
 6250            (path!("worktree-a/haystack.ts").to_string(), vec![3..9]),
 6251            (path!("worktree-b/haystack.ts").to_string(), vec![3..9])
 6252        ]),
 6253        "should return results from both worktrees"
 6254    );
 6255}
 6256
 6257#[gpui::test]
 6258async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
 6259    init_test(cx);
 6260
 6261    let fs = FakeFs::new(cx.background_executor.clone());
 6262    fs.insert_tree(
 6263        path!("/dir"),
 6264        json!({
 6265            ".git": {},
 6266            ".gitignore": "**/target\n/node_modules\n",
 6267            "target": {
 6268                "index.txt": "index_key:index_value"
 6269            },
 6270            "node_modules": {
 6271                "eslint": {
 6272                    "index.ts": "const eslint_key = 'eslint value'",
 6273                    "package.json": r#"{ "some_key": "some value" }"#,
 6274                },
 6275                "prettier": {
 6276                    "index.ts": "const prettier_key = 'prettier value'",
 6277                    "package.json": r#"{ "other_key": "other value" }"#,
 6278                },
 6279            },
 6280            "package.json": r#"{ "main_key": "main value" }"#,
 6281        }),
 6282    )
 6283    .await;
 6284    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 6285
 6286    let query = "key";
 6287    assert_eq!(
 6288        search(
 6289            &project,
 6290            SearchQuery::text(
 6291                query,
 6292                false,
 6293                false,
 6294                false,
 6295                Default::default(),
 6296                Default::default(),
 6297                false,
 6298                None,
 6299            )
 6300            .unwrap(),
 6301            cx
 6302        )
 6303        .await
 6304        .unwrap(),
 6305        HashMap::from_iter([(path!("dir/package.json").to_string(), vec![8..11])]),
 6306        "Only one non-ignored file should have the query"
 6307    );
 6308
 6309    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 6310    let path_style = PathStyle::local();
 6311    assert_eq!(
 6312        search(
 6313            &project,
 6314            SearchQuery::text(
 6315                query,
 6316                false,
 6317                false,
 6318                true,
 6319                Default::default(),
 6320                Default::default(),
 6321                false,
 6322                None,
 6323            )
 6324            .unwrap(),
 6325            cx
 6326        )
 6327        .await
 6328        .unwrap(),
 6329        HashMap::from_iter([
 6330            (path!("dir/package.json").to_string(), vec![8..11]),
 6331            (path!("dir/target/index.txt").to_string(), vec![6..9]),
 6332            (
 6333                path!("dir/node_modules/prettier/package.json").to_string(),
 6334                vec![9..12]
 6335            ),
 6336            (
 6337                path!("dir/node_modules/prettier/index.ts").to_string(),
 6338                vec![15..18]
 6339            ),
 6340            (
 6341                path!("dir/node_modules/eslint/index.ts").to_string(),
 6342                vec![13..16]
 6343            ),
 6344            (
 6345                path!("dir/node_modules/eslint/package.json").to_string(),
 6346                vec![8..11]
 6347            ),
 6348        ]),
 6349        "Unrestricted search with ignored directories should find every file with the query"
 6350    );
 6351
 6352    let files_to_include =
 6353        PathMatcher::new(&["node_modules/prettier/**".to_owned()], path_style).unwrap();
 6354    let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()], path_style).unwrap();
 6355    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 6356    assert_eq!(
 6357        search(
 6358            &project,
 6359            SearchQuery::text(
 6360                query,
 6361                false,
 6362                false,
 6363                true,
 6364                files_to_include,
 6365                files_to_exclude,
 6366                false,
 6367                None,
 6368            )
 6369            .unwrap(),
 6370            cx
 6371        )
 6372        .await
 6373        .unwrap(),
 6374        HashMap::from_iter([(
 6375            path!("dir/node_modules/prettier/package.json").to_string(),
 6376            vec![9..12]
 6377        )]),
 6378        "With search including ignored prettier directory and excluding TS files, only one file should be found"
 6379    );
 6380}
 6381
 6382#[gpui::test]
 6383async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
 6384    init_test(cx);
 6385
 6386    let fs = FakeFs::new(cx.executor());
 6387    fs.insert_tree(
 6388        path!("/dir"),
 6389        json!({
 6390            "one.rs": "// ПРИВЕТ? привет!",
 6391            "two.rs": "// ПРИВЕТ.",
 6392            "three.rs": "// привет",
 6393        }),
 6394    )
 6395    .await;
 6396    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 6397    let unicode_case_sensitive_query = SearchQuery::text(
 6398        "привет",
 6399        false,
 6400        true,
 6401        false,
 6402        Default::default(),
 6403        Default::default(),
 6404        false,
 6405        None,
 6406    );
 6407    assert_matches!(unicode_case_sensitive_query, Ok(SearchQuery::Text { .. }));
 6408    assert_eq!(
 6409        search(&project, unicode_case_sensitive_query.unwrap(), cx)
 6410            .await
 6411            .unwrap(),
 6412        HashMap::from_iter([
 6413            (path!("dir/one.rs").to_string(), vec![17..29]),
 6414            (path!("dir/three.rs").to_string(), vec![3..15]),
 6415        ])
 6416    );
 6417
 6418    let unicode_case_insensitive_query = SearchQuery::text(
 6419        "привет",
 6420        false,
 6421        false,
 6422        false,
 6423        Default::default(),
 6424        Default::default(),
 6425        false,
 6426        None,
 6427    );
 6428    assert_matches!(
 6429        unicode_case_insensitive_query,
 6430        Ok(SearchQuery::Regex { .. })
 6431    );
 6432    assert_eq!(
 6433        search(&project, unicode_case_insensitive_query.unwrap(), cx)
 6434            .await
 6435            .unwrap(),
 6436        HashMap::from_iter([
 6437            (path!("dir/one.rs").to_string(), vec![3..15, 17..29]),
 6438            (path!("dir/two.rs").to_string(), vec![3..15]),
 6439            (path!("dir/three.rs").to_string(), vec![3..15]),
 6440        ])
 6441    );
 6442
 6443    assert_eq!(
 6444        search(
 6445            &project,
 6446            SearchQuery::text(
 6447                "привет.",
 6448                false,
 6449                false,
 6450                false,
 6451                Default::default(),
 6452                Default::default(),
 6453                false,
 6454                None,
 6455            )
 6456            .unwrap(),
 6457            cx
 6458        )
 6459        .await
 6460        .unwrap(),
 6461        HashMap::from_iter([(path!("dir/two.rs").to_string(), vec![3..16]),])
 6462    );
 6463}
 6464
 6465#[gpui::test]
 6466async fn test_create_entry(cx: &mut gpui::TestAppContext) {
 6467    init_test(cx);
 6468
 6469    let fs = FakeFs::new(cx.executor());
 6470    fs.insert_tree(
 6471        "/one/two",
 6472        json!({
 6473            "three": {
 6474                "a.txt": "",
 6475                "four": {}
 6476            },
 6477            "c.rs": ""
 6478        }),
 6479    )
 6480    .await;
 6481
 6482    let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
 6483    project
 6484        .update(cx, |project, cx| {
 6485            let id = project.worktrees(cx).next().unwrap().read(cx).id();
 6486            project.create_entry((id, rel_path("b..")), true, cx)
 6487        })
 6488        .await
 6489        .unwrap()
 6490        .into_included()
 6491        .unwrap();
 6492
 6493    assert_eq!(
 6494        fs.paths(true),
 6495        vec![
 6496            PathBuf::from(path!("/")),
 6497            PathBuf::from(path!("/one")),
 6498            PathBuf::from(path!("/one/two")),
 6499            PathBuf::from(path!("/one/two/c.rs")),
 6500            PathBuf::from(path!("/one/two/three")),
 6501            PathBuf::from(path!("/one/two/three/a.txt")),
 6502            PathBuf::from(path!("/one/two/three/b..")),
 6503            PathBuf::from(path!("/one/two/three/four")),
 6504        ]
 6505    );
 6506}
 6507
 6508#[gpui::test]
 6509async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
 6510    init_test(cx);
 6511
 6512    let fs = FakeFs::new(cx.executor());
 6513    fs.insert_tree(
 6514        path!("/dir"),
 6515        json!({
 6516            "a.tsx": "a",
 6517        }),
 6518    )
 6519    .await;
 6520
 6521    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 6522
 6523    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 6524    language_registry.add(tsx_lang());
 6525    let language_server_names = [
 6526        "TypeScriptServer",
 6527        "TailwindServer",
 6528        "ESLintServer",
 6529        "NoHoverCapabilitiesServer",
 6530    ];
 6531    let mut language_servers = [
 6532        language_registry.register_fake_lsp(
 6533            "tsx",
 6534            FakeLspAdapter {
 6535                name: language_server_names[0],
 6536                capabilities: lsp::ServerCapabilities {
 6537                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 6538                    ..lsp::ServerCapabilities::default()
 6539                },
 6540                ..FakeLspAdapter::default()
 6541            },
 6542        ),
 6543        language_registry.register_fake_lsp(
 6544            "tsx",
 6545            FakeLspAdapter {
 6546                name: language_server_names[1],
 6547                capabilities: lsp::ServerCapabilities {
 6548                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 6549                    ..lsp::ServerCapabilities::default()
 6550                },
 6551                ..FakeLspAdapter::default()
 6552            },
 6553        ),
 6554        language_registry.register_fake_lsp(
 6555            "tsx",
 6556            FakeLspAdapter {
 6557                name: language_server_names[2],
 6558                capabilities: lsp::ServerCapabilities {
 6559                    hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 6560                    ..lsp::ServerCapabilities::default()
 6561                },
 6562                ..FakeLspAdapter::default()
 6563            },
 6564        ),
 6565        language_registry.register_fake_lsp(
 6566            "tsx",
 6567            FakeLspAdapter {
 6568                name: language_server_names[3],
 6569                capabilities: lsp::ServerCapabilities {
 6570                    hover_provider: None,
 6571                    ..lsp::ServerCapabilities::default()
 6572                },
 6573                ..FakeLspAdapter::default()
 6574            },
 6575        ),
 6576    ];
 6577
 6578    let (buffer, _handle) = project
 6579        .update(cx, |p, cx| {
 6580            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
 6581        })
 6582        .await
 6583        .unwrap();
 6584    cx.executor().run_until_parked();
 6585
 6586    let mut servers_with_hover_requests = HashMap::default();
 6587    for i in 0..language_server_names.len() {
 6588        let new_server = language_servers[i].next().await.unwrap_or_else(|| {
 6589            panic!(
 6590                "Failed to get language server #{i} with name {}",
 6591                &language_server_names[i]
 6592            )
 6593        });
 6594        let new_server_name = new_server.server.name();
 6595        assert!(
 6596            !servers_with_hover_requests.contains_key(&new_server_name),
 6597            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
 6598        );
 6599        match new_server_name.as_ref() {
 6600            "TailwindServer" | "TypeScriptServer" => {
 6601                servers_with_hover_requests.insert(
 6602                    new_server_name.clone(),
 6603                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
 6604                        move |_, _| {
 6605                            let name = new_server_name.clone();
 6606                            async move {
 6607                                Ok(Some(lsp::Hover {
 6608                                    contents: lsp::HoverContents::Scalar(
 6609                                        lsp::MarkedString::String(format!("{name} hover")),
 6610                                    ),
 6611                                    range: None,
 6612                                }))
 6613                            }
 6614                        },
 6615                    ),
 6616                );
 6617            }
 6618            "ESLintServer" => {
 6619                servers_with_hover_requests.insert(
 6620                    new_server_name,
 6621                    new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
 6622                        |_, _| async move { Ok(None) },
 6623                    ),
 6624                );
 6625            }
 6626            "NoHoverCapabilitiesServer" => {
 6627                let _never_handled = new_server
 6628                    .set_request_handler::<lsp::request::HoverRequest, _, _>(|_, _| async move {
 6629                        panic!(
 6630                            "Should not call for hovers server with no corresponding capabilities"
 6631                        )
 6632                    });
 6633            }
 6634            unexpected => panic!("Unexpected server name: {unexpected}"),
 6635        }
 6636    }
 6637
 6638    let hover_task = project.update(cx, |project, cx| {
 6639        project.hover(&buffer, Point::new(0, 0), cx)
 6640    });
 6641    let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
 6642        |mut hover_request| async move {
 6643            hover_request
 6644                .next()
 6645                .await
 6646                .expect("All hover requests should have been triggered")
 6647        },
 6648    ))
 6649    .await;
 6650    assert_eq!(
 6651        vec!["TailwindServer hover", "TypeScriptServer hover"],
 6652        hover_task
 6653            .await
 6654            .into_iter()
 6655            .flatten()
 6656            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
 6657            .sorted()
 6658            .collect::<Vec<_>>(),
 6659        "Should receive hover responses from all related servers with hover capabilities"
 6660    );
 6661}
 6662
 6663#[gpui::test]
 6664async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
 6665    init_test(cx);
 6666
 6667    let fs = FakeFs::new(cx.executor());
 6668    fs.insert_tree(
 6669        path!("/dir"),
 6670        json!({
 6671            "a.ts": "a",
 6672        }),
 6673    )
 6674    .await;
 6675
 6676    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 6677
 6678    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 6679    language_registry.add(typescript_lang());
 6680    let mut fake_language_servers = language_registry.register_fake_lsp(
 6681        "TypeScript",
 6682        FakeLspAdapter {
 6683            capabilities: lsp::ServerCapabilities {
 6684                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 6685                ..lsp::ServerCapabilities::default()
 6686            },
 6687            ..FakeLspAdapter::default()
 6688        },
 6689    );
 6690
 6691    let (buffer, _handle) = project
 6692        .update(cx, |p, cx| {
 6693            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 6694        })
 6695        .await
 6696        .unwrap();
 6697    cx.executor().run_until_parked();
 6698
 6699    let fake_server = fake_language_servers
 6700        .next()
 6701        .await
 6702        .expect("failed to get the language server");
 6703
 6704    let mut request_handled = fake_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
 6705        move |_, _| async move {
 6706            Ok(Some(lsp::Hover {
 6707                contents: lsp::HoverContents::Array(vec![
 6708                    lsp::MarkedString::String("".to_string()),
 6709                    lsp::MarkedString::String("      ".to_string()),
 6710                    lsp::MarkedString::String("\n\n\n".to_string()),
 6711                ]),
 6712                range: None,
 6713            }))
 6714        },
 6715    );
 6716
 6717    let hover_task = project.update(cx, |project, cx| {
 6718        project.hover(&buffer, Point::new(0, 0), cx)
 6719    });
 6720    let () = request_handled
 6721        .next()
 6722        .await
 6723        .expect("All hover requests should have been triggered");
 6724    assert_eq!(
 6725        Vec::<String>::new(),
 6726        hover_task
 6727            .await
 6728            .into_iter()
 6729            .flatten()
 6730            .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
 6731            .sorted()
 6732            .collect::<Vec<_>>(),
 6733        "Empty hover parts should be ignored"
 6734    );
 6735}
 6736
 6737#[gpui::test]
 6738async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
 6739    init_test(cx);
 6740
 6741    let fs = FakeFs::new(cx.executor());
 6742    fs.insert_tree(
 6743        path!("/dir"),
 6744        json!({
 6745            "a.ts": "a",
 6746        }),
 6747    )
 6748    .await;
 6749
 6750    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 6751
 6752    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 6753    language_registry.add(typescript_lang());
 6754    let mut fake_language_servers = language_registry.register_fake_lsp(
 6755        "TypeScript",
 6756        FakeLspAdapter {
 6757            capabilities: lsp::ServerCapabilities {
 6758                code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
 6759                ..lsp::ServerCapabilities::default()
 6760            },
 6761            ..FakeLspAdapter::default()
 6762        },
 6763    );
 6764
 6765    let (buffer, _handle) = project
 6766        .update(cx, |p, cx| {
 6767            p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
 6768        })
 6769        .await
 6770        .unwrap();
 6771    cx.executor().run_until_parked();
 6772
 6773    let fake_server = fake_language_servers
 6774        .next()
 6775        .await
 6776        .expect("failed to get the language server");
 6777
 6778    let mut request_handled = fake_server
 6779        .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |_, _| async move {
 6780            Ok(Some(vec![
 6781                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
 6782                    title: "organize imports".to_string(),
 6783                    kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
 6784                    ..lsp::CodeAction::default()
 6785                }),
 6786                lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
 6787                    title: "fix code".to_string(),
 6788                    kind: Some(CodeActionKind::SOURCE_FIX_ALL),
 6789                    ..lsp::CodeAction::default()
 6790                }),
 6791            ]))
 6792        });
 6793
 6794    let code_actions_task = project.update(cx, |project, cx| {
 6795        project.code_actions(
 6796            &buffer,
 6797            0..buffer.read(cx).len(),
 6798            Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]),
 6799            cx,
 6800        )
 6801    });
 6802
 6803    let () = request_handled
 6804        .next()
 6805        .await
 6806        .expect("The code action request should have been triggered");
 6807
 6808    let code_actions = code_actions_task.await.unwrap().unwrap();
 6809    assert_eq!(code_actions.len(), 1);
 6810    assert_eq!(
 6811        code_actions[0].lsp_action.action_kind(),
 6812        Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
 6813    );
 6814}
 6815
 6816#[gpui::test]
 6817async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
 6818    init_test(cx);
 6819
 6820    let fs = FakeFs::new(cx.executor());
 6821    fs.insert_tree(
 6822        path!("/dir"),
 6823        json!({
 6824            "a.tsx": "a",
 6825        }),
 6826    )
 6827    .await;
 6828
 6829    let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
 6830
 6831    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 6832    language_registry.add(tsx_lang());
 6833    let language_server_names = [
 6834        "TypeScriptServer",
 6835        "TailwindServer",
 6836        "ESLintServer",
 6837        "NoActionsCapabilitiesServer",
 6838    ];
 6839
 6840    let mut language_server_rxs = [
 6841        language_registry.register_fake_lsp(
 6842            "tsx",
 6843            FakeLspAdapter {
 6844                name: language_server_names[0],
 6845                capabilities: lsp::ServerCapabilities {
 6846                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
 6847                    ..lsp::ServerCapabilities::default()
 6848                },
 6849                ..FakeLspAdapter::default()
 6850            },
 6851        ),
 6852        language_registry.register_fake_lsp(
 6853            "tsx",
 6854            FakeLspAdapter {
 6855                name: language_server_names[1],
 6856                capabilities: lsp::ServerCapabilities {
 6857                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
 6858                    ..lsp::ServerCapabilities::default()
 6859                },
 6860                ..FakeLspAdapter::default()
 6861            },
 6862        ),
 6863        language_registry.register_fake_lsp(
 6864            "tsx",
 6865            FakeLspAdapter {
 6866                name: language_server_names[2],
 6867                capabilities: lsp::ServerCapabilities {
 6868                    code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
 6869                    ..lsp::ServerCapabilities::default()
 6870                },
 6871                ..FakeLspAdapter::default()
 6872            },
 6873        ),
 6874        language_registry.register_fake_lsp(
 6875            "tsx",
 6876            FakeLspAdapter {
 6877                name: language_server_names[3],
 6878                capabilities: lsp::ServerCapabilities {
 6879                    code_action_provider: None,
 6880                    ..lsp::ServerCapabilities::default()
 6881                },
 6882                ..FakeLspAdapter::default()
 6883            },
 6884        ),
 6885    ];
 6886
 6887    let (buffer, _handle) = project
 6888        .update(cx, |p, cx| {
 6889            p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
 6890        })
 6891        .await
 6892        .unwrap();
 6893    cx.executor().run_until_parked();
 6894
 6895    let mut servers_with_actions_requests = HashMap::default();
 6896    for i in 0..language_server_names.len() {
 6897        let new_server = language_server_rxs[i].next().await.unwrap_or_else(|| {
 6898            panic!(
 6899                "Failed to get language server #{i} with name {}",
 6900                &language_server_names[i]
 6901            )
 6902        });
 6903        let new_server_name = new_server.server.name();
 6904
 6905        assert!(
 6906            !servers_with_actions_requests.contains_key(&new_server_name),
 6907            "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
 6908        );
 6909        match new_server_name.0.as_ref() {
 6910            "TailwindServer" | "TypeScriptServer" => {
 6911                servers_with_actions_requests.insert(
 6912                    new_server_name.clone(),
 6913                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
 6914                        move |_, _| {
 6915                            let name = new_server_name.clone();
 6916                            async move {
 6917                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
 6918                                    lsp::CodeAction {
 6919                                        title: format!("{name} code action"),
 6920                                        ..lsp::CodeAction::default()
 6921                                    },
 6922                                )]))
 6923                            }
 6924                        },
 6925                    ),
 6926                );
 6927            }
 6928            "ESLintServer" => {
 6929                servers_with_actions_requests.insert(
 6930                    new_server_name,
 6931                    new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
 6932                        |_, _| async move { Ok(None) },
 6933                    ),
 6934                );
 6935            }
 6936            "NoActionsCapabilitiesServer" => {
 6937                let _never_handled = new_server
 6938                    .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
 6939                        panic!(
 6940                            "Should not call for code actions server with no corresponding capabilities"
 6941                        )
 6942                    });
 6943            }
 6944            unexpected => panic!("Unexpected server name: {unexpected}"),
 6945        }
 6946    }
 6947
 6948    let code_actions_task = project.update(cx, |project, cx| {
 6949        project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx)
 6950    });
 6951
 6952    // cx.run_until_parked();
 6953    let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
 6954        |mut code_actions_request| async move {
 6955            code_actions_request
 6956                .next()
 6957                .await
 6958                .expect("All code actions requests should have been triggered")
 6959        },
 6960    ))
 6961    .await;
 6962    assert_eq!(
 6963        vec!["TailwindServer code action", "TypeScriptServer code action"],
 6964        code_actions_task
 6965            .await
 6966            .unwrap()
 6967            .unwrap()
 6968            .into_iter()
 6969            .map(|code_action| code_action.lsp_action.title().to_owned())
 6970            .sorted()
 6971            .collect::<Vec<_>>(),
 6972        "Should receive code actions responses from all related servers with hover capabilities"
 6973    );
 6974}
 6975
 6976#[gpui::test]
 6977async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
 6978    init_test(cx);
 6979
 6980    let fs = FakeFs::new(cx.executor());
 6981    fs.insert_tree(
 6982        "/dir",
 6983        json!({
 6984            "a.rs": "let a = 1;",
 6985            "b.rs": "let b = 2;",
 6986            "c.rs": "let c = 2;",
 6987        }),
 6988    )
 6989    .await;
 6990
 6991    let project = Project::test(
 6992        fs,
 6993        [
 6994            "/dir/a.rs".as_ref(),
 6995            "/dir/b.rs".as_ref(),
 6996            "/dir/c.rs".as_ref(),
 6997        ],
 6998        cx,
 6999    )
 7000    .await;
 7001
 7002    // check the initial state and get the worktrees
 7003    let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
 7004        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7005        assert_eq!(worktrees.len(), 3);
 7006
 7007        let worktree_a = worktrees[0].read(cx);
 7008        let worktree_b = worktrees[1].read(cx);
 7009        let worktree_c = worktrees[2].read(cx);
 7010
 7011        // check they start in the right order
 7012        assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
 7013        assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
 7014        assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
 7015
 7016        (
 7017            worktrees[0].clone(),
 7018            worktrees[1].clone(),
 7019            worktrees[2].clone(),
 7020        )
 7021    });
 7022
 7023    // move first worktree to after the second
 7024    // [a, b, c] -> [b, a, c]
 7025    project
 7026        .update(cx, |project, cx| {
 7027            let first = worktree_a.read(cx);
 7028            let second = worktree_b.read(cx);
 7029            project.move_worktree(first.id(), second.id(), cx)
 7030        })
 7031        .expect("moving first after second");
 7032
 7033    // check the state after moving
 7034    project.update(cx, |project, cx| {
 7035        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7036        assert_eq!(worktrees.len(), 3);
 7037
 7038        let first = worktrees[0].read(cx);
 7039        let second = worktrees[1].read(cx);
 7040        let third = worktrees[2].read(cx);
 7041
 7042        // check they are now in the right order
 7043        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
 7044        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
 7045        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
 7046    });
 7047
 7048    // move the second worktree to before the first
 7049    // [b, a, c] -> [a, b, c]
 7050    project
 7051        .update(cx, |project, cx| {
 7052            let second = worktree_a.read(cx);
 7053            let first = worktree_b.read(cx);
 7054            project.move_worktree(first.id(), second.id(), cx)
 7055        })
 7056        .expect("moving second before first");
 7057
 7058    // check the state after moving
 7059    project.update(cx, |project, cx| {
 7060        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7061        assert_eq!(worktrees.len(), 3);
 7062
 7063        let first = worktrees[0].read(cx);
 7064        let second = worktrees[1].read(cx);
 7065        let third = worktrees[2].read(cx);
 7066
 7067        // check they are now in the right order
 7068        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
 7069        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
 7070        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
 7071    });
 7072
 7073    // move the second worktree to after the third
 7074    // [a, b, c] -> [a, c, b]
 7075    project
 7076        .update(cx, |project, cx| {
 7077            let second = worktree_b.read(cx);
 7078            let third = worktree_c.read(cx);
 7079            project.move_worktree(second.id(), third.id(), cx)
 7080        })
 7081        .expect("moving second after third");
 7082
 7083    // check the state after moving
 7084    project.update(cx, |project, cx| {
 7085        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7086        assert_eq!(worktrees.len(), 3);
 7087
 7088        let first = worktrees[0].read(cx);
 7089        let second = worktrees[1].read(cx);
 7090        let third = worktrees[2].read(cx);
 7091
 7092        // check they are now in the right order
 7093        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
 7094        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
 7095        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
 7096    });
 7097
 7098    // move the third worktree to before the second
 7099    // [a, c, b] -> [a, b, c]
 7100    project
 7101        .update(cx, |project, cx| {
 7102            let third = worktree_c.read(cx);
 7103            let second = worktree_b.read(cx);
 7104            project.move_worktree(third.id(), second.id(), cx)
 7105        })
 7106        .expect("moving third before second");
 7107
 7108    // check the state after moving
 7109    project.update(cx, |project, cx| {
 7110        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7111        assert_eq!(worktrees.len(), 3);
 7112
 7113        let first = worktrees[0].read(cx);
 7114        let second = worktrees[1].read(cx);
 7115        let third = worktrees[2].read(cx);
 7116
 7117        // check they are now in the right order
 7118        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
 7119        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
 7120        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
 7121    });
 7122
 7123    // move the first worktree to after the third
 7124    // [a, b, c] -> [b, c, a]
 7125    project
 7126        .update(cx, |project, cx| {
 7127            let first = worktree_a.read(cx);
 7128            let third = worktree_c.read(cx);
 7129            project.move_worktree(first.id(), third.id(), cx)
 7130        })
 7131        .expect("moving first after third");
 7132
 7133    // check the state after moving
 7134    project.update(cx, |project, cx| {
 7135        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7136        assert_eq!(worktrees.len(), 3);
 7137
 7138        let first = worktrees[0].read(cx);
 7139        let second = worktrees[1].read(cx);
 7140        let third = worktrees[2].read(cx);
 7141
 7142        // check they are now in the right order
 7143        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
 7144        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
 7145        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
 7146    });
 7147
 7148    // move the third worktree to before the first
 7149    // [b, c, a] -> [a, b, c]
 7150    project
 7151        .update(cx, |project, cx| {
 7152            let third = worktree_a.read(cx);
 7153            let first = worktree_b.read(cx);
 7154            project.move_worktree(third.id(), first.id(), cx)
 7155        })
 7156        .expect("moving third before first");
 7157
 7158    // check the state after moving
 7159    project.update(cx, |project, cx| {
 7160        let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
 7161        assert_eq!(worktrees.len(), 3);
 7162
 7163        let first = worktrees[0].read(cx);
 7164        let second = worktrees[1].read(cx);
 7165        let third = worktrees[2].read(cx);
 7166
 7167        // check they are now in the right order
 7168        assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
 7169        assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
 7170        assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
 7171    });
 7172}
 7173
 7174#[gpui::test]
 7175async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
 7176    init_test(cx);
 7177
 7178    let staged_contents = r#"
 7179        fn main() {
 7180            println!("hello world");
 7181        }
 7182    "#
 7183    .unindent();
 7184    let file_contents = r#"
 7185        // print goodbye
 7186        fn main() {
 7187            println!("goodbye world");
 7188        }
 7189    "#
 7190    .unindent();
 7191
 7192    let fs = FakeFs::new(cx.background_executor.clone());
 7193    fs.insert_tree(
 7194        "/dir",
 7195        json!({
 7196            ".git": {},
 7197           "src": {
 7198               "main.rs": file_contents,
 7199           }
 7200        }),
 7201    )
 7202    .await;
 7203
 7204    fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
 7205
 7206    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 7207
 7208    let buffer = project
 7209        .update(cx, |project, cx| {
 7210            project.open_local_buffer("/dir/src/main.rs", cx)
 7211        })
 7212        .await
 7213        .unwrap();
 7214    let unstaged_diff = project
 7215        .update(cx, |project, cx| {
 7216            project.open_unstaged_diff(buffer.clone(), cx)
 7217        })
 7218        .await
 7219        .unwrap();
 7220
 7221    cx.run_until_parked();
 7222    unstaged_diff.update(cx, |unstaged_diff, cx| {
 7223        let snapshot = buffer.read(cx).snapshot();
 7224        assert_hunks(
 7225            unstaged_diff.hunks(&snapshot, cx),
 7226            &snapshot,
 7227            &unstaged_diff.base_text_string().unwrap(),
 7228            &[
 7229                (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
 7230                (
 7231                    2..3,
 7232                    "    println!(\"hello world\");\n",
 7233                    "    println!(\"goodbye world\");\n",
 7234                    DiffHunkStatus::modified_none(),
 7235                ),
 7236            ],
 7237        );
 7238    });
 7239
 7240    let staged_contents = r#"
 7241        // print goodbye
 7242        fn main() {
 7243        }
 7244    "#
 7245    .unindent();
 7246
 7247    fs.set_index_for_repo(Path::new("/dir/.git"), &[("src/main.rs", staged_contents)]);
 7248
 7249    cx.run_until_parked();
 7250    unstaged_diff.update(cx, |unstaged_diff, cx| {
 7251        let snapshot = buffer.read(cx).snapshot();
 7252        assert_hunks(
 7253            unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
 7254            &snapshot,
 7255            &unstaged_diff.base_text().text(),
 7256            &[(
 7257                2..3,
 7258                "",
 7259                "    println!(\"goodbye world\");\n",
 7260                DiffHunkStatus::added_none(),
 7261            )],
 7262        );
 7263    });
 7264}
 7265
 7266#[gpui::test]
 7267async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
 7268    init_test(cx);
 7269
 7270    let committed_contents = r#"
 7271        fn main() {
 7272            println!("hello world");
 7273        }
 7274    "#
 7275    .unindent();
 7276    let staged_contents = r#"
 7277        fn main() {
 7278            println!("goodbye world");
 7279        }
 7280    "#
 7281    .unindent();
 7282    let file_contents = r#"
 7283        // print goodbye
 7284        fn main() {
 7285            println!("goodbye world");
 7286        }
 7287    "#
 7288    .unindent();
 7289
 7290    let fs = FakeFs::new(cx.background_executor.clone());
 7291    fs.insert_tree(
 7292        "/dir",
 7293        json!({
 7294            ".git": {},
 7295           "src": {
 7296               "modification.rs": file_contents,
 7297           }
 7298        }),
 7299    )
 7300    .await;
 7301
 7302    fs.set_head_for_repo(
 7303        Path::new("/dir/.git"),
 7304        &[
 7305            ("src/modification.rs", committed_contents),
 7306            ("src/deletion.rs", "// the-deleted-contents\n".into()),
 7307        ],
 7308        "deadbeef",
 7309    );
 7310    fs.set_index_for_repo(
 7311        Path::new("/dir/.git"),
 7312        &[
 7313            ("src/modification.rs", staged_contents),
 7314            ("src/deletion.rs", "// the-deleted-contents\n".into()),
 7315        ],
 7316    );
 7317
 7318    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 7319    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 7320    let language = rust_lang();
 7321    language_registry.add(language.clone());
 7322
 7323    let buffer_1 = project
 7324        .update(cx, |project, cx| {
 7325            project.open_local_buffer("/dir/src/modification.rs", cx)
 7326        })
 7327        .await
 7328        .unwrap();
 7329    let diff_1 = project
 7330        .update(cx, |project, cx| {
 7331            project.open_uncommitted_diff(buffer_1.clone(), cx)
 7332        })
 7333        .await
 7334        .unwrap();
 7335    diff_1.read_with(cx, |diff, _| {
 7336        assert_eq!(diff.base_text().language().cloned(), Some(language))
 7337    });
 7338    cx.run_until_parked();
 7339    diff_1.update(cx, |diff, cx| {
 7340        let snapshot = buffer_1.read(cx).snapshot();
 7341        assert_hunks(
 7342            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
 7343            &snapshot,
 7344            &diff.base_text_string().unwrap(),
 7345            &[
 7346                (
 7347                    0..1,
 7348                    "",
 7349                    "// print goodbye\n",
 7350                    DiffHunkStatus::added(DiffHunkSecondaryStatus::HasSecondaryHunk),
 7351                ),
 7352                (
 7353                    2..3,
 7354                    "    println!(\"hello world\");\n",
 7355                    "    println!(\"goodbye world\");\n",
 7356                    DiffHunkStatus::modified_none(),
 7357                ),
 7358            ],
 7359        );
 7360    });
 7361
 7362    // Reset HEAD to a version that differs from both the buffer and the index.
 7363    let committed_contents = r#"
 7364        // print goodbye
 7365        fn main() {
 7366        }
 7367    "#
 7368    .unindent();
 7369    fs.set_head_for_repo(
 7370        Path::new("/dir/.git"),
 7371        &[
 7372            ("src/modification.rs", committed_contents.clone()),
 7373            ("src/deletion.rs", "// the-deleted-contents\n".into()),
 7374        ],
 7375        "deadbeef",
 7376    );
 7377
 7378    // Buffer now has an unstaged hunk.
 7379    cx.run_until_parked();
 7380    diff_1.update(cx, |diff, cx| {
 7381        let snapshot = buffer_1.read(cx).snapshot();
 7382        assert_hunks(
 7383            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
 7384            &snapshot,
 7385            &diff.base_text().text(),
 7386            &[(
 7387                2..3,
 7388                "",
 7389                "    println!(\"goodbye world\");\n",
 7390                DiffHunkStatus::added_none(),
 7391            )],
 7392        );
 7393    });
 7394
 7395    // Open a buffer for a file that's been deleted.
 7396    let buffer_2 = project
 7397        .update(cx, |project, cx| {
 7398            project.open_local_buffer("/dir/src/deletion.rs", cx)
 7399        })
 7400        .await
 7401        .unwrap();
 7402    let diff_2 = project
 7403        .update(cx, |project, cx| {
 7404            project.open_uncommitted_diff(buffer_2.clone(), cx)
 7405        })
 7406        .await
 7407        .unwrap();
 7408    cx.run_until_parked();
 7409    diff_2.update(cx, |diff, cx| {
 7410        let snapshot = buffer_2.read(cx).snapshot();
 7411        assert_hunks(
 7412            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
 7413            &snapshot,
 7414            &diff.base_text_string().unwrap(),
 7415            &[(
 7416                0..0,
 7417                "// the-deleted-contents\n",
 7418                "",
 7419                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::HasSecondaryHunk),
 7420            )],
 7421        );
 7422    });
 7423
 7424    // Stage the deletion of this file
 7425    fs.set_index_for_repo(
 7426        Path::new("/dir/.git"),
 7427        &[("src/modification.rs", committed_contents.clone())],
 7428    );
 7429    cx.run_until_parked();
 7430    diff_2.update(cx, |diff, cx| {
 7431        let snapshot = buffer_2.read(cx).snapshot();
 7432        assert_hunks(
 7433            diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
 7434            &snapshot,
 7435            &diff.base_text_string().unwrap(),
 7436            &[(
 7437                0..0,
 7438                "// the-deleted-contents\n",
 7439                "",
 7440                DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
 7441            )],
 7442        );
 7443    });
 7444}
 7445
 7446#[gpui::test]
 7447async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
 7448    use DiffHunkSecondaryStatus::*;
 7449    init_test(cx);
 7450
 7451    let committed_contents = r#"
 7452        zero
 7453        one
 7454        two
 7455        three
 7456        four
 7457        five
 7458    "#
 7459    .unindent();
 7460    let file_contents = r#"
 7461        one
 7462        TWO
 7463        three
 7464        FOUR
 7465        five
 7466    "#
 7467    .unindent();
 7468
 7469    let fs = FakeFs::new(cx.background_executor.clone());
 7470    fs.insert_tree(
 7471        "/dir",
 7472        json!({
 7473            ".git": {},
 7474            "file.txt": file_contents.clone()
 7475        }),
 7476    )
 7477    .await;
 7478
 7479    fs.set_head_and_index_for_repo(
 7480        path!("/dir/.git").as_ref(),
 7481        &[("file.txt", committed_contents.clone())],
 7482    );
 7483
 7484    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 7485
 7486    let buffer = project
 7487        .update(cx, |project, cx| {
 7488            project.open_local_buffer("/dir/file.txt", cx)
 7489        })
 7490        .await
 7491        .unwrap();
 7492    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 7493    let uncommitted_diff = project
 7494        .update(cx, |project, cx| {
 7495            project.open_uncommitted_diff(buffer.clone(), cx)
 7496        })
 7497        .await
 7498        .unwrap();
 7499    let mut diff_events = cx.events(&uncommitted_diff);
 7500
 7501    // The hunks are initially unstaged.
 7502    uncommitted_diff.read_with(cx, |diff, cx| {
 7503        assert_hunks(
 7504            diff.hunks(&snapshot, cx),
 7505            &snapshot,
 7506            &diff.base_text_string().unwrap(),
 7507            &[
 7508                (
 7509                    0..0,
 7510                    "zero\n",
 7511                    "",
 7512                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7513                ),
 7514                (
 7515                    1..2,
 7516                    "two\n",
 7517                    "TWO\n",
 7518                    DiffHunkStatus::modified(HasSecondaryHunk),
 7519                ),
 7520                (
 7521                    3..4,
 7522                    "four\n",
 7523                    "FOUR\n",
 7524                    DiffHunkStatus::modified(HasSecondaryHunk),
 7525                ),
 7526            ],
 7527        );
 7528    });
 7529
 7530    // Stage a hunk. It appears as optimistically staged.
 7531    uncommitted_diff.update(cx, |diff, cx| {
 7532        let range =
 7533            snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
 7534        let hunks = diff
 7535            .hunks_intersecting_range(range, &snapshot, cx)
 7536            .collect::<Vec<_>>();
 7537        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
 7538
 7539        assert_hunks(
 7540            diff.hunks(&snapshot, cx),
 7541            &snapshot,
 7542            &diff.base_text_string().unwrap(),
 7543            &[
 7544                (
 7545                    0..0,
 7546                    "zero\n",
 7547                    "",
 7548                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7549                ),
 7550                (
 7551                    1..2,
 7552                    "two\n",
 7553                    "TWO\n",
 7554                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
 7555                ),
 7556                (
 7557                    3..4,
 7558                    "four\n",
 7559                    "FOUR\n",
 7560                    DiffHunkStatus::modified(HasSecondaryHunk),
 7561                ),
 7562            ],
 7563        );
 7564    });
 7565
 7566    // The diff emits a change event for the range of the staged hunk.
 7567    assert!(matches!(
 7568        diff_events.next().await.unwrap(),
 7569        BufferDiffEvent::HunksStagedOrUnstaged(_)
 7570    ));
 7571    let event = diff_events.next().await.unwrap();
 7572    if let BufferDiffEvent::DiffChanged {
 7573        changed_range: Some(changed_range),
 7574    } = event
 7575    {
 7576        let changed_range = changed_range.to_point(&snapshot);
 7577        assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
 7578    } else {
 7579        panic!("Unexpected event {event:?}");
 7580    }
 7581
 7582    // When the write to the index completes, it appears as staged.
 7583    cx.run_until_parked();
 7584    uncommitted_diff.update(cx, |diff, cx| {
 7585        assert_hunks(
 7586            diff.hunks(&snapshot, cx),
 7587            &snapshot,
 7588            &diff.base_text_string().unwrap(),
 7589            &[
 7590                (
 7591                    0..0,
 7592                    "zero\n",
 7593                    "",
 7594                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7595                ),
 7596                (
 7597                    1..2,
 7598                    "two\n",
 7599                    "TWO\n",
 7600                    DiffHunkStatus::modified(NoSecondaryHunk),
 7601                ),
 7602                (
 7603                    3..4,
 7604                    "four\n",
 7605                    "FOUR\n",
 7606                    DiffHunkStatus::modified(HasSecondaryHunk),
 7607                ),
 7608            ],
 7609        );
 7610    });
 7611
 7612    // The diff emits a change event for the changed index text.
 7613    let event = diff_events.next().await.unwrap();
 7614    if let BufferDiffEvent::DiffChanged {
 7615        changed_range: Some(changed_range),
 7616    } = event
 7617    {
 7618        let changed_range = changed_range.to_point(&snapshot);
 7619        assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
 7620    } else {
 7621        panic!("Unexpected event {event:?}");
 7622    }
 7623
 7624    // Simulate a problem writing to the git index.
 7625    fs.set_error_message_for_index_write(
 7626        "/dir/.git".as_ref(),
 7627        Some("failed to write git index".into()),
 7628    );
 7629
 7630    // Stage another hunk.
 7631    uncommitted_diff.update(cx, |diff, cx| {
 7632        let range =
 7633            snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
 7634        let hunks = diff
 7635            .hunks_intersecting_range(range, &snapshot, cx)
 7636            .collect::<Vec<_>>();
 7637        diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
 7638
 7639        assert_hunks(
 7640            diff.hunks(&snapshot, cx),
 7641            &snapshot,
 7642            &diff.base_text_string().unwrap(),
 7643            &[
 7644                (
 7645                    0..0,
 7646                    "zero\n",
 7647                    "",
 7648                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7649                ),
 7650                (
 7651                    1..2,
 7652                    "two\n",
 7653                    "TWO\n",
 7654                    DiffHunkStatus::modified(NoSecondaryHunk),
 7655                ),
 7656                (
 7657                    3..4,
 7658                    "four\n",
 7659                    "FOUR\n",
 7660                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
 7661                ),
 7662            ],
 7663        );
 7664    });
 7665    assert!(matches!(
 7666        diff_events.next().await.unwrap(),
 7667        BufferDiffEvent::HunksStagedOrUnstaged(_)
 7668    ));
 7669    let event = diff_events.next().await.unwrap();
 7670    if let BufferDiffEvent::DiffChanged {
 7671        changed_range: Some(changed_range),
 7672    } = event
 7673    {
 7674        let changed_range = changed_range.to_point(&snapshot);
 7675        assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
 7676    } else {
 7677        panic!("Unexpected event {event:?}");
 7678    }
 7679
 7680    // When the write fails, the hunk returns to being unstaged.
 7681    cx.run_until_parked();
 7682    uncommitted_diff.update(cx, |diff, cx| {
 7683        assert_hunks(
 7684            diff.hunks(&snapshot, cx),
 7685            &snapshot,
 7686            &diff.base_text_string().unwrap(),
 7687            &[
 7688                (
 7689                    0..0,
 7690                    "zero\n",
 7691                    "",
 7692                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7693                ),
 7694                (
 7695                    1..2,
 7696                    "two\n",
 7697                    "TWO\n",
 7698                    DiffHunkStatus::modified(NoSecondaryHunk),
 7699                ),
 7700                (
 7701                    3..4,
 7702                    "four\n",
 7703                    "FOUR\n",
 7704                    DiffHunkStatus::modified(HasSecondaryHunk),
 7705                ),
 7706            ],
 7707        );
 7708    });
 7709
 7710    let event = diff_events.next().await.unwrap();
 7711    if let BufferDiffEvent::DiffChanged {
 7712        changed_range: Some(changed_range),
 7713    } = event
 7714    {
 7715        let changed_range = changed_range.to_point(&snapshot);
 7716        assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
 7717    } else {
 7718        panic!("Unexpected event {event:?}");
 7719    }
 7720
 7721    // Allow writing to the git index to succeed again.
 7722    fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
 7723
 7724    // Stage two hunks with separate operations.
 7725    uncommitted_diff.update(cx, |diff, cx| {
 7726        let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
 7727        diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
 7728        diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
 7729    });
 7730
 7731    // Both staged hunks appear as pending.
 7732    uncommitted_diff.update(cx, |diff, cx| {
 7733        assert_hunks(
 7734            diff.hunks(&snapshot, cx),
 7735            &snapshot,
 7736            &diff.base_text_string().unwrap(),
 7737            &[
 7738                (
 7739                    0..0,
 7740                    "zero\n",
 7741                    "",
 7742                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
 7743                ),
 7744                (
 7745                    1..2,
 7746                    "two\n",
 7747                    "TWO\n",
 7748                    DiffHunkStatus::modified(NoSecondaryHunk),
 7749                ),
 7750                (
 7751                    3..4,
 7752                    "four\n",
 7753                    "FOUR\n",
 7754                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
 7755                ),
 7756            ],
 7757        );
 7758    });
 7759
 7760    // Both staging operations take effect.
 7761    cx.run_until_parked();
 7762    uncommitted_diff.update(cx, |diff, cx| {
 7763        assert_hunks(
 7764            diff.hunks(&snapshot, cx),
 7765            &snapshot,
 7766            &diff.base_text_string().unwrap(),
 7767            &[
 7768                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
 7769                (
 7770                    1..2,
 7771                    "two\n",
 7772                    "TWO\n",
 7773                    DiffHunkStatus::modified(NoSecondaryHunk),
 7774                ),
 7775                (
 7776                    3..4,
 7777                    "four\n",
 7778                    "FOUR\n",
 7779                    DiffHunkStatus::modified(NoSecondaryHunk),
 7780                ),
 7781            ],
 7782        );
 7783    });
 7784}
 7785
 7786#[gpui::test(seeds(340, 472))]
 7787async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
 7788    use DiffHunkSecondaryStatus::*;
 7789    init_test(cx);
 7790
 7791    let committed_contents = r#"
 7792        zero
 7793        one
 7794        two
 7795        three
 7796        four
 7797        five
 7798    "#
 7799    .unindent();
 7800    let file_contents = r#"
 7801        one
 7802        TWO
 7803        three
 7804        FOUR
 7805        five
 7806    "#
 7807    .unindent();
 7808
 7809    let fs = FakeFs::new(cx.background_executor.clone());
 7810    fs.insert_tree(
 7811        "/dir",
 7812        json!({
 7813            ".git": {},
 7814            "file.txt": file_contents.clone()
 7815        }),
 7816    )
 7817    .await;
 7818
 7819    fs.set_head_for_repo(
 7820        "/dir/.git".as_ref(),
 7821        &[("file.txt", committed_contents.clone())],
 7822        "deadbeef",
 7823    );
 7824    fs.set_index_for_repo(
 7825        "/dir/.git".as_ref(),
 7826        &[("file.txt", committed_contents.clone())],
 7827    );
 7828
 7829    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 7830
 7831    let buffer = project
 7832        .update(cx, |project, cx| {
 7833            project.open_local_buffer("/dir/file.txt", cx)
 7834        })
 7835        .await
 7836        .unwrap();
 7837    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 7838    let uncommitted_diff = project
 7839        .update(cx, |project, cx| {
 7840            project.open_uncommitted_diff(buffer.clone(), cx)
 7841        })
 7842        .await
 7843        .unwrap();
 7844
 7845    // The hunks are initially unstaged.
 7846    uncommitted_diff.read_with(cx, |diff, cx| {
 7847        assert_hunks(
 7848            diff.hunks(&snapshot, cx),
 7849            &snapshot,
 7850            &diff.base_text_string().unwrap(),
 7851            &[
 7852                (
 7853                    0..0,
 7854                    "zero\n",
 7855                    "",
 7856                    DiffHunkStatus::deleted(HasSecondaryHunk),
 7857                ),
 7858                (
 7859                    1..2,
 7860                    "two\n",
 7861                    "TWO\n",
 7862                    DiffHunkStatus::modified(HasSecondaryHunk),
 7863                ),
 7864                (
 7865                    3..4,
 7866                    "four\n",
 7867                    "FOUR\n",
 7868                    DiffHunkStatus::modified(HasSecondaryHunk),
 7869                ),
 7870            ],
 7871        );
 7872    });
 7873
 7874    // Pause IO events
 7875    fs.pause_events();
 7876
 7877    // Stage the first hunk.
 7878    uncommitted_diff.update(cx, |diff, cx| {
 7879        let hunk = diff.hunks(&snapshot, cx).next().unwrap();
 7880        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
 7881        assert_hunks(
 7882            diff.hunks(&snapshot, cx),
 7883            &snapshot,
 7884            &diff.base_text_string().unwrap(),
 7885            &[
 7886                (
 7887                    0..0,
 7888                    "zero\n",
 7889                    "",
 7890                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
 7891                ),
 7892                (
 7893                    1..2,
 7894                    "two\n",
 7895                    "TWO\n",
 7896                    DiffHunkStatus::modified(HasSecondaryHunk),
 7897                ),
 7898                (
 7899                    3..4,
 7900                    "four\n",
 7901                    "FOUR\n",
 7902                    DiffHunkStatus::modified(HasSecondaryHunk),
 7903                ),
 7904            ],
 7905        );
 7906    });
 7907
 7908    // Stage the second hunk *before* receiving the FS event for the first hunk.
 7909    cx.run_until_parked();
 7910    uncommitted_diff.update(cx, |diff, cx| {
 7911        let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
 7912        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
 7913        assert_hunks(
 7914            diff.hunks(&snapshot, cx),
 7915            &snapshot,
 7916            &diff.base_text_string().unwrap(),
 7917            &[
 7918                (
 7919                    0..0,
 7920                    "zero\n",
 7921                    "",
 7922                    DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
 7923                ),
 7924                (
 7925                    1..2,
 7926                    "two\n",
 7927                    "TWO\n",
 7928                    DiffHunkStatus::modified(SecondaryHunkRemovalPending),
 7929                ),
 7930                (
 7931                    3..4,
 7932                    "four\n",
 7933                    "FOUR\n",
 7934                    DiffHunkStatus::modified(HasSecondaryHunk),
 7935                ),
 7936            ],
 7937        );
 7938    });
 7939
 7940    // Process the FS event for staging the first hunk (second event is still pending).
 7941    fs.flush_events(1);
 7942    cx.run_until_parked();
 7943
 7944    // Stage the third hunk before receiving the second FS event.
 7945    uncommitted_diff.update(cx, |diff, cx| {
 7946        let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap();
 7947        diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
 7948    });
 7949
 7950    // Wait for all remaining IO.
 7951    cx.run_until_parked();
 7952    fs.flush_events(fs.buffered_event_count());
 7953
 7954    // Now all hunks are staged.
 7955    cx.run_until_parked();
 7956    uncommitted_diff.update(cx, |diff, cx| {
 7957        assert_hunks(
 7958            diff.hunks(&snapshot, cx),
 7959            &snapshot,
 7960            &diff.base_text_string().unwrap(),
 7961            &[
 7962                (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
 7963                (
 7964                    1..2,
 7965                    "two\n",
 7966                    "TWO\n",
 7967                    DiffHunkStatus::modified(NoSecondaryHunk),
 7968                ),
 7969                (
 7970                    3..4,
 7971                    "four\n",
 7972                    "FOUR\n",
 7973                    DiffHunkStatus::modified(NoSecondaryHunk),
 7974                ),
 7975            ],
 7976        );
 7977    });
 7978}
 7979
 7980#[gpui::test(iterations = 25)]
 7981async fn test_staging_random_hunks(
 7982    mut rng: StdRng,
 7983    executor: BackgroundExecutor,
 7984    cx: &mut gpui::TestAppContext,
 7985) {
 7986    let operations = env::var("OPERATIONS")
 7987        .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 7988        .unwrap_or(20);
 7989
 7990    // Try to induce races between diff recalculation and index writes.
 7991    if rng.random_bool(0.5) {
 7992        executor.deprioritize(*CALCULATE_DIFF_TASK);
 7993    }
 7994
 7995    use DiffHunkSecondaryStatus::*;
 7996    init_test(cx);
 7997
 7998    let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
 7999    let index_text = committed_text.clone();
 8000    let buffer_text = (0..30)
 8001        .map(|i| match i % 5 {
 8002            0 => format!("line {i} (modified)\n"),
 8003            _ => format!("line {i}\n"),
 8004        })
 8005        .collect::<String>();
 8006
 8007    let fs = FakeFs::new(cx.background_executor.clone());
 8008    fs.insert_tree(
 8009        path!("/dir"),
 8010        json!({
 8011            ".git": {},
 8012            "file.txt": buffer_text.clone()
 8013        }),
 8014    )
 8015    .await;
 8016    fs.set_head_for_repo(
 8017        path!("/dir/.git").as_ref(),
 8018        &[("file.txt", committed_text.clone())],
 8019        "deadbeef",
 8020    );
 8021    fs.set_index_for_repo(
 8022        path!("/dir/.git").as_ref(),
 8023        &[("file.txt", index_text.clone())],
 8024    );
 8025    let repo = fs
 8026        .open_repo(path!("/dir/.git").as_ref(), Some("git".as_ref()))
 8027        .unwrap();
 8028
 8029    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
 8030    let buffer = project
 8031        .update(cx, |project, cx| {
 8032            project.open_local_buffer(path!("/dir/file.txt"), cx)
 8033        })
 8034        .await
 8035        .unwrap();
 8036    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 8037    let uncommitted_diff = project
 8038        .update(cx, |project, cx| {
 8039            project.open_uncommitted_diff(buffer.clone(), cx)
 8040        })
 8041        .await
 8042        .unwrap();
 8043
 8044    let mut hunks =
 8045        uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
 8046    assert_eq!(hunks.len(), 6);
 8047
 8048    for _i in 0..operations {
 8049        let hunk_ix = rng.random_range(0..hunks.len());
 8050        let hunk = &mut hunks[hunk_ix];
 8051        let row = hunk.range.start.row;
 8052
 8053        if hunk.status().has_secondary_hunk() {
 8054            log::info!("staging hunk at {row}");
 8055            uncommitted_diff.update(cx, |diff, cx| {
 8056                diff.stage_or_unstage_hunks(true, std::slice::from_ref(hunk), &snapshot, true, cx);
 8057            });
 8058            hunk.secondary_status = SecondaryHunkRemovalPending;
 8059        } else {
 8060            log::info!("unstaging hunk at {row}");
 8061            uncommitted_diff.update(cx, |diff, cx| {
 8062                diff.stage_or_unstage_hunks(false, std::slice::from_ref(hunk), &snapshot, true, cx);
 8063            });
 8064            hunk.secondary_status = SecondaryHunkAdditionPending;
 8065        }
 8066
 8067        for _ in 0..rng.random_range(0..10) {
 8068            log::info!("yielding");
 8069            cx.executor().simulate_random_delay().await;
 8070        }
 8071    }
 8072
 8073    cx.executor().run_until_parked();
 8074
 8075    for hunk in &mut hunks {
 8076        if hunk.secondary_status == SecondaryHunkRemovalPending {
 8077            hunk.secondary_status = NoSecondaryHunk;
 8078        } else if hunk.secondary_status == SecondaryHunkAdditionPending {
 8079            hunk.secondary_status = HasSecondaryHunk;
 8080        }
 8081    }
 8082
 8083    log::info!(
 8084        "index text:\n{}",
 8085        repo.load_index_text(RepoPath::from_rel_path(rel_path("file.txt")))
 8086            .await
 8087            .unwrap()
 8088    );
 8089
 8090    uncommitted_diff.update(cx, |diff, cx| {
 8091        let expected_hunks = hunks
 8092            .iter()
 8093            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
 8094            .collect::<Vec<_>>();
 8095        let actual_hunks = diff
 8096            .hunks(&snapshot, cx)
 8097            .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
 8098            .collect::<Vec<_>>();
 8099        assert_eq!(actual_hunks, expected_hunks);
 8100    });
 8101}
 8102
 8103#[gpui::test]
 8104async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
 8105    init_test(cx);
 8106
 8107    let committed_contents = r#"
 8108        fn main() {
 8109            println!("hello from HEAD");
 8110        }
 8111    "#
 8112    .unindent();
 8113    let file_contents = r#"
 8114        fn main() {
 8115            println!("hello from the working copy");
 8116        }
 8117    "#
 8118    .unindent();
 8119
 8120    let fs = FakeFs::new(cx.background_executor.clone());
 8121    fs.insert_tree(
 8122        "/dir",
 8123        json!({
 8124            ".git": {},
 8125           "src": {
 8126               "main.rs": file_contents,
 8127           }
 8128        }),
 8129    )
 8130    .await;
 8131
 8132    fs.set_head_for_repo(
 8133        Path::new("/dir/.git"),
 8134        &[("src/main.rs", committed_contents.clone())],
 8135        "deadbeef",
 8136    );
 8137    fs.set_index_for_repo(
 8138        Path::new("/dir/.git"),
 8139        &[("src/main.rs", committed_contents.clone())],
 8140    );
 8141
 8142    let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
 8143
 8144    let buffer = project
 8145        .update(cx, |project, cx| {
 8146            project.open_local_buffer("/dir/src/main.rs", cx)
 8147        })
 8148        .await
 8149        .unwrap();
 8150    let uncommitted_diff = project
 8151        .update(cx, |project, cx| {
 8152            project.open_uncommitted_diff(buffer.clone(), cx)
 8153        })
 8154        .await
 8155        .unwrap();
 8156
 8157    cx.run_until_parked();
 8158    uncommitted_diff.update(cx, |uncommitted_diff, cx| {
 8159        let snapshot = buffer.read(cx).snapshot();
 8160        assert_hunks(
 8161            uncommitted_diff.hunks(&snapshot, cx),
 8162            &snapshot,
 8163            &uncommitted_diff.base_text_string().unwrap(),
 8164            &[(
 8165                1..2,
 8166                "    println!(\"hello from HEAD\");\n",
 8167                "    println!(\"hello from the working copy\");\n",
 8168                DiffHunkStatus {
 8169                    kind: DiffHunkStatusKind::Modified,
 8170                    secondary: DiffHunkSecondaryStatus::HasSecondaryHunk,
 8171                },
 8172            )],
 8173        );
 8174    });
 8175}
 8176
 8177#[gpui::test]
 8178async fn test_repository_and_path_for_project_path(
 8179    background_executor: BackgroundExecutor,
 8180    cx: &mut gpui::TestAppContext,
 8181) {
 8182    init_test(cx);
 8183    let fs = FakeFs::new(background_executor);
 8184    fs.insert_tree(
 8185        path!("/root"),
 8186        json!({
 8187            "c.txt": "",
 8188            "dir1": {
 8189                ".git": {},
 8190                "deps": {
 8191                    "dep1": {
 8192                        ".git": {},
 8193                        "src": {
 8194                            "a.txt": ""
 8195                        }
 8196                    }
 8197                },
 8198                "src": {
 8199                    "b.txt": ""
 8200                }
 8201            },
 8202        }),
 8203    )
 8204    .await;
 8205
 8206    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 8207    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 8208    let tree_id = tree.read_with(cx, |tree, _| tree.id());
 8209    project
 8210        .update(cx, |project, cx| project.git_scans_complete(cx))
 8211        .await;
 8212    cx.run_until_parked();
 8213
 8214    project.read_with(cx, |project, cx| {
 8215        let git_store = project.git_store().read(cx);
 8216        let pairs = [
 8217            ("c.txt", None),
 8218            ("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
 8219            (
 8220                "dir1/deps/dep1/src/a.txt",
 8221                Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
 8222            ),
 8223        ];
 8224        let expected = pairs
 8225            .iter()
 8226            .map(|(path, result)| {
 8227                (
 8228                    path,
 8229                    result.map(|(repo, repo_path)| {
 8230                        (Path::new(repo).into(), RepoPath::new(repo_path).unwrap())
 8231                    }),
 8232                )
 8233            })
 8234            .collect::<Vec<_>>();
 8235        let actual = pairs
 8236            .iter()
 8237            .map(|(path, _)| {
 8238                let project_path = (tree_id, rel_path(path)).into();
 8239                let result = maybe!({
 8240                    let (repo, repo_path) =
 8241                        git_store.repository_and_path_for_project_path(&project_path, cx)?;
 8242                    Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
 8243                });
 8244                (path, result)
 8245            })
 8246            .collect::<Vec<_>>();
 8247        pretty_assertions::assert_eq!(expected, actual);
 8248    });
 8249
 8250    fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
 8251        .await
 8252        .unwrap();
 8253    cx.run_until_parked();
 8254
 8255    project.read_with(cx, |project, cx| {
 8256        let git_store = project.git_store().read(cx);
 8257        assert_eq!(
 8258            git_store.repository_and_path_for_project_path(
 8259                &(tree_id, rel_path("dir1/src/b.txt")).into(),
 8260                cx
 8261            ),
 8262            None
 8263        );
 8264    });
 8265}
 8266
 8267#[gpui::test]
 8268async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
 8269    init_test(cx);
 8270    let fs = FakeFs::new(cx.background_executor.clone());
 8271    let home = paths::home_dir();
 8272    fs.insert_tree(
 8273        home,
 8274        json!({
 8275            ".git": {},
 8276            "project": {
 8277                "a.txt": "A"
 8278            },
 8279        }),
 8280    )
 8281    .await;
 8282
 8283    let project = Project::test(fs.clone(), [home.join("project").as_ref()], cx).await;
 8284    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 8285    let tree_id = tree.read_with(cx, |tree, _| tree.id());
 8286
 8287    project
 8288        .update(cx, |project, cx| project.git_scans_complete(cx))
 8289        .await;
 8290    tree.flush_fs_events(cx).await;
 8291
 8292    project.read_with(cx, |project, cx| {
 8293        let containing = project
 8294            .git_store()
 8295            .read(cx)
 8296            .repository_and_path_for_project_path(&(tree_id, rel_path("a.txt")).into(), cx);
 8297        assert!(containing.is_none());
 8298    });
 8299
 8300    let project = Project::test(fs.clone(), [home.as_ref()], cx).await;
 8301    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 8302    let tree_id = tree.read_with(cx, |tree, _| tree.id());
 8303    project
 8304        .update(cx, |project, cx| project.git_scans_complete(cx))
 8305        .await;
 8306    tree.flush_fs_events(cx).await;
 8307
 8308    project.read_with(cx, |project, cx| {
 8309        let containing = project
 8310            .git_store()
 8311            .read(cx)
 8312            .repository_and_path_for_project_path(&(tree_id, rel_path("project/a.txt")).into(), cx);
 8313        assert_eq!(
 8314            containing
 8315                .unwrap()
 8316                .0
 8317                .read(cx)
 8318                .work_directory_abs_path
 8319                .as_ref(),
 8320            home,
 8321        );
 8322    });
 8323}
 8324
 8325#[gpui::test]
 8326async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
 8327    init_test(cx);
 8328    cx.executor().allow_parking();
 8329
 8330    let root = TempTree::new(json!({
 8331        "project": {
 8332            "a.txt": "a",    // Modified
 8333            "b.txt": "bb",   // Added
 8334            "c.txt": "ccc",  // Unchanged
 8335            "d.txt": "dddd", // Deleted
 8336        },
 8337    }));
 8338
 8339    // Set up git repository before creating the project.
 8340    let work_dir = root.path().join("project");
 8341    let repo = git_init(work_dir.as_path());
 8342    git_add("a.txt", &repo);
 8343    git_add("c.txt", &repo);
 8344    git_add("d.txt", &repo);
 8345    git_commit("Initial commit", &repo);
 8346    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
 8347    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
 8348
 8349    let project = Project::test(
 8350        Arc::new(RealFs::new(None, cx.executor())),
 8351        [root.path()],
 8352        cx,
 8353    )
 8354    .await;
 8355
 8356    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 8357    tree.flush_fs_events(cx).await;
 8358    project
 8359        .update(cx, |project, cx| project.git_scans_complete(cx))
 8360        .await;
 8361    cx.executor().run_until_parked();
 8362
 8363    let repository = project.read_with(cx, |project, cx| {
 8364        project.repositories(cx).values().next().unwrap().clone()
 8365    });
 8366
 8367    // Check that the right git state is observed on startup
 8368    repository.read_with(cx, |repository, _| {
 8369        let entries = repository.cached_status().collect::<Vec<_>>();
 8370        assert_eq!(
 8371            entries,
 8372            [
 8373                StatusEntry {
 8374                    repo_path: repo_path("a.txt"),
 8375                    status: StatusCode::Modified.worktree(),
 8376                },
 8377                StatusEntry {
 8378                    repo_path: repo_path("b.txt"),
 8379                    status: FileStatus::Untracked,
 8380                },
 8381                StatusEntry {
 8382                    repo_path: repo_path("d.txt"),
 8383                    status: StatusCode::Deleted.worktree(),
 8384                },
 8385            ]
 8386        );
 8387    });
 8388
 8389    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
 8390
 8391    tree.flush_fs_events(cx).await;
 8392    project
 8393        .update(cx, |project, cx| project.git_scans_complete(cx))
 8394        .await;
 8395    cx.executor().run_until_parked();
 8396
 8397    repository.read_with(cx, |repository, _| {
 8398        let entries = repository.cached_status().collect::<Vec<_>>();
 8399        assert_eq!(
 8400            entries,
 8401            [
 8402                StatusEntry {
 8403                    repo_path: repo_path("a.txt"),
 8404                    status: StatusCode::Modified.worktree(),
 8405                },
 8406                StatusEntry {
 8407                    repo_path: repo_path("b.txt"),
 8408                    status: FileStatus::Untracked,
 8409                },
 8410                StatusEntry {
 8411                    repo_path: repo_path("c.txt"),
 8412                    status: StatusCode::Modified.worktree(),
 8413                },
 8414                StatusEntry {
 8415                    repo_path: repo_path("d.txt"),
 8416                    status: StatusCode::Deleted.worktree(),
 8417                },
 8418            ]
 8419        );
 8420    });
 8421
 8422    git_add("a.txt", &repo);
 8423    git_add("c.txt", &repo);
 8424    git_remove_index(Path::new("d.txt"), &repo);
 8425    git_commit("Another commit", &repo);
 8426    tree.flush_fs_events(cx).await;
 8427    project
 8428        .update(cx, |project, cx| project.git_scans_complete(cx))
 8429        .await;
 8430    cx.executor().run_until_parked();
 8431
 8432    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
 8433    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
 8434    tree.flush_fs_events(cx).await;
 8435    project
 8436        .update(cx, |project, cx| project.git_scans_complete(cx))
 8437        .await;
 8438    cx.executor().run_until_parked();
 8439
 8440    repository.read_with(cx, |repository, _cx| {
 8441        let entries = repository.cached_status().collect::<Vec<_>>();
 8442
 8443        // Deleting an untracked entry, b.txt, should leave no status
 8444        // a.txt was tracked, and so should have a status
 8445        assert_eq!(
 8446            entries,
 8447            [StatusEntry {
 8448                repo_path: repo_path("a.txt"),
 8449                status: StatusCode::Deleted.worktree(),
 8450            }]
 8451        );
 8452    });
 8453}
 8454
 8455#[gpui::test]
 8456async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
 8457    init_test(cx);
 8458    cx.executor().allow_parking();
 8459
 8460    let root = TempTree::new(json!({
 8461        "project": {
 8462            "sub": {},
 8463            "a.txt": "",
 8464        },
 8465    }));
 8466
 8467    let work_dir = root.path().join("project");
 8468    let repo = git_init(work_dir.as_path());
 8469    // a.txt exists in HEAD and the working copy but is deleted in the index.
 8470    git_add("a.txt", &repo);
 8471    git_commit("Initial commit", &repo);
 8472    git_remove_index("a.txt".as_ref(), &repo);
 8473    // `sub` is a nested git repository.
 8474    let _sub = git_init(&work_dir.join("sub"));
 8475
 8476    let project = Project::test(
 8477        Arc::new(RealFs::new(None, cx.executor())),
 8478        [root.path()],
 8479        cx,
 8480    )
 8481    .await;
 8482
 8483    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 8484    tree.flush_fs_events(cx).await;
 8485    project
 8486        .update(cx, |project, cx| project.git_scans_complete(cx))
 8487        .await;
 8488    cx.executor().run_until_parked();
 8489
 8490    let repository = project.read_with(cx, |project, cx| {
 8491        project
 8492            .repositories(cx)
 8493            .values()
 8494            .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
 8495            .unwrap()
 8496            .clone()
 8497    });
 8498
 8499    repository.read_with(cx, |repository, _cx| {
 8500        let entries = repository.cached_status().collect::<Vec<_>>();
 8501
 8502        // `sub` doesn't appear in our computed statuses.
 8503        // a.txt appears with a combined `DA` status.
 8504        assert_eq!(
 8505            entries,
 8506            [StatusEntry {
 8507                repo_path: repo_path("a.txt"),
 8508                status: TrackedStatus {
 8509                    index_status: StatusCode::Deleted,
 8510                    worktree_status: StatusCode::Added
 8511                }
 8512                .into(),
 8513            }]
 8514        )
 8515    });
 8516}
 8517
 8518#[track_caller]
 8519/// We merge lhs into rhs.
 8520fn merge_pending_ops_snapshots(
 8521    source: Vec<pending_op::PendingOps>,
 8522    mut target: Vec<pending_op::PendingOps>,
 8523) -> Vec<pending_op::PendingOps> {
 8524    for s_ops in source {
 8525        if let Some(idx) = target.iter().zip(0..).find_map(|(ops, idx)| {
 8526            if ops.repo_path == s_ops.repo_path {
 8527                Some(idx)
 8528            } else {
 8529                None
 8530            }
 8531        }) {
 8532            let t_ops = &mut target[idx];
 8533            for s_op in s_ops.ops {
 8534                if let Some(op_idx) = t_ops
 8535                    .ops
 8536                    .iter()
 8537                    .zip(0..)
 8538                    .find_map(|(op, idx)| if op.id == s_op.id { Some(idx) } else { None })
 8539                {
 8540                    let t_op = &mut t_ops.ops[op_idx];
 8541                    if s_op.finished {
 8542                        t_op.finished = true;
 8543                    }
 8544                } else {
 8545                    t_ops.ops.push(s_op);
 8546                }
 8547            }
 8548            t_ops.ops.sort_by(|l, r| l.id.cmp(&r.id));
 8549        } else {
 8550            target.push(s_ops);
 8551        }
 8552    }
 8553    target
 8554}
 8555
 8556#[gpui::test]
 8557async fn test_repository_pending_ops_staging(
 8558    executor: gpui::BackgroundExecutor,
 8559    cx: &mut gpui::TestAppContext,
 8560) {
 8561    init_test(cx);
 8562
 8563    let fs = FakeFs::new(executor);
 8564    fs.insert_tree(
 8565        path!("/root"),
 8566        json!({
 8567            "my-repo": {
 8568                ".git": {},
 8569                "a.txt": "a",
 8570            }
 8571
 8572        }),
 8573    )
 8574    .await;
 8575
 8576    fs.set_status_for_repo(
 8577        path!("/root/my-repo/.git").as_ref(),
 8578        &[("a.txt", FileStatus::Untracked)],
 8579    );
 8580
 8581    let project = Project::test(fs.clone(), [path!("/root/my-repo").as_ref()], cx).await;
 8582    let pending_ops_all = Arc::new(Mutex::new(SumTree::default()));
 8583    project.update(cx, |project, cx| {
 8584        let pending_ops_all = pending_ops_all.clone();
 8585        cx.subscribe(project.git_store(), move |_, _, e, _| {
 8586            if let GitStoreEvent::RepositoryUpdated(
 8587                _,
 8588                RepositoryEvent::PendingOpsChanged { pending_ops },
 8589                _,
 8590            ) = e
 8591            {
 8592                let merged = merge_pending_ops_snapshots(
 8593                    pending_ops.items(()),
 8594                    pending_ops_all.lock().items(()),
 8595                );
 8596                *pending_ops_all.lock() = SumTree::from_iter(merged.into_iter(), ());
 8597            }
 8598        })
 8599        .detach();
 8600    });
 8601    project
 8602        .update(cx, |project, cx| project.git_scans_complete(cx))
 8603        .await;
 8604
 8605    let repo = project.read_with(cx, |project, cx| {
 8606        project.repositories(cx).values().next().unwrap().clone()
 8607    });
 8608
 8609    // Ensure we have no pending ops for any of the untracked files
 8610    repo.read_with(cx, |repo, _cx| {
 8611        assert!(repo.pending_ops_by_path.is_empty());
 8612    });
 8613
 8614    let mut id = 1u16;
 8615
 8616    let mut assert_stage = async |path: RepoPath, stage| {
 8617        let git_status = if stage {
 8618            pending_op::GitStatus::Staged
 8619        } else {
 8620            pending_op::GitStatus::Unstaged
 8621        };
 8622        repo.update(cx, |repo, cx| {
 8623            let task = if stage {
 8624                repo.stage_entries(vec![path.clone()], cx)
 8625            } else {
 8626                repo.unstage_entries(vec![path.clone()], cx)
 8627            };
 8628            let ops = repo.pending_ops_for_path(&path).unwrap();
 8629            assert_eq!(
 8630                ops.ops.last(),
 8631                Some(&pending_op::PendingOp {
 8632                    id: id.into(),
 8633                    git_status,
 8634                    finished: false,
 8635                })
 8636            );
 8637            task
 8638        })
 8639        .await
 8640        .unwrap();
 8641
 8642        repo.read_with(cx, |repo, _cx| {
 8643            let ops = repo.pending_ops_for_path(&path).unwrap();
 8644            assert_eq!(
 8645                ops.ops.last(),
 8646                Some(&pending_op::PendingOp {
 8647                    id: id.into(),
 8648                    git_status,
 8649                    finished: true,
 8650                })
 8651            );
 8652        });
 8653
 8654        id += 1;
 8655    };
 8656
 8657    assert_stage(repo_path("a.txt"), true).await;
 8658    assert_stage(repo_path("a.txt"), false).await;
 8659    assert_stage(repo_path("a.txt"), true).await;
 8660    assert_stage(repo_path("a.txt"), false).await;
 8661    assert_stage(repo_path("a.txt"), true).await;
 8662
 8663    cx.run_until_parked();
 8664
 8665    assert_eq!(
 8666        pending_ops_all
 8667            .lock()
 8668            .get(&worktree::PathKey(repo_path("a.txt").as_ref().clone()), ())
 8669            .unwrap()
 8670            .ops,
 8671        vec![
 8672            pending_op::PendingOp {
 8673                id: 1u16.into(),
 8674                git_status: pending_op::GitStatus::Staged,
 8675                finished: true,
 8676            },
 8677            pending_op::PendingOp {
 8678                id: 2u16.into(),
 8679                git_status: pending_op::GitStatus::Unstaged,
 8680                finished: true,
 8681            },
 8682            pending_op::PendingOp {
 8683                id: 3u16.into(),
 8684                git_status: pending_op::GitStatus::Staged,
 8685                finished: true,
 8686            },
 8687            pending_op::PendingOp {
 8688                id: 4u16.into(),
 8689                git_status: pending_op::GitStatus::Unstaged,
 8690                finished: true,
 8691            },
 8692            pending_op::PendingOp {
 8693                id: 5u16.into(),
 8694                git_status: pending_op::GitStatus::Staged,
 8695                finished: true,
 8696            }
 8697        ],
 8698    );
 8699
 8700    repo.update(cx, |repo, _cx| {
 8701        let git_statuses = repo.cached_status().collect::<Vec<_>>();
 8702
 8703        assert_eq!(
 8704            git_statuses,
 8705            [StatusEntry {
 8706                repo_path: repo_path("a.txt"),
 8707                status: TrackedStatus {
 8708                    index_status: StatusCode::Added,
 8709                    worktree_status: StatusCode::Unmodified
 8710                }
 8711                .into(),
 8712            }]
 8713        );
 8714    });
 8715}
 8716
 8717#[gpui::test]
 8718async fn test_repository_pending_ops_long_running_staging(
 8719    executor: gpui::BackgroundExecutor,
 8720    cx: &mut gpui::TestAppContext,
 8721) {
 8722    init_test(cx);
 8723
 8724    let fs = FakeFs::new(executor);
 8725    fs.insert_tree(
 8726        path!("/root"),
 8727        json!({
 8728            "my-repo": {
 8729                ".git": {},
 8730                "a.txt": "a",
 8731            }
 8732
 8733        }),
 8734    )
 8735    .await;
 8736
 8737    fs.set_status_for_repo(
 8738        path!("/root/my-repo/.git").as_ref(),
 8739        &[("a.txt", FileStatus::Untracked)],
 8740    );
 8741
 8742    let project = Project::test(fs.clone(), [path!("/root/my-repo").as_ref()], cx).await;
 8743    let pending_ops_all = Arc::new(Mutex::new(SumTree::default()));
 8744    project.update(cx, |project, cx| {
 8745        let pending_ops_all = pending_ops_all.clone();
 8746        cx.subscribe(project.git_store(), move |_, _, e, _| {
 8747            if let GitStoreEvent::RepositoryUpdated(
 8748                _,
 8749                RepositoryEvent::PendingOpsChanged { pending_ops },
 8750                _,
 8751            ) = e
 8752            {
 8753                let merged = merge_pending_ops_snapshots(
 8754                    pending_ops.items(()),
 8755                    pending_ops_all.lock().items(()),
 8756                );
 8757                *pending_ops_all.lock() = SumTree::from_iter(merged.into_iter(), ());
 8758            }
 8759        })
 8760        .detach();
 8761    });
 8762
 8763    project
 8764        .update(cx, |project, cx| project.git_scans_complete(cx))
 8765        .await;
 8766
 8767    let repo = project.read_with(cx, |project, cx| {
 8768        project.repositories(cx).values().next().unwrap().clone()
 8769    });
 8770
 8771    repo.update(cx, |repo, cx| {
 8772        repo.stage_entries(vec![repo_path("a.txt")], cx)
 8773    })
 8774    .detach();
 8775
 8776    repo.update(cx, |repo, cx| {
 8777        repo.stage_entries(vec![repo_path("a.txt")], cx)
 8778    })
 8779    .unwrap()
 8780    .with_timeout(Duration::from_secs(1), &cx.executor())
 8781    .await
 8782    .unwrap();
 8783
 8784    cx.run_until_parked();
 8785
 8786    assert_eq!(
 8787        pending_ops_all
 8788            .lock()
 8789            .get(&worktree::PathKey(repo_path("a.txt").as_ref().clone()), ())
 8790            .unwrap()
 8791            .ops,
 8792        vec![pending_op::PendingOp {
 8793            id: 2u16.into(),
 8794            git_status: pending_op::GitStatus::Staged,
 8795            finished: true,
 8796        }],
 8797    );
 8798
 8799    repo.update(cx, |repo, _cx| {
 8800        let git_statuses = repo.cached_status().collect::<Vec<_>>();
 8801
 8802        assert_eq!(
 8803            git_statuses,
 8804            [StatusEntry {
 8805                repo_path: repo_path("a.txt"),
 8806                status: TrackedStatus {
 8807                    index_status: StatusCode::Added,
 8808                    worktree_status: StatusCode::Unmodified
 8809                }
 8810                .into(),
 8811            }]
 8812        );
 8813    });
 8814}
 8815
 8816#[gpui::test]
 8817async fn test_repository_pending_ops_stage_all(
 8818    executor: gpui::BackgroundExecutor,
 8819    cx: &mut gpui::TestAppContext,
 8820) {
 8821    init_test(cx);
 8822
 8823    let fs = FakeFs::new(executor);
 8824    fs.insert_tree(
 8825        path!("/root"),
 8826        json!({
 8827            "my-repo": {
 8828                ".git": {},
 8829                "a.txt": "a",
 8830                "b.txt": "b"
 8831            }
 8832
 8833        }),
 8834    )
 8835    .await;
 8836
 8837    fs.set_status_for_repo(
 8838        path!("/root/my-repo/.git").as_ref(),
 8839        &[
 8840            ("a.txt", FileStatus::Untracked),
 8841            ("b.txt", FileStatus::Untracked),
 8842        ],
 8843    );
 8844
 8845    let project = Project::test(fs.clone(), [path!("/root/my-repo").as_ref()], cx).await;
 8846    let pending_ops_all = Arc::new(Mutex::new(SumTree::default()));
 8847    project.update(cx, |project, cx| {
 8848        let pending_ops_all = pending_ops_all.clone();
 8849        cx.subscribe(project.git_store(), move |_, _, e, _| {
 8850            if let GitStoreEvent::RepositoryUpdated(
 8851                _,
 8852                RepositoryEvent::PendingOpsChanged { pending_ops },
 8853                _,
 8854            ) = e
 8855            {
 8856                let merged = merge_pending_ops_snapshots(
 8857                    pending_ops.items(()),
 8858                    pending_ops_all.lock().items(()),
 8859                );
 8860                *pending_ops_all.lock() = SumTree::from_iter(merged.into_iter(), ());
 8861            }
 8862        })
 8863        .detach();
 8864    });
 8865    project
 8866        .update(cx, |project, cx| project.git_scans_complete(cx))
 8867        .await;
 8868
 8869    let repo = project.read_with(cx, |project, cx| {
 8870        project.repositories(cx).values().next().unwrap().clone()
 8871    });
 8872
 8873    repo.update(cx, |repo, cx| {
 8874        repo.stage_entries(vec![repo_path("a.txt")], cx)
 8875    })
 8876    .await
 8877    .unwrap();
 8878    repo.update(cx, |repo, cx| repo.stage_all(cx))
 8879        .await
 8880        .unwrap();
 8881    repo.update(cx, |repo, cx| repo.unstage_all(cx))
 8882        .await
 8883        .unwrap();
 8884
 8885    cx.run_until_parked();
 8886
 8887    assert_eq!(
 8888        pending_ops_all
 8889            .lock()
 8890            .get(&worktree::PathKey(repo_path("a.txt").as_ref().clone()), ())
 8891            .unwrap()
 8892            .ops,
 8893        vec![
 8894            pending_op::PendingOp {
 8895                id: 1u16.into(),
 8896                git_status: pending_op::GitStatus::Staged,
 8897                finished: true,
 8898            },
 8899            pending_op::PendingOp {
 8900                id: 2u16.into(),
 8901                git_status: pending_op::GitStatus::Unstaged,
 8902                finished: true,
 8903            },
 8904        ],
 8905    );
 8906    assert_eq!(
 8907        pending_ops_all
 8908            .lock()
 8909            .get(&worktree::PathKey(repo_path("b.txt").as_ref().clone()), ())
 8910            .unwrap()
 8911            .ops,
 8912        vec![
 8913            pending_op::PendingOp {
 8914                id: 1u16.into(),
 8915                git_status: pending_op::GitStatus::Staged,
 8916                finished: true,
 8917            },
 8918            pending_op::PendingOp {
 8919                id: 2u16.into(),
 8920                git_status: pending_op::GitStatus::Unstaged,
 8921                finished: true,
 8922            },
 8923        ],
 8924    );
 8925
 8926    repo.update(cx, |repo, _cx| {
 8927        let git_statuses = repo.cached_status().collect::<Vec<_>>();
 8928
 8929        assert_eq!(
 8930            git_statuses,
 8931            [
 8932                StatusEntry {
 8933                    repo_path: repo_path("a.txt"),
 8934                    status: FileStatus::Untracked,
 8935                },
 8936                StatusEntry {
 8937                    repo_path: repo_path("b.txt"),
 8938                    status: FileStatus::Untracked,
 8939                },
 8940            ]
 8941        );
 8942    });
 8943}
 8944
 8945#[gpui::test]
 8946async fn test_repository_subfolder_git_status(
 8947    executor: gpui::BackgroundExecutor,
 8948    cx: &mut gpui::TestAppContext,
 8949) {
 8950    init_test(cx);
 8951
 8952    let fs = FakeFs::new(executor);
 8953    fs.insert_tree(
 8954        path!("/root"),
 8955        json!({
 8956            "my-repo": {
 8957                ".git": {},
 8958                "a.txt": "a",
 8959                "sub-folder-1": {
 8960                    "sub-folder-2": {
 8961                        "c.txt": "cc",
 8962                        "d": {
 8963                            "e.txt": "eee"
 8964                        }
 8965                    },
 8966                }
 8967            },
 8968        }),
 8969    )
 8970    .await;
 8971
 8972    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
 8973    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
 8974
 8975    fs.set_status_for_repo(
 8976        path!("/root/my-repo/.git").as_ref(),
 8977        &[(E_TXT, FileStatus::Untracked)],
 8978    );
 8979
 8980    let project = Project::test(
 8981        fs.clone(),
 8982        [path!("/root/my-repo/sub-folder-1/sub-folder-2").as_ref()],
 8983        cx,
 8984    )
 8985    .await;
 8986
 8987    project
 8988        .update(cx, |project, cx| project.git_scans_complete(cx))
 8989        .await;
 8990    cx.run_until_parked();
 8991
 8992    let repository = project.read_with(cx, |project, cx| {
 8993        project.repositories(cx).values().next().unwrap().clone()
 8994    });
 8995
 8996    // Ensure that the git status is loaded correctly
 8997    repository.read_with(cx, |repository, _cx| {
 8998        assert_eq!(
 8999            repository.work_directory_abs_path,
 9000            Path::new(path!("/root/my-repo")).into()
 9001        );
 9002
 9003        assert_eq!(repository.status_for_path(&repo_path(C_TXT)), None);
 9004        assert_eq!(
 9005            repository
 9006                .status_for_path(&repo_path(E_TXT))
 9007                .unwrap()
 9008                .status,
 9009            FileStatus::Untracked
 9010        );
 9011    });
 9012
 9013    fs.set_status_for_repo(path!("/root/my-repo/.git").as_ref(), &[]);
 9014    project
 9015        .update(cx, |project, cx| project.git_scans_complete(cx))
 9016        .await;
 9017    cx.run_until_parked();
 9018
 9019    repository.read_with(cx, |repository, _cx| {
 9020        assert_eq!(repository.status_for_path(&repo_path(C_TXT)), None);
 9021        assert_eq!(repository.status_for_path(&repo_path(E_TXT)), None);
 9022    });
 9023}
 9024
 9025// TODO: this test is flaky (especially on Windows but at least sometimes on all platforms).
 9026#[cfg(any())]
 9027#[gpui::test]
 9028async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
 9029    init_test(cx);
 9030    cx.executor().allow_parking();
 9031
 9032    let root = TempTree::new(json!({
 9033        "project": {
 9034            "a.txt": "a",
 9035        },
 9036    }));
 9037    let root_path = root.path();
 9038
 9039    let repo = git_init(&root_path.join("project"));
 9040    git_add("a.txt", &repo);
 9041    git_commit("init", &repo);
 9042
 9043    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
 9044
 9045    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9046    tree.flush_fs_events(cx).await;
 9047    project
 9048        .update(cx, |project, cx| project.git_scans_complete(cx))
 9049        .await;
 9050    cx.executor().run_until_parked();
 9051
 9052    let repository = project.read_with(cx, |project, cx| {
 9053        project.repositories(cx).values().next().unwrap().clone()
 9054    });
 9055
 9056    git_branch("other-branch", &repo);
 9057    git_checkout("refs/heads/other-branch", &repo);
 9058    std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
 9059    git_add("a.txt", &repo);
 9060    git_commit("capitalize", &repo);
 9061    let commit = repo
 9062        .head()
 9063        .expect("Failed to get HEAD")
 9064        .peel_to_commit()
 9065        .expect("HEAD is not a commit");
 9066    git_checkout("refs/heads/main", &repo);
 9067    std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
 9068    git_add("a.txt", &repo);
 9069    git_commit("improve letter", &repo);
 9070    git_cherry_pick(&commit, &repo);
 9071    std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
 9072        .expect("No CHERRY_PICK_HEAD");
 9073    pretty_assertions::assert_eq!(
 9074        git_status(&repo),
 9075        collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
 9076    );
 9077    tree.flush_fs_events(cx).await;
 9078    project
 9079        .update(cx, |project, cx| project.git_scans_complete(cx))
 9080        .await;
 9081    cx.executor().run_until_parked();
 9082    let conflicts = repository.update(cx, |repository, _| {
 9083        repository
 9084            .merge_conflicts
 9085            .iter()
 9086            .cloned()
 9087            .collect::<Vec<_>>()
 9088    });
 9089    pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
 9090
 9091    git_add("a.txt", &repo);
 9092    // Attempt to manually simulate what `git cherry-pick --continue` would do.
 9093    git_commit("whatevs", &repo);
 9094    std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
 9095        .expect("Failed to remove CHERRY_PICK_HEAD");
 9096    pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
 9097    tree.flush_fs_events(cx).await;
 9098    let conflicts = repository.update(cx, |repository, _| {
 9099        repository
 9100            .merge_conflicts
 9101            .iter()
 9102            .cloned()
 9103            .collect::<Vec<_>>()
 9104    });
 9105    pretty_assertions::assert_eq!(conflicts, []);
 9106}
 9107
 9108#[gpui::test]
 9109async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
 9110    init_test(cx);
 9111    let fs = FakeFs::new(cx.background_executor.clone());
 9112    fs.insert_tree(
 9113        path!("/root"),
 9114        json!({
 9115            ".git": {},
 9116            ".gitignore": "*.txt\n",
 9117            "a.xml": "<a></a>",
 9118            "b.txt": "Some text"
 9119        }),
 9120    )
 9121    .await;
 9122
 9123    fs.set_head_and_index_for_repo(
 9124        path!("/root/.git").as_ref(),
 9125        &[
 9126            (".gitignore", "*.txt\n".into()),
 9127            ("a.xml", "<a></a>".into()),
 9128        ],
 9129    );
 9130
 9131    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 9132
 9133    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9134    tree.flush_fs_events(cx).await;
 9135    project
 9136        .update(cx, |project, cx| project.git_scans_complete(cx))
 9137        .await;
 9138    cx.executor().run_until_parked();
 9139
 9140    let repository = project.read_with(cx, |project, cx| {
 9141        project.repositories(cx).values().next().unwrap().clone()
 9142    });
 9143
 9144    // One file is unmodified, the other is ignored.
 9145    cx.read(|cx| {
 9146        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
 9147        assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
 9148    });
 9149
 9150    // Change the gitignore, and stage the newly non-ignored file.
 9151    fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
 9152        .await
 9153        .unwrap();
 9154    fs.set_index_for_repo(
 9155        Path::new(path!("/root/.git")),
 9156        &[
 9157            (".gitignore", "*.txt\n".into()),
 9158            ("a.xml", "<a></a>".into()),
 9159            ("b.txt", "Some text".into()),
 9160        ],
 9161    );
 9162
 9163    cx.executor().run_until_parked();
 9164    cx.read(|cx| {
 9165        assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
 9166        assert_entry_git_state(
 9167            tree.read(cx),
 9168            repository.read(cx),
 9169            "b.txt",
 9170            Some(StatusCode::Added),
 9171            false,
 9172        );
 9173    });
 9174}
 9175
 9176// NOTE:
 9177// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
 9178// a directory which some program has already open.
 9179// This is a limitation of the Windows.
 9180// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
 9181// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 9182#[gpui::test]
 9183#[cfg_attr(target_os = "windows", ignore)]
 9184async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
 9185    init_test(cx);
 9186    cx.executor().allow_parking();
 9187    let root = TempTree::new(json!({
 9188        "projects": {
 9189            "project1": {
 9190                "a": "",
 9191                "b": "",
 9192            }
 9193        },
 9194
 9195    }));
 9196    let root_path = root.path();
 9197
 9198    let repo = git_init(&root_path.join("projects/project1"));
 9199    git_add("a", &repo);
 9200    git_commit("init", &repo);
 9201    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
 9202
 9203    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
 9204
 9205    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9206    tree.flush_fs_events(cx).await;
 9207    project
 9208        .update(cx, |project, cx| project.git_scans_complete(cx))
 9209        .await;
 9210    cx.executor().run_until_parked();
 9211
 9212    let repository = project.read_with(cx, |project, cx| {
 9213        project.repositories(cx).values().next().unwrap().clone()
 9214    });
 9215
 9216    repository.read_with(cx, |repository, _| {
 9217        assert_eq!(
 9218            repository.work_directory_abs_path.as_ref(),
 9219            root_path.join("projects/project1").as_path()
 9220        );
 9221        assert_eq!(
 9222            repository
 9223                .status_for_path(&repo_path("a"))
 9224                .map(|entry| entry.status),
 9225            Some(StatusCode::Modified.worktree()),
 9226        );
 9227        assert_eq!(
 9228            repository
 9229                .status_for_path(&repo_path("b"))
 9230                .map(|entry| entry.status),
 9231            Some(FileStatus::Untracked),
 9232        );
 9233    });
 9234
 9235    std::fs::rename(
 9236        root_path.join("projects/project1"),
 9237        root_path.join("projects/project2"),
 9238    )
 9239    .unwrap();
 9240    tree.flush_fs_events(cx).await;
 9241
 9242    repository.read_with(cx, |repository, _| {
 9243        assert_eq!(
 9244            repository.work_directory_abs_path.as_ref(),
 9245            root_path.join("projects/project2").as_path()
 9246        );
 9247        assert_eq!(
 9248            repository.status_for_path(&repo_path("a")).unwrap().status,
 9249            StatusCode::Modified.worktree(),
 9250        );
 9251        assert_eq!(
 9252            repository.status_for_path(&repo_path("b")).unwrap().status,
 9253            FileStatus::Untracked,
 9254        );
 9255    });
 9256}
 9257
 9258// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
 9259// you can't rename a directory which some program has already open. This is a
 9260// limitation of the Windows. See:
 9261// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
 9262// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information
 9263#[gpui::test]
 9264#[cfg_attr(target_os = "windows", ignore)]
 9265async fn test_file_status(cx: &mut gpui::TestAppContext) {
 9266    init_test(cx);
 9267    cx.executor().allow_parking();
 9268    const IGNORE_RULE: &str = "**/target";
 9269
 9270    let root = TempTree::new(json!({
 9271        "project": {
 9272            "a.txt": "a",
 9273            "b.txt": "bb",
 9274            "c": {
 9275                "d": {
 9276                    "e.txt": "eee"
 9277                }
 9278            },
 9279            "f.txt": "ffff",
 9280            "target": {
 9281                "build_file": "???"
 9282            },
 9283            ".gitignore": IGNORE_RULE
 9284        },
 9285
 9286    }));
 9287    let root_path = root.path();
 9288
 9289    const A_TXT: &str = "a.txt";
 9290    const B_TXT: &str = "b.txt";
 9291    const E_TXT: &str = "c/d/e.txt";
 9292    const F_TXT: &str = "f.txt";
 9293    const DOTGITIGNORE: &str = ".gitignore";
 9294    const BUILD_FILE: &str = "target/build_file";
 9295
 9296    // Set up git repository before creating the worktree.
 9297    let work_dir = root.path().join("project");
 9298    let mut repo = git_init(work_dir.as_path());
 9299    repo.add_ignore_rule(IGNORE_RULE).unwrap();
 9300    git_add(A_TXT, &repo);
 9301    git_add(E_TXT, &repo);
 9302    git_add(DOTGITIGNORE, &repo);
 9303    git_commit("Initial commit", &repo);
 9304
 9305    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
 9306
 9307    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9308    tree.flush_fs_events(cx).await;
 9309    project
 9310        .update(cx, |project, cx| project.git_scans_complete(cx))
 9311        .await;
 9312    cx.executor().run_until_parked();
 9313
 9314    let repository = project.read_with(cx, |project, cx| {
 9315        project.repositories(cx).values().next().unwrap().clone()
 9316    });
 9317
 9318    // Check that the right git state is observed on startup
 9319    repository.read_with(cx, |repository, _cx| {
 9320        assert_eq!(
 9321            repository.work_directory_abs_path.as_ref(),
 9322            root_path.join("project").as_path()
 9323        );
 9324
 9325        assert_eq!(
 9326            repository
 9327                .status_for_path(&repo_path(B_TXT))
 9328                .unwrap()
 9329                .status,
 9330            FileStatus::Untracked,
 9331        );
 9332        assert_eq!(
 9333            repository
 9334                .status_for_path(&repo_path(F_TXT))
 9335                .unwrap()
 9336                .status,
 9337            FileStatus::Untracked,
 9338        );
 9339    });
 9340
 9341    // Modify a file in the working copy.
 9342    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
 9343    tree.flush_fs_events(cx).await;
 9344    project
 9345        .update(cx, |project, cx| project.git_scans_complete(cx))
 9346        .await;
 9347    cx.executor().run_until_parked();
 9348
 9349    // The worktree detects that the file's git status has changed.
 9350    repository.read_with(cx, |repository, _| {
 9351        assert_eq!(
 9352            repository
 9353                .status_for_path(&repo_path(A_TXT))
 9354                .unwrap()
 9355                .status,
 9356            StatusCode::Modified.worktree(),
 9357        );
 9358    });
 9359
 9360    // Create a commit in the git repository.
 9361    git_add(A_TXT, &repo);
 9362    git_add(B_TXT, &repo);
 9363    git_commit("Committing modified and added", &repo);
 9364    tree.flush_fs_events(cx).await;
 9365    project
 9366        .update(cx, |project, cx| project.git_scans_complete(cx))
 9367        .await;
 9368    cx.executor().run_until_parked();
 9369
 9370    // The worktree detects that the files' git status have changed.
 9371    repository.read_with(cx, |repository, _cx| {
 9372        assert_eq!(
 9373            repository
 9374                .status_for_path(&repo_path(F_TXT))
 9375                .unwrap()
 9376                .status,
 9377            FileStatus::Untracked,
 9378        );
 9379        assert_eq!(repository.status_for_path(&repo_path(B_TXT)), None);
 9380        assert_eq!(repository.status_for_path(&repo_path(A_TXT)), None);
 9381    });
 9382
 9383    // Modify files in the working copy and perform git operations on other files.
 9384    git_reset(0, &repo);
 9385    git_remove_index(Path::new(B_TXT), &repo);
 9386    git_stash(&mut repo);
 9387    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
 9388    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
 9389    tree.flush_fs_events(cx).await;
 9390    project
 9391        .update(cx, |project, cx| project.git_scans_complete(cx))
 9392        .await;
 9393    cx.executor().run_until_parked();
 9394
 9395    // Check that more complex repo changes are tracked
 9396    repository.read_with(cx, |repository, _cx| {
 9397        assert_eq!(repository.status_for_path(&repo_path(A_TXT)), None);
 9398        assert_eq!(
 9399            repository
 9400                .status_for_path(&repo_path(B_TXT))
 9401                .unwrap()
 9402                .status,
 9403            FileStatus::Untracked,
 9404        );
 9405        assert_eq!(
 9406            repository
 9407                .status_for_path(&repo_path(E_TXT))
 9408                .unwrap()
 9409                .status,
 9410            StatusCode::Modified.worktree(),
 9411        );
 9412    });
 9413
 9414    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
 9415    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
 9416    std::fs::write(
 9417        work_dir.join(DOTGITIGNORE),
 9418        [IGNORE_RULE, "f.txt"].join("\n"),
 9419    )
 9420    .unwrap();
 9421
 9422    git_add(Path::new(DOTGITIGNORE), &repo);
 9423    git_commit("Committing modified git ignore", &repo);
 9424
 9425    tree.flush_fs_events(cx).await;
 9426    cx.executor().run_until_parked();
 9427
 9428    let mut renamed_dir_name = "first_directory/second_directory";
 9429    const RENAMED_FILE: &str = "rf.txt";
 9430
 9431    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
 9432    std::fs::write(
 9433        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
 9434        "new-contents",
 9435    )
 9436    .unwrap();
 9437
 9438    tree.flush_fs_events(cx).await;
 9439    project
 9440        .update(cx, |project, cx| project.git_scans_complete(cx))
 9441        .await;
 9442    cx.executor().run_until_parked();
 9443
 9444    repository.read_with(cx, |repository, _cx| {
 9445        assert_eq!(
 9446            repository
 9447                .status_for_path(&RepoPath::from_rel_path(
 9448                    &rel_path(renamed_dir_name).join(rel_path(RENAMED_FILE))
 9449                ))
 9450                .unwrap()
 9451                .status,
 9452            FileStatus::Untracked,
 9453        );
 9454    });
 9455
 9456    renamed_dir_name = "new_first_directory/second_directory";
 9457
 9458    std::fs::rename(
 9459        work_dir.join("first_directory"),
 9460        work_dir.join("new_first_directory"),
 9461    )
 9462    .unwrap();
 9463
 9464    tree.flush_fs_events(cx).await;
 9465    project
 9466        .update(cx, |project, cx| project.git_scans_complete(cx))
 9467        .await;
 9468    cx.executor().run_until_parked();
 9469
 9470    repository.read_with(cx, |repository, _cx| {
 9471        assert_eq!(
 9472            repository
 9473                .status_for_path(&RepoPath::from_rel_path(
 9474                    &rel_path(renamed_dir_name).join(rel_path(RENAMED_FILE))
 9475                ))
 9476                .unwrap()
 9477                .status,
 9478            FileStatus::Untracked,
 9479        );
 9480    });
 9481}
 9482
 9483#[gpui::test]
 9484#[ignore]
 9485async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
 9486    init_test(cx);
 9487    cx.executor().allow_parking();
 9488
 9489    const IGNORE_RULE: &str = "**/target";
 9490
 9491    let root = TempTree::new(json!({
 9492        "project": {
 9493            "src": {
 9494                "main.rs": "fn main() {}"
 9495            },
 9496            "target": {
 9497                "debug": {
 9498                    "important_text.txt": "important text",
 9499                },
 9500            },
 9501            ".gitignore": IGNORE_RULE
 9502        },
 9503
 9504    }));
 9505    let root_path = root.path();
 9506
 9507    // Set up git repository before creating the worktree.
 9508    let work_dir = root.path().join("project");
 9509    let repo = git_init(work_dir.as_path());
 9510    repo.add_ignore_rule(IGNORE_RULE).unwrap();
 9511    git_add("src/main.rs", &repo);
 9512    git_add(".gitignore", &repo);
 9513    git_commit("Initial commit", &repo);
 9514
 9515    let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
 9516    let repository_updates = Arc::new(Mutex::new(Vec::new()));
 9517    let project_events = Arc::new(Mutex::new(Vec::new()));
 9518    project.update(cx, |project, cx| {
 9519        let repo_events = repository_updates.clone();
 9520        cx.subscribe(project.git_store(), move |_, _, e, _| {
 9521            if let GitStoreEvent::RepositoryUpdated(_, e, _) = e {
 9522                repo_events.lock().push(e.clone());
 9523            }
 9524        })
 9525        .detach();
 9526        let project_events = project_events.clone();
 9527        cx.subscribe_self(move |_, e, _| {
 9528            if let Event::WorktreeUpdatedEntries(_, updates) = e {
 9529                project_events.lock().extend(
 9530                    updates
 9531                        .iter()
 9532                        .map(|(path, _, change)| (path.as_unix_str().to_string(), *change))
 9533                        .filter(|(path, _)| path != "fs-event-sentinel"),
 9534                );
 9535            }
 9536        })
 9537        .detach();
 9538    });
 9539
 9540    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9541    tree.flush_fs_events(cx).await;
 9542    tree.update(cx, |tree, cx| {
 9543        tree.load_file(rel_path("project/target/debug/important_text.txt"), cx)
 9544    })
 9545    .await
 9546    .unwrap();
 9547    tree.update(cx, |tree, _| {
 9548        assert_eq!(
 9549            tree.entries(true, 0)
 9550                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 9551                .collect::<Vec<_>>(),
 9552            vec![
 9553                (rel_path(""), false),
 9554                (rel_path("project/"), false),
 9555                (rel_path("project/.gitignore"), false),
 9556                (rel_path("project/src"), false),
 9557                (rel_path("project/src/main.rs"), false),
 9558                (rel_path("project/target"), true),
 9559                (rel_path("project/target/debug"), true),
 9560                (rel_path("project/target/debug/important_text.txt"), true),
 9561            ]
 9562        );
 9563    });
 9564
 9565    assert_eq!(
 9566        repository_updates.lock().drain(..).collect::<Vec<_>>(),
 9567        vec![
 9568            RepositoryEvent::StatusesChanged,
 9569            RepositoryEvent::MergeHeadsChanged,
 9570        ],
 9571        "Initial worktree scan should produce a repo update event"
 9572    );
 9573    assert_eq!(
 9574        project_events.lock().drain(..).collect::<Vec<_>>(),
 9575        vec![
 9576            ("project/target".to_string(), PathChange::Loaded),
 9577            ("project/target/debug".to_string(), PathChange::Loaded),
 9578            (
 9579                "project/target/debug/important_text.txt".to_string(),
 9580                PathChange::Loaded
 9581            ),
 9582        ],
 9583        "Initial project changes should show that all not-ignored and all opened files are loaded"
 9584    );
 9585
 9586    let deps_dir = work_dir.join("target").join("debug").join("deps");
 9587    std::fs::create_dir_all(&deps_dir).unwrap();
 9588    tree.flush_fs_events(cx).await;
 9589    project
 9590        .update(cx, |project, cx| project.git_scans_complete(cx))
 9591        .await;
 9592    cx.executor().run_until_parked();
 9593    std::fs::write(deps_dir.join("aa.tmp"), "something tmp").unwrap();
 9594    tree.flush_fs_events(cx).await;
 9595    project
 9596        .update(cx, |project, cx| project.git_scans_complete(cx))
 9597        .await;
 9598    cx.executor().run_until_parked();
 9599    std::fs::remove_dir_all(&deps_dir).unwrap();
 9600    tree.flush_fs_events(cx).await;
 9601    project
 9602        .update(cx, |project, cx| project.git_scans_complete(cx))
 9603        .await;
 9604    cx.executor().run_until_parked();
 9605
 9606    tree.update(cx, |tree, _| {
 9607        assert_eq!(
 9608            tree.entries(true, 0)
 9609                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 9610                .collect::<Vec<_>>(),
 9611            vec![
 9612                (rel_path(""), false),
 9613                (rel_path("project/"), false),
 9614                (rel_path("project/.gitignore"), false),
 9615                (rel_path("project/src"), false),
 9616                (rel_path("project/src/main.rs"), false),
 9617                (rel_path("project/target"), true),
 9618                (rel_path("project/target/debug"), true),
 9619                (rel_path("project/target/debug/important_text.txt"), true),
 9620            ],
 9621            "No stray temp files should be left after the flycheck changes"
 9622        );
 9623    });
 9624
 9625    assert_eq!(
 9626        repository_updates
 9627            .lock()
 9628            .iter()
 9629            .cloned()
 9630            .collect::<Vec<_>>(),
 9631        Vec::new(),
 9632        "No further RepositoryUpdated events should happen, as only ignored dirs' contents was changed",
 9633    );
 9634    assert_eq!(
 9635        project_events.lock().as_slice(),
 9636        vec![
 9637            ("project/target/debug/deps".to_string(), PathChange::Added),
 9638            ("project/target/debug/deps".to_string(), PathChange::Removed),
 9639        ],
 9640        "Due to `debug` directory being tracket, it should get updates for entries inside it.
 9641        No updates for more nested directories should happen as those are ignored",
 9642    );
 9643}
 9644
 9645#[gpui::test]
 9646async fn test_odd_events_for_ignored_dirs(
 9647    executor: BackgroundExecutor,
 9648    cx: &mut gpui::TestAppContext,
 9649) {
 9650    init_test(cx);
 9651    let fs = FakeFs::new(executor);
 9652    fs.insert_tree(
 9653        path!("/root"),
 9654        json!({
 9655            ".git": {},
 9656            ".gitignore": "**/target/",
 9657            "src": {
 9658                "main.rs": "fn main() {}",
 9659            },
 9660            "target": {
 9661                "debug": {
 9662                    "foo.txt": "foo",
 9663                    "deps": {}
 9664                }
 9665            }
 9666        }),
 9667    )
 9668    .await;
 9669    fs.set_head_and_index_for_repo(
 9670        path!("/root/.git").as_ref(),
 9671        &[
 9672            (".gitignore", "**/target/".into()),
 9673            ("src/main.rs", "fn main() {}".into()),
 9674        ],
 9675    );
 9676
 9677    let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
 9678    let repository_updates = Arc::new(Mutex::new(Vec::new()));
 9679    let project_events = Arc::new(Mutex::new(Vec::new()));
 9680    project.update(cx, |project, cx| {
 9681        let repository_updates = repository_updates.clone();
 9682        cx.subscribe(project.git_store(), move |_, _, e, _| {
 9683            if let GitStoreEvent::RepositoryUpdated(_, e, _) = e {
 9684                repository_updates.lock().push(e.clone());
 9685            }
 9686        })
 9687        .detach();
 9688        let project_events = project_events.clone();
 9689        cx.subscribe_self(move |_, e, _| {
 9690            if let Event::WorktreeUpdatedEntries(_, updates) = e {
 9691                project_events.lock().extend(
 9692                    updates
 9693                        .iter()
 9694                        .map(|(path, _, change)| (path.as_unix_str().to_string(), *change))
 9695                        .filter(|(path, _)| path != "fs-event-sentinel"),
 9696                );
 9697            }
 9698        })
 9699        .detach();
 9700    });
 9701
 9702    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9703    tree.update(cx, |tree, cx| {
 9704        tree.load_file(rel_path("target/debug/foo.txt"), cx)
 9705    })
 9706    .await
 9707    .unwrap();
 9708    tree.flush_fs_events(cx).await;
 9709    project
 9710        .update(cx, |project, cx| project.git_scans_complete(cx))
 9711        .await;
 9712    cx.run_until_parked();
 9713    tree.update(cx, |tree, _| {
 9714        assert_eq!(
 9715            tree.entries(true, 0)
 9716                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 9717                .collect::<Vec<_>>(),
 9718            vec![
 9719                (rel_path(""), false),
 9720                (rel_path(".gitignore"), false),
 9721                (rel_path("src"), false),
 9722                (rel_path("src/main.rs"), false),
 9723                (rel_path("target"), true),
 9724                (rel_path("target/debug"), true),
 9725                (rel_path("target/debug/deps"), true),
 9726                (rel_path("target/debug/foo.txt"), true),
 9727            ]
 9728        );
 9729    });
 9730
 9731    assert_eq!(
 9732        repository_updates.lock().drain(..).collect::<Vec<_>>(),
 9733        vec![
 9734            RepositoryEvent::MergeHeadsChanged,
 9735            RepositoryEvent::BranchChanged,
 9736            RepositoryEvent::StatusesChanged,
 9737            RepositoryEvent::StatusesChanged,
 9738        ],
 9739        "Initial worktree scan should produce a repo update event"
 9740    );
 9741    assert_eq!(
 9742        project_events.lock().drain(..).collect::<Vec<_>>(),
 9743        vec![
 9744            ("target".to_string(), PathChange::Loaded),
 9745            ("target/debug".to_string(), PathChange::Loaded),
 9746            ("target/debug/deps".to_string(), PathChange::Loaded),
 9747            ("target/debug/foo.txt".to_string(), PathChange::Loaded),
 9748        ],
 9749        "All non-ignored entries and all opened firs should be getting a project event",
 9750    );
 9751
 9752    // Emulate a flycheck spawn: it emits a `INODE_META_MOD`-flagged FS event on target/debug/deps, then creates and removes temp files inside.
 9753    // This may happen multiple times during a single flycheck, but once is enough for testing.
 9754    fs.emit_fs_event("/root/target/debug/deps", None);
 9755    tree.flush_fs_events(cx).await;
 9756    project
 9757        .update(cx, |project, cx| project.git_scans_complete(cx))
 9758        .await;
 9759    cx.executor().run_until_parked();
 9760
 9761    assert_eq!(
 9762        repository_updates
 9763            .lock()
 9764            .iter()
 9765            .cloned()
 9766            .collect::<Vec<_>>(),
 9767        Vec::new(),
 9768        "No further RepositoryUpdated events should happen, as only ignored dirs received FS events",
 9769    );
 9770    assert_eq!(
 9771        project_events.lock().as_slice(),
 9772        Vec::new(),
 9773        "No further project events should happen, as only ignored dirs received FS events",
 9774    );
 9775}
 9776
 9777#[gpui::test]
 9778async fn test_repos_in_invisible_worktrees(
 9779    executor: BackgroundExecutor,
 9780    cx: &mut gpui::TestAppContext,
 9781) {
 9782    init_test(cx);
 9783    let fs = FakeFs::new(executor);
 9784    fs.insert_tree(
 9785        path!("/root"),
 9786        json!({
 9787            "dir1": {
 9788                ".git": {},
 9789                "dep1": {
 9790                    ".git": {},
 9791                    "src": {
 9792                        "a.txt": "",
 9793                    },
 9794                },
 9795                "b.txt": "",
 9796            },
 9797        }),
 9798    )
 9799    .await;
 9800
 9801    let project = Project::test(fs.clone(), [path!("/root/dir1/dep1").as_ref()], cx).await;
 9802    let _visible_worktree =
 9803        project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9804    project
 9805        .update(cx, |project, cx| project.git_scans_complete(cx))
 9806        .await;
 9807
 9808    let repos = project.read_with(cx, |project, cx| {
 9809        project
 9810            .repositories(cx)
 9811            .values()
 9812            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
 9813            .collect::<Vec<_>>()
 9814    });
 9815    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
 9816
 9817    let (_invisible_worktree, _) = project
 9818        .update(cx, |project, cx| {
 9819            project.worktree_store.update(cx, |worktree_store, cx| {
 9820                worktree_store.find_or_create_worktree(path!("/root/dir1/b.txt"), false, cx)
 9821            })
 9822        })
 9823        .await
 9824        .expect("failed to create worktree");
 9825    project
 9826        .update(cx, |project, cx| project.git_scans_complete(cx))
 9827        .await;
 9828
 9829    let repos = project.read_with(cx, |project, cx| {
 9830        project
 9831            .repositories(cx)
 9832            .values()
 9833            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
 9834            .collect::<Vec<_>>()
 9835    });
 9836    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
 9837}
 9838
 9839#[gpui::test(iterations = 10)]
 9840async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
 9841    init_test(cx);
 9842    cx.update(|cx| {
 9843        cx.update_global::<SettingsStore, _>(|store, cx| {
 9844            store.update_user_settings(cx, |settings| {
 9845                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
 9846            });
 9847        });
 9848    });
 9849    let fs = FakeFs::new(cx.background_executor.clone());
 9850    fs.insert_tree(
 9851        path!("/root"),
 9852        json!({
 9853            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
 9854            "tree": {
 9855                ".git": {},
 9856                ".gitignore": "ignored-dir\n",
 9857                "tracked-dir": {
 9858                    "tracked-file1": "",
 9859                    "ancestor-ignored-file1": "",
 9860                },
 9861                "ignored-dir": {
 9862                    "ignored-file1": ""
 9863                }
 9864            }
 9865        }),
 9866    )
 9867    .await;
 9868    fs.set_head_and_index_for_repo(
 9869        path!("/root/tree/.git").as_ref(),
 9870        &[
 9871            (".gitignore", "ignored-dir\n".into()),
 9872            ("tracked-dir/tracked-file1", "".into()),
 9873        ],
 9874    );
 9875
 9876    let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
 9877
 9878    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
 9879    tree.flush_fs_events(cx).await;
 9880    project
 9881        .update(cx, |project, cx| project.git_scans_complete(cx))
 9882        .await;
 9883    cx.executor().run_until_parked();
 9884
 9885    let repository = project.read_with(cx, |project, cx| {
 9886        project.repositories(cx).values().next().unwrap().clone()
 9887    });
 9888
 9889    tree.read_with(cx, |tree, _| {
 9890        tree.as_local()
 9891            .unwrap()
 9892            .manually_refresh_entries_for_paths(vec![rel_path("ignored-dir").into()])
 9893    })
 9894    .recv()
 9895    .await;
 9896
 9897    cx.read(|cx| {
 9898        assert_entry_git_state(
 9899            tree.read(cx),
 9900            repository.read(cx),
 9901            "tracked-dir/tracked-file1",
 9902            None,
 9903            false,
 9904        );
 9905        assert_entry_git_state(
 9906            tree.read(cx),
 9907            repository.read(cx),
 9908            "tracked-dir/ancestor-ignored-file1",
 9909            None,
 9910            false,
 9911        );
 9912        assert_entry_git_state(
 9913            tree.read(cx),
 9914            repository.read(cx),
 9915            "ignored-dir/ignored-file1",
 9916            None,
 9917            true,
 9918        );
 9919    });
 9920
 9921    fs.create_file(
 9922        path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
 9923        Default::default(),
 9924    )
 9925    .await
 9926    .unwrap();
 9927    fs.set_index_for_repo(
 9928        path!("/root/tree/.git").as_ref(),
 9929        &[
 9930            (".gitignore", "ignored-dir\n".into()),
 9931            ("tracked-dir/tracked-file1", "".into()),
 9932            ("tracked-dir/tracked-file2", "".into()),
 9933        ],
 9934    );
 9935    fs.create_file(
 9936        path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
 9937        Default::default(),
 9938    )
 9939    .await
 9940    .unwrap();
 9941    fs.create_file(
 9942        path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
 9943        Default::default(),
 9944    )
 9945    .await
 9946    .unwrap();
 9947
 9948    cx.executor().run_until_parked();
 9949    cx.read(|cx| {
 9950        assert_entry_git_state(
 9951            tree.read(cx),
 9952            repository.read(cx),
 9953            "tracked-dir/tracked-file2",
 9954            Some(StatusCode::Added),
 9955            false,
 9956        );
 9957        assert_entry_git_state(
 9958            tree.read(cx),
 9959            repository.read(cx),
 9960            "tracked-dir/ancestor-ignored-file2",
 9961            None,
 9962            false,
 9963        );
 9964        assert_entry_git_state(
 9965            tree.read(cx),
 9966            repository.read(cx),
 9967            "ignored-dir/ignored-file2",
 9968            None,
 9969            true,
 9970        );
 9971        assert!(
 9972            tree.read(cx)
 9973                .entry_for_path(&rel_path(".git"))
 9974                .unwrap()
 9975                .is_ignored
 9976        );
 9977    });
 9978}
 9979
 9980#[gpui::test]
 9981async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
 9982    init_test(cx);
 9983
 9984    let fs = FakeFs::new(cx.executor());
 9985    fs.insert_tree(
 9986        path!("/project"),
 9987        json!({
 9988            ".git": {
 9989                "worktrees": {
 9990                    "some-worktree": {
 9991                        "commondir": "../..\n",
 9992                        // For is_git_dir
 9993                        "HEAD": "",
 9994                        "config": ""
 9995                    }
 9996                },
 9997                "modules": {
 9998                    "subdir": {
 9999                        "some-submodule": {
10000                            // For is_git_dir
10001                            "HEAD": "",
10002                            "config": "",
10003                        }
10004                    }
10005                }
10006            },
10007            "src": {
10008                "a.txt": "A",
10009            },
10010            "some-worktree": {
10011                ".git": "gitdir: ../.git/worktrees/some-worktree\n",
10012                "src": {
10013                    "b.txt": "B",
10014                }
10015            },
10016            "subdir": {
10017                "some-submodule": {
10018                    ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
10019                    "c.txt": "C",
10020                }
10021            }
10022        }),
10023    )
10024    .await;
10025
10026    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
10027    let scan_complete = project.update(cx, |project, cx| project.git_scans_complete(cx));
10028    scan_complete.await;
10029
10030    let mut repositories = project.update(cx, |project, cx| {
10031        project
10032            .repositories(cx)
10033            .values()
10034            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
10035            .collect::<Vec<_>>()
10036    });
10037    repositories.sort();
10038    pretty_assertions::assert_eq!(
10039        repositories,
10040        [
10041            Path::new(path!("/project")).into(),
10042            Path::new(path!("/project/some-worktree")).into(),
10043            Path::new(path!("/project/subdir/some-submodule")).into(),
10044        ]
10045    );
10046
10047    // Generate a git-related event for the worktree and check that it's refreshed.
10048    fs.with_git_state(
10049        path!("/project/some-worktree/.git").as_ref(),
10050        true,
10051        |state| {
10052            state
10053                .head_contents
10054                .insert(repo_path("src/b.txt"), "b".to_owned());
10055            state
10056                .index_contents
10057                .insert(repo_path("src/b.txt"), "b".to_owned());
10058        },
10059    )
10060    .unwrap();
10061    cx.run_until_parked();
10062
10063    let buffer = project
10064        .update(cx, |project, cx| {
10065            project.open_local_buffer(path!("/project/some-worktree/src/b.txt"), cx)
10066        })
10067        .await
10068        .unwrap();
10069    let (worktree_repo, barrier) = project.update(cx, |project, cx| {
10070        let (repo, _) = project
10071            .git_store()
10072            .read(cx)
10073            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
10074            .unwrap();
10075        pretty_assertions::assert_eq!(
10076            repo.read(cx).work_directory_abs_path,
10077            Path::new(path!("/project/some-worktree")).into(),
10078        );
10079        let barrier = repo.update(cx, |repo, _| repo.barrier());
10080        (repo.clone(), barrier)
10081    });
10082    barrier.await.unwrap();
10083    worktree_repo.update(cx, |repo, _| {
10084        pretty_assertions::assert_eq!(
10085            repo.status_for_path(&repo_path("src/b.txt"))
10086                .unwrap()
10087                .status,
10088            StatusCode::Modified.worktree(),
10089        );
10090    });
10091
10092    // The same for the submodule.
10093    fs.with_git_state(
10094        path!("/project/subdir/some-submodule/.git").as_ref(),
10095        true,
10096        |state| {
10097            state
10098                .head_contents
10099                .insert(repo_path("c.txt"), "c".to_owned());
10100            state
10101                .index_contents
10102                .insert(repo_path("c.txt"), "c".to_owned());
10103        },
10104    )
10105    .unwrap();
10106    cx.run_until_parked();
10107
10108    let buffer = project
10109        .update(cx, |project, cx| {
10110            project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
10111        })
10112        .await
10113        .unwrap();
10114    let (submodule_repo, barrier) = project.update(cx, |project, cx| {
10115        let (repo, _) = project
10116            .git_store()
10117            .read(cx)
10118            .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
10119            .unwrap();
10120        pretty_assertions::assert_eq!(
10121            repo.read(cx).work_directory_abs_path,
10122            Path::new(path!("/project/subdir/some-submodule")).into(),
10123        );
10124        let barrier = repo.update(cx, |repo, _| repo.barrier());
10125        (repo.clone(), barrier)
10126    });
10127    barrier.await.unwrap();
10128    submodule_repo.update(cx, |repo, _| {
10129        pretty_assertions::assert_eq!(
10130            repo.status_for_path(&repo_path("c.txt")).unwrap().status,
10131            StatusCode::Modified.worktree(),
10132        );
10133    });
10134}
10135
10136#[gpui::test]
10137async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) {
10138    init_test(cx);
10139    let fs = FakeFs::new(cx.background_executor.clone());
10140    fs.insert_tree(
10141        path!("/root"),
10142        json!({
10143            "project": {
10144                ".git": {},
10145                "child1": {
10146                    "a.txt": "A",
10147                },
10148                "child2": {
10149                    "b.txt": "B",
10150                }
10151            }
10152        }),
10153    )
10154    .await;
10155
10156    let project = Project::test(
10157        fs.clone(),
10158        [
10159            path!("/root/project/child1").as_ref(),
10160            path!("/root/project/child2").as_ref(),
10161        ],
10162        cx,
10163    )
10164    .await;
10165
10166    let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
10167    tree.flush_fs_events(cx).await;
10168    project
10169        .update(cx, |project, cx| project.git_scans_complete(cx))
10170        .await;
10171    cx.executor().run_until_parked();
10172
10173    let repos = project.read_with(cx, |project, cx| {
10174        project
10175            .repositories(cx)
10176            .values()
10177            .map(|repo| repo.read(cx).work_directory_abs_path.clone())
10178            .collect::<Vec<_>>()
10179    });
10180    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]);
10181}
10182
10183#[gpui::test]
10184async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppContext) {
10185    init_test(cx);
10186
10187    let file_1_committed = String::from(r#"file_1_committed"#);
10188    let file_1_staged = String::from(r#"file_1_staged"#);
10189    let file_2_committed = String::from(r#"file_2_committed"#);
10190    let file_2_staged = String::from(r#"file_2_staged"#);
10191    let buffer_contents = String::from(r#"buffer"#);
10192
10193    let fs = FakeFs::new(cx.background_executor.clone());
10194    fs.insert_tree(
10195        path!("/dir"),
10196        json!({
10197            ".git": {},
10198           "src": {
10199               "file_1.rs": file_1_committed.clone(),
10200               "file_2.rs": file_2_committed.clone(),
10201           }
10202        }),
10203    )
10204    .await;
10205
10206    fs.set_head_for_repo(
10207        path!("/dir/.git").as_ref(),
10208        &[
10209            ("src/file_1.rs", file_1_committed.clone()),
10210            ("src/file_2.rs", file_2_committed.clone()),
10211        ],
10212        "deadbeef",
10213    );
10214    fs.set_index_for_repo(
10215        path!("/dir/.git").as_ref(),
10216        &[
10217            ("src/file_1.rs", file_1_staged.clone()),
10218            ("src/file_2.rs", file_2_staged.clone()),
10219        ],
10220    );
10221
10222    let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10223
10224    let buffer = project
10225        .update(cx, |project, cx| {
10226            project.open_local_buffer(path!("/dir/src/file_1.rs"), cx)
10227        })
10228        .await
10229        .unwrap();
10230
10231    buffer.update(cx, |buffer, cx| {
10232        buffer.edit([(0..buffer.len(), buffer_contents.as_str())], None, cx);
10233    });
10234
10235    let unstaged_diff = project
10236        .update(cx, |project, cx| {
10237            project.open_unstaged_diff(buffer.clone(), cx)
10238        })
10239        .await
10240        .unwrap();
10241
10242    cx.run_until_parked();
10243
10244    unstaged_diff.update(cx, |unstaged_diff, _cx| {
10245        let base_text = unstaged_diff.base_text_string().unwrap();
10246        assert_eq!(base_text, file_1_staged, "Should start with file_1 staged");
10247    });
10248
10249    // Save the buffer as `file_2.rs`, which should trigger the
10250    // `BufferChangedFilePath` event.
10251    project
10252        .update(cx, |project, cx| {
10253            let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
10254            let path = ProjectPath {
10255                worktree_id,
10256                path: rel_path("src/file_2.rs").into(),
10257            };
10258            project.save_buffer_as(buffer.clone(), path, cx)
10259        })
10260        .await
10261        .unwrap();
10262
10263    cx.run_until_parked();
10264
10265    // Verify that the diff bases have been updated to file_2's contents due to
10266    // the `BufferChangedFilePath` event being handled.
10267    unstaged_diff.update(cx, |unstaged_diff, cx| {
10268        let snapshot = buffer.read(cx).snapshot();
10269        let base_text = unstaged_diff.base_text_string().unwrap();
10270        assert_eq!(
10271            base_text, file_2_staged,
10272            "Diff bases should be automatically updated to file_2 staged content"
10273        );
10274
10275        let hunks: Vec<_> = unstaged_diff.hunks(&snapshot, cx).collect();
10276        assert!(!hunks.is_empty(), "Should have diff hunks for file_2");
10277    });
10278
10279    let uncommitted_diff = project
10280        .update(cx, |project, cx| {
10281            project.open_uncommitted_diff(buffer.clone(), cx)
10282        })
10283        .await
10284        .unwrap();
10285
10286    cx.run_until_parked();
10287
10288    uncommitted_diff.update(cx, |uncommitted_diff, _cx| {
10289        let base_text = uncommitted_diff.base_text_string().unwrap();
10290        assert_eq!(
10291            base_text, file_2_committed,
10292            "Uncommitted diff should compare against file_2 committed content"
10293        );
10294    });
10295}
10296
10297async fn search(
10298    project: &Entity<Project>,
10299    query: SearchQuery,
10300    cx: &mut gpui::TestAppContext,
10301) -> Result<HashMap<String, Vec<Range<usize>>>> {
10302    let search_rx = project.update(cx, |project, cx| project.search(query, cx));
10303    let mut results = HashMap::default();
10304    while let Ok(search_result) = search_rx.recv().await {
10305        match search_result {
10306            SearchResult::Buffer { buffer, ranges } => {
10307                results.entry(buffer).or_insert(ranges);
10308            }
10309            SearchResult::LimitReached => {}
10310        }
10311    }
10312    Ok(results
10313        .into_iter()
10314        .map(|(buffer, ranges)| {
10315            buffer.update(cx, |buffer, cx| {
10316                let path = buffer
10317                    .file()
10318                    .unwrap()
10319                    .full_path(cx)
10320                    .to_string_lossy()
10321                    .to_string();
10322                let ranges = ranges
10323                    .into_iter()
10324                    .map(|range| range.to_offset(buffer))
10325                    .collect::<Vec<_>>();
10326                (path, ranges)
10327            })
10328        })
10329        .collect())
10330}
10331
10332pub fn init_test(cx: &mut gpui::TestAppContext) {
10333    zlog::init_test();
10334
10335    cx.update(|cx| {
10336        let settings_store = SettingsStore::test(cx);
10337        cx.set_global(settings_store);
10338        release_channel::init(SemanticVersion::default(), cx);
10339    });
10340}
10341
10342fn json_lang() -> Arc<Language> {
10343    Arc::new(Language::new(
10344        LanguageConfig {
10345            name: "JSON".into(),
10346            matcher: LanguageMatcher {
10347                path_suffixes: vec!["json".to_string()],
10348                ..Default::default()
10349            },
10350            ..Default::default()
10351        },
10352        None,
10353    ))
10354}
10355
10356fn js_lang() -> Arc<Language> {
10357    Arc::new(Language::new(
10358        LanguageConfig {
10359            name: "JavaScript".into(),
10360            matcher: LanguageMatcher {
10361                path_suffixes: vec!["js".to_string()],
10362                ..Default::default()
10363            },
10364            ..Default::default()
10365        },
10366        None,
10367    ))
10368}
10369
10370fn rust_lang() -> Arc<Language> {
10371    Arc::new(Language::new(
10372        LanguageConfig {
10373            name: "Rust".into(),
10374            matcher: LanguageMatcher {
10375                path_suffixes: vec!["rs".to_string()],
10376                ..Default::default()
10377            },
10378            ..Default::default()
10379        },
10380        Some(tree_sitter_rust::LANGUAGE.into()),
10381    ))
10382}
10383
10384fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
10385    struct PythonMootToolchainLister(Arc<FakeFs>);
10386    #[async_trait]
10387    impl ToolchainLister for PythonMootToolchainLister {
10388        async fn list(
10389            &self,
10390            worktree_root: PathBuf,
10391            subroot_relative_path: Arc<RelPath>,
10392            _: Option<HashMap<String, String>>,
10393            _: &dyn Fs,
10394        ) -> ToolchainList {
10395            // This lister will always return a path .venv directories within ancestors
10396            let ancestors = subroot_relative_path.ancestors().collect::<Vec<_>>();
10397            let mut toolchains = vec![];
10398            for ancestor in ancestors {
10399                let venv_path = worktree_root.join(ancestor.as_std_path()).join(".venv");
10400                if self.0.is_dir(&venv_path).await {
10401                    toolchains.push(Toolchain {
10402                        name: SharedString::new("Python Venv"),
10403                        path: venv_path.to_string_lossy().into_owned().into(),
10404                        language_name: LanguageName(SharedString::new_static("Python")),
10405                        as_json: serde_json::Value::Null,
10406                    })
10407                }
10408            }
10409            ToolchainList {
10410                toolchains,
10411                ..Default::default()
10412            }
10413        }
10414        async fn resolve(
10415            &self,
10416            _: PathBuf,
10417            _: Option<HashMap<String, String>>,
10418            _: &dyn Fs,
10419        ) -> anyhow::Result<Toolchain> {
10420            Err(anyhow::anyhow!("Not implemented"))
10421        }
10422        fn meta(&self) -> ToolchainMetadata {
10423            ToolchainMetadata {
10424                term: SharedString::new_static("Virtual Environment"),
10425                new_toolchain_placeholder: SharedString::new_static(
10426                    "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
10427                ),
10428                manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
10429            }
10430        }
10431        fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &gpui::App) -> Vec<String> {
10432            vec![]
10433        }
10434    }
10435    Arc::new(
10436        Language::new(
10437            LanguageConfig {
10438                name: "Python".into(),
10439                matcher: LanguageMatcher {
10440                    path_suffixes: vec!["py".to_string()],
10441                    ..Default::default()
10442                },
10443                ..Default::default()
10444            },
10445            None, // We're not testing Python parsing with this language.
10446        )
10447        .with_manifest(Some(ManifestName::from(SharedString::new_static(
10448            "pyproject.toml",
10449        ))))
10450        .with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))),
10451    )
10452}
10453
10454fn typescript_lang() -> Arc<Language> {
10455    Arc::new(Language::new(
10456        LanguageConfig {
10457            name: "TypeScript".into(),
10458            matcher: LanguageMatcher {
10459                path_suffixes: vec!["ts".to_string()],
10460                ..Default::default()
10461            },
10462            ..Default::default()
10463        },
10464        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10465    ))
10466}
10467
10468fn tsx_lang() -> Arc<Language> {
10469    Arc::new(Language::new(
10470        LanguageConfig {
10471            name: "tsx".into(),
10472            matcher: LanguageMatcher {
10473                path_suffixes: vec!["tsx".to_string()],
10474                ..Default::default()
10475            },
10476            ..Default::default()
10477        },
10478        Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10479    ))
10480}
10481
10482fn get_all_tasks(
10483    project: &Entity<Project>,
10484    task_contexts: Arc<TaskContexts>,
10485    cx: &mut App,
10486) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
10487    let new_tasks = project.update(cx, |project, cx| {
10488        project.task_store.update(cx, |task_store, cx| {
10489            task_store.task_inventory().unwrap().update(cx, |this, cx| {
10490                this.used_and_current_resolved_tasks(task_contexts, cx)
10491            })
10492        })
10493    });
10494
10495    cx.background_spawn(async move {
10496        let (mut old, new) = new_tasks.await;
10497        old.extend(new);
10498        old
10499    })
10500}
10501
10502#[track_caller]
10503fn assert_entry_git_state(
10504    tree: &Worktree,
10505    repository: &Repository,
10506    path: &str,
10507    index_status: Option<StatusCode>,
10508    is_ignored: bool,
10509) {
10510    assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
10511    let entry = tree
10512        .entry_for_path(&rel_path(path))
10513        .unwrap_or_else(|| panic!("entry {path} not found"));
10514    let status = repository
10515        .status_for_path(&repo_path(path))
10516        .map(|entry| entry.status);
10517    let expected = index_status.map(|index_status| {
10518        TrackedStatus {
10519            index_status,
10520            worktree_status: StatusCode::Unmodified,
10521        }
10522        .into()
10523    });
10524    assert_eq!(
10525        status, expected,
10526        "expected {path} to have git status: {expected:?}"
10527    );
10528    assert_eq!(
10529        entry.is_ignored, is_ignored,
10530        "expected {path} to have is_ignored: {is_ignored}"
10531    );
10532}
10533
10534#[track_caller]
10535fn git_init(path: &Path) -> git2::Repository {
10536    let mut init_opts = RepositoryInitOptions::new();
10537    init_opts.initial_head("main");
10538    git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
10539}
10540
10541#[track_caller]
10542fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
10543    let path = path.as_ref();
10544    let mut index = repo.index().expect("Failed to get index");
10545    index.add_path(path).expect("Failed to add file");
10546    index.write().expect("Failed to write index");
10547}
10548
10549#[track_caller]
10550fn git_remove_index(path: &Path, repo: &git2::Repository) {
10551    let mut index = repo.index().expect("Failed to get index");
10552    index.remove_path(path).expect("Failed to add file");
10553    index.write().expect("Failed to write index");
10554}
10555
10556#[track_caller]
10557fn git_commit(msg: &'static str, repo: &git2::Repository) {
10558    use git2::Signature;
10559
10560    let signature = Signature::now("test", "test@zed.dev").unwrap();
10561    let oid = repo.index().unwrap().write_tree().unwrap();
10562    let tree = repo.find_tree(oid).unwrap();
10563    if let Ok(head) = repo.head() {
10564        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
10565
10566        let parent_commit = parent_obj.as_commit().unwrap();
10567
10568        repo.commit(
10569            Some("HEAD"),
10570            &signature,
10571            &signature,
10572            msg,
10573            &tree,
10574            &[parent_commit],
10575        )
10576        .expect("Failed to commit with parent");
10577    } else {
10578        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
10579            .expect("Failed to commit");
10580    }
10581}
10582
10583#[cfg(any())]
10584#[track_caller]
10585fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
10586    repo.cherrypick(commit, None).expect("Failed to cherrypick");
10587}
10588
10589#[track_caller]
10590fn git_stash(repo: &mut git2::Repository) {
10591    use git2::Signature;
10592
10593    let signature = Signature::now("test", "test@zed.dev").unwrap();
10594    repo.stash_save(&signature, "N/A", None)
10595        .expect("Failed to stash");
10596}
10597
10598#[track_caller]
10599fn git_reset(offset: usize, repo: &git2::Repository) {
10600    let head = repo.head().expect("Couldn't get repo head");
10601    let object = head.peel(git2::ObjectType::Commit).unwrap();
10602    let commit = object.as_commit().unwrap();
10603    let new_head = commit
10604        .parents()
10605        .inspect(|parnet| {
10606            parnet.message();
10607        })
10608        .nth(offset)
10609        .expect("Not enough history");
10610    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
10611        .expect("Could not reset");
10612}
10613
10614#[cfg(any())]
10615#[track_caller]
10616fn git_branch(name: &str, repo: &git2::Repository) {
10617    let head = repo
10618        .head()
10619        .expect("Couldn't get repo head")
10620        .peel_to_commit()
10621        .expect("HEAD is not a commit");
10622    repo.branch(name, &head, false).expect("Failed to commit");
10623}
10624
10625#[cfg(any())]
10626#[track_caller]
10627fn git_checkout(name: &str, repo: &git2::Repository) {
10628    repo.set_head(name).expect("Failed to set head");
10629    repo.checkout_head(None).expect("Failed to check out head");
10630}
10631
10632#[cfg(any())]
10633#[track_caller]
10634fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
10635    repo.statuses(None)
10636        .unwrap()
10637        .iter()
10638        .map(|status| (status.path().unwrap().to_string(), status.status()))
10639        .collect()
10640}
10641
10642#[gpui::test]
10643async fn test_find_project_path_abs(
10644    background_executor: BackgroundExecutor,
10645    cx: &mut gpui::TestAppContext,
10646) {
10647    // find_project_path should work with absolute paths
10648    init_test(cx);
10649
10650    let fs = FakeFs::new(background_executor);
10651    fs.insert_tree(
10652        path!("/root"),
10653        json!({
10654            "project1": {
10655                "file1.txt": "content1",
10656                "subdir": {
10657                    "file2.txt": "content2"
10658                }
10659            },
10660            "project2": {
10661                "file3.txt": "content3"
10662            }
10663        }),
10664    )
10665    .await;
10666
10667    let project = Project::test(
10668        fs.clone(),
10669        [
10670            path!("/root/project1").as_ref(),
10671            path!("/root/project2").as_ref(),
10672        ],
10673        cx,
10674    )
10675    .await;
10676
10677    // Make sure the worktrees are fully initialized
10678    project
10679        .update(cx, |project, cx| project.git_scans_complete(cx))
10680        .await;
10681    cx.run_until_parked();
10682
10683    let (project1_abs_path, project1_id, project2_abs_path, project2_id) =
10684        project.read_with(cx, |project, cx| {
10685            let worktrees: Vec<_> = project.worktrees(cx).collect();
10686            let abs_path1 = worktrees[0].read(cx).abs_path().to_path_buf();
10687            let id1 = worktrees[0].read(cx).id();
10688            let abs_path2 = worktrees[1].read(cx).abs_path().to_path_buf();
10689            let id2 = worktrees[1].read(cx).id();
10690            (abs_path1, id1, abs_path2, id2)
10691        });
10692
10693    project.update(cx, |project, cx| {
10694        let abs_path = project1_abs_path.join("file1.txt");
10695        let found_path = project.find_project_path(abs_path, cx).unwrap();
10696        assert_eq!(found_path.worktree_id, project1_id);
10697        assert_eq!(&*found_path.path, rel_path("file1.txt"));
10698
10699        let abs_path = project1_abs_path.join("subdir").join("file2.txt");
10700        let found_path = project.find_project_path(abs_path, cx).unwrap();
10701        assert_eq!(found_path.worktree_id, project1_id);
10702        assert_eq!(&*found_path.path, rel_path("subdir/file2.txt"));
10703
10704        let abs_path = project2_abs_path.join("file3.txt");
10705        let found_path = project.find_project_path(abs_path, cx).unwrap();
10706        assert_eq!(found_path.worktree_id, project2_id);
10707        assert_eq!(&*found_path.path, rel_path("file3.txt"));
10708
10709        let abs_path = project1_abs_path.join("nonexistent.txt");
10710        let found_path = project.find_project_path(abs_path, cx);
10711        assert!(
10712            found_path.is_some(),
10713            "Should find project path for nonexistent file in worktree"
10714        );
10715
10716        // Test with an absolute path outside any worktree
10717        let abs_path = Path::new("/some/other/path");
10718        let found_path = project.find_project_path(abs_path, cx);
10719        assert!(
10720            found_path.is_none(),
10721            "Should not find project path for path outside any worktree"
10722        );
10723    });
10724}
10725
10726#[gpui::test]
10727async fn test_git_worktree_remove(cx: &mut gpui::TestAppContext) {
10728    init_test(cx);
10729
10730    let fs = FakeFs::new(cx.executor());
10731    fs.insert_tree(
10732        path!("/root"),
10733        json!({
10734            "a": {
10735                ".git": {},
10736                "src": {
10737                    "main.rs": "fn main() {}",
10738                }
10739            },
10740            "b": {
10741                ".git": {},
10742                "src": {
10743                    "main.rs": "fn main() {}",
10744                },
10745                "script": {
10746                    "run.sh": "#!/bin/bash"
10747                }
10748            }
10749        }),
10750    )
10751    .await;
10752
10753    let project = Project::test(
10754        fs.clone(),
10755        [
10756            path!("/root/a").as_ref(),
10757            path!("/root/b/script").as_ref(),
10758            path!("/root/b").as_ref(),
10759        ],
10760        cx,
10761    )
10762    .await;
10763    let scan_complete = project.update(cx, |project, cx| project.git_scans_complete(cx));
10764    scan_complete.await;
10765
10766    let worktrees = project.update(cx, |project, cx| project.worktrees(cx).collect::<Vec<_>>());
10767    assert_eq!(worktrees.len(), 3);
10768
10769    let worktree_id_by_abs_path = worktrees
10770        .into_iter()
10771        .map(|worktree| worktree.read_with(cx, |w, _| (w.abs_path(), w.id())))
10772        .collect::<HashMap<_, _>>();
10773    let worktree_id = worktree_id_by_abs_path
10774        .get(Path::new(path!("/root/b/script")))
10775        .unwrap();
10776
10777    let repos = project.update(cx, |p, cx| p.git_store().read(cx).repositories().clone());
10778    assert_eq!(repos.len(), 2);
10779
10780    project.update(cx, |project, cx| {
10781        project.remove_worktree(*worktree_id, cx);
10782    });
10783    cx.run_until_parked();
10784
10785    let mut repo_paths = project
10786        .update(cx, |p, cx| p.git_store().read(cx).repositories().clone())
10787        .values()
10788        .map(|repo| repo.read_with(cx, |r, _| r.work_directory_abs_path.clone()))
10789        .collect::<Vec<_>>();
10790    repo_paths.sort();
10791
10792    pretty_assertions::assert_eq!(
10793        repo_paths,
10794        [
10795            Path::new(path!("/root/a")).into(),
10796            Path::new(path!("/root/b")).into(),
10797        ]
10798    );
10799
10800    let active_repo_path = project
10801        .read_with(cx, |p, cx| {
10802            p.active_repository(cx)
10803                .map(|r| r.read(cx).work_directory_abs_path.clone())
10804        })
10805        .unwrap();
10806    assert_eq!(active_repo_path.as_ref(), Path::new(path!("/root/a")));
10807
10808    let worktree_id = worktree_id_by_abs_path
10809        .get(Path::new(path!("/root/a")))
10810        .unwrap();
10811    project.update(cx, |project, cx| {
10812        project.remove_worktree(*worktree_id, cx);
10813    });
10814    cx.run_until_parked();
10815
10816    let active_repo_path = project
10817        .read_with(cx, |p, cx| {
10818            p.active_repository(cx)
10819                .map(|r| r.read(cx).work_directory_abs_path.clone())
10820        })
10821        .unwrap();
10822    assert_eq!(active_repo_path.as_ref(), Path::new(path!("/root/b")));
10823
10824    let worktree_id = worktree_id_by_abs_path
10825        .get(Path::new(path!("/root/b")))
10826        .unwrap();
10827    project.update(cx, |project, cx| {
10828        project.remove_worktree(*worktree_id, cx);
10829    });
10830    cx.run_until_parked();
10831
10832    let active_repo_path = project.read_with(cx, |p, cx| {
10833        p.active_repository(cx)
10834            .map(|r| r.read(cx).work_directory_abs_path.clone())
10835    });
10836    assert!(active_repo_path.is_none());
10837}