Uncomment project2 tests (#3200)

Conrad Irwin created

Change summary

crates/gpui/src/executor.rs                  |    8 
crates/gpui2/src/app.rs                      |    2 
crates/gpui2/src/app/entity_map.rs           |   82 
crates/gpui2/src/app/test_context.rs         |   27 
crates/gpui2/src/executor.rs                 |   33 
crates/gpui2/src/platform/test/dispatcher.rs |   30 
crates/project/src/project_tests.rs          |  116 
crates/project2/Cargo.toml                   |    1 
crates/project2/src/project2.rs              |   66 
crates/project2/src/project_tests.rs         | 2365 ++++++++++++++++++++-
crates/project2/src/worktree.rs              |   93 
11 files changed, 2,552 insertions(+), 271 deletions(-)

Detailed changes

crates/gpui/src/executor.rs 🔗

@@ -84,7 +84,7 @@ struct DeterministicState {
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum ExecutorEvent {
     PollRunnable { id: usize },
-    EnqueuRunnable { id: usize },
+    EnqueueRunnable { id: usize },
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -199,7 +199,7 @@ impl Deterministic {
         let unparker = self.parker.lock().unparker();
         let (runnable, task) = async_task::spawn_local(future, move |runnable| {
             let mut state = state.lock();
-            state.push_to_history(ExecutorEvent::EnqueuRunnable { id });
+            state.push_to_history(ExecutorEvent::EnqueueRunnable { id });
             state
                 .scheduled_from_foreground
                 .entry(cx_id)
@@ -229,7 +229,7 @@ impl Deterministic {
             let mut state = state.lock();
             state
                 .poll_history
-                .push(ExecutorEvent::EnqueuRunnable { id });
+                .push(ExecutorEvent::EnqueueRunnable { id });
             state
                 .scheduled_from_background
                 .push(BackgroundRunnable { id, runnable });
@@ -616,7 +616,7 @@ impl ExecutorEvent {
     pub fn id(&self) -> usize {
         match self {
             ExecutorEvent::PollRunnable { id } => *id,
-            ExecutorEvent::EnqueuRunnable { id } => *id,
+            ExecutorEvent::EnqueueRunnable { id } => *id,
         }
     }
 }

crates/gpui2/src/app.rs 🔗

@@ -376,7 +376,7 @@ impl AppContext {
                 self.observers.remove(&entity_id);
                 self.event_listeners.remove(&entity_id);
                 for mut release_callback in self.release_listeners.remove(&entity_id) {
-                    release_callback(&mut entity, self);
+                    release_callback(entity.as_mut(), self);
                 }
             }
         }

crates/gpui2/src/app/entity_map.rs 🔗

@@ -106,7 +106,12 @@ impl EntityMap {
         dropped_entity_ids
             .into_iter()
             .map(|entity_id| {
-                ref_counts.counts.remove(entity_id);
+                let count = ref_counts.counts.remove(entity_id).unwrap();
+                debug_assert_eq!(
+                    count.load(SeqCst),
+                    0,
+                    "dropped an entity that was referenced"
+                );
                 (entity_id, self.entities.remove(entity_id).unwrap())
             })
             .collect()
@@ -211,7 +216,7 @@ impl Drop for AnyModel {
             let count = entity_map
                 .counts
                 .get(self.entity_id)
-                .expect("Detected over-release of a model.");
+                .expect("detected over-release of a handle.");
             let prev_count = count.fetch_sub(1, SeqCst);
             assert_ne!(prev_count, 0, "Detected over-release of a model.");
             if prev_count == 1 {
@@ -395,12 +400,16 @@ impl AnyWeakModel {
     }
 
     pub fn upgrade(&self) -> Option<AnyModel> {
-        let entity_map = self.entity_ref_counts.upgrade()?;
-        entity_map
-            .read()
-            .counts
-            .get(self.entity_id)?
-            .fetch_add(1, SeqCst);
+        let ref_counts = &self.entity_ref_counts.upgrade()?;
+        let ref_counts = ref_counts.read();
+        let ref_count = ref_counts.counts.get(self.entity_id)?;
+
+        // entity_id is in dropped_entity_ids
+        if ref_count.load(SeqCst) == 0 {
+            return None;
+        }
+        ref_count.fetch_add(1, SeqCst);
+
         Some(AnyModel {
             entity_id: self.entity_id,
             entity_type: self.entity_type,
@@ -499,3 +508,60 @@ impl<T> PartialEq<Model<T>> for WeakModel<T> {
         self.entity_id() == other.any_model.entity_id()
     }
 }
+
+#[cfg(test)]
+mod test {
+    use crate::EntityMap;
+
+    struct TestEntity {
+        pub i: i32,
+    }
+
+    #[test]
+    fn test_entity_map_slot_assignment_before_cleanup() {
+        // Tests that slots are not re-used before take_dropped.
+        let mut entity_map = EntityMap::new();
+
+        let slot = entity_map.reserve::<TestEntity>();
+        entity_map.insert(slot, TestEntity { i: 1 });
+
+        let slot = entity_map.reserve::<TestEntity>();
+        entity_map.insert(slot, TestEntity { i: 2 });
+
+        let dropped = entity_map.take_dropped();
+        assert_eq!(dropped.len(), 2);
+
+        assert_eq!(
+            dropped
+                .into_iter()
+                .map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
+                .collect::<Vec<i32>>(),
+            vec![1, 2],
+        );
+    }
+
+    #[test]
+    fn test_entity_map_weak_upgrade_before_cleanup() {
+        // Tests that weak handles are not upgraded before take_dropped
+        let mut entity_map = EntityMap::new();
+
+        let slot = entity_map.reserve::<TestEntity>();
+        let handle = entity_map.insert(slot, TestEntity { i: 1 });
+        let weak = handle.downgrade();
+        drop(handle);
+
+        let strong = weak.upgrade();
+        assert_eq!(strong, None);
+
+        let dropped = entity_map.take_dropped();
+        assert_eq!(dropped.len(), 1);
+
+        assert_eq!(
+            dropped
+                .into_iter()
+                .map(|(_, entity)| entity.downcast::<TestEntity>().unwrap().i)
+                .collect::<Vec<i32>>(),
+            vec![1],
+        );
+    }
+}

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
-    AnyWindowHandle, AppContext, AsyncAppContext, Context, Executor, MainThread, Model,
-    ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
+    AnyWindowHandle, AppContext, AsyncAppContext, Context, EventEmitter, Executor, MainThread,
+    Model, ModelContext, Result, Task, TestDispatcher, TestPlatform, WindowContext,
 };
+use futures::SinkExt;
 use parking_lot::Mutex;
 use std::{future::Future, sync::Arc};
 
@@ -63,8 +64,8 @@ impl TestAppContext {
     }
 
     pub fn update<R>(&self, f: impl FnOnce(&mut AppContext) -> R) -> R {
-        let mut lock = self.app.lock();
-        f(&mut *lock)
+        let mut cx = self.app.lock();
+        cx.update(f)
     }
 
     pub fn read_window<R>(
@@ -149,4 +150,22 @@ impl TestAppContext {
             executor: self.executor.clone(),
         }
     }
+
+    pub fn subscribe<T: 'static + EventEmitter + Send>(
+        &mut self,
+        entity: &Model<T>,
+    ) -> futures::channel::mpsc::UnboundedReceiver<T::Event>
+    where
+        T::Event: 'static + Send + Clone,
+    {
+        let (mut tx, rx) = futures::channel::mpsc::unbounded();
+        entity
+            .update(self, |_, cx: &mut ModelContext<T>| {
+                cx.subscribe(entity, move |_, _, event, cx| {
+                    cx.executor().block(tx.send(event.clone())).unwrap();
+                })
+            })
+            .detach();
+        rx
+    }
 }

crates/gpui2/src/executor.rs 🔗

@@ -6,7 +6,10 @@ use std::{
     marker::PhantomData,
     mem,
     pin::Pin,
-    sync::Arc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
     task::{Context, Poll},
     time::Duration,
 };
@@ -136,7 +139,11 @@ impl Executor {
     pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
         pin_mut!(future);
         let (parker, unparker) = parking::pair();
+        let awoken = Arc::new(AtomicBool::new(false));
+        let awoken2 = awoken.clone();
+
         let waker = waker_fn(move || {
+            awoken2.store(true, SeqCst);
             unparker.unpark();
         });
         let mut cx = std::task::Context::from_waker(&waker);
@@ -146,9 +153,20 @@ impl Executor {
                 Poll::Ready(result) => return result,
                 Poll::Pending => {
                     if !self.dispatcher.poll() {
+                        if awoken.swap(false, SeqCst) {
+                            continue;
+                        }
+
                         #[cfg(any(test, feature = "test-support"))]
-                        if let Some(_) = self.dispatcher.as_test() {
-                            panic!("blocked with nothing left to run")
+                        if let Some(test) = self.dispatcher.as_test() {
+                            if !test.parking_allowed() {
+                                let mut backtrace_message = String::new();
+                                if let Some(backtrace) = test.waiting_backtrace() {
+                                    backtrace_message =
+                                        format!("\nbacktrace of waiting future:\n{:?}", backtrace);
+                                }
+                                panic!("parked with nothing left to run\n{:?}", backtrace_message)
+                            }
                         }
                         parker.park();
                     }
@@ -206,12 +224,12 @@ impl Executor {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn start_waiting(&self) {
-        todo!("start_waiting")
+        self.dispatcher.as_test().unwrap().start_waiting();
     }
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn finish_waiting(&self) {
-        todo!("finish_waiting")
+        self.dispatcher.as_test().unwrap().finish_waiting();
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -229,6 +247,11 @@ impl Executor {
         self.dispatcher.as_test().unwrap().run_until_parked()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn allow_parking(&self) {
+        self.dispatcher.as_test().unwrap().allow_parking();
+    }
+
     pub fn num_cpus(&self) -> usize {
         num_cpus::get()
     }

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -1,5 +1,6 @@
 use crate::PlatformDispatcher;
 use async_task::Runnable;
+use backtrace::Backtrace;
 use collections::{HashMap, VecDeque};
 use parking_lot::Mutex;
 use rand::prelude::*;
@@ -28,6 +29,8 @@ struct TestDispatcherState {
     time: Duration,
     is_main_thread: bool,
     next_id: TestDispatcherId,
+    allow_parking: bool,
+    waiting_backtrace: Option<Backtrace>,
 }
 
 impl TestDispatcher {
@@ -40,6 +43,8 @@ impl TestDispatcher {
             time: Duration::ZERO,
             is_main_thread: true,
             next_id: TestDispatcherId(1),
+            allow_parking: false,
+            waiting_backtrace: None,
         };
 
         TestDispatcher {
@@ -66,7 +71,7 @@ impl TestDispatcher {
         self.state.lock().time = new_now;
     }
 
-    pub fn simulate_random_delay(&self) -> impl Future<Output = ()> {
+    pub fn simulate_random_delay(&self) -> impl 'static + Send + Future<Output = ()> {
         pub struct YieldNow {
             count: usize,
         }
@@ -93,6 +98,29 @@ impl TestDispatcher {
     pub fn run_until_parked(&self) {
         while self.poll() {}
     }
+
+    pub fn parking_allowed(&self) -> bool {
+        self.state.lock().allow_parking
+    }
+
+    pub fn allow_parking(&self) {
+        self.state.lock().allow_parking = true
+    }
+
+    pub fn start_waiting(&self) {
+        self.state.lock().waiting_backtrace = Some(Backtrace::new_unresolved());
+    }
+
+    pub fn finish_waiting(&self) {
+        self.state.lock().waiting_backtrace.take();
+    }
+
+    pub fn waiting_backtrace(&self) -> Option<Backtrace> {
+        self.state.lock().waiting_backtrace.take().map(|mut b| {
+            b.resolve();
+            b
+        })
+    }
 }
 
 impl Clone for TestDispatcher {

crates/project/src/project_tests.rs 🔗

@@ -2604,64 +2604,64 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
     assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
 }
 
-#[gpui::test]
-async fn test_save_as(cx: &mut gpui::TestAppContext) {
-    init_test(cx);
-
-    let fs = FakeFs::new(cx.background());
-    fs.insert_tree("/dir", json!({})).await;
-
-    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
-
-    let languages = project.read_with(cx, |project, _| project.languages().clone());
-    languages.register(
-        "/some/path",
-        LanguageConfig {
-            name: "Rust".into(),
-            path_suffixes: vec!["rs".into()],
-            ..Default::default()
-        },
-        tree_sitter_rust::language(),
-        vec![],
-        |_| Default::default(),
-    );
-
-    let buffer = project.update(cx, |project, cx| {
-        project.create_buffer("", None, cx).unwrap()
-    });
-    buffer.update(cx, |buffer, cx| {
-        buffer.edit([(0..0, "abc")], None, cx);
-        assert!(buffer.is_dirty());
-        assert!(!buffer.has_conflict());
-        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
-    });
-    project
-        .update(cx, |project, cx| {
-            project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
-
-    cx.foreground().run_until_parked();
-    buffer.read_with(cx, |buffer, cx| {
-        assert_eq!(
-            buffer.file().unwrap().full_path(cx),
-            Path::new("dir/file1.rs")
-        );
-        assert!(!buffer.is_dirty());
-        assert!(!buffer.has_conflict());
-        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
-    });
-
-    let opened_buffer = project
-        .update(cx, |project, cx| {
-            project.open_local_buffer("/dir/file1.rs", cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(opened_buffer, buffer);
-}
+// #[gpui::test]
+// async fn test_save_as(cx: &mut gpui::TestAppContext) {
+//     init_test(cx);
+
+//     let fs = FakeFs::new(cx.background());
+//     fs.insert_tree("/dir", json!({})).await;
+
+//     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+//     let languages = project.read_with(cx, |project, _| project.languages().clone());
+//     languages.register(
+//         "/some/path",
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".into()],
+//             ..Default::default()
+//         },
+//         tree_sitter_rust::language(),
+//         vec![],
+//         |_| Default::default(),
+//     );
+
+//     let buffer = project.update(cx, |project, cx| {
+//         project.create_buffer("", None, cx).unwrap()
+//     });
+//     buffer.update(cx, |buffer, cx| {
+//         buffer.edit([(0..0, "abc")], None, cx);
+//         assert!(buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//         assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
+//     });
+//     project
+//         .update(cx, |project, cx| {
+//             project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
+
+//     cx.foreground().run_until_parked();
+//     buffer.read_with(cx, |buffer, cx| {
+//         assert_eq!(
+//             buffer.file().unwrap().full_path(cx),
+//             Path::new("dir/file1.rs")
+//         );
+//         assert!(!buffer.is_dirty());
+//         assert!(!buffer.has_conflict());
+//         assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
+//     });
+
+//     let opened_buffer = project
+//         .update(cx, |project, cx| {
+//             project.open_local_buffer("/dir/file1.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     assert_eq!(opened_buffer, buffer);
+// }
 
 #[gpui::test(retries = 5)]
 async fn test_rescan_and_remote_updates(

crates/project2/Cargo.toml 🔗

@@ -16,6 +16,7 @@ test-support = [
     "settings2/test-support",
     "text/test-support",
     "prettier2/test-support",
+    "gpui2/test-support",
 ]
 
 [dependencies]

crates/project2/src/project2.rs 🔗

@@ -855,39 +855,39 @@ impl Project {
         }
     }
 
-    // #[cfg(any(test, feature = "test-support"))]
-    // pub async fn test(
-    //     fs: Arc<dyn Fs>,
-    //     root_paths: impl IntoIterator<Item = &Path>,
-    //     cx: &mut gpui::TestAppContext,
-    // ) -> Handle<Project> {
-    //     let mut languages = LanguageRegistry::test();
-    //     languages.set_executor(cx.background());
-    //     let http_client = util::http::FakeHttpClient::with_404_response();
-    //     let client = cx.update(|cx| client2::Client::new(http_client.clone(), cx));
-    //     let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-    //     let project = cx.update(|cx| {
-    //         Project::local(
-    //             client,
-    //             node_runtime::FakeNodeRuntime::new(),
-    //             user_store,
-    //             Arc::new(languages),
-    //             fs,
-    //             cx,
-    //         )
-    //     });
-    //     for path in root_paths {
-    //         let (tree, _) = project
-    //             .update(cx, |project, cx| {
-    //                 project.find_or_create_local_worktree(path, true, cx)
-    //             })
-    //             .await
-    //             .unwrap();
-    //         tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
-    //             .await;
-    //     }
-    //     project
-    // }
+    #[cfg(any(test, feature = "test-support"))]
+    pub async fn test(
+        fs: Arc<dyn Fs>,
+        root_paths: impl IntoIterator<Item = &Path>,
+        cx: &mut gpui2::TestAppContext,
+    ) -> Model<Project> {
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.executor().clone());
+        let http_client = util::http::FakeHttpClient::with_404_response();
+        let client = cx.update(|cx| client2::Client::new(http_client.clone(), cx));
+        let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let project = cx.update(|cx| {
+            Project::local(
+                client,
+                node_runtime::FakeNodeRuntime::new(),
+                user_store,
+                Arc::new(languages),
+                fs,
+                cx,
+            )
+        });
+        for path in root_paths {
+            let (tree, _) = project
+                .update(cx, |project, cx| {
+                    project.find_or_create_local_worktree(path, true, cx)
+                })
+                .await
+                .unwrap();
+            tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+                .await;
+        }
+        project
+    }
 
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
         let mut language_servers_to_start = Vec::new();

crates/project2/src/project_tests.rs 🔗

@@ -1,32 +1,24 @@
-// use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *};
-// use fs::{FakeFs, RealFs};
-// use futures::{future, StreamExt};
-// use gpui::{executor::Deterministic, test::subscribe, AppContext};
-// use language2::{
-//     language_settings::{AllLanguageSettings, LanguageSettingsContent},
-//     tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
-//     LineEnding, OffsetRangeExt, Point, ToPoint,
-// };
-// use lsp2::Url;
-// use parking_lot::Mutex;
-// use pretty_assertions::assert_eq;
-// use serde_json::json;
-// use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
-// use unindent::Unindent as _;
-// use util::{assert_set_eq, test::temp_tree};
-
-// #[cfg(test)]
-// #[ctor::ctor]
-// fn init_logger() {
-//     if std::env::var("RUST_LOG").is_ok() {
-//         env_logger::init();
-//     }
-// }
+use crate::{search::PathMatcher, Event, *};
+use fs2::FakeFs;
+use futures::{future, StreamExt};
+use gpui2::AppContext;
+use language2::{
+    language_settings::{AllLanguageSettings, LanguageSettingsContent},
+    tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, LineEnding, OffsetRangeExt,
+    Point, ToPoint,
+};
+use lsp2::Url;
+use parking_lot::Mutex;
+use pretty_assertions::assert_eq;
+use serde_json::json;
+use std::task::Poll;
+use unindent::Unindent as _;
+use util::assert_set_eq;
 
-// #[gpui::test]
-// async fn test_symlinks(cx: &mut gpui::TestAppContext) {
+// #[gpui2::test]
+// async fn test_symlinks(cx: &mut gpui2::TestAppContext) {
 //     init_test(cx);
-//     cx.foreground().allow_parking();
+//     cx.executor().allow_parking();
 
 //     let dir = temp_tree(json!({
 //         "root": {
@@ -52,8 +44,8 @@
 //     .unwrap();
 
 //     let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
-//     project.read_with(cx, |project, cx| {
-//         let tree = project.worktrees(cx).next().unwrap().read(cx);
+//     project.update(cx, |project, cx| {
+//         let tree = project.worktrees().next().unwrap().read(cx);
 //         assert_eq!(tree.file_count(), 5);
 //         assert_eq!(
 //             tree.inode_for_path("fennel/grape"),
@@ -62,107 +54,2260 @@
 //     });
 // }
 
-// #[gpui::test]
-// async fn test_managing_project_specific_settings(
-//     deterministic: Arc<Deterministic>,
-//     cx: &mut gpui::TestAppContext,
-// ) {
+#[gpui2::test]
+async fn test_managing_project_specific_settings(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            ".zed": {
+                "settings.json": r#"{ "tab_size": 8 }"#
+            },
+            "a": {
+                "a.rs": "fn a() {\n    A\n}"
+            },
+            "b": {
+                ".zed": {
+                    "settings.json": r#"{ "tab_size": 2 }"#
+                },
+                "b.rs": "fn b() {\n  B\n}"
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
+
+    cx.executor().run_until_parked();
+    cx.update(|cx| {
+        let tree = worktree.read(cx);
+
+        let settings_a = language_settings(
+            None,
+            Some(
+                &(File::for_entry(
+                    tree.entry_for_path("a/a.rs").unwrap().clone(),
+                    worktree.clone(),
+                ) as _),
+            ),
+            cx,
+        );
+        let settings_b = language_settings(
+            None,
+            Some(
+                &(File::for_entry(
+                    tree.entry_for_path("b/b.rs").unwrap().clone(),
+                    worktree.clone(),
+                ) as _),
+            ),
+            cx,
+        );
+
+        assert_eq!(settings_a.tab_size.get(), 8);
+        assert_eq!(settings_b.tab_size.get(), 2);
+    });
+}
+
+#[gpui2::test]
+async fn test_managing_language_servers(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut rust_language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut json_language = Language::new(
+        LanguageConfig {
+            name: "JSON".into(),
+            path_suffixes: vec!["json".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_rust_servers = rust_language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-rust-language-server",
+            capabilities: lsp2::ServerCapabilities {
+                completion_provider: Some(lsp2::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+    let mut fake_json_servers = json_language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-json-language-server",
+            capabilities: lsp2::ServerCapabilities {
+                completion_provider: Some(lsp2::CompletionOptions {
+                    trigger_characters: Some(vec![":".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "test.rs": "const A: i32 = 1;",
+            "test2.rs": "",
+            "Cargo.toml": "a = 1",
+            "package.json": "{\"a\": 1}",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+    // Open a buffer without an associated language server.
+    let toml_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/Cargo.toml", cx)
+        })
+        .await
+        .unwrap();
+
+    // Open a buffer with an associated language server before the language for it has been loaded.
+    let rust_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/test.rs", cx)
+        })
+        .await
+        .unwrap();
+    rust_buffer.update(cx, |buffer, _| {
+        assert_eq!(buffer.language().map(|l| l.name()), None);
+    });
+
+    // Now we add the languages to the project, and ensure they get assigned to all
+    // the relevant open buffers.
+    project.update(cx, |project, _| {
+        project.languages.add(Arc::new(json_language));
+        project.languages.add(Arc::new(rust_language));
+    });
+    cx.executor().run_until_parked();
+    rust_buffer.update(cx, |buffer, _| {
+        assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
+    });
+
+    // A server is started up, and it is notified about Rust files.
+    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentItem {
+            uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+            version: 0,
+            text: "const A: i32 = 1;".to_string(),
+            language_id: Default::default()
+        }
+    );
+
+    // The buffer is configured based on the language server's capabilities.
+    rust_buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer.completion_triggers(),
+            &[".".to_string(), "::".to_string()]
+        );
+    });
+    toml_buffer.update(cx, |buffer, _| {
+        assert!(buffer.completion_triggers().is_empty());
+    });
+
+    // Edit a buffer. The changes are reported to the language server.
+    rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+            .await
+            .text_document,
+        lsp2::VersionedTextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+            1
+        )
+    );
+
+    // Open a third buffer with a different associated language server.
+    let json_buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/package.json", cx)
+        })
+        .await
+        .unwrap();
+
+    // A json language server is started up and is only notified about the json buffer.
+    let mut fake_json_server = fake_json_servers.next().await.unwrap();
+    assert_eq!(
+        fake_json_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentItem {
+            uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+            version: 0,
+            text: "{\"a\": 1}".to_string(),
+            language_id: Default::default()
+        }
+    );
+
+    // This buffer is configured based on the second language server's
+    // capabilities.
+    json_buffer.update(cx, |buffer, _| {
+        assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
+    });
+
+    // When opening another buffer whose language server is already running,
+    // it is also configured based on the existing language server's capabilities.
+    let rust_buffer2 = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/test2.rs", cx)
+        })
+        .await
+        .unwrap();
+    rust_buffer2.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer.completion_triggers(),
+            &[".".to_string(), "::".to_string()]
+        );
+    });
+
+    // Changes are reported only to servers matching the buffer's language.
+    toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
+    rust_buffer2.update(cx, |buffer, cx| {
+        buffer.edit([(0..0, "let x = 1;")], None, cx)
+    });
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+            .await
+            .text_document,
+        lsp2::VersionedTextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(),
+            1
+        )
+    );
+
+    // Save notifications are reported to all servers.
+    project
+        .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
+        .await
+        .unwrap();
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidSaveTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
+        )
+    );
+    assert_eq!(
+        fake_json_server
+            .receive_notification::<lsp2::notification::DidSaveTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
+        )
+    );
+
+    // Renames are reported only to servers matching the buffer's language.
+    fs.rename(
+        Path::new("/the-root/test2.rs"),
+        Path::new("/the-root/test3.rs"),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()),
+    );
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentItem {
+            uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),
+            version: 0,
+            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
+            language_id: Default::default()
+        },
+    );
+
+    rust_buffer2.update(cx, |buffer, cx| {
+        buffer.update_diagnostics(
+            LanguageServerId(0),
+            DiagnosticSet::from_sorted_entries(
+                vec![DiagnosticEntry {
+                    diagnostic: Default::default(),
+                    range: Anchor::MIN..Anchor::MAX,
+                }],
+                &buffer.snapshot(),
+            ),
+            cx,
+        );
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+                .count(),
+            1
+        );
+    });
+
+    // When the rename changes the extension of the file, the buffer gets closed on the old
+    // language server and gets opened on the new one.
+    fs.rename(
+        Path::new("/the-root/test3.rs"),
+        Path::new("/the-root/test3.json"),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),),
+    );
+    assert_eq!(
+        fake_json_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentItem {
+            uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+            version: 0,
+            text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
+            language_id: Default::default()
+        },
+    );
+
+    // We clear the diagnostics, since the language has changed.
+    rust_buffer2.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+                .count(),
+            0
+        );
+    });
+
+    // The renamed file's version resets after changing language server.
+    rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
+    assert_eq!(
+        fake_json_server
+            .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+            .await
+            .text_document,
+        lsp2::VersionedTextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+            1
+        )
+    );
+
+    // Restart language servers
+    project.update(cx, |project, cx| {
+        project.restart_language_servers_for_buffers(
+            vec![rust_buffer.clone(), json_buffer.clone()],
+            cx,
+        );
+    });
+
+    let mut rust_shutdown_requests = fake_rust_server
+        .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
+    let mut json_shutdown_requests = fake_json_server
+        .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
+    futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
+
+    let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
+    let mut fake_json_server = fake_json_servers.next().await.unwrap();
+
+    // Ensure rust document is reopened in new rust language server
+    assert_eq!(
+        fake_rust_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document,
+        lsp2::TextDocumentItem {
+            uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
+            version: 0,
+            text: rust_buffer.update(cx, |buffer, _| buffer.text()),
+            language_id: Default::default()
+        }
+    );
+
+    // Ensure json documents are reopened in new json language server
+    assert_set_eq!(
+        [
+            fake_json_server
+                .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+                .await
+                .text_document,
+            fake_json_server
+                .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+                .await
+                .text_document,
+        ],
+        [
+            lsp2::TextDocumentItem {
+                uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+                version: 0,
+                text: json_buffer.update(cx, |buffer, _| buffer.text()),
+                language_id: Default::default()
+            },
+            lsp2::TextDocumentItem {
+                uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
+                version: 0,
+                text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
+                language_id: Default::default()
+            }
+        ]
+    );
+
+    // Close notifications are reported only to servers matching the buffer's language.
+    cx.update(|_| drop(json_buffer));
+    let close_message = lsp2::DidCloseTextDocumentParams {
+        text_document: lsp2::TextDocumentIdentifier::new(
+            lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
+        ),
+    };
+    assert_eq!(
+        fake_json_server
+            .receive_notification::<lsp2::notification::DidCloseTextDocument>()
+            .await,
+        close_message,
+    );
+}
+
+#[gpui2::test]
+async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-language-server",
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            ".gitignore": "target\n",
+            "src": {
+                "a.rs": "",
+                "b.rs": "",
+            },
+            "target": {
+                "x": {
+                    "out": {
+                        "x.rs": ""
+                    }
+                },
+                "y": {
+                    "out": {
+                        "y.rs": "",
+                    }
+                },
+                "z": {
+                    "out": {
+                        "z.rs": ""
+                    }
+                }
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages.add(Arc::new(language));
+    });
+    cx.executor().run_until_parked();
+
+    // Start the language server by opening a buffer with a compatible file extension.
+    let _buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/src/a.rs", cx)
+        })
+        .await
+        .unwrap();
+
+    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
+    project.update(cx, |project, cx| {
+        let worktree = project.worktrees().next().unwrap();
+        assert_eq!(
+            worktree
+                .read(cx)
+                .snapshot()
+                .entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+                (Path::new("target"), true),
+            ]
+        );
+    });
+
+    let prev_read_dir_count = fs.read_dir_call_count();
+
+    // Keep track of the FS events reported to the language server.
+    let fake_server = fake_servers.next().await.unwrap();
+    let file_changes = Arc::new(Mutex::new(Vec::new()));
+    fake_server
+        .request::<lsp2::request::RegisterCapability>(lsp2::RegistrationParams {
+            registrations: vec![lsp2::Registration {
+                id: Default::default(),
+                method: "workspace/didChangeWatchedFiles".to_string(),
+                register_options: serde_json::to_value(
+                    lsp2::DidChangeWatchedFilesRegistrationOptions {
+                        watchers: vec![
+                            lsp2::FileSystemWatcher {
+                                glob_pattern: lsp2::GlobPattern::String(
+                                    "/the-root/Cargo.toml".to_string(),
+                                ),
+                                kind: None,
+                            },
+                            lsp2::FileSystemWatcher {
+                                glob_pattern: lsp2::GlobPattern::String(
+                                    "/the-root/src/*.{rs,c}".to_string(),
+                                ),
+                                kind: None,
+                            },
+                            lsp2::FileSystemWatcher {
+                                glob_pattern: lsp2::GlobPattern::String(
+                                    "/the-root/target/y/**/*.rs".to_string(),
+                                ),
+                                kind: None,
+                            },
+                        ],
+                    },
+                )
+                .ok(),
+            }],
+        })
+        .await
+        .unwrap();
+    fake_server.handle_notification::<lsp2::notification::DidChangeWatchedFiles, _>({
+        let file_changes = file_changes.clone();
+        move |params, _| {
+            let mut file_changes = file_changes.lock();
+            file_changes.extend(params.changes);
+            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
+        }
+    });
+
+    cx.executor().run_until_parked();
+    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
+    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
+
+    // Now the language server has asked us to watch an ignored directory path,
+    // so we recursively load it.
+    project.update(cx, |project, cx| {
+        let worktree = project.worktrees().next().unwrap();
+        assert_eq!(
+            worktree
+                .read(cx)
+                .snapshot()
+                .entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+                (Path::new("target"), true),
+                (Path::new("target/x"), true),
+                (Path::new("target/y"), true),
+                (Path::new("target/y/out"), true),
+                (Path::new("target/y/out/y.rs"), true),
+                (Path::new("target/z"), true),
+            ]
+        );
+    });
+
+    // Perform some file system mutations, two of which match the watched patterns,
+    // and one of which does not.
+    fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+
+    // The language server receives events for the FS mutations that match its watch patterns.
+    cx.executor().run_until_parked();
+    assert_eq!(
+        &*file_changes.lock(),
+        &[
+            lsp2::FileEvent {
+                uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(),
+                typ: lsp2::FileChangeType::DELETED,
+            },
+            lsp2::FileEvent {
+                uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(),
+                typ: lsp2::FileChangeType::CREATED,
+            },
+            lsp2::FileEvent {
+                uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
+                typ: lsp2::FileChangeType::CREATED,
+            },
+        ]
+    );
+}
+
+#[gpui2::test]
+async fn test_single_file_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.rs": "let a = 1;",
+            "b.rs": "let b = 2;"
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
+
+    let buffer_a = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+    let buffer_b = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+        .await
+        .unwrap();
+
+    project.update(cx, |project, cx| {
+        project
+            .update_diagnostics(
+                LanguageServerId(0),
+                lsp2::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/dir/a.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp2::Diagnostic {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 4),
+                            lsp2::Position::new(0, 5),
+                        ),
+                        severity: Some(lsp2::DiagnosticSeverity::ERROR),
+                        message: "error 1".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostics(
+                LanguageServerId(0),
+                lsp2::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/dir/b.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp2::Diagnostic {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 4),
+                            lsp2::Position::new(0, 5),
+                        ),
+                        severity: Some(lsp2::DiagnosticSeverity::WARNING),
+                        message: "error 2".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+    });
+
+    buffer_a.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("a", Some(DiagnosticSeverity::ERROR)),
+                (" = 1;", None),
+            ]
+        );
+    });
+    buffer_b.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("b", Some(DiagnosticSeverity::WARNING)),
+                (" = 2;", None),
+            ]
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_hidden_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "dir": {
+                "a.rs": "let a = 1;",
+            },
+            "other.rs": "let b = c;"
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_local_worktree("/root/other.rs", false, cx)
+        })
+        .await
+        .unwrap();
+    let worktree_id = worktree.update(cx, |tree, _| tree.id());
+
+    project.update(cx, |project, cx| {
+        project
+            .update_diagnostics(
+                LanguageServerId(0),
+                lsp2::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/root/other.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp2::Diagnostic {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 8),
+                            lsp2::Position::new(0, 9),
+                        ),
+                        severity: Some(lsp2::DiagnosticSeverity::ERROR),
+                        message: "unknown variable 'c'".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+    });
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+        .await
+        .unwrap();
+    buffer.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let b = ", None),
+                ("c", Some(DiagnosticSeverity::ERROR)),
+                (";", None),
+            ]
+        );
+    });
+
+    project.update(cx, |project, cx| {
+        assert_eq!(project.diagnostic_summaries(cx).next(), None);
+        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+    });
+}
+
+#[gpui2::test]
+async fn test_disk_based_diagnostics_progress(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let progress_token = "the-progress-token";
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            disk_based_diagnostics_progress_token: Some(progress_token.into()),
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.rs": "fn a() { A }",
+            "b.rs": "const y: i32 = 1",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+    let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
+
+    // Cause worktree to start the fake language server
+    let _buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+        .await
+        .unwrap();
+
+    let mut events = cx.subscribe(&project);
+
+    let fake_server = fake_servers.next().await.unwrap();
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::LanguageServerAdded(LanguageServerId(0)),
+    );
+
+    fake_server
+        .start_progress(format!("{}/0", progress_token))
+        .await;
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiskBasedDiagnosticsStarted {
+            language_server_id: LanguageServerId(0),
+        }
+    );
+
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+        version: None,
+        diagnostics: vec![lsp2::Diagnostic {
+            range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+            severity: Some(lsp2::DiagnosticSeverity::ERROR),
+            message: "undefined variable 'A'".to_string(),
+            ..Default::default()
+        }],
+    });
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiagnosticsUpdated {
+            language_server_id: LanguageServerId(0),
+            path: (worktree_id, Path::new("a.rs")).into()
+        }
+    );
+
+    fake_server.end_progress(format!("{}/0", progress_token));
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiskBasedDiagnosticsFinished {
+            language_server_id: LanguageServerId(0)
+        }
+    );
+
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    buffer.update(cx, |buffer, _| {
+        let snapshot = buffer.snapshot();
+        let diagnostics = snapshot
+            .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+            .collect::<Vec<_>>();
+        assert_eq!(
+            diagnostics,
+            &[DiagnosticEntry {
+                range: Point::new(0, 9)..Point::new(0, 10),
+                diagnostic: Diagnostic {
+                    severity: lsp2::DiagnosticSeverity::ERROR,
+                    message: "undefined variable 'A'".to_string(),
+                    group_id: 0,
+                    is_primary: true,
+                    ..Default::default()
+                }
+            }]
+        )
+    });
+
+    // Ensure publishing empty diagnostics twice only results in one update event.
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+        version: None,
+        diagnostics: Default::default(),
+    });
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiagnosticsUpdated {
+            language_server_id: LanguageServerId(0),
+            path: (worktree_id, Path::new("a.rs")).into()
+        }
+    );
+
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+        version: None,
+        diagnostics: Default::default(),
+    });
+    cx.executor().run_until_parked();
+    assert_eq!(futures::poll!(events.next()), Poll::Pending);
+}
+
+#[gpui2::test]
+async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let progress_token = "the-progress-token";
+    let mut language = Language::new(
+        LanguageConfig {
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            disk_based_diagnostics_progress_token: Some(progress_token.into()),
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    // Simulate diagnostics starting to update.
+    let fake_server = fake_servers.next().await.unwrap();
+    fake_server.start_progress(progress_token).await;
+
+    // Restart the server before the diagnostics finish updating.
+    project.update(cx, |project, cx| {
+        project.restart_language_servers_for_buffers([buffer], cx);
+    });
+    let mut events = cx.subscribe(&project);
+
+    // Simulate the newly started server sending more diagnostics.
+    let fake_server = fake_servers.next().await.unwrap();
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::LanguageServerAdded(LanguageServerId(1))
+    );
+    fake_server.start_progress(progress_token).await;
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiskBasedDiagnosticsStarted {
+            language_server_id: LanguageServerId(1)
+        }
+    );
+    project.update(cx, |project, _| {
+        assert_eq!(
+            project
+                .language_servers_running_disk_based_diagnostics()
+                .collect::<Vec<_>>(),
+            [LanguageServerId(1)]
+        );
+    });
+
+    // All diagnostics are considered done, despite the old server's diagnostic
+    // task never completing.
+    fake_server.end_progress(progress_token);
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::DiskBasedDiagnosticsFinished {
+            language_server_id: LanguageServerId(1)
+        }
+    );
+    project.update(cx, |project, _| {
+        assert_eq!(
+            project
+                .language_servers_running_disk_based_diagnostics()
+                .collect::<Vec<_>>(),
+            [LanguageServerId(0); 0]
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    // Publish diagnostics
+    let fake_server = fake_servers.next().await.unwrap();
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+        version: None,
+        diagnostics: vec![lsp2::Diagnostic {
+            range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)),
+            severity: Some(lsp2::DiagnosticSeverity::ERROR),
+            message: "the message".to_string(),
+            ..Default::default()
+        }],
+    });
+
+    cx.executor().run_until_parked();
+    buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..1, false)
+                .map(|entry| entry.diagnostic.message.clone())
+                .collect::<Vec<_>>(),
+            ["the message".to_string()]
+        );
+    });
+    project.update(cx, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 1,
+                warning_count: 0,
+            }
+        );
+    });
+
+    project.update(cx, |project, cx| {
+        project.restart_language_servers_for_buffers([buffer.clone()], cx);
+    });
+
+    // The diagnostics are cleared.
+    cx.executor().run_until_parked();
+    buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..1, false)
+                .map(|entry| entry.diagnostic.message.clone())
+                .collect::<Vec<_>>(),
+            Vec::<String>::new(),
+        );
+    });
+    project.update(cx, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 0,
+                warning_count: 0,
+            }
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-lsp",
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    // Before restarting the server, report diagnostics with an unknown buffer version.
+    let fake_server = fake_servers.next().await.unwrap();
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+        version: Some(10000),
+        diagnostics: Vec::new(),
+    });
+    cx.executor().run_until_parked();
+
+    project.update(cx, |project, cx| {
+        project.restart_language_servers_for_buffers([buffer.clone()], cx);
+    });
+    let mut fake_server = fake_servers.next().await.unwrap();
+    let notification = fake_server
+        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+        .await
+        .text_document;
+    assert_eq!(notification.version, 0);
+}
+
+#[gpui2::test]
+async fn test_toggling_enable_language_server(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut rust = Language::new(
+        LanguageConfig {
+            name: Arc::from("Rust"),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_rust_servers = rust
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "rust-lsp",
+            ..Default::default()
+        }))
+        .await;
+    let mut js = Language::new(
+        LanguageConfig {
+            name: Arc::from("JavaScript"),
+            path_suffixes: vec!["js".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_js_servers = js
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "js-lsp",
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
+        .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages.add(Arc::new(rust));
+        project.languages.add(Arc::new(js));
+    });
+
+    let _rs_buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+    let _js_buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
+        .await
+        .unwrap();
+
+    let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
+    assert_eq!(
+        fake_rust_server_1
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document
+            .uri
+            .as_str(),
+        "file:///dir/a.rs"
+    );
+
+    let mut fake_js_server = fake_js_servers.next().await.unwrap();
+    assert_eq!(
+        fake_js_server
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document
+            .uri
+            .as_str(),
+        "file:///dir/b.js"
+    );
+
+    // Disable Rust language server, ensuring only that server gets stopped.
+    cx.update(|cx| {
+        cx.update_global(|settings: &mut SettingsStore, cx| {
+            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.languages.insert(
+                    Arc::from("Rust"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(false),
+                        ..Default::default()
+                    },
+                );
+            });
+        })
+    });
+    fake_rust_server_1
+        .receive_notification::<lsp2::notification::Exit>()
+        .await;
+
+    // Enable Rust and disable JavaScript language servers, ensuring that the
+    // former gets started again and that the latter stops.
+    cx.update(|cx| {
+        cx.update_global(|settings: &mut SettingsStore, cx| {
+            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.languages.insert(
+                    Arc::from("Rust"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(true),
+                        ..Default::default()
+                    },
+                );
+                settings.languages.insert(
+                    Arc::from("JavaScript"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(false),
+                        ..Default::default()
+                    },
+                );
+            });
+        })
+    });
+    let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
+    assert_eq!(
+        fake_rust_server_2
+            .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+            .await
+            .text_document
+            .uri
+            .as_str(),
+        "file:///dir/a.rs"
+    );
+    fake_js_server
+        .receive_notification::<lsp2::notification::Exit>()
+        .await;
+}
+
+#[gpui2::test(iterations = 3)]
+async fn test_transforming_diagnostics(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            disk_based_diagnostics_sources: vec!["disk".into()],
+            ..Default::default()
+        }))
+        .await;
+
+    let text = "
+        fn a() { A }
+        fn b() { BB }
+        fn c() { CCC }
+    "
+    .unindent();
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    let mut fake_server = fake_servers.next().await.unwrap();
+    let open_notification = fake_server
+        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+        .await;
+
+    // Edit the buffer, moving the content down
+    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
+    let change_notification_1 = fake_server
+        .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+        .await;
+    assert!(change_notification_1.text_document.version > open_notification.text_document.version);
+
+    // Report some diagnostics for the initial version of the buffer
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+        version: Some(open_notification.text_document.version),
+        diagnostics: vec![
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                message: "undefined variable 'A'".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                message: "undefined variable 'BB'".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                source: Some("disk".to_string()),
+                message: "undefined variable 'CCC'".to_string(),
+                ..Default::default()
+            },
+        ],
+    });
+
+    // The diagnostics have moved down since they were created.
+    cx.executor().run_until_parked();
+    buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
+                .collect::<Vec<_>>(),
+            &[
+                DiagnosticEntry {
+                    range: Point::new(3, 9)..Point::new(3, 11),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'BB'".to_string(),
+                        is_disk_based: true,
+                        group_id: 1,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                },
+                DiagnosticEntry {
+                    range: Point::new(4, 9)..Point::new(4, 12),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'CCC'".to_string(),
+                        is_disk_based: true,
+                        group_id: 2,
+                        is_primary: true,
+                        ..Default::default()
+                    }
+                }
+            ]
+        );
+        assert_eq!(
+            chunks_with_diagnostics(buffer, 0..buffer.len()),
+            [
+                ("\n\nfn a() { ".to_string(), None),
+                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
+                (" }\nfn b() { ".to_string(), None),
+                ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
+                (" }\nfn c() { ".to_string(), None),
+                ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
+                (" }\n".to_string(), None),
+            ]
+        );
+        assert_eq!(
+            chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
+            [
+                ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
+                (" }\nfn c() { ".to_string(), None),
+                ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
+            ]
+        );
+    });
+
+    // Ensure overlapping diagnostics are highlighted correctly.
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+        version: Some(open_notification.text_document.version),
+        diagnostics: vec![
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                message: "undefined variable 'A'".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)),
+                severity: Some(DiagnosticSeverity::WARNING),
+                message: "unreachable statement".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+        ],
+    });
+
+    cx.executor().run_until_parked();
+    buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
+                .collect::<Vec<_>>(),
+            &[
+                DiagnosticEntry {
+                    range: Point::new(2, 9)..Point::new(2, 12),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "unreachable statement".to_string(),
+                        is_disk_based: true,
+                        group_id: 4,
+                        is_primary: true,
+                        ..Default::default()
+                    }
+                },
+                DiagnosticEntry {
+                    range: Point::new(2, 9)..Point::new(2, 10),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'A'".to_string(),
+                        is_disk_based: true,
+                        group_id: 3,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                }
+            ]
+        );
+        assert_eq!(
+            chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
+            [
+                ("fn a() { ".to_string(), None),
+                ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
+                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
+                ("\n".to_string(), None),
+            ]
+        );
+        assert_eq!(
+            chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
+            [
+                (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
+                ("\n".to_string(), None),
+            ]
+        );
+    });
+
+    // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
+    // changes since the last save.
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "    ")], None, cx);
+        buffer.edit(
+            [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
+            None,
+            cx,
+        );
+        buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
+    });
+    let change_notification_2 = fake_server
+        .receive_notification::<lsp2::notification::DidChangeTextDocument>()
+        .await;
+    assert!(
+        change_notification_2.text_document.version > change_notification_1.text_document.version
+    );
+
+    // Handle out-of-order diagnostics
+    fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
+        uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+        version: Some(change_notification_2.text_document.version),
+        diagnostics: vec![
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
+                severity: Some(DiagnosticSeverity::ERROR),
+                message: "undefined variable 'BB'".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+            lsp2::Diagnostic {
+                range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+                severity: Some(DiagnosticSeverity::WARNING),
+                message: "undefined variable 'A'".to_string(),
+                source: Some("disk".to_string()),
+                ..Default::default()
+            },
+        ],
+    });
+
+    cx.executor().run_until_parked();
+    buffer.update(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
+                .collect::<Vec<_>>(),
+            &[
+                DiagnosticEntry {
+                    range: Point::new(2, 21)..Point::new(2, 22),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::WARNING,
+                        message: "undefined variable 'A'".to_string(),
+                        is_disk_based: true,
+                        group_id: 6,
+                        is_primary: true,
+                        ..Default::default()
+                    }
+                },
+                DiagnosticEntry {
+                    range: Point::new(3, 9)..Point::new(3, 14),
+                    diagnostic: Diagnostic {
+                        source: Some("disk".into()),
+                        severity: DiagnosticSeverity::ERROR,
+                        message: "undefined variable 'BB'".to_string(),
+                        is_disk_based: true,
+                        group_id: 5,
+                        is_primary: true,
+                        ..Default::default()
+                    },
+                }
+            ]
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_empty_diagnostic_ranges(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let text = concat!(
+        "let one = ;\n", //
+        "let two = \n",
+        "let three = 3;\n",
+    );
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": text })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    project.update(cx, |project, cx| {
+        project
+            .update_buffer_diagnostics(
+                &buffer,
+                LanguageServerId(0),
+                None,
+                vec![
+                    DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "syntax error 1".to_string(),
+                            ..Default::default()
+                        },
+                    },
+                    DiagnosticEntry {
+                        range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
+                        diagnostic: Diagnostic {
+                            severity: DiagnosticSeverity::ERROR,
+                            message: "syntax error 2".to_string(),
+                            ..Default::default()
+                        },
+                    },
+                ],
+                cx,
+            )
+            .unwrap();
+    });
+
+    // An empty range is extended forward to include the following character.
+    // At the end of a line, an empty range is extended backward to include
+    // the preceding character.
+    buffer.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let one = ", None),
+                (";", Some(DiagnosticSeverity::ERROR)),
+                ("\nlet two =", None),
+                (" ", Some(DiagnosticSeverity::ERROR)),
+                ("\nlet three = 3;\n", None)
+            ]
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
+        .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+
+    project.update(cx, |project, cx| {
+        project
+            .update_diagnostic_entries(
+                LanguageServerId(0),
+                Path::new("/dir/a.rs").to_owned(),
+                None,
+                vec![DiagnosticEntry {
+                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        is_primary: true,
+                        message: "syntax error a1".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostic_entries(
+                LanguageServerId(1),
+                Path::new("/dir/a.rs").to_owned(),
+                None,
+                vec![DiagnosticEntry {
+                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
+                    diagnostic: Diagnostic {
+                        severity: DiagnosticSeverity::ERROR,
+                        is_primary: true,
+                        message: "syntax error b1".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 2,
+                warning_count: 0,
+            }
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
+
+    let text = "
+        fn a() {
+            f1();
+        }
+        fn b() {
+            f2();
+        }
+        fn c() {
+            f3();
+        }
+    "
+    .unindent();
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.rs": text.clone(),
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    let mut fake_server = fake_servers.next().await.unwrap();
+    let lsp_document_version = fake_server
+        .receive_notification::<lsp2::notification::DidOpenTextDocument>()
+        .await
+        .text_document
+        .version;
+
+    // Simulate editing the buffer after the language server computes some edits.
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit(
+            [(
+                Point::new(0, 0)..Point::new(0, 0),
+                "// above first function\n",
+            )],
+            None,
+            cx,
+        );
+        buffer.edit(
+            [(
+                Point::new(2, 0)..Point::new(2, 0),
+                "    // inside first function\n",
+            )],
+            None,
+            cx,
+        );
+        buffer.edit(
+            [(
+                Point::new(6, 4)..Point::new(6, 4),
+                "// inside second function ",
+            )],
+            None,
+            cx,
+        );
+
+        assert_eq!(
+            buffer.text(),
+            "
+                // above first function
+                fn a() {
+                    // inside first function
+                    f1();
+                }
+                fn b() {
+                    // inside second function f2();
+                }
+                fn c() {
+                    f3();
+                }
+            "
+            .unindent()
+        );
+    });
+
+    let edits = project
+        .update(cx, |project, cx| {
+            project.edits_from_lsp(
+                &buffer,
+                vec![
+                    // replace body of first function
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 0),
+                            lsp2::Position::new(3, 0),
+                        ),
+                        new_text: "
+                            fn a() {
+                                f10();
+                            }
+                            "
+                        .unindent(),
+                    },
+                    // edit inside second function
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(4, 6),
+                            lsp2::Position::new(4, 6),
+                        ),
+                        new_text: "00".into(),
+                    },
+                    // edit inside third function via two distinct edits
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(7, 5),
+                            lsp2::Position::new(7, 5),
+                        ),
+                        new_text: "4000".into(),
+                    },
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(7, 5),
+                            lsp2::Position::new(7, 6),
+                        ),
+                        new_text: "".into(),
+                    },
+                ],
+                LanguageServerId(0),
+                Some(lsp_document_version),
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    buffer.update(cx, |buffer, cx| {
+        for (range, new_text) in edits {
+            buffer.edit([(range, new_text)], None, cx);
+        }
+        assert_eq!(
+            buffer.text(),
+            "
+                // above first function
+                fn a() {
+                    // inside first function
+                    f10();
+                }
+                fn b() {
+                    // inside second function f200();
+                }
+                fn c() {
+                    f4000();
+                }
+                "
+            .unindent()
+        );
+    });
+}
+
+#[gpui2::test]
+async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let text = "
+        use a::b;
+        use a::c;
+
+        fn f() {
+            b();
+            c();
+        }
+    "
+    .unindent();
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.rs": text.clone(),
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    // Simulate the language server sending us a small edit in the form of a very large diff.
+    // Rust-analyzer does this when performing a merge-imports code action.
+    let edits = project
+        .update(cx, |project, cx| {
+            project.edits_from_lsp(
+                &buffer,
+                [
+                    // Replace the first use statement without editing the semicolon.
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 4),
+                            lsp2::Position::new(0, 8),
+                        ),
+                        new_text: "a::{b, c}".into(),
+                    },
+                    // Reinsert the remainder of the file between the semicolon and the final
+                    // newline of the file.
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 9),
+                            lsp2::Position::new(0, 9),
+                        ),
+                        new_text: "\n\n".into(),
+                    },
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(0, 9),
+                            lsp2::Position::new(0, 9),
+                        ),
+                        new_text: "
+                            fn f() {
+                                b();
+                                c();
+                            }"
+                        .unindent(),
+                    },
+                    // Delete everything after the first newline of the file.
+                    lsp2::TextEdit {
+                        range: lsp2::Range::new(
+                            lsp2::Position::new(1, 0),
+                            lsp2::Position::new(7, 0),
+                        ),
+                        new_text: "".into(),
+                    },
+                ],
+                LanguageServerId(0),
+                None,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    buffer.update(cx, |buffer, cx| {
+        let edits = edits
+            .into_iter()
+            .map(|(range, text)| {
+                (
+                    range.start.to_point(buffer)..range.end.to_point(buffer),
+                    text,
+                )
+            })
+            .collect::<Vec<_>>();
+
+        assert_eq!(
+            edits,
+            [
+                (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
+                (Point::new(1, 0)..Point::new(2, 0), "".into())
+            ]
+        );
+
+        for (range, new_text) in edits {
+            buffer.edit([(range, new_text)], None, cx);
+        }
+        assert_eq!(
+            buffer.text(),
+            "
+                use a::{b, c};
+
+                fn f() {
+                    b();
+                    c();
+                }
+            "
+            .unindent()
+        );
+    });
+}
+
+// #[gpui2::test]
+// async fn test_invalid_edits_from_lsp2(cx: &mut gpui2::TestAppContext) {
 //     init_test(cx);
 
-//     let fs = FakeFs::new(cx.background());
+//     let text = "
+//         use a::b;
+//         use a::c;
+
+//         fn f() {
+//             b();
+//             c();
+//         }
+//     "
+//     .unindent();
+
+//     let fs = FakeFs::new(cx.executor().clone());
 //     fs.insert_tree(
-//         "/the-root",
+//         "/dir",
 //         json!({
-//             ".zed": {
-//                 "settings.json": r#"{ "tab_size": 8 }"#
-//             },
-//             "a": {
-//                 "a.rs": "fn a() {\n    A\n}"
-//             },
-//             "b": {
-//                 ".zed": {
-//                     "settings.json": r#"{ "tab_size": 2 }"#
-//                 },
-//                 "b.rs": "fn b() {\n  B\n}"
-//             }
+//             "a.rs": text.clone(),
 //         }),
 //     )
 //     .await;
 
-//     let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
-//     let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
-
-//     deterministic.run_until_parked();
-//     cx.read(|cx| {
-//         let tree = worktree.read(cx);
-
-//         let settings_a = language_settings(
-//             None,
-//             Some(
-//                 &(File::for_entry(
-//                     tree.entry_for_path("a/a.rs").unwrap().clone(),
-//                     worktree.clone(),
-//                 ) as _),
-//             ),
-//             cx,
-//         );
-//         let settings_b = language_settings(
-//             None,
-//             Some(
-//                 &(File::for_entry(
-//                     tree.entry_for_path("b/b.rs").unwrap().clone(),
-//                     worktree.clone(),
-//                 ) as _),
-//             ),
-//             cx,
+//     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+//     let buffer = project
+//         .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+//         .await
+//         .unwrap();
+
+//     // Simulate the language server sending us edits in a non-ordered fashion,
+//     // with ranges sometimes being inverted or pointing to invalid locations.
+//     let edits = project
+//         .update(cx, |project, cx| {
+//             project.edits_from_lsp(
+//                 &buffer,
+//                 [
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "\n\n".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 8),
+//                             lsp2::Position::new(0, 4),
+//                         ),
+//                         new_text: "a::{b, c}".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(1, 0),
+//                             lsp2::Position::new(99, 0),
+//                         ),
+//                         new_text: "".into(),
+//                     },
+//                     lsp2::TextEdit {
+//                         range: lsp2::Range::new(
+//                             lsp2::Position::new(0, 9),
+//                             lsp2::Position::new(0, 9),
+//                         ),
+//                         new_text: "
+//                             fn f() {
+//                                 b();
+//                                 c();
+//                             }"
+//                         .unindent(),
+//                     },
+//                 ],
+//                 LanguageServerId(0),
+//                 None,
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+
+//     buffer.update(cx, |buffer, cx| {
+//         let edits = edits
+//             .into_iter()
+//             .map(|(range, text)| {
+//                 (
+//                     range.start.to_point(buffer)..range.end.to_point(buffer),
+//                     text,
+//                 )
+//             })
+//             .collect::<Vec<_>>();
+
+//         assert_eq!(
+//             edits,
+//             [
+//                 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
+//                 (Point::new(1, 0)..Point::new(2, 0), "".into())
+//             ]
 //         );
 
-//         assert_eq!(settings_a.tab_size.get(), 8);
-//         assert_eq!(settings_b.tab_size.get(), 2);
+//         for (range, new_text) in edits {
+//             buffer.edit([(range, new_text)], None, cx);
+//         }
+//         assert_eq!(
+//             buffer.text(),
+//             "
+//                 use a::{b, c};
+
+//                 fn f() {
+//                     b();
+//                     c();
+//                 }
+//             "
+//             .unindent()
+//         );
 //     });
 // }
 
-// #[gpui::test]
-// async fn test_managing_language_servers(
-//     deterministic: Arc<Deterministic>,
-//     cx: &mut gpui::TestAppContext,
-// ) {
+fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
+    buffer: &Buffer,
+    range: Range<T>,
+) -> Vec<(String, Option<DiagnosticSeverity>)> {
+    let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
+    for chunk in buffer.snapshot().chunks(range, true) {
+        if chunks.last().map_or(false, |prev_chunk| {
+            prev_chunk.1 == chunk.diagnostic_severity
+        }) {
+            chunks.last_mut().unwrap().0.push_str(chunk.text);
+        } else {
+            chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
+        }
+    }
+    chunks
+}
+
+#[gpui2::test(iterations = 10)]
+async fn test_definition(cx: &mut gpui2::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "a.rs": "const fn a() { A }",
+            "b.rs": "const y: i32 = crate::a()",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
+        .await
+        .unwrap();
+
+    let fake_server = fake_servers.next().await.unwrap();
+    fake_server.handle_request::<lsp2::request::GotoDefinition, _, _>(|params, _| async move {
+        let params = params.text_document_position_params;
+        assert_eq!(
+            params.text_document.uri.to_file_path().unwrap(),
+            Path::new("/dir/b.rs"),
+        );
+        assert_eq!(params.position, lsp2::Position::new(0, 22));
+
+        Ok(Some(lsp2::GotoDefinitionResponse::Scalar(
+            lsp2::Location::new(
+                lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
+                lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
+            ),
+        )))
+    });
+
+    let mut definitions = project
+        .update(cx, |project, cx| project.definition(&buffer, 22, cx))
+        .await
+        .unwrap();
+
+    // Assert no new language server started
+    cx.executor().run_until_parked();
+    assert!(fake_servers.try_next().is_err());
+
+    assert_eq!(definitions.len(), 1);
+    let definition = definitions.pop().unwrap();
+    cx.update(|cx| {
+        let target_buffer = definition.target.buffer.read(cx);
+        assert_eq!(
+            target_buffer
+                .file()
+                .unwrap()
+                .as_local()
+                .unwrap()
+                .abs_path(cx),
+            Path::new("/dir/a.rs"),
+        );
+        assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
+        assert_eq!(
+            list_worktrees(&project, cx),
+            [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
+        );
+
+        drop(definition);
+    });
+    cx.update(|cx| {
+        assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
+    });
+
+    fn list_worktrees<'a>(
+        project: &'a Model<Project>,
+        cx: &'a AppContext,
+    ) -> Vec<(&'a Path, bool)> {
+        project
+            .read(cx)
+            .worktrees()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                (
+                    worktree.as_local().unwrap().abs_path().as_ref(),
+                    worktree.is_visible(),
+                )
+            })
+            .collect::<Vec<_>>()
+    }
+}
+
+// #[gpui2::test]
+// async fn test_completions_without_edit_ranges(cx: &mut gpui2::TestAppContext) {
 //     init_test(cx);
 
-//     let mut rust_language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut json_language = Language::new(
+//     let mut language = Language::new(
 //         LanguageConfig {
-//             name: "JSON".into(),
-//             path_suffixes: vec!["json".to_string()],
+//             name: "TypeScript".into(),
+//             path_suffixes: vec!["ts".to_string()],
 //             ..Default::default()
 //         },
-//         None,
+//         Some(tree_sitter_typescript::language_typescript()),
 //     );
-//     let mut fake_rust_servers = rust_language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             name: "the-rust-language-server",
-//             capabilities: lsp::ServerCapabilities {
-//                 completion_provider: Some(lsp::CompletionOptions {
-//                     trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
-//                     ..Default::default()
-//                 }),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
-//     let mut fake_json_servers = json_language
+//     let mut fake_language_servers = language
 //         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             name: "the-json-language-server",
-//             capabilities: lsp::ServerCapabilities {
-//                 completion_provider: Some(lsp::CompletionOptions {
+//             capabilities: lsp2::ServerCapabilities {
+//                 completion_provider: Some(lsp2::CompletionOptions {
 //                     trigger_characters: Some(vec![":".to_string()]),
 //                     ..Default::default()
 //                 }),

crates/project2/src/worktree.rs 🔗

@@ -4030,53 +4030,52 @@ struct UpdateIgnoreStatusJob {
     scan_queue: Sender<ScanJob>,
 }
 
-// todo!("re-enable when we have tests")
-// pub trait WorktreeModelHandle {
-// #[cfg(any(test, feature = "test-support"))]
-// fn flush_fs_events<'a>(
-//     &self,
-//     cx: &'a gpui::TestAppContext,
-// ) -> futures::future::LocalBoxFuture<'a, ()>;
-// }
-
-// impl WorktreeModelHandle for Handle<Worktree> {
-//     // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
-//     // occurred before the worktree was constructed. These events can cause the worktree to perform
-//     // extra directory scans, and emit extra scan-state notifications.
-//     //
-//     // This function mutates the worktree's directory and waits for those mutations to be picked up,
-//     // to ensure that all redundant FS events have already been processed.
-//     #[cfg(any(test, feature = "test-support"))]
-//     fn flush_fs_events<'a>(
-//         &self,
-//         cx: &'a gpui::TestAppContext,
-//     ) -> futures::future::LocalBoxFuture<'a, ()> {
-//         let filename = "fs-event-sentinel";
-//         let tree = self.clone();
-//         let (fs, root_path) = self.read_with(cx, |tree, _| {
-//             let tree = tree.as_local().unwrap();
-//             (tree.fs.clone(), tree.abs_path().clone())
-//         });
-
-//         async move {
-//             fs.create_file(&root_path.join(filename), Default::default())
-//                 .await
-//                 .unwrap();
-//             tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_some())
-//                 .await;
-
-//             fs.remove_file(&root_path.join(filename), Default::default())
-//                 .await
-//                 .unwrap();
-//             tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_none())
-//                 .await;
-
-//             cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-//                 .await;
-//         }
-//         .boxed_local()
-//     }
-// }
+pub trait WorktreeModelHandle {
+    #[cfg(any(test, feature = "test-support"))]
+    fn flush_fs_events<'a>(
+        &self,
+        cx: &'a mut gpui2::TestAppContext,
+    ) -> futures::future::LocalBoxFuture<'a, ()>;
+}
+
+impl WorktreeModelHandle for Model<Worktree> {
+    // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
+    // occurred before the worktree was constructed. These events can cause the worktree to perform
+    // extra directory scans, and emit extra scan-state notifications.
+    //
+    // This function mutates the worktree's directory and waits for those mutations to be picked up,
+    // to ensure that all redundant FS events have already been processed.
+    #[cfg(any(test, feature = "test-support"))]
+    fn flush_fs_events<'a>(
+        &self,
+        cx: &'a mut gpui2::TestAppContext,
+    ) -> futures::future::LocalBoxFuture<'a, ()> {
+        let filename = "fs-event-sentinel";
+        let tree = self.clone();
+        let (fs, root_path) = self.update(cx, |tree, _| {
+            let tree = tree.as_local().unwrap();
+            (tree.fs.clone(), tree.abs_path().clone())
+        });
+
+        async move {
+            fs.create_file(&root_path.join(filename), Default::default())
+                .await
+                .unwrap();
+            cx.executor().run_until_parked();
+            assert!(tree.update(cx, |tree, _| tree.entry_for_path(filename).is_some()));
+
+            fs.remove_file(&root_path.join(filename), Default::default())
+                .await
+                .unwrap();
+            cx.executor().run_until_parked();
+            assert!(tree.update(cx, |tree, _| tree.entry_for_path(filename).is_none()));
+
+            cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+                .await;
+        }
+        .boxed_local()
+    }
+}
 
 #[derive(Clone, Debug)]
 struct TraversalProgress<'a> {