Represent all randomized test actions as operations

Max Brunsfeld created

Change summary

crates/collab/src/tests/randomized_integration_tests.rs | 1740 +++++-----
1 file changed, 932 insertions(+), 808 deletions(-)

Detailed changes

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
 use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
-use project::{search::SearchQuery, Project};
+use project::{search::SearchQuery, Project, ProjectPath};
 use rand::prelude::*;
 use std::{
     env,
@@ -22,6 +22,7 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
+use util::ResultExt;
 
 #[gpui::test(iterations = 100)]
 async fn test_random_collaboration(
@@ -84,10 +85,12 @@ async fn test_random_collaboration(
     }
 
     let plan = Arc::new(Mutex::new(TestPlan {
-        users,
         allow_server_restarts: rng.gen_bool(0.7),
         allow_client_reconnection: rng.gen_bool(0.7),
         allow_client_disconnection: rng.gen_bool(0.1),
+        operation_ix: 0,
+        max_operations,
+        users,
         rng,
     }));
 
@@ -96,9 +99,8 @@ async fn test_random_collaboration(
     let mut operation_channels = Vec::new();
     let mut next_entity_id = 100000;
 
-    let mut i = 0;
-    while i < max_operations {
-        let next_operation = plan.lock().next_operation(&clients).await;
+    loop {
+        let Some(next_operation) = plan.lock().next_operation(&clients).await else { break };
         match next_operation {
             Operation::AddConnection { user_id } => {
                 let username = {
@@ -132,7 +134,6 @@ async fn test_random_collaboration(
                 )));
 
                 log::info!("Added connection for {}", username);
-                i += 1;
             }
 
             Operation::RemoveConnection { user_id } => {
@@ -196,7 +197,6 @@ async fn test_random_collaboration(
                     cx.clear_globals();
                     drop(client);
                 });
-                i += 1;
             }
 
             Operation::BounceConnection { user_id } => {
@@ -210,7 +210,6 @@ async fn test_random_collaboration(
                 let peer_id = user_connection_ids[0].into();
                 server.disconnect_client(peer_id);
                 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-                i += 1;
             }
 
             Operation::RestartServer => {
@@ -227,7 +226,6 @@ async fn test_random_collaboration(
                     .await
                     .unwrap();
                 assert_eq!(stale_room_ids, vec![]);
-                i += 1;
             }
 
             Operation::MutateClients { user_ids, quiesce } => {
@@ -237,7 +235,6 @@ async fn test_random_collaboration(
                         .position(|(client, cx)| client.current_user_id(cx) == user_id)
                         .unwrap();
                     operation_channels[client_ix].unbounded_send(()).unwrap();
-                    i += 1;
                 }
 
                 if quiesce {
@@ -427,891 +424,1018 @@ async fn test_random_collaboration(
     }
 }
 
-struct TestPlan {
-    rng: StdRng,
-    users: Vec<UserTestPlan>,
-    allow_server_restarts: bool,
-    allow_client_reconnection: bool,
-    allow_client_disconnection: bool,
-}
+async fn apply_client_operation(
+    client: &TestClient,
+    operation: ClientOperation,
+    cx: &mut TestAppContext,
+) -> Result<()> {
+    match operation {
+        ClientOperation::AcceptIncomingCall => {
+            log::info!("{}: accepting incoming call", client.username);
 
-struct UserTestPlan {
-    user_id: UserId,
-    username: String,
-    next_root_id: usize,
-    online: bool,
-}
+            let active_call = cx.read(ActiveCall::global);
+            active_call
+                .update(cx, |call, cx| call.accept_incoming(cx))
+                .await?;
+        }
 
-#[derive(Debug)]
-enum Operation {
-    AddConnection {
-        user_id: UserId,
-    },
-    RemoveConnection {
-        user_id: UserId,
-    },
-    BounceConnection {
-        user_id: UserId,
-    },
-    RestartServer,
-    MutateClients {
-        user_ids: Vec<UserId>,
-        quiesce: bool,
-    },
-}
+        ClientOperation::RejectIncomingCall => {
+            log::info!("{}: declining incoming call", client.username);
 
-#[derive(Debug)]
-enum ClientOperation {
-    AcceptIncomingCall,
-    RejectIncomingCall,
-    LeaveCall,
-    InviteContactToCall {
-        user_id: UserId,
-    },
-    OpenLocalProject {
-        first_root_name: String,
-    },
-    OpenRemoteProject {
-        host_id: UserId,
-        first_root_name: String,
-    },
-    AddWorktreeToProject {
-        project_root_name: String,
-        new_root_path: PathBuf,
-    },
-    CloseRemoteProject {
-        project_root_name: String,
-    },
-    OpenBuffer {
-        project_root_name: String,
-        full_path: PathBuf,
-    },
-    SearchProject {
-        project_root_name: String,
-        query: String,
-        detach: bool,
-    },
-    EditBuffer {
-        project_root_name: String,
-        full_path: PathBuf,
-        edits: Vec<(Range<usize>, Arc<str>)>,
-    },
-    CloseBuffer {
-        project_root_name: String,
-        full_path: PathBuf,
-    },
-    SaveBuffer {
-        project_root_name: String,
-        full_path: PathBuf,
-        detach: bool,
-    },
-    RequestLspDataInBuffer {
-        project_root_name: String,
-        full_path: PathBuf,
-        offset: usize,
-        kind: LspRequestKind,
-        detach: bool,
-    },
-    Other,
-}
+            let active_call = cx.read(ActiveCall::global);
+            active_call.update(cx, |call, _| call.decline_incoming())?;
+        }
 
-#[derive(Debug)]
-enum LspRequestKind {
-    Rename,
-    Completion,
-    CodeAction,
-    Definition,
-    Highlights,
-}
+        ClientOperation::LeaveCall => {
+            log::info!("{}: hanging up", client.username);
 
-impl TestPlan {
-    async fn next_operation(&mut self, clients: &[(Rc<TestClient>, TestAppContext)]) -> Operation {
-        let operation = loop {
-            break match self.rng.gen_range(0..100) {
-                0..=29 if clients.len() < self.users.len() => {
-                    let user = self
-                        .users
-                        .iter()
-                        .filter(|u| !u.online)
-                        .choose(&mut self.rng)
-                        .unwrap();
-                    Operation::AddConnection {
-                        user_id: user.user_id,
-                    }
-                }
-                30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
-                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
-                    let user_id = client.current_user_id(cx);
-                    Operation::RemoveConnection { user_id }
-                }
-                35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
-                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
-                    let user_id = client.current_user_id(cx);
-                    Operation::BounceConnection { user_id }
-                }
-                40..=44 if self.allow_server_restarts && clients.len() > 1 => {
-                    Operation::RestartServer
-                }
-                _ if !clients.is_empty() => {
-                    let user_ids = (0..self.rng.gen_range(0..10))
-                        .map(|_| {
-                            let ix = self.rng.gen_range(0..clients.len());
-                            let (client, cx) = &clients[ix];
-                            client.current_user_id(cx)
-                        })
-                        .collect();
-                    Operation::MutateClients {
-                        user_ids,
-                        quiesce: self.rng.gen(),
-                    }
-                }
-                _ => continue,
-            };
-        };
-        operation
-    }
+            let active_call = cx.read(ActiveCall::global);
+            active_call.update(cx, |call, cx| call.hang_up(cx))?;
+        }
 
-    async fn next_client_operation(
-        &mut self,
-        client: &TestClient,
-        cx: &TestAppContext,
-    ) -> ClientOperation {
-        let user_id = client.current_user_id(cx);
-        let call = cx.read(ActiveCall::global);
-        let operation = loop {
-            match self.rng.gen_range(0..100) {
-                // Mutate the call
-                0..=29 => {
-                    // Respond to an incoming call
-                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
-                        break if self.rng.gen_bool(0.7) {
-                            ClientOperation::AcceptIncomingCall
-                        } else {
-                            ClientOperation::RejectIncomingCall
-                        };
-                    }
+        ClientOperation::InviteContactToCall { user_id } => {
+            log::info!("{}: inviting {}", client.username, user_id,);
 
-                    match self.rng.gen_range(0..100_u32) {
-                        // Invite a contact to the current call
-                        0..=70 => {
-                            let available_contacts =
-                                client.user_store.read_with(cx, |user_store, _| {
-                                    user_store
-                                        .contacts()
-                                        .iter()
-                                        .filter(|contact| contact.online && !contact.busy)
-                                        .cloned()
-                                        .collect::<Vec<_>>()
-                                });
-                            if !available_contacts.is_empty() {
-                                let contact = available_contacts.choose(&mut self.rng).unwrap();
-                                break ClientOperation::InviteContactToCall {
-                                    user_id: UserId(contact.user.id as i32),
-                                };
-                            }
-                        }
+            let active_call = cx.read(ActiveCall::global);
+            active_call
+                .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
+                .await
+                .log_err();
+        }
 
-                        // Leave the current call
-                        71.. => {
-                            if self.allow_client_disconnection
-                                && call.read_with(cx, |call, _| call.room().is_some())
-                            {
-                                break ClientOperation::LeaveCall;
-                            }
-                        }
-                    }
-                }
+        ClientOperation::OpenLocalProject { first_root_name } => {
+            log::info!(
+                "{}: opening local project at {:?}",
+                client.username,
+                first_root_name
+            );
 
-                // Mutate projects
-                39..=59 => match self.rng.gen_range(0..100_u32) {
-                    // Open a new project
-                    0..=70 => {
-                        // Open a remote project
-                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
-                            let existing_remote_project_ids = cx.read(|cx| {
-                                client
-                                    .remote_projects()
-                                    .iter()
-                                    .map(|p| p.read(cx).remote_id().unwrap())
-                                    .collect::<Vec<_>>()
-                            });
-                            let new_remote_projects = room.read_with(cx, |room, _| {
-                                room.remote_participants()
-                                    .values()
-                                    .flat_map(|participant| {
-                                        participant.projects.iter().filter_map(|project| {
-                                            if existing_remote_project_ids.contains(&project.id) {
-                                                None
-                                            } else {
-                                                Some((
-                                                    UserId::from_proto(participant.user.id),
-                                                    project.worktree_root_names[0].clone(),
-                                                ))
-                                            }
-                                        })
-                                    })
-                                    .collect::<Vec<_>>()
-                            });
-                            if !new_remote_projects.is_empty() {
-                                let (host_id, first_root_name) =
-                                    new_remote_projects.choose(&mut self.rng).unwrap().clone();
-                                break ClientOperation::OpenRemoteProject {
-                                    host_id,
-                                    first_root_name,
-                                };
-                            }
-                        }
-                        // Open a local project
-                        else {
-                            let first_root_name = self.next_root_dir_name(user_id);
-                            break ClientOperation::OpenLocalProject { first_root_name };
-                        }
-                    }
-
-                    // Close a remote project
-                    71..=80 => {
-                        if !client.remote_projects().is_empty() {
-                            let project = client
-                                .remote_projects()
-                                .choose(&mut self.rng)
-                                .unwrap()
-                                .clone();
-                            let first_root_name = root_name_for_project(&project, cx);
-                            break ClientOperation::CloseRemoteProject {
-                                project_root_name: first_root_name,
-                            };
-                        }
-                    }
+            let root_path = Path::new("/").join(&first_root_name);
+            client.fs.create_dir(&root_path).await.unwrap();
+            client
+                .fs
+                .create_file(&root_path.join("main.rs"), Default::default())
+                .await
+                .unwrap();
+            let project = client.build_local_project(root_path, cx).await.0;
+            ensure_project_shared(&project, client, cx).await;
+            client.local_projects_mut().push(project.clone());
+        }
 
-                    // Add a worktree to a local project
-                    81.. => {
-                        if !client.local_projects().is_empty() {
-                            let project = client
-                                .local_projects()
-                                .choose(&mut self.rng)
-                                .unwrap()
-                                .clone();
-                            let project_root_name = root_name_for_project(&project, cx);
+        ClientOperation::AddWorktreeToProject {
+            project_root_name,
+            new_root_path,
+        } => {
+            log::info!(
+                "{}: finding/creating local worktree at {:?} to project with root path {}",
+                client.username,
+                new_root_path,
+                project_root_name
+            );
 
-                            let mut paths = client.fs.paths().await;
-                            paths.remove(0);
-                            let new_root_path = if paths.is_empty() || self.rng.gen() {
-                                Path::new("/").join(&self.next_root_dir_name(user_id))
-                            } else {
-                                paths.choose(&mut self.rng).unwrap().clone()
-                            };
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .expect("invalid project in test operation");
+            ensure_project_shared(&project, client, cx).await;
+            if !client.fs.paths().await.contains(&new_root_path) {
+                client.fs.create_dir(&new_root_path).await.unwrap();
+            }
+            project
+                .update(cx, |project, cx| {
+                    project.find_or_create_local_worktree(&new_root_path, true, cx)
+                })
+                .await
+                .unwrap();
+        }
 
-                            break ClientOperation::AddWorktreeToProject {
-                                project_root_name,
-                                new_root_path,
-                            };
-                        }
-                    }
-                },
+        ClientOperation::CloseRemoteProject { project_root_name } => {
+            log::info!(
+                "{}: closing remote project with root path {}",
+                client.username,
+                project_root_name,
+            );
 
-                // Query and mutate buffers
-                60.. => {
-                    let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
-                    let project_root_name = root_name_for_project(&project, cx);
+            let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx)
+                .expect("invalid project in test operation");
+            cx.update(|_| client.remote_projects_mut().remove(ix));
+        }
 
-                    match self.rng.gen_range(0..100_u32) {
-                        // Manipulate an existing buffer
-                        0..=70 => {
-                            let Some(buffer) = client
-                                .buffers_for_project(&project)
-                                .iter()
-                                .choose(&mut self.rng)
-                                .cloned() else { continue };
+        ClientOperation::OpenRemoteProject {
+            host_id,
+            first_root_name,
+        } => {
+            log::info!(
+                "{}: joining remote project of user {}, root name {}",
+                client.username,
+                host_id,
+                first_root_name,
+            );
 
-                            let full_path = buffer
-                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+            let active_call = cx.read(ActiveCall::global);
+            let project_id = active_call
+                .read_with(cx, |call, cx| {
+                    let room = call.room().cloned()?;
+                    let participant = room
+                        .read(cx)
+                        .remote_participants()
+                        .get(&host_id.to_proto())?;
+                    let project = participant
+                        .projects
+                        .iter()
+                        .find(|project| project.worktree_root_names[0] == first_root_name)?;
+                    Some(project.id)
+                })
+                .expect("invalid project in test operation");
+            let project = client.build_remote_project(project_id, cx).await;
+            client.remote_projects_mut().push(project);
+        }
 
-                            match self.rng.gen_range(0..100_u32) {
-                                // Close the buffer
-                                0..=15 => {
-                                    break ClientOperation::CloseBuffer {
-                                        project_root_name,
-                                        full_path,
-                                    };
-                                }
-                                // Save the buffer
-                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
-                                    let detach = self.rng.gen_bool(0.3);
-                                    break ClientOperation::SaveBuffer {
-                                        project_root_name,
-                                        full_path,
-                                        detach,
-                                    };
-                                }
-                                // Edit the buffer
-                                30..=69 => {
-                                    let edits = buffer.read_with(cx, |buffer, _| {
-                                        buffer.get_random_edits(&mut self.rng, 3)
-                                    });
-                                    break ClientOperation::EditBuffer {
-                                        project_root_name,
-                                        full_path,
-                                        edits,
-                                    };
-                                }
-                                // Make an LSP request
-                                _ => {
-                                    let offset = buffer.read_with(cx, |buffer, _| {
-                                        buffer.clip_offset(
-                                            self.rng.gen_range(0..=buffer.len()),
-                                            language::Bias::Left,
-                                        )
-                                    });
-                                    let detach = self.rng.gen();
-                                    break ClientOperation::RequestLspDataInBuffer {
-                                        project_root_name,
-                                        full_path,
-                                        offset,
-                                        kind: match self.rng.gen_range(0..5_u32) {
-                                            0 => LspRequestKind::Rename,
-                                            1 => LspRequestKind::Highlights,
-                                            2 => LspRequestKind::Definition,
-                                            3 => LspRequestKind::CodeAction,
-                                            4.. => LspRequestKind::Completion,
-                                        },
-                                        detach,
-                                    };
-                                }
-                            }
-                        }
+        ClientOperation::CreateWorktreeEntry {
+            project_root_name,
+            is_local,
+            full_path,
+            is_dir,
+        } => {
+            log::info!(
+                "{}: creating {} at path {:?} in {} project {}",
+                client.username,
+                if is_dir { "dir" } else { "file" },
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+            );
 
-                        71..=80 => {
-                            let query = self.rng.gen_range('a'..='z').to_string();
-                            let detach = self.rng.gen_bool(0.3);
-                            break ClientOperation::SearchProject {
-                                project_root_name,
-                                query,
-                                detach,
-                            };
-                        }
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .expect("invalid project in test operation");
+            ensure_project_shared(&project, client, cx).await;
+            let project_path = project_path_for_full_path(&project, &full_path, cx)
+                .expect("invalid worktree path in test operation");
+            project
+                .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
+                .unwrap()
+                .await?;
+        }
 
-                        // Open a buffer
-                        81.. => {
-                            let worktree = project.read_with(cx, |project, cx| {
-                                project
-                                    .worktrees(cx)
-                                    .filter(|worktree| {
-                                        let worktree = worktree.read(cx);
-                                        worktree.is_visible()
-                                            && worktree.entries(false).any(|e| e.is_file())
-                                    })
-                                    .choose(&mut self.rng)
-                            });
-                            let Some(worktree) = worktree else { continue };
-                            let full_path = worktree.read_with(cx, |worktree, _| {
-                                let entry = worktree
-                                    .entries(false)
-                                    .filter(|e| e.is_file())
-                                    .choose(&mut self.rng)
-                                    .unwrap();
-                                if entry.path.as_ref() == Path::new("") {
-                                    Path::new(worktree.root_name()).into()
-                                } else {
-                                    Path::new(worktree.root_name()).join(&entry.path)
-                                }
-                            });
-                            break ClientOperation::OpenBuffer {
-                                project_root_name,
-                                full_path,
-                            };
-                        }
-                    }
-                }
+        ClientOperation::OpenBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+        } => {
+            log::info!(
+                "{}: opening buffer {:?} in {} project {}",
+                client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+            );
 
-                _ => break ClientOperation::Other,
-            }
-        };
-        operation
-    }
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .expect("invalid project in test operation");
+            ensure_project_shared(&project, client, cx).await;
+            let project_path = project_path_for_full_path(&project, &full_path, cx)
+                .expect("invalid buffer path in test operation");
+            let buffer = project
+                .update(cx, |project, cx| project.open_buffer(project_path, cx))
+                .await?;
+            client.buffers_for_project(&project).insert(buffer);
+        }
 
-    fn next_root_dir_name(&mut self, user_id: UserId) -> String {
-        let user_ix = self
-            .users
-            .iter()
-            .position(|user| user.user_id == user_id)
-            .unwrap();
-        let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
-        format!("dir-{user_id}-{root_id}")
-    }
-
-    fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
-        let ix = self
-            .users
-            .iter()
-            .position(|user| user.user_id == user_id)
-            .unwrap();
-        &mut self.users[ix]
-    }
-}
-
-async fn simulate_client(
-    client: Rc<TestClient>,
-    mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
-    plan: Arc<Mutex<TestPlan>>,
-    mut cx: TestAppContext,
-) {
-    // Setup language server
-    let mut language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            path_suffixes: vec!["rs".to_string()],
-            ..Default::default()
-        },
-        None,
-    );
-    let _fake_language_servers = language
-        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-            name: "the-fake-language-server",
-            capabilities: lsp::LanguageServer::full_capabilities(),
-            initializer: Some(Box::new({
-                let plan = plan.clone();
-                let fs = client.fs.clone();
-                move |fake_server: &mut FakeLanguageServer| {
-                    fake_server.handle_request::<lsp::request::Completion, _, _>(
-                        |_, _| async move {
-                            Ok(Some(lsp::CompletionResponse::Array(vec![
-                                lsp::CompletionItem {
-                                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                        range: lsp::Range::new(
-                                            lsp::Position::new(0, 0),
-                                            lsp::Position::new(0, 0),
-                                        ),
-                                        new_text: "the-new-text".to_string(),
-                                    })),
-                                    ..Default::default()
-                                },
-                            ])))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
-                        |_, _| async move {
-                            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                                lsp::CodeAction {
-                                    title: "the-code-action".to_string(),
-                                    ..Default::default()
-                                },
-                            )]))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
-                        |params, _| async move {
-                            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                                params.position,
-                                params.position,
-                            ))))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
-                        let fs = fs.clone();
-                        let plan = plan.clone();
-                        move |_, _| {
-                            let fs = fs.clone();
-                            let plan = plan.clone();
-                            async move {
-                                let files = fs.files().await;
-                                let mut plan = plan.lock();
-                                let count = plan.rng.gen_range::<usize, _>(1..3);
-                                let files = (0..count)
-                                    .map(|_| files.choose(&mut plan.rng).unwrap())
-                                    .collect::<Vec<_>>();
-                                log::info!("LSP: Returning definitions in files {:?}", &files);
-                                Ok(Some(lsp::GotoDefinitionResponse::Array(
-                                    files
-                                        .into_iter()
-                                        .map(|file| lsp::Location {
-                                            uri: lsp::Url::from_file_path(file).unwrap(),
-                                            range: Default::default(),
-                                        })
-                                        .collect(),
-                                )))
-                            }
-                        }
-                    });
-
-                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
-                        let plan = plan.clone();
-                        move |_, _| {
-                            let mut highlights = Vec::new();
-                            let highlight_count = plan.lock().rng.gen_range(1..=5);
-                            for _ in 0..highlight_count {
-                                let start_row = plan.lock().rng.gen_range(0..100);
-                                let start_column = plan.lock().rng.gen_range(0..100);
-                                let start = PointUtf16::new(start_row, start_column);
-                                let end_row = plan.lock().rng.gen_range(0..100);
-                                let end_column = plan.lock().rng.gen_range(0..100);
-                                let end = PointUtf16::new(end_row, end_column);
-                                let range = if start > end { end..start } else { start..end };
-                                highlights.push(lsp::DocumentHighlight {
-                                    range: range_to_lsp(range.clone()),
-                                    kind: Some(lsp::DocumentHighlightKind::READ),
-                                });
-                            }
-                            highlights.sort_unstable_by_key(|highlight| {
-                                (highlight.range.start, highlight.range.end)
-                            });
-                            async move { Ok(Some(highlights)) }
-                        }
-                    });
-                }
-            })),
-            ..Default::default()
-        }))
-        .await;
-    client.language_registry.add(Arc::new(language));
-
-    while operation_rx.next().await.is_some() {
-        let operation = plan.lock().next_client_operation(&client, &cx).await;
-        if let Err(error) = apply_client_operation(&client, plan.clone(), operation, &mut cx).await
-        {
-            log::error!("{} error: {}", client.username, error);
-        }
-        cx.background().simulate_random_delay().await;
-    }
-    log::info!("{}: done", client.username);
-}
-
-async fn apply_client_operation(
-    client: &TestClient,
-    plan: Arc<Mutex<TestPlan>>,
-    operation: ClientOperation,
-    cx: &mut TestAppContext,
-) -> Result<()> {
-    match operation {
-        ClientOperation::AcceptIncomingCall => {
-            log::info!("{}: accepting incoming call", client.username);
-            let active_call = cx.read(ActiveCall::global);
-            active_call
-                .update(cx, |call, cx| call.accept_incoming(cx))
-                .await?;
-        }
-
-        ClientOperation::RejectIncomingCall => {
-            log::info!("{}: declining incoming call", client.username);
-            let active_call = cx.read(ActiveCall::global);
-            active_call.update(cx, |call, _| call.decline_incoming())?;
-        }
-
-        ClientOperation::LeaveCall => {
-            log::info!("{}: hanging up", client.username);
-            let active_call = cx.read(ActiveCall::global);
-            active_call.update(cx, |call, cx| call.hang_up(cx))?;
-        }
-
-        ClientOperation::InviteContactToCall { user_id } => {
-            log::info!("{}: inviting {}", client.username, user_id,);
-            let active_call = cx.read(ActiveCall::global);
-            active_call
-                .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
-                .await?;
-        }
-
-        ClientOperation::OpenLocalProject { first_root_name } => {
+        ClientOperation::EditBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+            edits,
+        } => {
             log::info!(
-                "{}: opening local project at {:?}",
+                "{}: editing buffer {:?} in {} project {} with {:?}",
                 client.username,
-                first_root_name
+                full_path,
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                edits
             );
-            let root_path = Path::new("/").join(&first_root_name);
-            client.fs.create_dir(&root_path).await.unwrap();
-            client
-                .fs
-                .create_file(&root_path.join("main.rs"), Default::default())
-                .await
-                .unwrap();
-            let project = client.build_local_project(root_path, cx).await.0;
+
+            let project = project_for_root_name(client, &project_root_name, cx)
+                .expect("invalid project in test operation");
             ensure_project_shared(&project, client, cx).await;
-            client.local_projects_mut().push(project.clone());
+            let buffer =
+                buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
+                    .expect("invalid buffer path in test operation");
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
         }
 
-        ClientOperation::AddWorktreeToProject {
+        ClientOperation::CloseBuffer {
             project_root_name,
-            new_root_path,
+            is_local,
+            full_path,
         } => {
             log::info!(
-                "{}: finding/creating local worktree at {:?} to project with root path {}",
+                "{}: dropping buffer {:?} in {} project {}",
                 client.username,
-                new_root_path,
+                full_path,
+                if is_local { "local" } else { "remote" },
                 project_root_name
             );
+
             let project = project_for_root_name(client, &project_root_name, cx)
                 .expect("invalid project in test operation");
             ensure_project_shared(&project, client, cx).await;
-            if !client.fs.paths().await.contains(&new_root_path) {
-                client.fs.create_dir(&new_root_path).await.unwrap();
-            }
-            project
-                .update(cx, |project, cx| {
-                    project.find_or_create_local_worktree(&new_root_path, true, cx)
-                })
-                .await
-                .unwrap();
+            let buffer =
+                buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
+                    .expect("invalid buffer path in test operation");
+            cx.update(|_| {
+                client.buffers_for_project(&project).remove(&buffer);
+                drop(buffer);
+            });
         }
 
-        ClientOperation::CloseRemoteProject { project_root_name } => {
+        ClientOperation::SaveBuffer {
+            project_root_name,
+            is_local,
+            full_path,
+            detach,
+        } => {
             log::info!(
-                "{}: closing remote project with root path {}",
+                "{}: saving buffer {:?} in {} project {}{}",
                 client.username,
+                full_path,
+                if is_local { "local" } else { "remote" },
                 project_root_name,
+                if detach { ", detaching" } else { ", awaiting" }
             );
-            let ix = project_ix_for_root_name(&*client.remote_projects(), &project_root_name, cx)
-                .expect("invalid project in test operation");
-            cx.update(|_| client.remote_projects_mut().remove(ix));
-        }
 
-        ClientOperation::OpenRemoteProject {
-            host_id,
-            first_root_name,
-        } => {
-            log::info!(
-                "{}: joining remote project of user {}, root name {}",
-                client.username,
-                host_id,
-                first_root_name,
-            );
-            let active_call = cx.read(ActiveCall::global);
-            let project_id = active_call
-                .read_with(cx, |call, cx| {
-                    let room = call.room().cloned()?;
-                    let participant = room
-                        .read(cx)
-                        .remote_participants()
-                        .get(&host_id.to_proto())?;
-                    let project = participant
-                        .projects
-                        .iter()
-                        .find(|project| project.worktree_root_names[0] == first_root_name)?;
-                    Some(project.id)
-                })
-                .expect("invalid project in test operation");
-            let project = client.build_remote_project(project_id, cx).await;
-            client.remote_projects_mut().push(project);
-        }
-
-        ClientOperation::OpenBuffer {
-            project_root_name,
-            full_path,
-        } => {
-            log::info!(
-                "{}: opening buffer {:?} in project {}",
-                client.username,
-                full_path,
-                project_root_name,
-            );
-            let project = project_for_root_name(client, &project_root_name, cx)
-                .expect("invalid project in test operation");
-            // ensure_project_shared(&project, client, cx).await;
-            let mut components = full_path.components();
-            let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
-            let path = components.as_path();
-            let worktree_id = project
-                .read_with(cx, |project, cx| {
-                    project.worktrees(cx).find_map(|worktree| {
-                        let worktree = worktree.read(cx);
-                        if worktree.root_name() == root_name {
-                            Some(worktree.id())
-                        } else {
-                            None
-                        }
-                    })
-                })
-                .expect("invalid buffer path in test operation");
-            let buffer = project
-                .update(cx, |project, cx| {
-                    project.open_buffer((worktree_id, &path), cx)
-                })
-                .await?;
-            client.buffers_for_project(&project).insert(buffer);
-        }
-
-        ClientOperation::EditBuffer {
-            project_root_name,
-            full_path,
-            edits,
-        } => {
-            log::info!(
-                "{}: editing buffer {:?} in project {} with {:?}",
-                client.username,
-                full_path,
-                project_root_name,
-                edits
-            );
             let project = project_for_root_name(client, &project_root_name, cx)
                 .expect("invalid project in test operation");
+            ensure_project_shared(&project, client, cx).await;
             let buffer =
                 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
                     .expect("invalid buffer path in test operation");
-            buffer.update(cx, |buffer, cx| {
-                buffer.edit(edits, None, cx);
+            let (requested_version, save) =
+                buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
+            let save = cx.background().spawn(async move {
+                let (saved_version, _, _) = save
+                    .await
+                    .map_err(|err| anyhow!("save request failed: {:?}", err))?;
+                assert!(saved_version.observed_all(&requested_version));
+                anyhow::Ok(())
             });
+            if detach {
+                log::info!("{}: detaching save request", client.username);
+                cx.update(|cx| save.detach_and_log_err(cx));
+            } else {
+                save.await?;
+            }
         }
 
-        ClientOperation::CloseBuffer {
+        ClientOperation::RequestLspDataInBuffer {
             project_root_name,
+            is_local,
             full_path,
+            offset,
+            kind,
+            detach,
         } => {
             log::info!(
-                "{}: dropping buffer {:?} in project {}",
+                "{}: request LSP {:?} for buffer {:?} in {} project {}{}",
                 client.username,
+                kind,
                 full_path,
-                project_root_name
+                if is_local { "local" } else { "remote" },
+                project_root_name,
+                if detach { ", detaching" } else { ", awaiting" }
             );
+
             let project = project_for_root_name(client, &project_root_name, cx)
                 .expect("invalid project in test operation");
             let buffer =
                 buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
                     .expect("invalid buffer path in test operation");
-            cx.update(|_| {
-                client.buffers_for_project(&project).remove(&buffer);
-                drop(buffer);
-            });
+            let request = match kind {
+                LspRequestKind::Rename => cx.spawn(|mut cx| async move {
+                    project
+                        .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
+                        .await?;
+                    anyhow::Ok(())
+                }),
+                LspRequestKind::Completion => cx.spawn(|mut cx| async move {
+                    project
+                        .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
+                        .await?;
+                    Ok(())
+                }),
+                LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
+                    project
+                        .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
+                        .await?;
+                    Ok(())
+                }),
+                LspRequestKind::Definition => cx.spawn(|mut cx| async move {
+                    project
+                        .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
+                        .await?;
+                    Ok(())
+                }),
+                LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
+                    project
+                        .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
+                        .await?;
+                    Ok(())
+                }),
+            };
+            if detach {
+                request.detach();
+            } else {
+                request.await?;
+            }
         }
 
-        ClientOperation::SaveBuffer {
+        ClientOperation::SearchProject {
             project_root_name,
-            full_path,
+            query,
             detach,
         } => {
             log::info!(
-                "{}: saving buffer {:?} in project {}{}",
+                "{}: search project {} for {:?}{}",
                 client.username,
-                full_path,
                 project_root_name,
+                query,
                 if detach { ", detaching" } else { ", awaiting" }
             );
             let project = project_for_root_name(client, &project_root_name, cx)
                 .expect("invalid project in test operation");
-            let buffer =
-                buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
-                    .expect("invalid buffer path in test operation");
-            let (requested_version, save) =
-                buffer.update(cx, |buffer, cx| (buffer.version(), buffer.save(cx)));
-            let save = cx.background().spawn(async move {
-                let (saved_version, _, _) = save
+            let search = project.update(cx, |project, cx| {
+                project.search(SearchQuery::text(query, false, false), cx)
+            });
+            let search = cx.background().spawn(async move {
+                search
                     .await
-                    .map_err(|err| anyhow!("save request failed: {:?}", err))?;
-                assert!(saved_version.observed_all(&requested_version));
-                anyhow::Ok(())
+                    .map_err(|err| anyhow!("search request failed: {:?}", err))
             });
             if detach {
                 log::info!("{}: detaching save request", client.username);
-                cx.update(|cx| save.detach_and_log_err(cx));
+                cx.update(|cx| search.detach_and_log_err(cx));
             } else {
-                save.await?;
+                search.await?;
+            }
+        }
+
+        ClientOperation::CreateFsEntry { path, is_dir } => {
+            log::info!(
+                "{}: creating {} at {:?}",
+                client.username,
+                if is_dir { "dir" } else { "file" },
+                path
+            );
+            if is_dir {
+                client.fs.create_dir(&path).await.unwrap();
+            } else {
+                client
+                    .fs
+                    .create_file(&path, Default::default())
+                    .await
+                    .unwrap();
+            }
+        }
+    }
+    Ok(())
+}
+
+struct TestPlan {
+    rng: StdRng,
+    max_operations: usize,
+    operation_ix: usize,
+    users: Vec<UserTestPlan>,
+    allow_server_restarts: bool,
+    allow_client_reconnection: bool,
+    allow_client_disconnection: bool,
+}
+
+struct UserTestPlan {
+    user_id: UserId,
+    username: String,
+    next_root_id: usize,
+    online: bool,
+}
+
+#[derive(Debug)]
+enum Operation {
+    AddConnection {
+        user_id: UserId,
+    },
+    RemoveConnection {
+        user_id: UserId,
+    },
+    BounceConnection {
+        user_id: UserId,
+    },
+    RestartServer,
+    MutateClients {
+        user_ids: Vec<UserId>,
+        quiesce: bool,
+    },
+}
+
+#[derive(Debug)]
+enum ClientOperation {
+    AcceptIncomingCall,
+    RejectIncomingCall,
+    LeaveCall,
+    InviteContactToCall {
+        user_id: UserId,
+    },
+    OpenLocalProject {
+        first_root_name: String,
+    },
+    OpenRemoteProject {
+        host_id: UserId,
+        first_root_name: String,
+    },
+    AddWorktreeToProject {
+        project_root_name: String,
+        new_root_path: PathBuf,
+    },
+    CloseRemoteProject {
+        project_root_name: String,
+    },
+    OpenBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+    },
+    SearchProject {
+        project_root_name: String,
+        query: String,
+        detach: bool,
+    },
+    EditBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        edits: Vec<(Range<usize>, Arc<str>)>,
+    },
+    CloseBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+    },
+    SaveBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        detach: bool,
+    },
+    RequestLspDataInBuffer {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        offset: usize,
+        kind: LspRequestKind,
+        detach: bool,
+    },
+    CreateWorktreeEntry {
+        project_root_name: String,
+        is_local: bool,
+        full_path: PathBuf,
+        is_dir: bool,
+    },
+    CreateFsEntry {
+        path: PathBuf,
+        is_dir: bool,
+    },
+}
+
+#[derive(Debug)]
+enum LspRequestKind {
+    Rename,
+    Completion,
+    CodeAction,
+    Definition,
+    Highlights,
+}
+
+impl TestPlan {
+    async fn next_operation(
+        &mut self,
+        clients: &[(Rc<TestClient>, TestAppContext)],
+    ) -> Option<Operation> {
+        if self.operation_ix == self.max_operations {
+            return None;
+        }
+
+        let operation = loop {
+            break match self.rng.gen_range(0..100) {
+                0..=29 if clients.len() < self.users.len() => {
+                    let user = self
+                        .users
+                        .iter()
+                        .filter(|u| !u.online)
+                        .choose(&mut self.rng)
+                        .unwrap();
+                    self.operation_ix += 1;
+                    Operation::AddConnection {
+                        user_id: user.user_id,
+                    }
+                }
+                30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
+                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+                    let user_id = client.current_user_id(cx);
+                    self.operation_ix += 1;
+                    Operation::RemoveConnection { user_id }
+                }
+                35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
+                    let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+                    let user_id = client.current_user_id(cx);
+                    self.operation_ix += 1;
+                    Operation::BounceConnection { user_id }
+                }
+                40..=44 if self.allow_server_restarts && clients.len() > 1 => {
+                    self.operation_ix += 1;
+                    Operation::RestartServer
+                }
+                _ if !clients.is_empty() => {
+                    let count = self
+                        .rng
+                        .gen_range(1..10)
+                        .min(self.max_operations - self.operation_ix);
+                    let user_ids = (0..count)
+                        .map(|_| {
+                            let ix = self.rng.gen_range(0..clients.len());
+                            let (client, cx) = &clients[ix];
+                            client.current_user_id(cx)
+                        })
+                        .collect();
+                    Operation::MutateClients {
+                        user_ids,
+                        quiesce: self.rng.gen(),
+                    }
+                }
+                _ => continue,
+            };
+        };
+        Some(operation)
+    }
+
+    async fn next_client_operation(
+        &mut self,
+        client: &TestClient,
+        cx: &TestAppContext,
+    ) -> Option<ClientOperation> {
+        if self.operation_ix == self.max_operations {
+            return None;
+        }
+
+        let user_id = client.current_user_id(cx);
+        let call = cx.read(ActiveCall::global);
+        let operation = loop {
+            match self.rng.gen_range(0..100_u32) {
+                // Mutate the call
+                0..=29 => {
+                    // Respond to an incoming call
+                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
+                        break if self.rng.gen_bool(0.7) {
+                            ClientOperation::AcceptIncomingCall
+                        } else {
+                            ClientOperation::RejectIncomingCall
+                        };
+                    }
+
+                    match self.rng.gen_range(0..100_u32) {
+                        // Invite a contact to the current call
+                        0..=70 => {
+                            let available_contacts =
+                                client.user_store.read_with(cx, |user_store, _| {
+                                    user_store
+                                        .contacts()
+                                        .iter()
+                                        .filter(|contact| contact.online && !contact.busy)
+                                        .cloned()
+                                        .collect::<Vec<_>>()
+                                });
+                            if !available_contacts.is_empty() {
+                                let contact = available_contacts.choose(&mut self.rng).unwrap();
+                                break ClientOperation::InviteContactToCall {
+                                    user_id: UserId(contact.user.id as i32),
+                                };
+                            }
+                        }
+
+                        // Leave the current call
+                        71.. => {
+                            if self.allow_client_disconnection
+                                && call.read_with(cx, |call, _| call.room().is_some())
+                            {
+                                break ClientOperation::LeaveCall;
+                            }
+                        }
+                    }
+                }
+
+                // Mutate projects
+                30..=59 => match self.rng.gen_range(0..100_u32) {
+                    // Open a new project
+                    0..=70 => {
+                        // Open a remote project
+                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
+                            let existing_remote_project_ids = cx.read(|cx| {
+                                client
+                                    .remote_projects()
+                                    .iter()
+                                    .map(|p| p.read(cx).remote_id().unwrap())
+                                    .collect::<Vec<_>>()
+                            });
+                            let new_remote_projects = room.read_with(cx, |room, _| {
+                                room.remote_participants()
+                                    .values()
+                                    .flat_map(|participant| {
+                                        participant.projects.iter().filter_map(|project| {
+                                            if existing_remote_project_ids.contains(&project.id) {
+                                                None
+                                            } else {
+                                                Some((
+                                                    UserId::from_proto(participant.user.id),
+                                                    project.worktree_root_names[0].clone(),
+                                                ))
+                                            }
+                                        })
+                                    })
+                                    .collect::<Vec<_>>()
+                            });
+                            if !new_remote_projects.is_empty() {
+                                let (host_id, first_root_name) =
+                                    new_remote_projects.choose(&mut self.rng).unwrap().clone();
+                                break ClientOperation::OpenRemoteProject {
+                                    host_id,
+                                    first_root_name,
+                                };
+                            }
+                        }
+                        // Open a local project
+                        else {
+                            let first_root_name = self.next_root_dir_name(user_id);
+                            break ClientOperation::OpenLocalProject { first_root_name };
+                        }
+                    }
+
+                    // Close a remote project
+                    71..=80 => {
+                        if !client.remote_projects().is_empty() {
+                            let project = client
+                                .remote_projects()
+                                .choose(&mut self.rng)
+                                .unwrap()
+                                .clone();
+                            let first_root_name = root_name_for_project(&project, cx);
+                            break ClientOperation::CloseRemoteProject {
+                                project_root_name: first_root_name,
+                            };
+                        }
+                    }
+
+                    // Mutate project worktrees
+                    81.. => match self.rng.gen_range(0..100_u32) {
+                        // Add a worktree to a local project
+                        0..=50 => {
+                            let Some(project) = client
+                                    .local_projects()
+                                    .choose(&mut self.rng)
+                                    .cloned() else { continue };
+                            let project_root_name = root_name_for_project(&project, cx);
+                            let mut paths = client.fs.paths().await;
+                            paths.remove(0);
+                            let new_root_path = if paths.is_empty() || self.rng.gen() {
+                                Path::new("/").join(&self.next_root_dir_name(user_id))
+                            } else {
+                                paths.choose(&mut self.rng).unwrap().clone()
+                            };
+                            break ClientOperation::AddWorktreeToProject {
+                                project_root_name,
+                                new_root_path,
+                            };
+                        }
+
+                        // Add an entry to a worktree
+                        _ => {
+                            let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+                            let project_root_name = root_name_for_project(&project, cx);
+                            let is_local = project.read_with(cx, |project, _| project.is_local());
+                            let worktree = project.read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
+                                    })
+                                    .choose(&mut self.rng)
+                            });
+                            let Some(worktree) = worktree else { continue };
+                            let is_dir = self.rng.gen::<bool>();
+                            let mut full_path =
+                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
+                            full_path.push(gen_file_name(&mut self.rng));
+                            if !is_dir {
+                                full_path.set_extension("rs");
+                            }
+                            break ClientOperation::CreateWorktreeEntry {
+                                project_root_name,
+                                is_local,
+                                full_path,
+                                is_dir,
+                            };
+                        }
+                    },
+                },
+
+                // Query and mutate buffers
+                60..=95 => {
+                    let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
+                    let project_root_name = root_name_for_project(&project, cx);
+                    let is_local = project.read_with(cx, |project, _| project.is_local());
+
+                    match self.rng.gen_range(0..100_u32) {
+                        // Manipulate an existing buffer
+                        0..=70 => {
+                            let Some(buffer) = client
+                                .buffers_for_project(&project)
+                                .iter()
+                                .choose(&mut self.rng)
+                                .cloned() else { continue };
+
+                            let full_path = buffer
+                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
+
+                            match self.rng.gen_range(0..100_u32) {
+                                // Close the buffer
+                                0..=15 => {
+                                    break ClientOperation::CloseBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                    };
+                                }
+                                // Save the buffer
+                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
+                                    let detach = self.rng.gen_bool(0.3);
+                                    break ClientOperation::SaveBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                        detach,
+                                    };
+                                }
+                                // Edit the buffer
+                                30..=69 => {
+                                    let edits = buffer.read_with(cx, |buffer, _| {
+                                        buffer.get_random_edits(&mut self.rng, 3)
+                                    });
+                                    break ClientOperation::EditBuffer {
+                                        project_root_name,
+                                        is_local,
+                                        full_path,
+                                        edits,
+                                    };
+                                }
+                                // Make an LSP request
+                                _ => {
+                                    let offset = buffer.read_with(cx, |buffer, _| {
+                                        buffer.clip_offset(
+                                            self.rng.gen_range(0..=buffer.len()),
+                                            language::Bias::Left,
+                                        )
+                                    });
+                                    let detach = self.rng.gen();
+                                    break ClientOperation::RequestLspDataInBuffer {
+                                        project_root_name,
+                                        full_path,
+                                        offset,
+                                        is_local,
+                                        kind: match self.rng.gen_range(0..5_u32) {
+                                            0 => LspRequestKind::Rename,
+                                            1 => LspRequestKind::Highlights,
+                                            2 => LspRequestKind::Definition,
+                                            3 => LspRequestKind::CodeAction,
+                                            4.. => LspRequestKind::Completion,
+                                        },
+                                        detach,
+                                    };
+                                }
+                            }
+                        }
+
+                        71..=80 => {
+                            let query = self.rng.gen_range('a'..='z').to_string();
+                            let detach = self.rng.gen_bool(0.3);
+                            break ClientOperation::SearchProject {
+                                project_root_name,
+                                query,
+                                detach,
+                            };
+                        }
+
+                        // Open a buffer
+                        81.. => {
+                            let worktree = project.read_with(cx, |project, cx| {
+                                project
+                                    .worktrees(cx)
+                                    .filter(|worktree| {
+                                        let worktree = worktree.read(cx);
+                                        worktree.is_visible()
+                                            && worktree.entries(false).any(|e| e.is_file())
+                                    })
+                                    .choose(&mut self.rng)
+                            });
+                            let Some(worktree) = worktree else { continue };
+                            let full_path = worktree.read_with(cx, |worktree, _| {
+                                let entry = worktree
+                                    .entries(false)
+                                    .filter(|e| e.is_file())
+                                    .choose(&mut self.rng)
+                                    .unwrap();
+                                if entry.path.as_ref() == Path::new("") {
+                                    Path::new(worktree.root_name()).into()
+                                } else {
+                                    Path::new(worktree.root_name()).join(&entry.path)
+                                }
+                            });
+                            break ClientOperation::OpenBuffer {
+                                project_root_name,
+                                is_local,
+                                full_path,
+                            };
+                        }
+                    }
+                }
+
+                // Create a file or directory
+                96.. => {
+                    let is_dir = self.rng.gen::<bool>();
+                    let mut path = client
+                        .fs
+                        .directories()
+                        .await
+                        .choose(&mut self.rng)
+                        .unwrap()
+                        .clone();
+                    path.push(gen_file_name(&mut self.rng));
+                    if !is_dir {
+                        path.set_extension("rs");
+                    }
+                    break ClientOperation::CreateFsEntry { path, is_dir };
+                }
             }
-        }
+        };
+        self.operation_ix += 1;
+        Some(operation)
+    }
 
-        ClientOperation::RequestLspDataInBuffer {
-            project_root_name,
-            full_path,
-            offset,
-            kind,
-            detach,
-        } => {
-            log::info!(
-                "{}: request LSP {:?} for buffer {:?} in project {}{}",
-                client.username,
-                kind,
-                full_path,
-                project_root_name,
-                if detach { ", detaching" } else { ", awaiting" }
-            );
+    fn next_root_dir_name(&mut self, user_id: UserId) -> String {
+        let user_ix = self
+            .users
+            .iter()
+            .position(|user| user.user_id == user_id)
+            .unwrap();
+        let root_id = util::post_inc(&mut self.users[user_ix].next_root_id);
+        format!("dir-{user_id}-{root_id}")
+    }
 
-            let project = project_for_root_name(client, &project_root_name, cx)
-                .expect("invalid project in test operation");
-            let buffer =
-                buffer_for_full_path(&*client.buffers_for_project(&project), &full_path, cx)
-                    .expect("invalid buffer path in test operation");
-            let request = match kind {
-                LspRequestKind::Rename => cx.spawn(|mut cx| async move {
-                    project
-                        .update(&mut cx, |p, cx| p.prepare_rename(buffer, offset, cx))
-                        .await?;
-                    anyhow::Ok(())
-                }),
-                LspRequestKind::Completion => cx.spawn(|mut cx| async move {
-                    project
-                        .update(&mut cx, |p, cx| p.completions(&buffer, offset, cx))
-                        .await?;
-                    Ok(())
-                }),
-                LspRequestKind::CodeAction => cx.spawn(|mut cx| async move {
-                    project
-                        .update(&mut cx, |p, cx| p.code_actions(&buffer, offset..offset, cx))
-                        .await?;
-                    Ok(())
-                }),
-                LspRequestKind::Definition => cx.spawn(|mut cx| async move {
-                    project
-                        .update(&mut cx, |p, cx| p.definition(&buffer, offset, cx))
-                        .await?;
-                    Ok(())
-                }),
-                LspRequestKind::Highlights => cx.spawn(|mut cx| async move {
-                    project
-                        .update(&mut cx, |p, cx| p.document_highlights(&buffer, offset, cx))
-                        .await?;
-                    Ok(())
-                }),
-            };
-            if detach {
-                request.detach();
-            } else {
-                request.await?;
-            }
-        }
+    fn user(&mut self, user_id: UserId) -> &mut UserTestPlan {
+        let ix = self
+            .users
+            .iter()
+            .position(|user| user.user_id == user_id)
+            .unwrap();
+        &mut self.users[ix]
+    }
+}
 
-        ClientOperation::SearchProject {
-            project_root_name,
-            query,
-            detach,
-        } => {
-            log::info!(
-                "{}: search project {} for {:?}{}",
-                client.username,
-                project_root_name,
-                query,
-                if detach { ", detaching" } else { ", awaiting" }
-            );
-            let project = project_for_root_name(client, &project_root_name, cx)
-                .expect("invalid project in test operation");
-            let search = project.update(cx, |project, cx| {
-                project.search(SearchQuery::text(query, false, false), cx)
-            });
-            let search = cx.background().spawn(async move {
-                search
-                    .await
-                    .map_err(|err| anyhow!("search request failed: {:?}", err))
-            });
-            if detach {
-                log::info!("{}: detaching save request", client.username);
-                cx.update(|cx| search.detach_and_log_err(cx));
-            } else {
-                search.await?;
-            }
-        }
+async fn simulate_client(
+    client: Rc<TestClient>,
+    mut operation_rx: futures::channel::mpsc::UnboundedReceiver<()>,
+    plan: Arc<Mutex<TestPlan>>,
+    mut cx: TestAppContext,
+) {
+    // Setup language server
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let _fake_language_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-fake-language-server",
+            capabilities: lsp::LanguageServer::full_capabilities(),
+            initializer: Some(Box::new({
+                let plan = plan.clone();
+                let fs = client.fs.clone();
+                move |fake_server: &mut FakeLanguageServer| {
+                    fake_server.handle_request::<lsp::request::Completion, _, _>(
+                        |_, _| async move {
+                            Ok(Some(lsp::CompletionResponse::Array(vec![
+                                lsp::CompletionItem {
+                                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                                        range: lsp::Range::new(
+                                            lsp::Position::new(0, 0),
+                                            lsp::Position::new(0, 0),
+                                        ),
+                                        new_text: "the-new-text".to_string(),
+                                    })),
+                                    ..Default::default()
+                                },
+                            ])))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
+                        |_, _| async move {
+                            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                                lsp::CodeAction {
+                                    title: "the-code-action".to_string(),
+                                    ..Default::default()
+                                },
+                            )]))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+                        |params, _| async move {
+                            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                                params.position,
+                                params.position,
+                            ))))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+                        let fs = fs.clone();
+                        let plan = plan.clone();
+                        move |_, _| {
+                            let fs = fs.clone();
+                            let plan = plan.clone();
+                            async move {
+                                let files = fs.files().await;
+                                let mut plan = plan.lock();
+                                let count = plan.rng.gen_range::<usize, _>(1..3);
+                                let files = (0..count)
+                                    .map(|_| files.choose(&mut plan.rng).unwrap())
+                                    .collect::<Vec<_>>();
+                                log::info!("LSP: Returning definitions in files {:?}", &files);
+                                Ok(Some(lsp::GotoDefinitionResponse::Array(
+                                    files
+                                        .into_iter()
+                                        .map(|file| lsp::Location {
+                                            uri: lsp::Url::from_file_path(file).unwrap(),
+                                            range: Default::default(),
+                                        })
+                                        .collect(),
+                                )))
+                            }
+                        }
+                    });
 
-        ClientOperation::Other => {
-            let choice = plan.lock().rng.gen_range(0..100);
-            match choice {
-                0..=59
-                    if !client.local_projects().is_empty()
-                        || !client.remote_projects().is_empty() =>
-                {
-                    randomly_mutate_worktrees(client, &plan, cx).await?;
+                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
+                        let plan = plan.clone();
+                        move |_, _| {
+                            let mut highlights = Vec::new();
+                            let highlight_count = plan.lock().rng.gen_range(1..=5);
+                            for _ in 0..highlight_count {
+                                let start_row = plan.lock().rng.gen_range(0..100);
+                                let start_column = plan.lock().rng.gen_range(0..100);
+                                let start = PointUtf16::new(start_row, start_column);
+                                let end_row = plan.lock().rng.gen_range(0..100);
+                                let end_column = plan.lock().rng.gen_range(0..100);
+                                let end = PointUtf16::new(end_row, end_column);
+                                let range = if start > end { end..start } else { start..end };
+                                highlights.push(lsp::DocumentHighlight {
+                                    range: range_to_lsp(range.clone()),
+                                    kind: Some(lsp::DocumentHighlightKind::READ),
+                                });
+                            }
+                            highlights.sort_unstable_by_key(|highlight| {
+                                (highlight.range.start, highlight.range.end)
+                            });
+                            async move { Ok(Some(highlights)) }
+                        }
+                    });
                 }
-                _ => randomly_mutate_fs(client, &plan).await,
-            }
+            })),
+            ..Default::default()
+        }))
+        .await;
+    client.language_registry.add(Arc::new(language));
+
+    while operation_rx.next().await.is_some() {
+        let Some(operation) = plan.lock().next_client_operation(&client, &cx).await else { break };
+        if let Err(error) = apply_client_operation(&client, operation, &mut cx).await {
+            log::error!("{} error: {}", client.username, error);
         }
+        cx.background().simulate_random_delay().await;
     }
-    Ok(())
+    log::info!("{}: done", client.username);
 }
 
 fn buffer_for_full_path(