
I've observed that Zed's implementation of [Code Actions On Format](https://zed.dev/docs/configuring-zed#code-actions-on-format) uses the [CodeActionContext.only](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext) parameter to request specific code action kinds from the server. The issue is that it does not filter out code actions from the response, believing that the server will do it. The [LSP specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext) says that the client is responsible for filtering out unwanted code actions: ```js /** * Requested kind of actions to return. * * Actions not of this kind are filtered out by the client before being * shown. So servers can omit computing them. */ only?: CodeActionKind[]; ``` This PR will filter out unwanted code action on the client side. I have initially encountered this issue because the [ZLS language server](https://github.com/zigtools/zls) (until https://github.com/zigtools/zls/pull/2087) does not filter code action based on `CodeActionContext.only` so Zed runs all received code actions even if they are explicitly disabled in the `code_actions_on_format` setting. Release Notes: - Fix the `code_actions_on_format` setting when used with a language server like ZLS --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
1602 lines
66 KiB
Rust
1602 lines
66 KiB
Rust
use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
|
use crate::{db::UserId, tests::run_randomized_test};
|
|
use anyhow::{anyhow, Result};
|
|
use async_trait::async_trait;
|
|
use call::ActiveCall;
|
|
use collections::{BTreeMap, HashMap};
|
|
use editor::Bias;
|
|
use fs::{FakeFs, Fs as _};
|
|
use futures::StreamExt;
|
|
use git::repository::GitFileStatus;
|
|
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
|
use language::{
|
|
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
|
|
};
|
|
use lsp::FakeLanguageServer;
|
|
use pretty_assertions::assert_eq;
|
|
use project::{
|
|
search::SearchQuery, search::SearchResult, Project, ProjectPath, DEFAULT_COMPLETION_CONTEXT,
|
|
};
|
|
use rand::{
|
|
distributions::{Alphanumeric, DistString},
|
|
prelude::*,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
ops::{Deref, Range},
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::Arc,
|
|
};
|
|
use util::ResultExt;
|
|
|
|
#[gpui::test(
|
|
iterations = 100,
|
|
on_failure = "crate::tests::save_randomized_test_plan"
|
|
)]
|
|
async fn test_random_project_collaboration(
|
|
cx: &mut TestAppContext,
|
|
executor: BackgroundExecutor,
|
|
rng: StdRng,
|
|
) {
|
|
run_randomized_test::<ProjectCollaborationTest>(cx, executor, rng).await;
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
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,
|
|
is_local: bool,
|
|
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,
|
|
},
|
|
WriteFsEntry {
|
|
path: PathBuf,
|
|
is_dir: bool,
|
|
content: String,
|
|
},
|
|
GitOperation {
|
|
operation: GitOperation,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
enum GitOperation {
|
|
WriteGitIndex {
|
|
repo_path: PathBuf,
|
|
contents: Vec<(PathBuf, String)>,
|
|
},
|
|
WriteGitBranch {
|
|
repo_path: PathBuf,
|
|
new_branch: Option<String>,
|
|
},
|
|
WriteGitStatuses {
|
|
repo_path: PathBuf,
|
|
statuses: Vec<(PathBuf, GitFileStatus)>,
|
|
git_operation: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
enum LspRequestKind {
|
|
Rename,
|
|
Completion,
|
|
CodeAction,
|
|
Definition,
|
|
Highlights,
|
|
}
|
|
|
|
struct ProjectCollaborationTest;
|
|
|
|
#[async_trait(?Send)]
|
|
impl RandomizedTest for ProjectCollaborationTest {
|
|
type Operation = ClientOperation;
|
|
|
|
async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
|
|
let db = &server.app_state.db;
|
|
for (ix, user_a) in users.iter().enumerate() {
|
|
for user_b in &users[ix + 1..] {
|
|
db.send_contact_request(user_a.user_id, user_b.user_id)
|
|
.await
|
|
.unwrap();
|
|
db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn generate_operation(
|
|
client: &TestClient,
|
|
rng: &mut StdRng,
|
|
plan: &mut UserTestPlan,
|
|
cx: &TestAppContext,
|
|
) -> ClientOperation {
|
|
let call = cx.read(ActiveCall::global);
|
|
loop {
|
|
match 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 rng.gen_bool(0.7) {
|
|
ClientOperation::AcceptIncomingCall
|
|
} else {
|
|
ClientOperation::RejectIncomingCall
|
|
};
|
|
}
|
|
|
|
match 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(rng).unwrap();
|
|
break ClientOperation::InviteContactToCall {
|
|
user_id: UserId(contact.user.id as i32),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Leave the current call
|
|
71.. => {
|
|
if plan.allow_client_disconnection
|
|
&& call.read_with(cx, |call, _| call.room().is_some())
|
|
{
|
|
break ClientOperation::LeaveCall;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mutate projects
|
|
30..=59 => match 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_dev_server_project_ids = cx.read(|cx| {
|
|
client
|
|
.dev_server_projects()
|
|
.iter()
|
|
.map(|p| p.read(cx).remote_id().unwrap())
|
|
.collect::<Vec<_>>()
|
|
});
|
|
let new_dev_server_projects = room.read_with(cx, |room, _| {
|
|
room.remote_participants()
|
|
.values()
|
|
.flat_map(|participant| {
|
|
participant.projects.iter().filter_map(|project| {
|
|
if existing_dev_server_project_ids.contains(&project.id)
|
|
{
|
|
None
|
|
} else {
|
|
Some((
|
|
UserId::from_proto(participant.user.id),
|
|
project.worktree_root_names[0].clone(),
|
|
))
|
|
}
|
|
})
|
|
})
|
|
.collect::<Vec<_>>()
|
|
});
|
|
if !new_dev_server_projects.is_empty() {
|
|
let (host_id, first_root_name) =
|
|
new_dev_server_projects.choose(rng).unwrap().clone();
|
|
break ClientOperation::OpenRemoteProject {
|
|
host_id,
|
|
first_root_name,
|
|
};
|
|
}
|
|
}
|
|
// Open a local project
|
|
else {
|
|
let first_root_name = plan.next_root_dir_name();
|
|
break ClientOperation::OpenLocalProject { first_root_name };
|
|
}
|
|
}
|
|
|
|
// Close a remote project
|
|
71..=80 => {
|
|
if !client.dev_server_projects().is_empty() {
|
|
let project = client.dev_server_projects().choose(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 rng.gen_range(0..100_u32) {
|
|
// Add a worktree to a local project
|
|
0..=50 => {
|
|
let Some(project) = client.local_projects().choose(rng).cloned() else {
|
|
continue;
|
|
};
|
|
let project_root_name = root_name_for_project(&project, cx);
|
|
let mut paths = client.fs().paths(false);
|
|
paths.remove(0);
|
|
let new_root_path = if paths.is_empty() || rng.gen() {
|
|
Path::new("/").join(plan.next_root_dir_name())
|
|
} else {
|
|
paths.choose(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, 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, 0).any(|e| e.is_file())
|
|
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
|
})
|
|
.choose(rng)
|
|
});
|
|
let Some(worktree) = worktree else { continue };
|
|
let is_dir = rng.gen::<bool>();
|
|
let mut full_path =
|
|
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
|
|
full_path.push(gen_file_name(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..=90 => {
|
|
let Some(project) = choose_random_project(client, 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 rng.gen_range(0..100_u32) {
|
|
// Manipulate an existing buffer
|
|
0..=70 => {
|
|
let Some(buffer) = client
|
|
.buffers_for_project(&project)
|
|
.iter()
|
|
.choose(rng)
|
|
.cloned()
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let full_path = buffer
|
|
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
|
|
|
|
match 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 = 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(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(
|
|
rng.gen_range(0..=buffer.len()),
|
|
language::Bias::Left,
|
|
)
|
|
});
|
|
let detach = rng.gen();
|
|
break ClientOperation::RequestLspDataInBuffer {
|
|
project_root_name,
|
|
full_path,
|
|
offset,
|
|
is_local,
|
|
kind: match 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 = rng.gen_range('a'..='z').to_string();
|
|
let detach = rng.gen_bool(0.3);
|
|
break ClientOperation::SearchProject {
|
|
project_root_name,
|
|
is_local,
|
|
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, 0).any(|e| e.is_file())
|
|
})
|
|
.choose(rng)
|
|
});
|
|
let Some(worktree) = worktree else { continue };
|
|
let full_path = worktree.read_with(cx, |worktree, _| {
|
|
let entry = worktree
|
|
.entries(false, 0)
|
|
.filter(|e| e.is_file())
|
|
.choose(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,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update a git related action
|
|
91..=95 => {
|
|
break ClientOperation::GitOperation {
|
|
operation: generate_git_operation(rng, client),
|
|
};
|
|
}
|
|
|
|
// Create or update a file or directory
|
|
96.. => {
|
|
let is_dir = rng.gen::<bool>();
|
|
let content;
|
|
let mut path;
|
|
let dir_paths = client.fs().directories(false);
|
|
|
|
if is_dir {
|
|
content = String::new();
|
|
path = dir_paths.choose(rng).unwrap().clone();
|
|
path.push(gen_file_name(rng));
|
|
} else {
|
|
content = Alphanumeric.sample_string(rng, 16);
|
|
|
|
// Create a new file or overwrite an existing file
|
|
let file_paths = client.fs().files();
|
|
if file_paths.is_empty() || rng.gen_bool(0.5) {
|
|
path = dir_paths.choose(rng).unwrap().clone();
|
|
path.push(gen_file_name(rng));
|
|
path.set_extension("rs");
|
|
} else {
|
|
path = file_paths.choose(rng).unwrap().clone()
|
|
};
|
|
}
|
|
break ClientOperation::WriteFsEntry {
|
|
path,
|
|
is_dir,
|
|
content,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn apply_operation(
|
|
client: &TestClient,
|
|
operation: ClientOperation,
|
|
cx: &mut TestAppContext,
|
|
) -> Result<(), TestError> {
|
|
match operation {
|
|
ClientOperation::AcceptIncomingCall => {
|
|
let active_call = cx.read(ActiveCall::global);
|
|
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
|
|
Err(TestError::Inapplicable)?;
|
|
}
|
|
|
|
log::info!("{}: accepting incoming call", client.username);
|
|
active_call
|
|
.update(cx, |call, cx| call.accept_incoming(cx))
|
|
.await?;
|
|
}
|
|
|
|
ClientOperation::RejectIncomingCall => {
|
|
let active_call = cx.read(ActiveCall::global);
|
|
if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
|
|
Err(TestError::Inapplicable)?;
|
|
}
|
|
|
|
log::info!("{}: declining incoming call", client.username);
|
|
active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
|
|
}
|
|
|
|
ClientOperation::LeaveCall => {
|
|
let active_call = cx.read(ActiveCall::global);
|
|
if active_call.read_with(cx, |call, _| call.room().is_none()) {
|
|
Err(TestError::Inapplicable)?;
|
|
}
|
|
|
|
log::info!("{}: hanging up", client.username);
|
|
active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
|
|
}
|
|
|
|
ClientOperation::InviteContactToCall { user_id } => {
|
|
let active_call = cx.read(ActiveCall::global);
|
|
|
|
log::info!("{}: inviting {}", client.username, user_id,);
|
|
active_call
|
|
.update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
|
|
.await
|
|
.log_err();
|
|
}
|
|
|
|
ClientOperation::OpenLocalProject { first_root_name } => {
|
|
log::info!(
|
|
"{}: opening local project at {:?}",
|
|
client.username,
|
|
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());
|
|
}
|
|
|
|
ClientOperation::AddWorktreeToProject {
|
|
project_root_name,
|
|
new_root_path,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: finding/creating local worktree at {:?} to project with root path {}",
|
|
client.username,
|
|
new_root_path,
|
|
project_root_name
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
if !client.fs().paths(false).contains(&new_root_path) {
|
|
client.fs().create_dir(&new_root_path).await.unwrap();
|
|
}
|
|
project
|
|
.update(cx, |project, cx| {
|
|
project.find_or_create_worktree(&new_root_path, true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
ClientOperation::CloseRemoteProject { project_root_name } => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: closing remote project with root path {}",
|
|
client.username,
|
|
project_root_name,
|
|
);
|
|
|
|
let ix = client
|
|
.dev_server_projects()
|
|
.iter()
|
|
.position(|p| p == &project)
|
|
.unwrap();
|
|
cx.update(|_| {
|
|
client.dev_server_projects_mut().remove(ix);
|
|
client.buffers().retain(|p, _| *p != project);
|
|
drop(project);
|
|
});
|
|
}
|
|
|
|
ClientOperation::OpenRemoteProject {
|
|
host_id,
|
|
first_root_name,
|
|
} => {
|
|
let active_call = cx.read(ActiveCall::global);
|
|
let project = active_call
|
|
.update(cx, |call, cx| {
|
|
let room = call.room().cloned()?;
|
|
let participant = room
|
|
.read(cx)
|
|
.remote_participants()
|
|
.get(&host_id.to_proto())?;
|
|
let project_id = participant
|
|
.projects
|
|
.iter()
|
|
.find(|project| project.worktree_root_names[0] == first_root_name)?
|
|
.id;
|
|
Some(room.update(cx, |room, cx| {
|
|
room.join_project(
|
|
project_id,
|
|
client.language_registry().clone(),
|
|
FakeFs::new(cx.background_executor().clone()),
|
|
cx,
|
|
)
|
|
}))
|
|
})
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: joining remote project of user {}, root name {}",
|
|
client.username,
|
|
host_id,
|
|
first_root_name,
|
|
);
|
|
|
|
let project = project.await?;
|
|
client.dev_server_projects_mut().push(project.clone());
|
|
}
|
|
|
|
ClientOperation::CreateWorktreeEntry {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
is_dir,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let project_path = project_path_for_full_path(&project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
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,
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
project
|
|
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
|
.await?;
|
|
}
|
|
|
|
ClientOperation::OpenBuffer {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let project_path = project_path_for_full_path(&project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: opening buffer {:?} in {} project {}",
|
|
client.username,
|
|
full_path,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name,
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
let buffer = project
|
|
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
|
.await?;
|
|
client.buffers_for_project(&project).insert(buffer);
|
|
}
|
|
|
|
ClientOperation::EditBuffer {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
edits,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: editing buffer {:?} in {} project {} with {:?}",
|
|
client.username,
|
|
full_path,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name,
|
|
edits
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
buffer.update(cx, |buffer, cx| {
|
|
let snapshot = buffer.snapshot();
|
|
buffer.edit(
|
|
edits.into_iter().map(|(range, text)| {
|
|
let start = snapshot.clip_offset(range.start, Bias::Left);
|
|
let end = snapshot.clip_offset(range.end, Bias::Right);
|
|
(start..end, text)
|
|
}),
|
|
None,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
|
|
ClientOperation::CloseBuffer {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: closing buffer {:?} in {} project {}",
|
|
client.username,
|
|
full_path,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
cx.update(|_| {
|
|
client.buffers_for_project(&project).remove(&buffer);
|
|
drop(buffer);
|
|
});
|
|
}
|
|
|
|
ClientOperation::SaveBuffer {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
detach,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: saving buffer {:?} in {} project {}, {}",
|
|
client.username,
|
|
full_path,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name,
|
|
if detach { "detaching" } else { "awaiting" }
|
|
);
|
|
|
|
ensure_project_shared(&project, client, cx).await;
|
|
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
|
|
let save =
|
|
project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
|
|
let save = cx.spawn(|cx| async move {
|
|
save.await
|
|
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
|
|
assert!(buffer
|
|
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
|
|
.expect("App should not be dropped")
|
|
.observed_all(&requested_version));
|
|
anyhow::Ok(())
|
|
});
|
|
if detach {
|
|
cx.update(|cx| save.detach_and_log_err(cx));
|
|
} else {
|
|
save.await?;
|
|
}
|
|
}
|
|
|
|
ClientOperation::RequestLspDataInBuffer {
|
|
project_root_name,
|
|
is_local,
|
|
full_path,
|
|
offset,
|
|
kind,
|
|
detach,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
let buffer = buffer_for_full_path(client, &project, &full_path, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
|
|
client.username,
|
|
kind,
|
|
full_path,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name,
|
|
if detach { "detaching" } else { "awaiting" }
|
|
);
|
|
|
|
use futures::{FutureExt as _, TryFutureExt as _};
|
|
let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
|
|
|
|
let process_lsp_request = project.update(cx, |project, cx| match kind {
|
|
LspRequestKind::Rename => project
|
|
.prepare_rename(buffer, offset, cx)
|
|
.map_ok(|_| ())
|
|
.boxed(),
|
|
LspRequestKind::Completion => project
|
|
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
|
|
.map_ok(|_| ())
|
|
.boxed(),
|
|
LspRequestKind::CodeAction => project
|
|
.code_actions(&buffer, offset..offset, None, cx)
|
|
.map(|_| Ok(()))
|
|
.boxed(),
|
|
LspRequestKind::Definition => project
|
|
.definition(&buffer, offset, cx)
|
|
.map_ok(|_| ())
|
|
.boxed(),
|
|
LspRequestKind::Highlights => project
|
|
.document_highlights(&buffer, offset, cx)
|
|
.map_ok(|_| ())
|
|
.boxed(),
|
|
});
|
|
let request = cx.foreground_executor().spawn(process_lsp_request);
|
|
if detach {
|
|
request.detach();
|
|
} else {
|
|
request.await?;
|
|
}
|
|
}
|
|
|
|
ClientOperation::SearchProject {
|
|
project_root_name,
|
|
is_local,
|
|
query,
|
|
detach,
|
|
} => {
|
|
let project = project_for_root_name(client, &project_root_name, cx)
|
|
.ok_or(TestError::Inapplicable)?;
|
|
|
|
log::info!(
|
|
"{}: search {} project {} for {:?}, {}",
|
|
client.username,
|
|
if is_local { "local" } else { "remote" },
|
|
project_root_name,
|
|
query,
|
|
if detach { "detaching" } else { "awaiting" }
|
|
);
|
|
|
|
let mut search = project.update(cx, |project, cx| {
|
|
project.search(
|
|
SearchQuery::text(
|
|
query,
|
|
false,
|
|
false,
|
|
false,
|
|
Default::default(),
|
|
Default::default(),
|
|
None,
|
|
)
|
|
.unwrap(),
|
|
cx,
|
|
)
|
|
});
|
|
drop(project);
|
|
let search = cx.executor().spawn(async move {
|
|
let mut results = HashMap::default();
|
|
while let Some(result) = search.next().await {
|
|
if let SearchResult::Buffer { buffer, ranges } = result {
|
|
results.entry(buffer).or_insert(ranges);
|
|
}
|
|
}
|
|
results
|
|
});
|
|
search.await;
|
|
}
|
|
|
|
ClientOperation::WriteFsEntry {
|
|
path,
|
|
is_dir,
|
|
content,
|
|
} => {
|
|
if !client
|
|
.fs()
|
|
.directories(false)
|
|
.contains(&path.parent().unwrap().to_owned())
|
|
{
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
|
|
if is_dir {
|
|
log::info!("{}: creating dir at {:?}", client.username, path);
|
|
client.fs().create_dir(&path).await.unwrap();
|
|
} else {
|
|
let exists = client.fs().metadata(&path).await?.is_some();
|
|
let verb = if exists { "updating" } else { "creating" };
|
|
log::info!("{}: {} file at {:?}", verb, client.username, path);
|
|
|
|
client
|
|
.fs()
|
|
.save(&path, &content.as_str().into(), text::LineEnding::Unix)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
ClientOperation::GitOperation { operation } => match operation {
|
|
GitOperation::WriteGitIndex {
|
|
repo_path,
|
|
contents,
|
|
} => {
|
|
if !client.fs().directories(false).contains(&repo_path) {
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
|
|
for (path, _) in contents.iter() {
|
|
if !client.fs().files().contains(&repo_path.join(path)) {
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
}
|
|
|
|
log::info!(
|
|
"{}: writing git index for repo {:?}: {:?}",
|
|
client.username,
|
|
repo_path,
|
|
contents
|
|
);
|
|
|
|
let dot_git_dir = repo_path.join(".git");
|
|
let contents = contents
|
|
.iter()
|
|
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
|
.collect::<Vec<_>>();
|
|
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
|
client.fs().create_dir(&dot_git_dir).await?;
|
|
}
|
|
client.fs().set_index_for_repo(&dot_git_dir, &contents);
|
|
}
|
|
GitOperation::WriteGitBranch {
|
|
repo_path,
|
|
new_branch,
|
|
} => {
|
|
if !client.fs().directories(false).contains(&repo_path) {
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
|
|
log::info!(
|
|
"{}: writing git branch for repo {:?}: {:?}",
|
|
client.username,
|
|
repo_path,
|
|
new_branch
|
|
);
|
|
|
|
let dot_git_dir = repo_path.join(".git");
|
|
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
|
client.fs().create_dir(&dot_git_dir).await?;
|
|
}
|
|
client
|
|
.fs()
|
|
.set_branch_name(&dot_git_dir, new_branch.clone());
|
|
}
|
|
GitOperation::WriteGitStatuses {
|
|
repo_path,
|
|
statuses,
|
|
git_operation,
|
|
} => {
|
|
if !client.fs().directories(false).contains(&repo_path) {
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
for (path, _) in statuses.iter() {
|
|
if !client.fs().files().contains(&repo_path.join(path)) {
|
|
return Err(TestError::Inapplicable);
|
|
}
|
|
}
|
|
|
|
log::info!(
|
|
"{}: writing git statuses for repo {:?}: {:?}",
|
|
client.username,
|
|
repo_path,
|
|
statuses
|
|
);
|
|
|
|
let dot_git_dir = repo_path.join(".git");
|
|
|
|
let statuses = statuses
|
|
.iter()
|
|
.map(|(path, val)| (path.as_path(), *val))
|
|
.collect::<Vec<_>>();
|
|
|
|
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
|
client.fs().create_dir(&dot_git_dir).await?;
|
|
}
|
|
|
|
if git_operation {
|
|
client.fs().set_status_for_repo_via_git_operation(
|
|
&dot_git_dir,
|
|
statuses.as_slice(),
|
|
);
|
|
} else {
|
|
client.fs().set_status_for_repo_via_working_copy_change(
|
|
&dot_git_dir,
|
|
statuses.as_slice(),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
|
|
client.language_registry().add(Arc::new(Language::new(
|
|
LanguageConfig {
|
|
name: "Rust".into(),
|
|
matcher: LanguageMatcher {
|
|
path_suffixes: vec!["rs".to_string()],
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)));
|
|
client.language_registry().register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
name: "the-fake-language-server",
|
|
capabilities: lsp::LanguageServer::full_capabilities(),
|
|
initializer: Some(Box::new({
|
|
let fs = client.app_state.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();
|
|
move |_, cx| {
|
|
let background = cx.background_executor();
|
|
let mut rng = background.rng();
|
|
let count = rng.gen_range::<usize, _>(1..3);
|
|
let files = fs.as_fake().files();
|
|
let files = (0..count)
|
|
.map(|_| files.choose(&mut rng).unwrap().clone())
|
|
.collect::<Vec<_>>();
|
|
async move {
|
|
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, _, _>(
|
|
move |_, cx| {
|
|
let mut highlights = Vec::new();
|
|
let background = cx.background_executor();
|
|
let mut rng = background.rng();
|
|
|
|
let highlight_count = rng.gen_range(1..=5);
|
|
for _ in 0..highlight_count {
|
|
let start_row = rng.gen_range(0..100);
|
|
let start_column = rng.gen_range(0..100);
|
|
let end_row = rng.gen_range(0..100);
|
|
let end_column = rng.gen_range(0..100);
|
|
let start = PointUtf16::new(start_row, start_column);
|
|
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()
|
|
},
|
|
);
|
|
}
|
|
|
|
async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
|
|
for (client, client_cx) in clients.iter() {
|
|
for guest_project in client.dev_server_projects().iter() {
|
|
guest_project.read_with(client_cx, |guest_project, cx| {
|
|
let host_project = clients.iter().find_map(|(client, cx)| {
|
|
let project = client
|
|
.local_projects()
|
|
.iter()
|
|
.find(|host_project| {
|
|
host_project.read_with(cx, |host_project, _| {
|
|
host_project.remote_id() == guest_project.remote_id()
|
|
})
|
|
})?
|
|
.clone();
|
|
Some((project, cx))
|
|
});
|
|
|
|
if !guest_project.is_disconnected(cx) {
|
|
if let Some((host_project, host_cx)) = host_project {
|
|
let host_worktree_snapshots =
|
|
host_project.read_with(host_cx, |host_project, cx| {
|
|
host_project
|
|
.worktrees(cx)
|
|
.map(|worktree| {
|
|
let worktree = worktree.read(cx);
|
|
(worktree.id(), worktree.snapshot())
|
|
})
|
|
.collect::<BTreeMap<_, _>>()
|
|
});
|
|
let guest_worktree_snapshots = guest_project
|
|
.worktrees(cx)
|
|
.map(|worktree| {
|
|
let worktree = worktree.read(cx);
|
|
(worktree.id(), worktree.snapshot())
|
|
})
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
assert_eq!(
|
|
guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
|
|
host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
|
|
"{} has different worktrees than the host for project {:?}",
|
|
client.username, guest_project.remote_id(),
|
|
);
|
|
|
|
for (id, host_snapshot) in &host_worktree_snapshots {
|
|
let guest_snapshot = &guest_worktree_snapshots[id];
|
|
assert_eq!(
|
|
guest_snapshot.root_name(),
|
|
host_snapshot.root_name(),
|
|
"{} has different root name than the host for worktree {}, project {:?}",
|
|
client.username,
|
|
id,
|
|
guest_project.remote_id(),
|
|
);
|
|
assert_eq!(
|
|
guest_snapshot.abs_path(),
|
|
host_snapshot.abs_path(),
|
|
"{} has different abs path than the host for worktree {}, project: {:?}",
|
|
client.username,
|
|
id,
|
|
guest_project.remote_id(),
|
|
);
|
|
assert_eq!(
|
|
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
|
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
|
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
|
client.username,
|
|
host_snapshot.abs_path(),
|
|
id,
|
|
guest_project.remote_id(),
|
|
);
|
|
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
|
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
|
client.username,
|
|
host_snapshot.abs_path(),
|
|
guest_project.remote_id(),
|
|
);
|
|
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
|
|
"{} has different scan id than the host for worktree {:?} and project {:?}",
|
|
client.username,
|
|
host_snapshot.abs_path(),
|
|
guest_project.remote_id(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
for buffer in guest_project.opened_buffers(cx) {
|
|
let buffer = buffer.read(cx);
|
|
assert_eq!(
|
|
buffer.deferred_ops_len(),
|
|
0,
|
|
"{} has deferred operations for buffer {:?} in project {:?}",
|
|
client.username,
|
|
buffer.file().unwrap().full_path(cx),
|
|
guest_project.remote_id(),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
let buffers = client.buffers().clone();
|
|
for (guest_project, guest_buffers) in &buffers {
|
|
let project_id = if guest_project.read_with(client_cx, |project, cx| {
|
|
project.is_local() || project.is_disconnected(cx)
|
|
}) {
|
|
continue;
|
|
} else {
|
|
guest_project
|
|
.read_with(client_cx, |project, _| project.remote_id())
|
|
.unwrap()
|
|
};
|
|
let guest_user_id = client.user_id().unwrap();
|
|
|
|
let host_project = clients.iter().find_map(|(client, cx)| {
|
|
let project = client
|
|
.local_projects()
|
|
.iter()
|
|
.find(|host_project| {
|
|
host_project.read_with(cx, |host_project, _| {
|
|
host_project.remote_id() == Some(project_id)
|
|
})
|
|
})?
|
|
.clone();
|
|
Some((client.user_id().unwrap(), project, cx))
|
|
});
|
|
|
|
let (host_user_id, host_project, host_cx) =
|
|
if let Some((host_user_id, host_project, host_cx)) = host_project {
|
|
(host_user_id, host_project, host_cx)
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
for guest_buffer in guest_buffers {
|
|
let buffer_id =
|
|
guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
|
|
let host_buffer = host_project.read_with(host_cx, |project, cx| {
|
|
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
|
|
panic!(
|
|
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
|
|
client.username,
|
|
client.peer_id(),
|
|
buffer_id
|
|
)
|
|
})
|
|
});
|
|
let path = host_buffer
|
|
.read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
|
|
|
|
assert_eq!(
|
|
guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
|
|
0,
|
|
"{}, buffer {}, path {:?} has deferred operations",
|
|
client.username,
|
|
buffer_id,
|
|
path,
|
|
);
|
|
assert_eq!(
|
|
guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
|
|
host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
|
|
"{}, buffer {}, path {:?}, differs from the host's buffer",
|
|
client.username,
|
|
buffer_id,
|
|
path
|
|
);
|
|
|
|
let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
|
|
let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
|
|
match (host_file, guest_file) {
|
|
(Some(host_file), Some(guest_file)) => {
|
|
assert_eq!(guest_file.path(), host_file.path());
|
|
assert_eq!(guest_file.disk_state(), host_file.disk_state(),
|
|
"guest {} disk_state does not match host {} for path {:?} in project {}",
|
|
guest_user_id,
|
|
host_user_id,
|
|
guest_file.path(),
|
|
project_id,
|
|
);
|
|
}
|
|
(None, None) => {}
|
|
(None, _) => panic!("host's file is None, guest's isn't"),
|
|
(_, None) => panic!("guest's file is None, hosts's isn't"),
|
|
}
|
|
|
|
let host_diff_base = host_buffer
|
|
.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
|
|
let guest_diff_base = guest_buffer
|
|
.read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
|
|
assert_eq!(
|
|
guest_diff_base, host_diff_base,
|
|
"guest {} diff base does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
|
|
let host_saved_version =
|
|
host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
|
|
let guest_saved_version =
|
|
guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
|
|
assert_eq!(
|
|
guest_saved_version, host_saved_version,
|
|
"guest {} saved version does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
|
|
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
|
|
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
|
|
assert_eq!(
|
|
guest_is_dirty, host_is_dirty,
|
|
"guest {} dirty state does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
|
|
let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
|
|
let guest_saved_mtime =
|
|
guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
|
|
assert_eq!(
|
|
guest_saved_mtime, host_saved_mtime,
|
|
"guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
|
|
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
|
|
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
|
|
assert_eq!(guest_is_dirty, host_is_dirty,
|
|
"guest {} dirty status does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
|
|
let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
|
|
let guest_has_conflict =
|
|
guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
|
|
assert_eq!(guest_has_conflict, host_has_conflict,
|
|
"guest {} conflict status does not match host's for path {path:?} in project {project_id}",
|
|
client.username
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
|
|
fn generate_file_paths(
|
|
repo_path: &Path,
|
|
rng: &mut StdRng,
|
|
client: &TestClient,
|
|
) -> Vec<PathBuf> {
|
|
let mut paths = client
|
|
.fs()
|
|
.files()
|
|
.into_iter()
|
|
.filter(|path| path.starts_with(repo_path))
|
|
.collect::<Vec<_>>();
|
|
|
|
let count = rng.gen_range(0..=paths.len());
|
|
paths.shuffle(rng);
|
|
paths.truncate(count);
|
|
|
|
paths
|
|
.iter()
|
|
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
|
|
|
|
match rng.gen_range(0..100_u32) {
|
|
0..=25 => {
|
|
let file_paths = generate_file_paths(&repo_path, rng, client);
|
|
|
|
let contents = file_paths
|
|
.into_iter()
|
|
.map(|path| (path, Alphanumeric.sample_string(rng, 16)))
|
|
.collect();
|
|
|
|
GitOperation::WriteGitIndex {
|
|
repo_path,
|
|
contents,
|
|
}
|
|
}
|
|
26..=63 => {
|
|
let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
|
|
|
|
GitOperation::WriteGitBranch {
|
|
repo_path,
|
|
new_branch,
|
|
}
|
|
}
|
|
64..=100 => {
|
|
let file_paths = generate_file_paths(&repo_path, rng, client);
|
|
|
|
let statuses = file_paths
|
|
.into_iter()
|
|
.map(|paths| {
|
|
(
|
|
paths,
|
|
match rng.gen_range(0..3_u32) {
|
|
0 => GitFileStatus::Added,
|
|
1 => GitFileStatus::Modified,
|
|
2 => GitFileStatus::Conflict,
|
|
_ => unreachable!(),
|
|
},
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let git_operation = rng.gen::<bool>();
|
|
|
|
GitOperation::WriteGitStatuses {
|
|
repo_path,
|
|
statuses,
|
|
git_operation,
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn buffer_for_full_path(
|
|
client: &TestClient,
|
|
project: &Model<Project>,
|
|
full_path: &PathBuf,
|
|
cx: &TestAppContext,
|
|
) -> Option<Model<language::Buffer>> {
|
|
client
|
|
.buffers_for_project(project)
|
|
.iter()
|
|
.find(|buffer| {
|
|
buffer.read_with(cx, |buffer, cx| {
|
|
buffer.file().unwrap().full_path(cx) == *full_path
|
|
})
|
|
})
|
|
.cloned()
|
|
}
|
|
|
|
fn project_for_root_name(
|
|
client: &TestClient,
|
|
root_name: &str,
|
|
cx: &TestAppContext,
|
|
) -> Option<Model<Project>> {
|
|
if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
|
|
return Some(client.local_projects()[ix].clone());
|
|
}
|
|
if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
|
|
{
|
|
return Some(client.dev_server_projects()[ix].clone());
|
|
}
|
|
None
|
|
}
|
|
|
|
fn project_ix_for_root_name(
|
|
projects: &[Model<Project>],
|
|
root_name: &str,
|
|
cx: &TestAppContext,
|
|
) -> Option<usize> {
|
|
projects.iter().position(|project| {
|
|
project.read_with(cx, |project, cx| {
|
|
let worktree = project.visible_worktrees(cx).next().unwrap();
|
|
worktree.read(cx).root_name() == root_name
|
|
})
|
|
})
|
|
}
|
|
|
|
fn root_name_for_project(project: &Model<Project>, cx: &TestAppContext) -> String {
|
|
project.read_with(cx, |project, cx| {
|
|
project
|
|
.visible_worktrees(cx)
|
|
.next()
|
|
.unwrap()
|
|
.read(cx)
|
|
.root_name()
|
|
.to_string()
|
|
})
|
|
}
|
|
|
|
fn project_path_for_full_path(
|
|
project: &Model<Project>,
|
|
full_path: &Path,
|
|
cx: &TestAppContext,
|
|
) -> Option<ProjectPath> {
|
|
let mut components = full_path.components();
|
|
let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
|
|
let path = components.as_path().into();
|
|
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
|
|
}
|
|
})
|
|
})?;
|
|
Some(ProjectPath { worktree_id, path })
|
|
}
|
|
|
|
async fn ensure_project_shared(
|
|
project: &Model<Project>,
|
|
client: &TestClient,
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
let first_root_name = root_name_for_project(project, cx);
|
|
let active_call = cx.read(ActiveCall::global);
|
|
if active_call.read_with(cx, |call, _| call.room().is_some())
|
|
&& project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
|
|
{
|
|
match active_call
|
|
.update(cx, |call, cx| call.share_project(project.clone(), cx))
|
|
.await
|
|
{
|
|
Ok(project_id) => {
|
|
log::info!(
|
|
"{}: shared project {} with id {}",
|
|
client.username,
|
|
first_root_name,
|
|
project_id
|
|
);
|
|
}
|
|
Err(error) => {
|
|
log::error!(
|
|
"{}: error sharing project {}: {:?}",
|
|
client.username,
|
|
first_root_name,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<Project>> {
|
|
client
|
|
.local_projects()
|
|
.deref()
|
|
.iter()
|
|
.chain(client.dev_server_projects().iter())
|
|
.choose(rng)
|
|
.cloned()
|
|
}
|
|
|
|
fn gen_file_name(rng: &mut StdRng) -> String {
|
|
let mut name = String::new();
|
|
for _ in 0..10 {
|
|
let letter = rng.gen_range('a'..='z');
|
|
name.push(letter);
|
|
}
|
|
name
|
|
}
|