1use crate::{
2 db::{tests::TestDb, UserId},
3 rpc::{Executor, Server, Store},
4 AppState,
5};
6use ::rpc::Peer;
7use anyhow::anyhow;
8use client::{
9 self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
10 Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
11};
12use collections::{BTreeMap, HashMap, HashSet};
13use editor::{
14 self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
15 ToOffset, ToggleCodeActions, Undo,
16};
17use futures::{channel::mpsc, Future, StreamExt as _};
18use gpui::{
19 executor::{self, Deterministic},
20 geometry::vector::vec2f,
21 ModelHandle, Task, TestAppContext, ViewHandle,
22};
23use language::{
24 range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
25 LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
26};
27use lsp::{self, FakeLanguageServer};
28use parking_lot::Mutex;
29use project::{
30 fs::{FakeFs, Fs as _},
31 search::SearchQuery,
32 worktree::WorktreeHandle,
33 DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
34};
35use rand::prelude::*;
36use rpc::PeerId;
37use serde_json::json;
38use settings::Settings;
39use sqlx::types::time::OffsetDateTime;
40use std::{
41 cell::RefCell,
42 env,
43 ops::Deref,
44 path::{Path, PathBuf},
45 rc::Rc,
46 sync::{
47 atomic::{AtomicBool, Ordering::SeqCst},
48 Arc,
49 },
50 time::Duration,
51};
52use theme::ThemeRegistry;
53use tokio::sync::RwLockReadGuard;
54use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
55
56#[ctor::ctor]
57fn init_logger() {
58 if std::env::var("RUST_LOG").is_ok() {
59 env_logger::init();
60 }
61}
62
63#[gpui::test(iterations = 10)]
64async fn test_share_project(
65 deterministic: Arc<Deterministic>,
66 cx_a: &mut TestAppContext,
67 cx_b: &mut TestAppContext,
68 cx_b2: &mut TestAppContext,
69) {
70 cx_a.foreground().forbid_parking();
71 let (window_b, _) = cx_b.add_window(|_| EmptyView);
72 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
73 let mut client_a = server.create_client(cx_a, "user_a").await;
74 let mut client_b = server.create_client(cx_b, "user_b").await;
75 server
76 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
77 .await;
78
79 let fs = FakeFs::new(cx_a.background());
80 fs.insert_tree(
81 "/a",
82 json!({
83 ".gitignore": "ignored-dir",
84 "a.txt": "a-contents",
85 "b.txt": "b-contents",
86 "ignored-dir": {
87 "c.txt": "",
88 "d.txt": "",
89 }
90 }),
91 )
92 .await;
93
94 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
95 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
96
97 // Join that project as client B
98 let client_b_peer_id = client_b.peer_id;
99 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
100 let replica_id_b = project_b.read_with(cx_b, |project, _| {
101 assert_eq!(
102 project
103 .collaborators()
104 .get(&client_a.peer_id)
105 .unwrap()
106 .user
107 .github_login,
108 "user_a"
109 );
110 project.replica_id()
111 });
112
113 deterministic.run_until_parked();
114 project_a.read_with(cx_a, |project, _| {
115 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
116 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
117 assert_eq!(client_b_collaborator.user.github_login, "user_b");
118 });
119 project_b.read_with(cx_b, |project, cx| {
120 let worktree = project.worktrees(cx).next().unwrap().read(cx);
121 assert_eq!(
122 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
123 [
124 Path::new(".gitignore"),
125 Path::new("a.txt"),
126 Path::new("b.txt"),
127 Path::new("ignored-dir"),
128 Path::new("ignored-dir/c.txt"),
129 Path::new("ignored-dir/d.txt"),
130 ]
131 );
132 });
133
134 // Open the same file as client B and client A.
135 let buffer_b = project_b
136 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
137 .await
138 .unwrap();
139 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
140 project_a.read_with(cx_a, |project, cx| {
141 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
142 });
143 let buffer_a = project_a
144 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
145 .await
146 .unwrap();
147
148 let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
149
150 // TODO
151 // // Create a selection set as client B and see that selection set as client A.
152 // buffer_a
153 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
154 // .await;
155
156 // Edit the buffer as client B and see that edit as client A.
157 editor_b.update(cx_b, |editor, cx| {
158 editor.handle_input(&Input("ok, ".into()), cx)
159 });
160 buffer_a
161 .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
162 .await;
163
164 // TODO
165 // // Remove the selection set as client B, see those selections disappear as client A.
166 cx_b.update(move |_| drop(editor_b));
167 // buffer_a
168 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
169 // .await;
170
171 // Client B can join again on a different window because they are already a participant.
172 let client_b2 = server.create_client(cx_b2, "user_b").await;
173 let project_b2 = Project::remote(
174 project_id,
175 client_b2.client.clone(),
176 client_b2.user_store.clone(),
177 client_b2.project_store.clone(),
178 client_b2.language_registry.clone(),
179 FakeFs::new(cx_b2.background()),
180 cx_b2.to_async(),
181 )
182 .await
183 .unwrap();
184 deterministic.run_until_parked();
185 project_a.read_with(cx_a, |project, _| {
186 assert_eq!(project.collaborators().len(), 2);
187 });
188 project_b.read_with(cx_b, |project, _| {
189 assert_eq!(project.collaborators().len(), 2);
190 });
191 project_b2.read_with(cx_b2, |project, _| {
192 assert_eq!(project.collaborators().len(), 2);
193 });
194
195 // Dropping client B's first project removes only that from client A's collaborators.
196 cx_b.update(move |_| {
197 drop(client_b.project.take());
198 drop(project_b);
199 });
200 deterministic.run_until_parked();
201 project_a.read_with(cx_a, |project, _| {
202 assert_eq!(project.collaborators().len(), 1);
203 });
204 project_b2.read_with(cx_b2, |project, _| {
205 assert_eq!(project.collaborators().len(), 1);
206 });
207}
208
209#[gpui::test(iterations = 10)]
210async fn test_unshare_project(
211 deterministic: Arc<Deterministic>,
212 cx_a: &mut TestAppContext,
213 cx_b: &mut TestAppContext,
214) {
215 cx_a.foreground().forbid_parking();
216 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
217 let mut client_a = server.create_client(cx_a, "user_a").await;
218 let mut client_b = server.create_client(cx_b, "user_b").await;
219 server
220 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
221 .await;
222
223 let fs = FakeFs::new(cx_a.background());
224 fs.insert_tree(
225 "/a",
226 json!({
227 "a.txt": "a-contents",
228 "b.txt": "b-contents",
229 }),
230 )
231 .await;
232
233 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
234 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
235 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
236 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
237
238 project_b
239 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
240 .await
241 .unwrap();
242
243 // When client B leaves the project, it gets automatically unshared.
244 cx_b.update(|_| {
245 drop(client_b.project.take());
246 drop(project_b);
247 });
248 deterministic.run_until_parked();
249 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
250
251 // When client B joins again, the project gets re-shared.
252 let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
253 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
254 project_b2
255 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
256 .await
257 .unwrap();
258
259 // When client A (the host) leaves, the project gets unshared and guests are notified.
260 cx_a.update(|_| {
261 drop(project_a);
262 client_a.project.take();
263 });
264 deterministic.run_until_parked();
265 project_b2.read_with(cx_b, |project, _| {
266 assert!(project.is_read_only());
267 assert!(project.collaborators().is_empty());
268 });
269}
270
271#[gpui::test(iterations = 10)]
272async fn test_host_disconnect(
273 deterministic: Arc<Deterministic>,
274 cx_a: &mut TestAppContext,
275 cx_b: &mut TestAppContext,
276 cx_c: &mut TestAppContext,
277) {
278 cx_a.foreground().forbid_parking();
279 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
280 let mut client_a = server.create_client(cx_a, "user_a").await;
281 let mut client_b = server.create_client(cx_b, "user_b").await;
282 let client_c = server.create_client(cx_c, "user_c").await;
283 server
284 .make_contacts(vec![
285 (&client_a, cx_a),
286 (&client_b, cx_b),
287 (&client_c, cx_c),
288 ])
289 .await;
290
291 let fs = FakeFs::new(cx_a.background());
292 fs.insert_tree(
293 "/a",
294 json!({
295 "a.txt": "a-contents",
296 "b.txt": "b-contents",
297 }),
298 )
299 .await;
300
301 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
302 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
303 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
304
305 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
306 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
307
308 project_b
309 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
310 .await
311 .unwrap();
312
313 // Request to join that project as client C
314 let project_c = cx_c.spawn(|cx| {
315 Project::remote(
316 project_id,
317 client_c.client.clone(),
318 client_c.user_store.clone(),
319 client_c.project_store.clone(),
320 client_c.language_registry.clone(),
321 FakeFs::new(cx.background()),
322 cx,
323 )
324 });
325 deterministic.run_until_parked();
326
327 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
328 server.disconnect_client(client_a.current_user_id(cx_a));
329 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
330 project_a
331 .condition(cx_a, |project, _| project.collaborators().is_empty())
332 .await;
333 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
334 project_b
335 .condition(cx_b, |project, _| project.is_read_only())
336 .await;
337 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
338 cx_b.update(|_| {
339 drop(project_b);
340 });
341 assert!(matches!(
342 project_c.await.unwrap_err(),
343 project::JoinProjectError::HostWentOffline
344 ));
345
346 // Ensure guests can still join.
347 let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
348 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
349 project_b2
350 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
351 .await
352 .unwrap();
353}
354
355#[gpui::test(iterations = 10)]
356async fn test_decline_join_request(
357 deterministic: Arc<Deterministic>,
358 cx_a: &mut TestAppContext,
359 cx_b: &mut TestAppContext,
360) {
361 cx_a.foreground().forbid_parking();
362 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
363 let mut client_a = server.create_client(cx_a, "user_a").await;
364 let client_b = server.create_client(cx_b, "user_b").await;
365 server
366 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
367 .await;
368
369 let fs = FakeFs::new(cx_a.background());
370 fs.insert_tree("/a", json!({})).await;
371
372 let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
373 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
374
375 // Request to join that project as client B
376 let project_b = cx_b.spawn(|cx| {
377 Project::remote(
378 project_id,
379 client_b.client.clone(),
380 client_b.user_store.clone(),
381 client_b.project_store.clone(),
382 client_b.language_registry.clone(),
383 FakeFs::new(cx.background()),
384 cx,
385 )
386 });
387 deterministic.run_until_parked();
388 project_a.update(cx_a, |project, cx| {
389 project.respond_to_join_request(client_b.user_id().unwrap(), false, cx)
390 });
391 assert!(matches!(
392 project_b.await.unwrap_err(),
393 project::JoinProjectError::HostDeclined
394 ));
395
396 // Request to join the project again as client B
397 let project_b = cx_b.spawn(|cx| {
398 Project::remote(
399 project_id,
400 client_b.client.clone(),
401 client_b.user_store.clone(),
402 client_b.project_store.clone(),
403 client_b.language_registry.clone(),
404 FakeFs::new(cx.background()),
405 cx,
406 )
407 });
408
409 // Close the project on the host
410 deterministic.run_until_parked();
411 cx_a.update(|_| {
412 drop(project_a);
413 client_a.project.take();
414 });
415 deterministic.run_until_parked();
416 assert!(matches!(
417 project_b.await.unwrap_err(),
418 project::JoinProjectError::HostClosedProject
419 ));
420}
421
422#[gpui::test(iterations = 10)]
423async fn test_cancel_join_request(
424 deterministic: Arc<Deterministic>,
425 cx_a: &mut TestAppContext,
426 cx_b: &mut TestAppContext,
427) {
428 cx_a.foreground().forbid_parking();
429 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
430 let mut client_a = server.create_client(cx_a, "user_a").await;
431 let client_b = server.create_client(cx_b, "user_b").await;
432 server
433 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
434 .await;
435
436 let fs = FakeFs::new(cx_a.background());
437 fs.insert_tree("/a", json!({})).await;
438
439 let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
440 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
441
442 let user_b = client_a
443 .user_store
444 .update(cx_a, |store, cx| {
445 store.fetch_user(client_b.user_id().unwrap(), cx)
446 })
447 .await
448 .unwrap();
449
450 let project_a_events = Rc::new(RefCell::new(Vec::new()));
451 project_a.update(cx_a, {
452 let project_a_events = project_a_events.clone();
453 move |_, cx| {
454 cx.subscribe(&cx.handle(), move |_, _, event, _| {
455 project_a_events.borrow_mut().push(event.clone());
456 })
457 .detach();
458 }
459 });
460
461 // Request to join that project as client B
462 let project_b = cx_b.spawn(|cx| {
463 Project::remote(
464 project_id,
465 client_b.client.clone(),
466 client_b.user_store.clone(),
467 client_b.project_store.clone(),
468 client_b.language_registry.clone().clone(),
469 FakeFs::new(cx.background()),
470 cx,
471 )
472 });
473 deterministic.run_until_parked();
474 assert_eq!(
475 &*project_a_events.borrow(),
476 &[project::Event::ContactRequestedJoin(user_b.clone())]
477 );
478 project_a_events.borrow_mut().clear();
479
480 // Cancel the join request by leaving the project
481 client_b
482 .client
483 .send(proto::LeaveProject { project_id })
484 .unwrap();
485 drop(project_b);
486
487 deterministic.run_until_parked();
488 assert_eq!(
489 &*project_a_events.borrow(),
490 &[project::Event::ContactCancelledJoinRequest(user_b.clone())]
491 );
492}
493
494#[gpui::test(iterations = 3)]
495async fn test_private_projects(
496 deterministic: Arc<Deterministic>,
497 cx_a: &mut TestAppContext,
498 cx_b: &mut TestAppContext,
499) {
500 cx_a.foreground().forbid_parking();
501 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
502 let mut client_a = server.create_client(cx_a, "user_a").await;
503 let client_b = server.create_client(cx_b, "user_b").await;
504 server
505 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
506 .await;
507
508 let user_a = UserId::from_proto(client_a.user_id().unwrap());
509
510 let fs = FakeFs::new(cx_a.background());
511 fs.insert_tree("/a", json!({})).await;
512
513 // Create a private project
514 let project_a = cx_a.update(|cx| {
515 Project::local(
516 false,
517 client_a.client.clone(),
518 client_a.user_store.clone(),
519 client_a.project_store.clone(),
520 client_a.language_registry.clone(),
521 fs.clone(),
522 cx,
523 )
524 });
525 client_a.project = Some(project_a.clone());
526
527 // Private projects are not registered with the server.
528 deterministic.run_until_parked();
529 assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none()));
530 assert!(client_b
531 .user_store
532 .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
533
534 // The project is registered when it is made public.
535 project_a.update(cx_a, |project, cx| project.set_public(true, cx));
536 deterministic.run_until_parked();
537 assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some()));
538 assert!(!client_b
539 .user_store
540 .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
541
542 // The project is registered again when it loses and regains connection.
543 server.disconnect_client(user_a);
544 server.forbid_connections();
545 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
546 // deterministic.run_until_parked();
547 assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_none()));
548 assert!(client_b
549 .user_store
550 .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
551 server.allow_connections();
552 cx_b.foreground().advance_clock(Duration::from_secs(10));
553 assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some()));
554 assert!(!client_b
555 .user_store
556 .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
557}
558
559#[gpui::test(iterations = 10)]
560async fn test_propagate_saves_and_fs_changes(
561 cx_a: &mut TestAppContext,
562 cx_b: &mut TestAppContext,
563 cx_c: &mut TestAppContext,
564) {
565 cx_a.foreground().forbid_parking();
566 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
567 let mut client_a = server.create_client(cx_a, "user_a").await;
568 let mut client_b = server.create_client(cx_b, "user_b").await;
569 let mut client_c = server.create_client(cx_c, "user_c").await;
570 server
571 .make_contacts(vec![
572 (&client_a, cx_a),
573 (&client_b, cx_b),
574 (&client_c, cx_c),
575 ])
576 .await;
577
578 let fs = FakeFs::new(cx_a.background());
579 fs.insert_tree(
580 "/a",
581 json!({
582 "file1": "",
583 "file2": ""
584 }),
585 )
586 .await;
587
588 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
589 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
590
591 // Join that worktree as clients B and C.
592 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
593 let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
594 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
595 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
596
597 // Open and edit a buffer as both guests B and C.
598 let buffer_b = project_b
599 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
600 .await
601 .unwrap();
602 let buffer_c = project_c
603 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
604 .await
605 .unwrap();
606 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], cx));
607 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], cx));
608
609 // Open and edit that buffer as the host.
610 let buffer_a = project_a
611 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
612 .await
613 .unwrap();
614
615 buffer_a
616 .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
617 .await;
618 buffer_a.update(cx_a, |buf, cx| {
619 buf.edit([(buf.len()..buf.len(), "i-am-a")], cx)
620 });
621
622 // Wait for edits to propagate
623 buffer_a
624 .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
625 .await;
626 buffer_b
627 .condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
628 .await;
629 buffer_c
630 .condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
631 .await;
632
633 // Edit the buffer as the host and concurrently save as guest B.
634 let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
635 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], cx));
636 save_b.await.unwrap();
637 assert_eq!(
638 fs.load("/a/file1".as_ref()).await.unwrap(),
639 "hi-a, i-am-c, i-am-b, i-am-a"
640 );
641 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
642 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
643 buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
644
645 worktree_a.flush_fs_events(cx_a).await;
646
647 // Make changes on host's file system, see those changes on guest worktrees.
648 fs.rename(
649 "/a/file1".as_ref(),
650 "/a/file1-renamed".as_ref(),
651 Default::default(),
652 )
653 .await
654 .unwrap();
655
656 fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
657 .await
658 .unwrap();
659 fs.insert_file(Path::new("/a/file4"), "4".into()).await;
660
661 worktree_a
662 .condition(&cx_a, |tree, _| {
663 tree.paths()
664 .map(|p| p.to_string_lossy())
665 .collect::<Vec<_>>()
666 == ["file1-renamed", "file3", "file4"]
667 })
668 .await;
669 worktree_b
670 .condition(&cx_b, |tree, _| {
671 tree.paths()
672 .map(|p| p.to_string_lossy())
673 .collect::<Vec<_>>()
674 == ["file1-renamed", "file3", "file4"]
675 })
676 .await;
677 worktree_c
678 .condition(&cx_c, |tree, _| {
679 tree.paths()
680 .map(|p| p.to_string_lossy())
681 .collect::<Vec<_>>()
682 == ["file1-renamed", "file3", "file4"]
683 })
684 .await;
685
686 // Ensure buffer files are updated as well.
687 buffer_a
688 .condition(&cx_a, |buf, _| {
689 buf.file().unwrap().path().to_str() == Some("file1-renamed")
690 })
691 .await;
692 buffer_b
693 .condition(&cx_b, |buf, _| {
694 buf.file().unwrap().path().to_str() == Some("file1-renamed")
695 })
696 .await;
697 buffer_c
698 .condition(&cx_c, |buf, _| {
699 buf.file().unwrap().path().to_str() == Some("file1-renamed")
700 })
701 .await;
702}
703
704#[gpui::test(iterations = 10)]
705async fn test_fs_operations(
706 executor: Arc<Deterministic>,
707 cx_a: &mut TestAppContext,
708 cx_b: &mut TestAppContext,
709) {
710 executor.forbid_parking();
711 let fs = FakeFs::new(cx_a.background());
712
713 // Connect to a server as 2 clients.
714 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
715 let mut client_a = server.create_client(cx_a, "user_a").await;
716 let mut client_b = server.create_client(cx_b, "user_b").await;
717 server
718 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
719 .await;
720
721 // Share a project as client A
722 fs.insert_tree(
723 "/dir",
724 json!({
725 "a.txt": "a-contents",
726 "b.txt": "b-contents",
727 }),
728 )
729 .await;
730
731 let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
732 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
733
734 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
735 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
736
737 let entry = project_b
738 .update(cx_b, |project, cx| {
739 project
740 .create_entry((worktree_id, "c.txt"), false, cx)
741 .unwrap()
742 })
743 .await
744 .unwrap();
745 worktree_a.read_with(cx_a, |worktree, _| {
746 assert_eq!(
747 worktree
748 .paths()
749 .map(|p| p.to_string_lossy())
750 .collect::<Vec<_>>(),
751 ["a.txt", "b.txt", "c.txt"]
752 );
753 });
754 worktree_b.read_with(cx_b, |worktree, _| {
755 assert_eq!(
756 worktree
757 .paths()
758 .map(|p| p.to_string_lossy())
759 .collect::<Vec<_>>(),
760 ["a.txt", "b.txt", "c.txt"]
761 );
762 });
763
764 project_b
765 .update(cx_b, |project, cx| {
766 project.rename_entry(entry.id, Path::new("d.txt"), cx)
767 })
768 .unwrap()
769 .await
770 .unwrap();
771 worktree_a.read_with(cx_a, |worktree, _| {
772 assert_eq!(
773 worktree
774 .paths()
775 .map(|p| p.to_string_lossy())
776 .collect::<Vec<_>>(),
777 ["a.txt", "b.txt", "d.txt"]
778 );
779 });
780 worktree_b.read_with(cx_b, |worktree, _| {
781 assert_eq!(
782 worktree
783 .paths()
784 .map(|p| p.to_string_lossy())
785 .collect::<Vec<_>>(),
786 ["a.txt", "b.txt", "d.txt"]
787 );
788 });
789
790 let dir_entry = project_b
791 .update(cx_b, |project, cx| {
792 project
793 .create_entry((worktree_id, "DIR"), true, cx)
794 .unwrap()
795 })
796 .await
797 .unwrap();
798 worktree_a.read_with(cx_a, |worktree, _| {
799 assert_eq!(
800 worktree
801 .paths()
802 .map(|p| p.to_string_lossy())
803 .collect::<Vec<_>>(),
804 ["DIR", "a.txt", "b.txt", "d.txt"]
805 );
806 });
807 worktree_b.read_with(cx_b, |worktree, _| {
808 assert_eq!(
809 worktree
810 .paths()
811 .map(|p| p.to_string_lossy())
812 .collect::<Vec<_>>(),
813 ["DIR", "a.txt", "b.txt", "d.txt"]
814 );
815 });
816
817 project_b
818 .update(cx_b, |project, cx| {
819 project
820 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
821 .unwrap()
822 })
823 .await
824 .unwrap();
825 project_b
826 .update(cx_b, |project, cx| {
827 project
828 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
829 .unwrap()
830 })
831 .await
832 .unwrap();
833 project_b
834 .update(cx_b, |project, cx| {
835 project
836 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
837 .unwrap()
838 })
839 .await
840 .unwrap();
841 worktree_a.read_with(cx_a, |worktree, _| {
842 assert_eq!(
843 worktree
844 .paths()
845 .map(|p| p.to_string_lossy())
846 .collect::<Vec<_>>(),
847 [
848 "DIR",
849 "DIR/SUBDIR",
850 "DIR/SUBDIR/f.txt",
851 "DIR/e.txt",
852 "a.txt",
853 "b.txt",
854 "d.txt"
855 ]
856 );
857 });
858 worktree_b.read_with(cx_b, |worktree, _| {
859 assert_eq!(
860 worktree
861 .paths()
862 .map(|p| p.to_string_lossy())
863 .collect::<Vec<_>>(),
864 [
865 "DIR",
866 "DIR/SUBDIR",
867 "DIR/SUBDIR/f.txt",
868 "DIR/e.txt",
869 "a.txt",
870 "b.txt",
871 "d.txt"
872 ]
873 );
874 });
875
876 project_b
877 .update(cx_b, |project, cx| {
878 project
879 .copy_entry(entry.id, Path::new("f.txt"), cx)
880 .unwrap()
881 })
882 .await
883 .unwrap();
884 worktree_a.read_with(cx_a, |worktree, _| {
885 assert_eq!(
886 worktree
887 .paths()
888 .map(|p| p.to_string_lossy())
889 .collect::<Vec<_>>(),
890 [
891 "DIR",
892 "DIR/SUBDIR",
893 "DIR/SUBDIR/f.txt",
894 "DIR/e.txt",
895 "a.txt",
896 "b.txt",
897 "d.txt",
898 "f.txt"
899 ]
900 );
901 });
902 worktree_b.read_with(cx_b, |worktree, _| {
903 assert_eq!(
904 worktree
905 .paths()
906 .map(|p| p.to_string_lossy())
907 .collect::<Vec<_>>(),
908 [
909 "DIR",
910 "DIR/SUBDIR",
911 "DIR/SUBDIR/f.txt",
912 "DIR/e.txt",
913 "a.txt",
914 "b.txt",
915 "d.txt",
916 "f.txt"
917 ]
918 );
919 });
920
921 project_b
922 .update(cx_b, |project, cx| {
923 project.delete_entry(dir_entry.id, cx).unwrap()
924 })
925 .await
926 .unwrap();
927 worktree_a.read_with(cx_a, |worktree, _| {
928 assert_eq!(
929 worktree
930 .paths()
931 .map(|p| p.to_string_lossy())
932 .collect::<Vec<_>>(),
933 ["a.txt", "b.txt", "d.txt", "f.txt"]
934 );
935 });
936 worktree_b.read_with(cx_b, |worktree, _| {
937 assert_eq!(
938 worktree
939 .paths()
940 .map(|p| p.to_string_lossy())
941 .collect::<Vec<_>>(),
942 ["a.txt", "b.txt", "d.txt", "f.txt"]
943 );
944 });
945
946 project_b
947 .update(cx_b, |project, cx| {
948 project.delete_entry(entry.id, cx).unwrap()
949 })
950 .await
951 .unwrap();
952 worktree_a.read_with(cx_a, |worktree, _| {
953 assert_eq!(
954 worktree
955 .paths()
956 .map(|p| p.to_string_lossy())
957 .collect::<Vec<_>>(),
958 ["a.txt", "b.txt", "f.txt"]
959 );
960 });
961 worktree_b.read_with(cx_b, |worktree, _| {
962 assert_eq!(
963 worktree
964 .paths()
965 .map(|p| p.to_string_lossy())
966 .collect::<Vec<_>>(),
967 ["a.txt", "b.txt", "f.txt"]
968 );
969 });
970}
971
972#[gpui::test(iterations = 10)]
973async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
974 cx_a.foreground().forbid_parking();
975 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
976 let mut client_a = server.create_client(cx_a, "user_a").await;
977 let mut client_b = server.create_client(cx_b, "user_b").await;
978 server
979 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
980 .await;
981
982 let fs = FakeFs::new(cx_a.background());
983 fs.insert_tree(
984 "/dir",
985 json!({
986 "a.txt": "a-contents",
987 }),
988 )
989 .await;
990
991 let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
992 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
993
994 // Open a buffer as client B
995 let buffer_b = project_b
996 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
997 .await
998 .unwrap();
999
1000 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], cx));
1001 buffer_b.read_with(cx_b, |buf, _| {
1002 assert!(buf.is_dirty());
1003 assert!(!buf.has_conflict());
1004 });
1005
1006 buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
1007 buffer_b
1008 .condition(&cx_b, |buffer_b, _| !buffer_b.is_dirty())
1009 .await;
1010 buffer_b.read_with(cx_b, |buf, _| {
1011 assert!(!buf.has_conflict());
1012 });
1013
1014 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], cx));
1015 buffer_b.read_with(cx_b, |buf, _| {
1016 assert!(buf.is_dirty());
1017 assert!(!buf.has_conflict());
1018 });
1019}
1020
1021#[gpui::test(iterations = 10)]
1022async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1023 cx_a.foreground().forbid_parking();
1024 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1025 let mut client_a = server.create_client(cx_a, "user_a").await;
1026 let mut client_b = server.create_client(cx_b, "user_b").await;
1027 server
1028 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1029 .await;
1030
1031 let fs = FakeFs::new(cx_a.background());
1032 fs.insert_tree(
1033 "/dir",
1034 json!({
1035 "a.txt": "a-contents",
1036 }),
1037 )
1038 .await;
1039
1040 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/dir", cx_a).await;
1041 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1042
1043 // Open a buffer as client B
1044 let buffer_b = project_b
1045 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1046 .await
1047 .unwrap();
1048 buffer_b.read_with(cx_b, |buf, _| {
1049 assert!(!buf.is_dirty());
1050 assert!(!buf.has_conflict());
1051 });
1052
1053 fs.save(Path::new("/dir/a.txt"), &"new contents".into())
1054 .await
1055 .unwrap();
1056 buffer_b
1057 .condition(&cx_b, |buf, _| {
1058 buf.text() == "new contents" && !buf.is_dirty()
1059 })
1060 .await;
1061 buffer_b.read_with(cx_b, |buf, _| {
1062 assert!(!buf.has_conflict());
1063 });
1064}
1065
1066#[gpui::test(iterations = 10)]
1067async fn test_editing_while_guest_opens_buffer(
1068 cx_a: &mut TestAppContext,
1069 cx_b: &mut TestAppContext,
1070) {
1071 cx_a.foreground().forbid_parking();
1072 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1073 let mut client_a = server.create_client(cx_a, "user_a").await;
1074 let mut client_b = server.create_client(cx_b, "user_b").await;
1075 server
1076 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1077 .await;
1078
1079 let fs = FakeFs::new(cx_a.background());
1080 fs.insert_tree(
1081 "/dir",
1082 json!({
1083 "a.txt": "a-contents",
1084 }),
1085 )
1086 .await;
1087
1088 let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
1089 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1090
1091 // Open a buffer as client A
1092 let buffer_a = project_a
1093 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1094 .await
1095 .unwrap();
1096
1097 // Start opening the same buffer as client B
1098 let buffer_b = cx_b
1099 .background()
1100 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
1101
1102 // Edit the buffer as client A while client B is still opening it.
1103 cx_b.background().simulate_random_delay().await;
1104 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], cx));
1105 cx_b.background().simulate_random_delay().await;
1106 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], cx));
1107
1108 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
1109 let buffer_b = buffer_b.await.unwrap();
1110 buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
1111}
1112
1113#[gpui::test(iterations = 10)]
1114async fn test_leaving_worktree_while_opening_buffer(
1115 cx_a: &mut TestAppContext,
1116 cx_b: &mut TestAppContext,
1117) {
1118 cx_a.foreground().forbid_parking();
1119 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1120 let mut client_a = server.create_client(cx_a, "user_a").await;
1121 let mut client_b = server.create_client(cx_b, "user_b").await;
1122 server
1123 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1124 .await;
1125
1126 let fs = FakeFs::new(cx_a.background());
1127 fs.insert_tree(
1128 "/dir",
1129 json!({
1130 "a.txt": "a-contents",
1131 }),
1132 )
1133 .await;
1134
1135 let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
1136 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1137
1138 // See that a guest has joined as client A.
1139 project_a
1140 .condition(&cx_a, |p, _| p.collaborators().len() == 1)
1141 .await;
1142
1143 // Begin opening a buffer as client B, but leave the project before the open completes.
1144 let buffer_b = cx_b
1145 .background()
1146 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
1147 cx_b.update(|_| {
1148 drop(client_b.project.take());
1149 drop(project_b);
1150 });
1151 drop(buffer_b);
1152
1153 // See that the guest has left.
1154 project_a
1155 .condition(&cx_a, |p, _| p.collaborators().len() == 0)
1156 .await;
1157}
1158
1159#[gpui::test(iterations = 10)]
1160async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1161 cx_a.foreground().forbid_parking();
1162 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1163 let mut client_a = server.create_client(cx_a, "user_a").await;
1164 let mut client_b = server.create_client(cx_b, "user_b").await;
1165 server
1166 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1167 .await;
1168
1169 let fs = FakeFs::new(cx_a.background());
1170 fs.insert_tree(
1171 "/a",
1172 json!({
1173 "a.txt": "a-contents",
1174 "b.txt": "b-contents",
1175 }),
1176 )
1177 .await;
1178
1179 let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
1180 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1181
1182 // Client A sees that a guest has joined.
1183 project_a
1184 .condition(cx_a, |p, _| p.collaborators().len() == 1)
1185 .await;
1186
1187 // Drop client B's connection and ensure client A observes client B leaving the project.
1188 client_b.disconnect(&cx_b.to_async()).unwrap();
1189 project_a
1190 .condition(cx_a, |p, _| p.collaborators().len() == 0)
1191 .await;
1192
1193 // Rejoin the project as client B
1194 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1195
1196 // Client A sees that a guest has re-joined.
1197 project_a
1198 .condition(cx_a, |p, _| p.collaborators().len() == 1)
1199 .await;
1200
1201 // Simulate connection loss for client B and ensure client A observes client B leaving the project.
1202 client_b.wait_for_current_user(cx_b).await;
1203 server.disconnect_client(client_b.current_user_id(cx_b));
1204 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
1205 project_a
1206 .condition(cx_a, |p, _| p.collaborators().len() == 0)
1207 .await;
1208}
1209
1210#[gpui::test(iterations = 10)]
1211async fn test_collaborating_with_diagnostics(
1212 deterministic: Arc<Deterministic>,
1213 cx_a: &mut TestAppContext,
1214 cx_b: &mut TestAppContext,
1215 cx_c: &mut TestAppContext,
1216) {
1217 deterministic.forbid_parking();
1218 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1219 let mut client_a = server.create_client(cx_a, "user_a").await;
1220 let mut client_b = server.create_client(cx_b, "user_b").await;
1221 let mut client_c = server.create_client(cx_c, "user_c").await;
1222 server
1223 .make_contacts(vec![
1224 (&client_a, cx_a),
1225 (&client_b, cx_b),
1226 (&client_c, cx_c),
1227 ])
1228 .await;
1229
1230 // Set up a fake language server.
1231 let mut language = Language::new(
1232 LanguageConfig {
1233 name: "Rust".into(),
1234 path_suffixes: vec!["rs".to_string()],
1235 ..Default::default()
1236 },
1237 Some(tree_sitter_rust::language()),
1238 );
1239 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1240 client_a.language_registry.add(Arc::new(language));
1241
1242 // Connect to a server as 2 clients.
1243
1244 // Share a project as client A
1245 let fs = FakeFs::new(cx_a.background());
1246 fs.insert_tree(
1247 "/a",
1248 json!({
1249 "a.rs": "let one = two",
1250 "other.rs": "",
1251 }),
1252 )
1253 .await;
1254 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
1255 let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
1256
1257 // Cause the language server to start.
1258 let _buffer = cx_a
1259 .background()
1260 .spawn(project_a.update(cx_a, |project, cx| {
1261 project.open_buffer(
1262 ProjectPath {
1263 worktree_id,
1264 path: Path::new("other.rs").into(),
1265 },
1266 cx,
1267 )
1268 }))
1269 .await
1270 .unwrap();
1271
1272 // Join the worktree as client B.
1273 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1274
1275 // Simulate a language server reporting errors for a file.
1276 let mut fake_language_server = fake_language_servers.next().await.unwrap();
1277 fake_language_server
1278 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1279 .await;
1280 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1281 lsp::PublishDiagnosticsParams {
1282 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1283 version: None,
1284 diagnostics: vec![lsp::Diagnostic {
1285 severity: Some(lsp::DiagnosticSeverity::ERROR),
1286 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
1287 message: "message 1".to_string(),
1288 ..Default::default()
1289 }],
1290 },
1291 );
1292
1293 // Wait for server to see the diagnostics update.
1294 deterministic.run_until_parked();
1295 {
1296 let store = server.store.read().await;
1297 let project = store.project(project_id).unwrap();
1298 let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
1299 assert!(!worktree.diagnostic_summaries.is_empty());
1300 }
1301
1302 // Ensure client B observes the new diagnostics.
1303 project_b.read_with(cx_b, |project, cx| {
1304 assert_eq!(
1305 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1306 &[(
1307 ProjectPath {
1308 worktree_id,
1309 path: Arc::from(Path::new("a.rs")),
1310 },
1311 DiagnosticSummary {
1312 error_count: 1,
1313 warning_count: 0,
1314 ..Default::default()
1315 },
1316 )]
1317 )
1318 });
1319
1320 // Join project as client C and observe the diagnostics.
1321 let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
1322 project_c.read_with(cx_c, |project, cx| {
1323 assert_eq!(
1324 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1325 &[(
1326 ProjectPath {
1327 worktree_id,
1328 path: Arc::from(Path::new("a.rs")),
1329 },
1330 DiagnosticSummary {
1331 error_count: 1,
1332 warning_count: 0,
1333 ..Default::default()
1334 },
1335 )]
1336 )
1337 });
1338
1339 // Simulate a language server reporting more errors for a file.
1340 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1341 lsp::PublishDiagnosticsParams {
1342 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1343 version: None,
1344 diagnostics: vec![
1345 lsp::Diagnostic {
1346 severity: Some(lsp::DiagnosticSeverity::ERROR),
1347 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
1348 message: "message 1".to_string(),
1349 ..Default::default()
1350 },
1351 lsp::Diagnostic {
1352 severity: Some(lsp::DiagnosticSeverity::WARNING),
1353 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
1354 message: "message 2".to_string(),
1355 ..Default::default()
1356 },
1357 ],
1358 },
1359 );
1360
1361 // Clients B and C get the updated summaries
1362 deterministic.run_until_parked();
1363 project_b.read_with(cx_b, |project, cx| {
1364 assert_eq!(
1365 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1366 [(
1367 ProjectPath {
1368 worktree_id,
1369 path: Arc::from(Path::new("a.rs")),
1370 },
1371 DiagnosticSummary {
1372 error_count: 1,
1373 warning_count: 1,
1374 ..Default::default()
1375 },
1376 )]
1377 );
1378 });
1379 project_c.read_with(cx_c, |project, cx| {
1380 assert_eq!(
1381 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1382 [(
1383 ProjectPath {
1384 worktree_id,
1385 path: Arc::from(Path::new("a.rs")),
1386 },
1387 DiagnosticSummary {
1388 error_count: 1,
1389 warning_count: 1,
1390 ..Default::default()
1391 },
1392 )]
1393 );
1394 });
1395
1396 // Open the file with the errors on client B. They should be present.
1397 let buffer_b = cx_b
1398 .background()
1399 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1400 .await
1401 .unwrap();
1402
1403 buffer_b.read_with(cx_b, |buffer, _| {
1404 assert_eq!(
1405 buffer
1406 .snapshot()
1407 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1408 .map(|entry| entry)
1409 .collect::<Vec<_>>(),
1410 &[
1411 DiagnosticEntry {
1412 range: Point::new(0, 4)..Point::new(0, 7),
1413 diagnostic: Diagnostic {
1414 group_id: 1,
1415 message: "message 1".to_string(),
1416 severity: lsp::DiagnosticSeverity::ERROR,
1417 is_primary: true,
1418 ..Default::default()
1419 }
1420 },
1421 DiagnosticEntry {
1422 range: Point::new(0, 10)..Point::new(0, 13),
1423 diagnostic: Diagnostic {
1424 group_id: 2,
1425 severity: lsp::DiagnosticSeverity::WARNING,
1426 message: "message 2".to_string(),
1427 is_primary: true,
1428 ..Default::default()
1429 }
1430 }
1431 ]
1432 );
1433 });
1434
1435 // Simulate a language server reporting no errors for a file.
1436 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1437 lsp::PublishDiagnosticsParams {
1438 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1439 version: None,
1440 diagnostics: vec![],
1441 },
1442 );
1443 deterministic.run_until_parked();
1444 project_a.read_with(cx_a, |project, cx| {
1445 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1446 });
1447 project_b.read_with(cx_b, |project, cx| {
1448 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1449 });
1450 project_c.read_with(cx_c, |project, cx| {
1451 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1452 });
1453}
1454
1455#[gpui::test(iterations = 10)]
1456async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1457 cx_a.foreground().forbid_parking();
1458 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1459 let mut client_a = server.create_client(cx_a, "user_a").await;
1460 let mut client_b = server.create_client(cx_b, "user_b").await;
1461 server
1462 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1463 .await;
1464
1465 // Set up a fake language server.
1466 let mut language = Language::new(
1467 LanguageConfig {
1468 name: "Rust".into(),
1469 path_suffixes: vec!["rs".to_string()],
1470 ..Default::default()
1471 },
1472 Some(tree_sitter_rust::language()),
1473 );
1474 let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
1475 capabilities: lsp::ServerCapabilities {
1476 completion_provider: Some(lsp::CompletionOptions {
1477 trigger_characters: Some(vec![".".to_string()]),
1478 ..Default::default()
1479 }),
1480 ..Default::default()
1481 },
1482 ..Default::default()
1483 });
1484 client_a.language_registry.add(Arc::new(language));
1485
1486 let fs = FakeFs::new(cx_a.background());
1487 fs.insert_tree(
1488 "/a",
1489 json!({
1490 "main.rs": "fn main() { a }",
1491 "other.rs": "",
1492 }),
1493 )
1494 .await;
1495
1496 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
1497 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1498
1499 // Open a file in an editor as the guest.
1500 let buffer_b = project_b
1501 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1502 .await
1503 .unwrap();
1504 let (window_b, _) = cx_b.add_window(|_| EmptyView);
1505 let editor_b = cx_b.add_view(window_b, |cx| {
1506 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
1507 });
1508
1509 let fake_language_server = fake_language_servers.next().await.unwrap();
1510 buffer_b
1511 .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
1512 .await;
1513
1514 // Type a completion trigger character as the guest.
1515 editor_b.update(cx_b, |editor, cx| {
1516 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1517 editor.handle_input(&Input(".".into()), cx);
1518 cx.focus(&editor_b);
1519 });
1520
1521 // Receive a completion request as the host's language server.
1522 // Return some completions from the host's language server.
1523 cx_a.foreground().start_waiting();
1524 fake_language_server
1525 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
1526 assert_eq!(
1527 params.text_document_position.text_document.uri,
1528 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1529 );
1530 assert_eq!(
1531 params.text_document_position.position,
1532 lsp::Position::new(0, 14),
1533 );
1534
1535 Ok(Some(lsp::CompletionResponse::Array(vec![
1536 lsp::CompletionItem {
1537 label: "first_method(…)".into(),
1538 detail: Some("fn(&mut self, B) -> C".into()),
1539 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1540 new_text: "first_method($1)".to_string(),
1541 range: lsp::Range::new(
1542 lsp::Position::new(0, 14),
1543 lsp::Position::new(0, 14),
1544 ),
1545 })),
1546 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1547 ..Default::default()
1548 },
1549 lsp::CompletionItem {
1550 label: "second_method(…)".into(),
1551 detail: Some("fn(&mut self, C) -> D<E>".into()),
1552 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1553 new_text: "second_method()".to_string(),
1554 range: lsp::Range::new(
1555 lsp::Position::new(0, 14),
1556 lsp::Position::new(0, 14),
1557 ),
1558 })),
1559 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1560 ..Default::default()
1561 },
1562 ])))
1563 })
1564 .next()
1565 .await
1566 .unwrap();
1567 cx_a.foreground().finish_waiting();
1568
1569 // Open the buffer on the host.
1570 let buffer_a = project_a
1571 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1572 .await
1573 .unwrap();
1574 buffer_a
1575 .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
1576 .await;
1577
1578 // Confirm a completion on the guest.
1579 editor_b
1580 .condition(&cx_b, |editor, _| editor.context_menu_visible())
1581 .await;
1582 editor_b.update(cx_b, |editor, cx| {
1583 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
1584 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
1585 });
1586
1587 // Return a resolved completion from the host's language server.
1588 // The resolved completion has an additional text edit.
1589 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
1590 |params, _| async move {
1591 assert_eq!(params.label, "first_method(…)");
1592 Ok(lsp::CompletionItem {
1593 label: "first_method(…)".into(),
1594 detail: Some("fn(&mut self, B) -> C".into()),
1595 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1596 new_text: "first_method($1)".to_string(),
1597 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1598 })),
1599 additional_text_edits: Some(vec![lsp::TextEdit {
1600 new_text: "use d::SomeTrait;\n".to_string(),
1601 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1602 }]),
1603 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1604 ..Default::default()
1605 })
1606 },
1607 );
1608
1609 // The additional edit is applied.
1610 buffer_a
1611 .condition(&cx_a, |buffer, _| {
1612 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1613 })
1614 .await;
1615 buffer_b
1616 .condition(&cx_b, |buffer, _| {
1617 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1618 })
1619 .await;
1620}
1621
1622#[gpui::test(iterations = 10)]
1623async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1624 cx_a.foreground().forbid_parking();
1625 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1626 let mut client_a = server.create_client(cx_a, "user_a").await;
1627 let mut client_b = server.create_client(cx_b, "user_b").await;
1628 server
1629 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1630 .await;
1631
1632 let fs = FakeFs::new(cx_a.background());
1633 fs.insert_tree(
1634 "/a",
1635 json!({
1636 "a.rs": "let one = 1;",
1637 }),
1638 )
1639 .await;
1640
1641 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
1642 let buffer_a = project_a
1643 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
1644 .await
1645 .unwrap();
1646
1647 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1648
1649 let buffer_b = cx_b
1650 .background()
1651 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1652 .await
1653 .unwrap();
1654 buffer_b.update(cx_b, |buffer, cx| {
1655 buffer.edit([(4..7, "six")], cx);
1656 buffer.edit([(10..11, "6")], cx);
1657 assert_eq!(buffer.text(), "let six = 6;");
1658 assert!(buffer.is_dirty());
1659 assert!(!buffer.has_conflict());
1660 });
1661 buffer_a
1662 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
1663 .await;
1664
1665 fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;"))
1666 .await
1667 .unwrap();
1668 buffer_a
1669 .condition(cx_a, |buffer, _| buffer.has_conflict())
1670 .await;
1671 buffer_b
1672 .condition(cx_b, |buffer, _| buffer.has_conflict())
1673 .await;
1674
1675 project_b
1676 .update(cx_b, |project, cx| {
1677 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
1678 })
1679 .await
1680 .unwrap();
1681 buffer_a.read_with(cx_a, |buffer, _| {
1682 assert_eq!(buffer.text(), "let seven = 7;");
1683 assert!(!buffer.is_dirty());
1684 assert!(!buffer.has_conflict());
1685 });
1686 buffer_b.read_with(cx_b, |buffer, _| {
1687 assert_eq!(buffer.text(), "let seven = 7;");
1688 assert!(!buffer.is_dirty());
1689 assert!(!buffer.has_conflict());
1690 });
1691
1692 buffer_a.update(cx_a, |buffer, cx| {
1693 // Undoing on the host is a no-op when the reload was initiated by the guest.
1694 buffer.undo(cx);
1695 assert_eq!(buffer.text(), "let seven = 7;");
1696 assert!(!buffer.is_dirty());
1697 assert!(!buffer.has_conflict());
1698 });
1699 buffer_b.update(cx_b, |buffer, cx| {
1700 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
1701 buffer.undo(cx);
1702 assert_eq!(buffer.text(), "let six = 6;");
1703 assert!(buffer.is_dirty());
1704 assert!(!buffer.has_conflict());
1705 });
1706}
1707
1708#[gpui::test(iterations = 10)]
1709async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1710 cx_a.foreground().forbid_parking();
1711 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1712 let mut client_a = server.create_client(cx_a, "user_a").await;
1713 let mut client_b = server.create_client(cx_b, "user_b").await;
1714 server
1715 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1716 .await;
1717
1718 // Set up a fake language server.
1719 let mut language = Language::new(
1720 LanguageConfig {
1721 name: "Rust".into(),
1722 path_suffixes: vec!["rs".to_string()],
1723 ..Default::default()
1724 },
1725 Some(tree_sitter_rust::language()),
1726 );
1727 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1728 client_a.language_registry.add(Arc::new(language));
1729
1730 let fs = FakeFs::new(cx_a.background());
1731 fs.insert_tree(
1732 "/a",
1733 json!({
1734 "a.rs": "let one = two",
1735 }),
1736 )
1737 .await;
1738
1739 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
1740 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1741
1742 let buffer_b = cx_b
1743 .background()
1744 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1745 .await
1746 .unwrap();
1747
1748 let fake_language_server = fake_language_servers.next().await.unwrap();
1749 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
1750 Ok(Some(vec![
1751 lsp::TextEdit {
1752 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
1753 new_text: "h".to_string(),
1754 },
1755 lsp::TextEdit {
1756 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
1757 new_text: "y".to_string(),
1758 },
1759 ]))
1760 });
1761
1762 project_b
1763 .update(cx_b, |project, cx| {
1764 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
1765 })
1766 .await
1767 .unwrap();
1768 assert_eq!(
1769 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
1770 "let honey = two"
1771 );
1772}
1773
1774#[gpui::test(iterations = 10)]
1775async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1776 cx_a.foreground().forbid_parking();
1777 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1778 let mut client_a = server.create_client(cx_a, "user_a").await;
1779 let mut client_b = server.create_client(cx_b, "user_b").await;
1780 server
1781 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1782 .await;
1783
1784 let fs = FakeFs::new(cx_a.background());
1785 fs.insert_tree(
1786 "/root-1",
1787 json!({
1788 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
1789 }),
1790 )
1791 .await;
1792 fs.insert_tree(
1793 "/root-2",
1794 json!({
1795 "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;",
1796 }),
1797 )
1798 .await;
1799
1800 // Set up a fake language server.
1801 let mut language = Language::new(
1802 LanguageConfig {
1803 name: "Rust".into(),
1804 path_suffixes: vec!["rs".to_string()],
1805 ..Default::default()
1806 },
1807 Some(tree_sitter_rust::language()),
1808 );
1809 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1810 client_a.language_registry.add(Arc::new(language));
1811
1812 let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
1813 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1814
1815 // Open the file on client B.
1816 let buffer_b = cx_b
1817 .background()
1818 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1819 .await
1820 .unwrap();
1821
1822 // Request the definition of a symbol as the guest.
1823 let fake_language_server = fake_language_servers.next().await.unwrap();
1824 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
1825 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1826 lsp::Location::new(
1827 lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
1828 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
1829 ),
1830 )))
1831 });
1832
1833 let definitions_1 = project_b
1834 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
1835 .await
1836 .unwrap();
1837 cx_b.read(|cx| {
1838 assert_eq!(definitions_1.len(), 1);
1839 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
1840 let target_buffer = definitions_1[0].buffer.read(cx);
1841 assert_eq!(
1842 target_buffer.text(),
1843 "const TWO: usize = 2;\nconst THREE: usize = 3;"
1844 );
1845 assert_eq!(
1846 definitions_1[0].range.to_point(target_buffer),
1847 Point::new(0, 6)..Point::new(0, 9)
1848 );
1849 });
1850
1851 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
1852 // the previous call to `definition`.
1853 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
1854 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1855 lsp::Location::new(
1856 lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
1857 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
1858 ),
1859 )))
1860 });
1861
1862 let definitions_2 = project_b
1863 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
1864 .await
1865 .unwrap();
1866 cx_b.read(|cx| {
1867 assert_eq!(definitions_2.len(), 1);
1868 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
1869 let target_buffer = definitions_2[0].buffer.read(cx);
1870 assert_eq!(
1871 target_buffer.text(),
1872 "const TWO: usize = 2;\nconst THREE: usize = 3;"
1873 );
1874 assert_eq!(
1875 definitions_2[0].range.to_point(target_buffer),
1876 Point::new(1, 6)..Point::new(1, 11)
1877 );
1878 });
1879 assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer);
1880}
1881
1882#[gpui::test(iterations = 10)]
1883async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1884 cx_a.foreground().forbid_parking();
1885 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1886 let mut client_a = server.create_client(cx_a, "user_a").await;
1887 let mut client_b = server.create_client(cx_b, "user_b").await;
1888 server
1889 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1890 .await;
1891
1892 let fs = FakeFs::new(cx_a.background());
1893 fs.insert_tree(
1894 "/root-1",
1895 json!({
1896 "one.rs": "const ONE: usize = 1;",
1897 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
1898 }),
1899 )
1900 .await;
1901 fs.insert_tree(
1902 "/root-2",
1903 json!({
1904 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
1905 }),
1906 )
1907 .await;
1908
1909 // Set up a fake language server.
1910 let mut language = Language::new(
1911 LanguageConfig {
1912 name: "Rust".into(),
1913 path_suffixes: vec!["rs".to_string()],
1914 ..Default::default()
1915 },
1916 Some(tree_sitter_rust::language()),
1917 );
1918 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1919 client_a.language_registry.add(Arc::new(language));
1920
1921 let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
1922 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1923
1924 // Open the file on client B.
1925 let buffer_b = cx_b
1926 .background()
1927 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
1928 .await
1929 .unwrap();
1930
1931 // Request references to a symbol as the guest.
1932 let fake_language_server = fake_language_servers.next().await.unwrap();
1933 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
1934 assert_eq!(
1935 params.text_document_position.text_document.uri.as_str(),
1936 "file:///root-1/one.rs"
1937 );
1938 Ok(Some(vec![
1939 lsp::Location {
1940 uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
1941 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
1942 },
1943 lsp::Location {
1944 uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
1945 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
1946 },
1947 lsp::Location {
1948 uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(),
1949 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
1950 },
1951 ]))
1952 });
1953
1954 let references = project_b
1955 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
1956 .await
1957 .unwrap();
1958 cx_b.read(|cx| {
1959 assert_eq!(references.len(), 3);
1960 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
1961
1962 let two_buffer = references[0].buffer.read(cx);
1963 let three_buffer = references[2].buffer.read(cx);
1964 assert_eq!(
1965 two_buffer.file().unwrap().path().as_ref(),
1966 Path::new("two.rs")
1967 );
1968 assert_eq!(references[1].buffer, references[0].buffer);
1969 assert_eq!(
1970 three_buffer.file().unwrap().full_path(cx),
1971 Path::new("three.rs")
1972 );
1973
1974 assert_eq!(references[0].range.to_offset(&two_buffer), 24..27);
1975 assert_eq!(references[1].range.to_offset(&two_buffer), 35..38);
1976 assert_eq!(references[2].range.to_offset(&three_buffer), 37..40);
1977 });
1978}
1979
1980#[gpui::test(iterations = 10)]
1981async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1982 cx_a.foreground().forbid_parking();
1983 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1984 let mut client_a = server.create_client(cx_a, "user_a").await;
1985 let mut client_b = server.create_client(cx_b, "user_b").await;
1986 server
1987 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1988 .await;
1989
1990 let fs = FakeFs::new(cx_a.background());
1991 fs.insert_tree(
1992 "/root-1",
1993 json!({
1994 "a": "hello world",
1995 "b": "goodnight moon",
1996 "c": "a world of goo",
1997 "d": "world champion of clown world",
1998 }),
1999 )
2000 .await;
2001 fs.insert_tree(
2002 "/root-2",
2003 json!({
2004 "e": "disney world is fun",
2005 }),
2006 )
2007 .await;
2008
2009 let (project_a, _) = client_a.build_local_project(fs, "/root-1", cx_a).await;
2010 let (worktree_2, _) = project_a
2011 .update(cx_a, |p, cx| {
2012 p.find_or_create_local_worktree("/root-2", true, cx)
2013 })
2014 .await
2015 .unwrap();
2016 worktree_2
2017 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
2018 .await;
2019
2020 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2021
2022 // Perform a search as the guest.
2023 let results = project_b
2024 .update(cx_b, |project, cx| {
2025 project.search(SearchQuery::text("world", false, false), cx)
2026 })
2027 .await
2028 .unwrap();
2029
2030 let mut ranges_by_path = results
2031 .into_iter()
2032 .map(|(buffer, ranges)| {
2033 buffer.read_with(cx_b, |buffer, cx| {
2034 let path = buffer.file().unwrap().full_path(cx);
2035 let offset_ranges = ranges
2036 .into_iter()
2037 .map(|range| range.to_offset(buffer))
2038 .collect::<Vec<_>>();
2039 (path, offset_ranges)
2040 })
2041 })
2042 .collect::<Vec<_>>();
2043 ranges_by_path.sort_by_key(|(path, _)| path.clone());
2044
2045 assert_eq!(
2046 ranges_by_path,
2047 &[
2048 (PathBuf::from("root-1/a"), vec![6..11]),
2049 (PathBuf::from("root-1/c"), vec![2..7]),
2050 (PathBuf::from("root-1/d"), vec![0..5, 24..29]),
2051 (PathBuf::from("root-2/e"), vec![7..12]),
2052 ]
2053 );
2054}
2055
2056#[gpui::test(iterations = 10)]
2057async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2058 cx_a.foreground().forbid_parking();
2059 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2060 let mut client_a = server.create_client(cx_a, "user_a").await;
2061 let mut client_b = server.create_client(cx_b, "user_b").await;
2062 server
2063 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2064 .await;
2065
2066 let fs = FakeFs::new(cx_a.background());
2067 fs.insert_tree(
2068 "/root-1",
2069 json!({
2070 "main.rs": "fn double(number: i32) -> i32 { number + number }",
2071 }),
2072 )
2073 .await;
2074
2075 // Set up a fake language server.
2076 let mut language = Language::new(
2077 LanguageConfig {
2078 name: "Rust".into(),
2079 path_suffixes: vec!["rs".to_string()],
2080 ..Default::default()
2081 },
2082 Some(tree_sitter_rust::language()),
2083 );
2084 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2085 client_a.language_registry.add(Arc::new(language));
2086
2087 let (project_a, worktree_id) = client_a.build_local_project(fs, "/root-1", cx_a).await;
2088 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2089
2090 // Open the file on client B.
2091 let buffer_b = cx_b
2092 .background()
2093 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2094 .await
2095 .unwrap();
2096
2097 // Request document highlights as the guest.
2098 let fake_language_server = fake_language_servers.next().await.unwrap();
2099 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
2100 |params, _| async move {
2101 assert_eq!(
2102 params
2103 .text_document_position_params
2104 .text_document
2105 .uri
2106 .as_str(),
2107 "file:///root-1/main.rs"
2108 );
2109 assert_eq!(
2110 params.text_document_position_params.position,
2111 lsp::Position::new(0, 34)
2112 );
2113 Ok(Some(vec![
2114 lsp::DocumentHighlight {
2115 kind: Some(lsp::DocumentHighlightKind::WRITE),
2116 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
2117 },
2118 lsp::DocumentHighlight {
2119 kind: Some(lsp::DocumentHighlightKind::READ),
2120 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
2121 },
2122 lsp::DocumentHighlight {
2123 kind: Some(lsp::DocumentHighlightKind::READ),
2124 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
2125 },
2126 ]))
2127 },
2128 );
2129
2130 let highlights = project_b
2131 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
2132 .await
2133 .unwrap();
2134 buffer_b.read_with(cx_b, |buffer, _| {
2135 let snapshot = buffer.snapshot();
2136
2137 let highlights = highlights
2138 .into_iter()
2139 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
2140 .collect::<Vec<_>>();
2141 assert_eq!(
2142 highlights,
2143 &[
2144 (lsp::DocumentHighlightKind::WRITE, 10..16),
2145 (lsp::DocumentHighlightKind::READ, 32..38),
2146 (lsp::DocumentHighlightKind::READ, 41..47)
2147 ]
2148 )
2149 });
2150}
2151
2152#[gpui::test(iterations = 10)]
2153async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2154 cx_a.foreground().forbid_parking();
2155 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2156 let mut client_a = server.create_client(cx_a, "user_a").await;
2157 let mut client_b = server.create_client(cx_b, "user_b").await;
2158 server
2159 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2160 .await;
2161
2162 // Set up a fake language server.
2163 let mut language = Language::new(
2164 LanguageConfig {
2165 name: "Rust".into(),
2166 path_suffixes: vec!["rs".to_string()],
2167 ..Default::default()
2168 },
2169 Some(tree_sitter_rust::language()),
2170 );
2171 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2172 client_a.language_registry.add(Arc::new(language));
2173
2174 let fs = FakeFs::new(cx_a.background());
2175 fs.insert_tree(
2176 "/code",
2177 json!({
2178 "crate-1": {
2179 "one.rs": "const ONE: usize = 1;",
2180 },
2181 "crate-2": {
2182 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
2183 },
2184 "private": {
2185 "passwords.txt": "the-password",
2186 }
2187 }),
2188 )
2189 .await;
2190
2191 let (project_a, worktree_id) = client_a
2192 .build_local_project(fs, "/code/crate-1", cx_a)
2193 .await;
2194 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2195
2196 // Cause the language server to start.
2197 let _buffer = cx_b
2198 .background()
2199 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2200 .await
2201 .unwrap();
2202
2203 let fake_language_server = fake_language_servers.next().await.unwrap();
2204 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
2205 #[allow(deprecated)]
2206 Ok(Some(vec![lsp::SymbolInformation {
2207 name: "TWO".into(),
2208 location: lsp::Location {
2209 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
2210 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2211 },
2212 kind: lsp::SymbolKind::CONSTANT,
2213 tags: None,
2214 container_name: None,
2215 deprecated: None,
2216 }]))
2217 });
2218
2219 // Request the definition of a symbol as the guest.
2220 let symbols = project_b
2221 .update(cx_b, |p, cx| p.symbols("two", cx))
2222 .await
2223 .unwrap();
2224 assert_eq!(symbols.len(), 1);
2225 assert_eq!(symbols[0].name, "TWO");
2226
2227 // Open one of the returned symbols.
2228 let buffer_b_2 = project_b
2229 .update(cx_b, |project, cx| {
2230 project.open_buffer_for_symbol(&symbols[0], cx)
2231 })
2232 .await
2233 .unwrap();
2234 buffer_b_2.read_with(cx_b, |buffer, _| {
2235 assert_eq!(
2236 buffer.file().unwrap().path().as_ref(),
2237 Path::new("../crate-2/two.rs")
2238 );
2239 });
2240
2241 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
2242 let mut fake_symbol = symbols[0].clone();
2243 fake_symbol.path = Path::new("/code/secrets").into();
2244 let error = project_b
2245 .update(cx_b, |project, cx| {
2246 project.open_buffer_for_symbol(&fake_symbol, cx)
2247 })
2248 .await
2249 .unwrap_err();
2250 assert!(error.to_string().contains("invalid symbol signature"));
2251}
2252
2253#[gpui::test(iterations = 10)]
2254async fn test_open_buffer_while_getting_definition_pointing_to_it(
2255 cx_a: &mut TestAppContext,
2256 cx_b: &mut TestAppContext,
2257 mut rng: StdRng,
2258) {
2259 cx_a.foreground().forbid_parking();
2260 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2261 let mut client_a = server.create_client(cx_a, "user_a").await;
2262 let mut client_b = server.create_client(cx_b, "user_b").await;
2263 server
2264 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2265 .await;
2266
2267 // Set up a fake language server.
2268 let mut language = Language::new(
2269 LanguageConfig {
2270 name: "Rust".into(),
2271 path_suffixes: vec!["rs".to_string()],
2272 ..Default::default()
2273 },
2274 Some(tree_sitter_rust::language()),
2275 );
2276 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2277 client_a.language_registry.add(Arc::new(language));
2278
2279 let fs = FakeFs::new(cx_a.background());
2280 fs.insert_tree(
2281 "/root",
2282 json!({
2283 "a.rs": "const ONE: usize = b::TWO;",
2284 "b.rs": "const TWO: usize = 2",
2285 }),
2286 )
2287 .await;
2288
2289 let (project_a, worktree_id) = client_a.build_local_project(fs, "/root", cx_a).await;
2290 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2291
2292 let buffer_b1 = cx_b
2293 .background()
2294 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2295 .await
2296 .unwrap();
2297
2298 let fake_language_server = fake_language_servers.next().await.unwrap();
2299 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2300 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2301 lsp::Location::new(
2302 lsp::Url::from_file_path("/root/b.rs").unwrap(),
2303 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2304 ),
2305 )))
2306 });
2307
2308 let definitions;
2309 let buffer_b2;
2310 if rng.gen() {
2311 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2312 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2313 } else {
2314 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2315 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2316 }
2317
2318 let buffer_b2 = buffer_b2.await.unwrap();
2319 let definitions = definitions.await.unwrap();
2320 assert_eq!(definitions.len(), 1);
2321 assert_eq!(definitions[0].buffer, buffer_b2);
2322}
2323
2324#[gpui::test(iterations = 10)]
2325async fn test_collaborating_with_code_actions(
2326 cx_a: &mut TestAppContext,
2327 cx_b: &mut TestAppContext,
2328) {
2329 cx_a.foreground().forbid_parking();
2330 cx_b.update(|cx| editor::init(cx));
2331 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2332 let mut client_a = server.create_client(cx_a, "user_a").await;
2333 let mut client_b = server.create_client(cx_b, "user_b").await;
2334 server
2335 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2336 .await;
2337
2338 // Set up a fake language server.
2339 let mut language = Language::new(
2340 LanguageConfig {
2341 name: "Rust".into(),
2342 path_suffixes: vec!["rs".to_string()],
2343 ..Default::default()
2344 },
2345 Some(tree_sitter_rust::language()),
2346 );
2347 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2348 client_a.language_registry.add(Arc::new(language));
2349
2350 let fs = FakeFs::new(cx_a.background());
2351 fs.insert_tree(
2352 "/a",
2353 json!({
2354 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2355 "other.rs": "pub fn foo() -> usize { 4 }",
2356 }),
2357 )
2358 .await;
2359 let (project_a, worktree_id) = client_a.build_local_project(fs, "/a", cx_a).await;
2360
2361 // Join the project as client B.
2362 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2363 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2364 let editor_b = workspace_b
2365 .update(cx_b, |workspace, cx| {
2366 workspace.open_path((worktree_id, "main.rs"), true, cx)
2367 })
2368 .await
2369 .unwrap()
2370 .downcast::<Editor>()
2371 .unwrap();
2372
2373 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2374 fake_language_server
2375 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2376 assert_eq!(
2377 params.text_document.uri,
2378 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2379 );
2380 assert_eq!(params.range.start, lsp::Position::new(0, 0));
2381 assert_eq!(params.range.end, lsp::Position::new(0, 0));
2382 Ok(None)
2383 })
2384 .next()
2385 .await;
2386
2387 // Move cursor to a location that contains code actions.
2388 editor_b.update(cx_b, |editor, cx| {
2389 editor.change_selections(None, cx, |s| {
2390 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
2391 });
2392 cx.focus(&editor_b);
2393 });
2394
2395 fake_language_server
2396 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2397 assert_eq!(
2398 params.text_document.uri,
2399 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2400 );
2401 assert_eq!(params.range.start, lsp::Position::new(1, 31));
2402 assert_eq!(params.range.end, lsp::Position::new(1, 31));
2403
2404 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
2405 lsp::CodeAction {
2406 title: "Inline into all callers".to_string(),
2407 edit: Some(lsp::WorkspaceEdit {
2408 changes: Some(
2409 [
2410 (
2411 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2412 vec![lsp::TextEdit::new(
2413 lsp::Range::new(
2414 lsp::Position::new(1, 22),
2415 lsp::Position::new(1, 34),
2416 ),
2417 "4".to_string(),
2418 )],
2419 ),
2420 (
2421 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2422 vec![lsp::TextEdit::new(
2423 lsp::Range::new(
2424 lsp::Position::new(0, 0),
2425 lsp::Position::new(0, 27),
2426 ),
2427 "".to_string(),
2428 )],
2429 ),
2430 ]
2431 .into_iter()
2432 .collect(),
2433 ),
2434 ..Default::default()
2435 }),
2436 data: Some(json!({
2437 "codeActionParams": {
2438 "range": {
2439 "start": {"line": 1, "column": 31},
2440 "end": {"line": 1, "column": 31},
2441 }
2442 }
2443 })),
2444 ..Default::default()
2445 },
2446 )]))
2447 })
2448 .next()
2449 .await;
2450
2451 // Toggle code actions and wait for them to display.
2452 editor_b.update(cx_b, |editor, cx| {
2453 editor.toggle_code_actions(
2454 &ToggleCodeActions {
2455 deployed_from_indicator: false,
2456 },
2457 cx,
2458 );
2459 });
2460 editor_b
2461 .condition(&cx_b, |editor, _| editor.context_menu_visible())
2462 .await;
2463
2464 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
2465
2466 // Confirming the code action will trigger a resolve request.
2467 let confirm_action = workspace_b
2468 .update(cx_b, |workspace, cx| {
2469 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
2470 })
2471 .unwrap();
2472 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2473 |_, _| async move {
2474 Ok(lsp::CodeAction {
2475 title: "Inline into all callers".to_string(),
2476 edit: Some(lsp::WorkspaceEdit {
2477 changes: Some(
2478 [
2479 (
2480 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2481 vec![lsp::TextEdit::new(
2482 lsp::Range::new(
2483 lsp::Position::new(1, 22),
2484 lsp::Position::new(1, 34),
2485 ),
2486 "4".to_string(),
2487 )],
2488 ),
2489 (
2490 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2491 vec![lsp::TextEdit::new(
2492 lsp::Range::new(
2493 lsp::Position::new(0, 0),
2494 lsp::Position::new(0, 27),
2495 ),
2496 "".to_string(),
2497 )],
2498 ),
2499 ]
2500 .into_iter()
2501 .collect(),
2502 ),
2503 ..Default::default()
2504 }),
2505 ..Default::default()
2506 })
2507 },
2508 );
2509
2510 // After the action is confirmed, an editor containing both modified files is opened.
2511 confirm_action.await.unwrap();
2512 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2513 workspace
2514 .active_item(cx)
2515 .unwrap()
2516 .downcast::<Editor>()
2517 .unwrap()
2518 });
2519 code_action_editor.update(cx_b, |editor, cx| {
2520 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2521 editor.undo(&Undo, cx);
2522 assert_eq!(
2523 editor.text(cx),
2524 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
2525 );
2526 editor.redo(&Redo, cx);
2527 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2528 });
2529}
2530
2531#[gpui::test(iterations = 10)]
2532async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2533 cx_a.foreground().forbid_parking();
2534 cx_b.update(|cx| editor::init(cx));
2535 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2536 let mut client_a = server.create_client(cx_a, "user_a").await;
2537 let mut client_b = server.create_client(cx_b, "user_b").await;
2538 server
2539 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2540 .await;
2541
2542 // Set up a fake language server.
2543 let mut language = Language::new(
2544 LanguageConfig {
2545 name: "Rust".into(),
2546 path_suffixes: vec!["rs".to_string()],
2547 ..Default::default()
2548 },
2549 Some(tree_sitter_rust::language()),
2550 );
2551 let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
2552 capabilities: lsp::ServerCapabilities {
2553 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
2554 prepare_provider: Some(true),
2555 work_done_progress_options: Default::default(),
2556 })),
2557 ..Default::default()
2558 },
2559 ..Default::default()
2560 });
2561 client_a.language_registry.add(Arc::new(language));
2562
2563 let fs = FakeFs::new(cx_a.background());
2564 fs.insert_tree(
2565 "/dir",
2566 json!({
2567 "one.rs": "const ONE: usize = 1;",
2568 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
2569 }),
2570 )
2571 .await;
2572
2573 let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
2574 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2575
2576 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2577 let editor_b = workspace_b
2578 .update(cx_b, |workspace, cx| {
2579 workspace.open_path((worktree_id, "one.rs"), true, cx)
2580 })
2581 .await
2582 .unwrap()
2583 .downcast::<Editor>()
2584 .unwrap();
2585 let fake_language_server = fake_language_servers.next().await.unwrap();
2586
2587 // Move cursor to a location that can be renamed.
2588 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
2589 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
2590 editor.rename(&Rename, cx).unwrap()
2591 });
2592
2593 fake_language_server
2594 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
2595 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
2596 assert_eq!(params.position, lsp::Position::new(0, 7));
2597 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
2598 lsp::Position::new(0, 6),
2599 lsp::Position::new(0, 9),
2600 ))))
2601 })
2602 .next()
2603 .await
2604 .unwrap();
2605 prepare_rename.await.unwrap();
2606 editor_b.update(cx_b, |editor, cx| {
2607 let rename = editor.pending_rename().unwrap();
2608 let buffer = editor.buffer().read(cx).snapshot(cx);
2609 assert_eq!(
2610 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
2611 6..9
2612 );
2613 rename.editor.update(cx, |rename_editor, cx| {
2614 rename_editor.buffer().update(cx, |rename_buffer, cx| {
2615 rename_buffer.edit([(0..3, "THREE")], cx);
2616 });
2617 });
2618 });
2619
2620 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
2621 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
2622 });
2623 fake_language_server
2624 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
2625 assert_eq!(
2626 params.text_document_position.text_document.uri.as_str(),
2627 "file:///dir/one.rs"
2628 );
2629 assert_eq!(
2630 params.text_document_position.position,
2631 lsp::Position::new(0, 6)
2632 );
2633 assert_eq!(params.new_name, "THREE");
2634 Ok(Some(lsp::WorkspaceEdit {
2635 changes: Some(
2636 [
2637 (
2638 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
2639 vec![lsp::TextEdit::new(
2640 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2641 "THREE".to_string(),
2642 )],
2643 ),
2644 (
2645 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
2646 vec![
2647 lsp::TextEdit::new(
2648 lsp::Range::new(
2649 lsp::Position::new(0, 24),
2650 lsp::Position::new(0, 27),
2651 ),
2652 "THREE".to_string(),
2653 ),
2654 lsp::TextEdit::new(
2655 lsp::Range::new(
2656 lsp::Position::new(0, 35),
2657 lsp::Position::new(0, 38),
2658 ),
2659 "THREE".to_string(),
2660 ),
2661 ],
2662 ),
2663 ]
2664 .into_iter()
2665 .collect(),
2666 ),
2667 ..Default::default()
2668 }))
2669 })
2670 .next()
2671 .await
2672 .unwrap();
2673 confirm_rename.await.unwrap();
2674
2675 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2676 workspace
2677 .active_item(cx)
2678 .unwrap()
2679 .downcast::<Editor>()
2680 .unwrap()
2681 });
2682 rename_editor.update(cx_b, |editor, cx| {
2683 assert_eq!(
2684 editor.text(cx),
2685 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
2686 );
2687 editor.undo(&Undo, cx);
2688 assert_eq!(
2689 editor.text(cx),
2690 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
2691 );
2692 editor.redo(&Redo, cx);
2693 assert_eq!(
2694 editor.text(cx),
2695 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
2696 );
2697 });
2698
2699 // Ensure temporary rename edits cannot be undone/redone.
2700 editor_b.update(cx_b, |editor, cx| {
2701 editor.undo(&Undo, cx);
2702 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
2703 editor.undo(&Undo, cx);
2704 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
2705 editor.redo(&Redo, cx);
2706 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
2707 })
2708}
2709
2710#[gpui::test(iterations = 10)]
2711async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2712 cx_a.foreground().forbid_parking();
2713 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2714 let client_a = server.create_client(cx_a, "user_a").await;
2715 let client_b = server.create_client(cx_b, "user_b").await;
2716
2717 // Create an org that includes these 2 users.
2718 let db = &server.app_state.db;
2719 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
2720 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
2721 .await
2722 .unwrap();
2723 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
2724 .await
2725 .unwrap();
2726
2727 // Create a channel that includes all the users.
2728 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
2729 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
2730 .await
2731 .unwrap();
2732 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
2733 .await
2734 .unwrap();
2735 db.create_channel_message(
2736 channel_id,
2737 client_b.current_user_id(&cx_b),
2738 "hello A, it's B.",
2739 OffsetDateTime::now_utc(),
2740 1,
2741 )
2742 .await
2743 .unwrap();
2744
2745 let channels_a =
2746 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
2747 channels_a
2748 .condition(cx_a, |list, _| list.available_channels().is_some())
2749 .await;
2750 channels_a.read_with(cx_a, |list, _| {
2751 assert_eq!(
2752 list.available_channels().unwrap(),
2753 &[ChannelDetails {
2754 id: channel_id.to_proto(),
2755 name: "test-channel".to_string()
2756 }]
2757 )
2758 });
2759 let channel_a = channels_a.update(cx_a, |this, cx| {
2760 this.get_channel(channel_id.to_proto(), cx).unwrap()
2761 });
2762 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
2763 channel_a
2764 .condition(&cx_a, |channel, _| {
2765 channel_messages(channel)
2766 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
2767 })
2768 .await;
2769
2770 let channels_b =
2771 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
2772 channels_b
2773 .condition(cx_b, |list, _| list.available_channels().is_some())
2774 .await;
2775 channels_b.read_with(cx_b, |list, _| {
2776 assert_eq!(
2777 list.available_channels().unwrap(),
2778 &[ChannelDetails {
2779 id: channel_id.to_proto(),
2780 name: "test-channel".to_string()
2781 }]
2782 )
2783 });
2784
2785 let channel_b = channels_b.update(cx_b, |this, cx| {
2786 this.get_channel(channel_id.to_proto(), cx).unwrap()
2787 });
2788 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
2789 channel_b
2790 .condition(&cx_b, |channel, _| {
2791 channel_messages(channel)
2792 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
2793 })
2794 .await;
2795
2796 channel_a
2797 .update(cx_a, |channel, cx| {
2798 channel
2799 .send_message("oh, hi B.".to_string(), cx)
2800 .unwrap()
2801 .detach();
2802 let task = channel.send_message("sup".to_string(), cx).unwrap();
2803 assert_eq!(
2804 channel_messages(channel),
2805 &[
2806 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
2807 ("user_a".to_string(), "oh, hi B.".to_string(), true),
2808 ("user_a".to_string(), "sup".to_string(), true)
2809 ]
2810 );
2811 task
2812 })
2813 .await
2814 .unwrap();
2815
2816 channel_b
2817 .condition(&cx_b, |channel, _| {
2818 channel_messages(channel)
2819 == [
2820 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
2821 ("user_a".to_string(), "oh, hi B.".to_string(), false),
2822 ("user_a".to_string(), "sup".to_string(), false),
2823 ]
2824 })
2825 .await;
2826
2827 assert_eq!(
2828 server
2829 .state()
2830 .await
2831 .channel(channel_id)
2832 .unwrap()
2833 .connection_ids
2834 .len(),
2835 2
2836 );
2837 cx_b.update(|_| drop(channel_b));
2838 server
2839 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
2840 .await;
2841
2842 cx_a.update(|_| drop(channel_a));
2843 server
2844 .condition(|state| state.channel(channel_id).is_none())
2845 .await;
2846}
2847
2848#[gpui::test(iterations = 10)]
2849async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
2850 cx_a.foreground().forbid_parking();
2851 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2852 let client_a = server.create_client(cx_a, "user_a").await;
2853
2854 let db = &server.app_state.db;
2855 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
2856 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
2857 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
2858 .await
2859 .unwrap();
2860 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
2861 .await
2862 .unwrap();
2863
2864 let channels_a =
2865 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
2866 channels_a
2867 .condition(cx_a, |list, _| list.available_channels().is_some())
2868 .await;
2869 let channel_a = channels_a.update(cx_a, |this, cx| {
2870 this.get_channel(channel_id.to_proto(), cx).unwrap()
2871 });
2872
2873 // Messages aren't allowed to be too long.
2874 channel_a
2875 .update(cx_a, |channel, cx| {
2876 let long_body = "this is long.\n".repeat(1024);
2877 channel.send_message(long_body, cx).unwrap()
2878 })
2879 .await
2880 .unwrap_err();
2881
2882 // Messages aren't allowed to be blank.
2883 channel_a.update(cx_a, |channel, cx| {
2884 channel.send_message(String::new(), cx).unwrap_err()
2885 });
2886
2887 // Leading and trailing whitespace are trimmed.
2888 channel_a
2889 .update(cx_a, |channel, cx| {
2890 channel
2891 .send_message("\n surrounded by whitespace \n".to_string(), cx)
2892 .unwrap()
2893 })
2894 .await
2895 .unwrap();
2896 assert_eq!(
2897 db.get_channel_messages(channel_id, 10, None)
2898 .await
2899 .unwrap()
2900 .iter()
2901 .map(|m| &m.body)
2902 .collect::<Vec<_>>(),
2903 &["surrounded by whitespace"]
2904 );
2905}
2906
2907#[gpui::test(iterations = 10)]
2908async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2909 cx_a.foreground().forbid_parking();
2910 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2911 let client_a = server.create_client(cx_a, "user_a").await;
2912 let client_b = server.create_client(cx_b, "user_b").await;
2913
2914 let mut status_b = client_b.status();
2915
2916 // Create an org that includes these 2 users.
2917 let db = &server.app_state.db;
2918 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
2919 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
2920 .await
2921 .unwrap();
2922 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
2923 .await
2924 .unwrap();
2925
2926 // Create a channel that includes all the users.
2927 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
2928 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
2929 .await
2930 .unwrap();
2931 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
2932 .await
2933 .unwrap();
2934 db.create_channel_message(
2935 channel_id,
2936 client_b.current_user_id(&cx_b),
2937 "hello A, it's B.",
2938 OffsetDateTime::now_utc(),
2939 2,
2940 )
2941 .await
2942 .unwrap();
2943
2944 let channels_a =
2945 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
2946 channels_a
2947 .condition(cx_a, |list, _| list.available_channels().is_some())
2948 .await;
2949
2950 channels_a.read_with(cx_a, |list, _| {
2951 assert_eq!(
2952 list.available_channels().unwrap(),
2953 &[ChannelDetails {
2954 id: channel_id.to_proto(),
2955 name: "test-channel".to_string()
2956 }]
2957 )
2958 });
2959 let channel_a = channels_a.update(cx_a, |this, cx| {
2960 this.get_channel(channel_id.to_proto(), cx).unwrap()
2961 });
2962 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
2963 channel_a
2964 .condition(&cx_a, |channel, _| {
2965 channel_messages(channel)
2966 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
2967 })
2968 .await;
2969
2970 let channels_b =
2971 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
2972 channels_b
2973 .condition(cx_b, |list, _| list.available_channels().is_some())
2974 .await;
2975 channels_b.read_with(cx_b, |list, _| {
2976 assert_eq!(
2977 list.available_channels().unwrap(),
2978 &[ChannelDetails {
2979 id: channel_id.to_proto(),
2980 name: "test-channel".to_string()
2981 }]
2982 )
2983 });
2984
2985 let channel_b = channels_b.update(cx_b, |this, cx| {
2986 this.get_channel(channel_id.to_proto(), cx).unwrap()
2987 });
2988 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
2989 channel_b
2990 .condition(&cx_b, |channel, _| {
2991 channel_messages(channel)
2992 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
2993 })
2994 .await;
2995
2996 // Disconnect client B, ensuring we can still access its cached channel data.
2997 server.forbid_connections();
2998 server.disconnect_client(client_b.current_user_id(&cx_b));
2999 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
3000 while !matches!(
3001 status_b.next().await,
3002 Some(client::Status::ReconnectionError { .. })
3003 ) {}
3004
3005 channels_b.read_with(cx_b, |channels, _| {
3006 assert_eq!(
3007 channels.available_channels().unwrap(),
3008 [ChannelDetails {
3009 id: channel_id.to_proto(),
3010 name: "test-channel".to_string()
3011 }]
3012 )
3013 });
3014 channel_b.read_with(cx_b, |channel, _| {
3015 assert_eq!(
3016 channel_messages(channel),
3017 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3018 )
3019 });
3020
3021 // Send a message from client B while it is disconnected.
3022 channel_b
3023 .update(cx_b, |channel, cx| {
3024 let task = channel
3025 .send_message("can you see this?".to_string(), cx)
3026 .unwrap();
3027 assert_eq!(
3028 channel_messages(channel),
3029 &[
3030 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3031 ("user_b".to_string(), "can you see this?".to_string(), true)
3032 ]
3033 );
3034 task
3035 })
3036 .await
3037 .unwrap_err();
3038
3039 // Send a message from client A while B is disconnected.
3040 channel_a
3041 .update(cx_a, |channel, cx| {
3042 channel
3043 .send_message("oh, hi B.".to_string(), cx)
3044 .unwrap()
3045 .detach();
3046 let task = channel.send_message("sup".to_string(), cx).unwrap();
3047 assert_eq!(
3048 channel_messages(channel),
3049 &[
3050 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3051 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3052 ("user_a".to_string(), "sup".to_string(), true)
3053 ]
3054 );
3055 task
3056 })
3057 .await
3058 .unwrap();
3059
3060 // Give client B a chance to reconnect.
3061 server.allow_connections();
3062 cx_b.foreground().advance_clock(Duration::from_secs(10));
3063
3064 // Verify that B sees the new messages upon reconnection, as well as the message client B
3065 // sent while offline.
3066 channel_b
3067 .condition(&cx_b, |channel, _| {
3068 channel_messages(channel)
3069 == [
3070 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3071 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3072 ("user_a".to_string(), "sup".to_string(), false),
3073 ("user_b".to_string(), "can you see this?".to_string(), false),
3074 ]
3075 })
3076 .await;
3077
3078 // Ensure client A and B can communicate normally after reconnection.
3079 channel_a
3080 .update(cx_a, |channel, cx| {
3081 channel.send_message("you online?".to_string(), cx).unwrap()
3082 })
3083 .await
3084 .unwrap();
3085 channel_b
3086 .condition(&cx_b, |channel, _| {
3087 channel_messages(channel)
3088 == [
3089 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3090 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3091 ("user_a".to_string(), "sup".to_string(), false),
3092 ("user_b".to_string(), "can you see this?".to_string(), false),
3093 ("user_a".to_string(), "you online?".to_string(), false),
3094 ]
3095 })
3096 .await;
3097
3098 channel_b
3099 .update(cx_b, |channel, cx| {
3100 channel.send_message("yep".to_string(), cx).unwrap()
3101 })
3102 .await
3103 .unwrap();
3104 channel_a
3105 .condition(&cx_a, |channel, _| {
3106 channel_messages(channel)
3107 == [
3108 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3109 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3110 ("user_a".to_string(), "sup".to_string(), false),
3111 ("user_b".to_string(), "can you see this?".to_string(), false),
3112 ("user_a".to_string(), "you online?".to_string(), false),
3113 ("user_b".to_string(), "yep".to_string(), false),
3114 ]
3115 })
3116 .await;
3117}
3118
3119#[gpui::test(iterations = 10)]
3120async fn test_contacts(
3121 deterministic: Arc<Deterministic>,
3122 cx_a: &mut TestAppContext,
3123 cx_b: &mut TestAppContext,
3124 cx_c: &mut TestAppContext,
3125) {
3126 cx_a.foreground().forbid_parking();
3127 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3128 let mut client_a = server.create_client(cx_a, "user_a").await;
3129 let mut client_b = server.create_client(cx_b, "user_b").await;
3130 let client_c = server.create_client(cx_c, "user_c").await;
3131 server
3132 .make_contacts(vec![
3133 (&client_a, cx_a),
3134 (&client_b, cx_b),
3135 (&client_c, cx_c),
3136 ])
3137 .await;
3138
3139 deterministic.run_until_parked();
3140 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3141 client.user_store.read_with(*cx, |store, _| {
3142 assert_eq!(
3143 contacts(store),
3144 [
3145 ("user_a", true, vec![]),
3146 ("user_b", true, vec![]),
3147 ("user_c", true, vec![])
3148 ],
3149 "{} has the wrong contacts",
3150 client.username
3151 )
3152 });
3153 }
3154
3155 // Share a project as client A.
3156 let fs = FakeFs::new(cx_a.background());
3157 fs.create_dir(Path::new("/a")).await.unwrap();
3158 let (project_a, _) = client_a.build_local_project(fs, "/a", cx_a).await;
3159
3160 deterministic.run_until_parked();
3161 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3162 client.user_store.read_with(*cx, |store, _| {
3163 assert_eq!(
3164 contacts(store),
3165 [
3166 ("user_a", true, vec![("a", vec![])]),
3167 ("user_b", true, vec![]),
3168 ("user_c", true, vec![])
3169 ],
3170 "{} has the wrong contacts",
3171 client.username
3172 )
3173 });
3174 }
3175
3176 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3177
3178 deterministic.run_until_parked();
3179 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3180 client.user_store.read_with(*cx, |store, _| {
3181 assert_eq!(
3182 contacts(store),
3183 [
3184 ("user_a", true, vec![("a", vec!["user_b"])]),
3185 ("user_b", true, vec![]),
3186 ("user_c", true, vec![])
3187 ],
3188 "{} has the wrong contacts",
3189 client.username
3190 )
3191 });
3192 }
3193
3194 // Add a local project as client B
3195 let fs = FakeFs::new(cx_b.background());
3196 fs.create_dir(Path::new("/b")).await.unwrap();
3197 let (_project_b, _) = client_b.build_local_project(fs, "/b", cx_b).await;
3198
3199 deterministic.run_until_parked();
3200 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3201 client.user_store.read_with(*cx, |store, _| {
3202 assert_eq!(
3203 contacts(store),
3204 [
3205 ("user_a", true, vec![("a", vec!["user_b"])]),
3206 ("user_b", true, vec![("b", vec![])]),
3207 ("user_c", true, vec![])
3208 ],
3209 "{} has the wrong contacts",
3210 client.username
3211 )
3212 });
3213 }
3214
3215 project_a
3216 .condition(&cx_a, |project, _| {
3217 project.collaborators().contains_key(&client_b.peer_id)
3218 })
3219 .await;
3220
3221 client_a.project.take();
3222 cx_a.update(move |_| drop(project_a));
3223 deterministic.run_until_parked();
3224 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3225 client.user_store.read_with(*cx, |store, _| {
3226 assert_eq!(
3227 contacts(store),
3228 [
3229 ("user_a", true, vec![]),
3230 ("user_b", true, vec![("b", vec![])]),
3231 ("user_c", true, vec![])
3232 ],
3233 "{} has the wrong contacts",
3234 client.username
3235 )
3236 });
3237 }
3238
3239 server.disconnect_client(client_c.current_user_id(cx_c));
3240 server.forbid_connections();
3241 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
3242 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
3243 client.user_store.read_with(*cx, |store, _| {
3244 assert_eq!(
3245 contacts(store),
3246 [
3247 ("user_a", true, vec![]),
3248 ("user_b", true, vec![("b", vec![])]),
3249 ("user_c", false, vec![])
3250 ],
3251 "{} has the wrong contacts",
3252 client.username
3253 )
3254 });
3255 }
3256 client_c
3257 .user_store
3258 .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
3259
3260 server.allow_connections();
3261 client_c
3262 .authenticate_and_connect(false, &cx_c.to_async())
3263 .await
3264 .unwrap();
3265
3266 deterministic.run_until_parked();
3267 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3268 client.user_store.read_with(*cx, |store, _| {
3269 assert_eq!(
3270 contacts(store),
3271 [
3272 ("user_a", true, vec![]),
3273 ("user_b", true, vec![("b", vec![])]),
3274 ("user_c", true, vec![])
3275 ],
3276 "{} has the wrong contacts",
3277 client.username
3278 )
3279 });
3280 }
3281
3282 fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
3283 user_store
3284 .contacts()
3285 .iter()
3286 .map(|contact| {
3287 let projects = contact
3288 .projects
3289 .iter()
3290 .map(|p| {
3291 (
3292 p.worktree_root_names[0].as_str(),
3293 p.guests.iter().map(|p| p.github_login.as_str()).collect(),
3294 )
3295 })
3296 .collect();
3297 (contact.user.github_login.as_str(), contact.online, projects)
3298 })
3299 .collect()
3300 }
3301}
3302
3303#[gpui::test(iterations = 10)]
3304async fn test_contact_requests(
3305 executor: Arc<Deterministic>,
3306 cx_a: &mut TestAppContext,
3307 cx_a2: &mut TestAppContext,
3308 cx_b: &mut TestAppContext,
3309 cx_b2: &mut TestAppContext,
3310 cx_c: &mut TestAppContext,
3311 cx_c2: &mut TestAppContext,
3312) {
3313 cx_a.foreground().forbid_parking();
3314
3315 // Connect to a server as 3 clients.
3316 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3317 let client_a = server.create_client(cx_a, "user_a").await;
3318 let client_a2 = server.create_client(cx_a2, "user_a").await;
3319 let client_b = server.create_client(cx_b, "user_b").await;
3320 let client_b2 = server.create_client(cx_b2, "user_b").await;
3321 let client_c = server.create_client(cx_c, "user_c").await;
3322 let client_c2 = server.create_client(cx_c2, "user_c").await;
3323
3324 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
3325 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
3326 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
3327
3328 // User A and User C request that user B become their contact.
3329 client_a
3330 .user_store
3331 .update(cx_a, |store, cx| {
3332 store.request_contact(client_b.user_id().unwrap(), cx)
3333 })
3334 .await
3335 .unwrap();
3336 client_c
3337 .user_store
3338 .update(cx_c, |store, cx| {
3339 store.request_contact(client_b.user_id().unwrap(), cx)
3340 })
3341 .await
3342 .unwrap();
3343 executor.run_until_parked();
3344
3345 // All users see the pending request appear in all their clients.
3346 assert_eq!(
3347 client_a.summarize_contacts(&cx_a).outgoing_requests,
3348 &["user_b"]
3349 );
3350 assert_eq!(
3351 client_a2.summarize_contacts(&cx_a2).outgoing_requests,
3352 &["user_b"]
3353 );
3354 assert_eq!(
3355 client_b.summarize_contacts(&cx_b).incoming_requests,
3356 &["user_a", "user_c"]
3357 );
3358 assert_eq!(
3359 client_b2.summarize_contacts(&cx_b2).incoming_requests,
3360 &["user_a", "user_c"]
3361 );
3362 assert_eq!(
3363 client_c.summarize_contacts(&cx_c).outgoing_requests,
3364 &["user_b"]
3365 );
3366 assert_eq!(
3367 client_c2.summarize_contacts(&cx_c2).outgoing_requests,
3368 &["user_b"]
3369 );
3370
3371 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
3372 disconnect_and_reconnect(&client_a, cx_a).await;
3373 disconnect_and_reconnect(&client_b, cx_b).await;
3374 disconnect_and_reconnect(&client_c, cx_c).await;
3375 executor.run_until_parked();
3376 assert_eq!(
3377 client_a.summarize_contacts(&cx_a).outgoing_requests,
3378 &["user_b"]
3379 );
3380 assert_eq!(
3381 client_b.summarize_contacts(&cx_b).incoming_requests,
3382 &["user_a", "user_c"]
3383 );
3384 assert_eq!(
3385 client_c.summarize_contacts(&cx_c).outgoing_requests,
3386 &["user_b"]
3387 );
3388
3389 // User B accepts the request from user A.
3390 client_b
3391 .user_store
3392 .update(cx_b, |store, cx| {
3393 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
3394 })
3395 .await
3396 .unwrap();
3397
3398 executor.run_until_parked();
3399
3400 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
3401 let contacts_b = client_b.summarize_contacts(&cx_b);
3402 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3403 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
3404 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3405 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3406 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
3407
3408 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
3409 let contacts_a = client_a.summarize_contacts(&cx_a);
3410 assert_eq!(contacts_a.current, &["user_a", "user_b"]);
3411 assert!(contacts_a.outgoing_requests.is_empty());
3412 let contacts_a2 = client_a2.summarize_contacts(&cx_a2);
3413 assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
3414 assert!(contacts_a2.outgoing_requests.is_empty());
3415
3416 // Contacts are present upon connecting (tested here via disconnect/reconnect)
3417 disconnect_and_reconnect(&client_a, cx_a).await;
3418 disconnect_and_reconnect(&client_b, cx_b).await;
3419 disconnect_and_reconnect(&client_c, cx_c).await;
3420 executor.run_until_parked();
3421 assert_eq!(
3422 client_a.summarize_contacts(&cx_a).current,
3423 &["user_a", "user_b"]
3424 );
3425 assert_eq!(
3426 client_b.summarize_contacts(&cx_b).current,
3427 &["user_a", "user_b"]
3428 );
3429 assert_eq!(
3430 client_b.summarize_contacts(&cx_b).incoming_requests,
3431 &["user_c"]
3432 );
3433 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3434 assert_eq!(
3435 client_c.summarize_contacts(&cx_c).outgoing_requests,
3436 &["user_b"]
3437 );
3438
3439 // User B rejects the request from user C.
3440 client_b
3441 .user_store
3442 .update(cx_b, |store, cx| {
3443 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
3444 })
3445 .await
3446 .unwrap();
3447
3448 executor.run_until_parked();
3449
3450 // User B doesn't see user C as their contact, and the incoming request from them is removed.
3451 let contacts_b = client_b.summarize_contacts(&cx_b);
3452 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3453 assert!(contacts_b.incoming_requests.is_empty());
3454 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3455 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3456 assert!(contacts_b2.incoming_requests.is_empty());
3457
3458 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
3459 let contacts_c = client_c.summarize_contacts(&cx_c);
3460 assert_eq!(contacts_c.current, &["user_c"]);
3461 assert!(contacts_c.outgoing_requests.is_empty());
3462 let contacts_c2 = client_c2.summarize_contacts(&cx_c2);
3463 assert_eq!(contacts_c2.current, &["user_c"]);
3464 assert!(contacts_c2.outgoing_requests.is_empty());
3465
3466 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
3467 disconnect_and_reconnect(&client_a, cx_a).await;
3468 disconnect_and_reconnect(&client_b, cx_b).await;
3469 disconnect_and_reconnect(&client_c, cx_c).await;
3470 executor.run_until_parked();
3471 assert_eq!(
3472 client_a.summarize_contacts(&cx_a).current,
3473 &["user_a", "user_b"]
3474 );
3475 assert_eq!(
3476 client_b.summarize_contacts(&cx_b).current,
3477 &["user_a", "user_b"]
3478 );
3479 assert!(client_b
3480 .summarize_contacts(&cx_b)
3481 .incoming_requests
3482 .is_empty());
3483 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3484 assert!(client_c
3485 .summarize_contacts(&cx_c)
3486 .outgoing_requests
3487 .is_empty());
3488
3489 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
3490 client.disconnect(&cx.to_async()).unwrap();
3491 client.clear_contacts(cx).await;
3492 client
3493 .authenticate_and_connect(false, &cx.to_async())
3494 .await
3495 .unwrap();
3496 }
3497}
3498
3499#[gpui::test(iterations = 10)]
3500async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3501 cx_a.foreground().forbid_parking();
3502 let fs = FakeFs::new(cx_a.background());
3503
3504 // 2 clients connect to a server.
3505 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3506 let mut client_a = server.create_client(cx_a, "user_a").await;
3507 let mut client_b = server.create_client(cx_b, "user_b").await;
3508 server
3509 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3510 .await;
3511 cx_a.update(editor::init);
3512 cx_b.update(editor::init);
3513
3514 // Client A shares a project.
3515 fs.insert_tree(
3516 "/a",
3517 json!({
3518 "1.txt": "one",
3519 "2.txt": "two",
3520 "3.txt": "three",
3521 }),
3522 )
3523 .await;
3524 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
3525
3526 // Client B joins the project.
3527 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3528
3529 // Client A opens some editors.
3530 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3531 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
3532 let editor_a1 = workspace_a
3533 .update(cx_a, |workspace, cx| {
3534 workspace.open_path((worktree_id, "1.txt"), true, cx)
3535 })
3536 .await
3537 .unwrap()
3538 .downcast::<Editor>()
3539 .unwrap();
3540 let editor_a2 = workspace_a
3541 .update(cx_a, |workspace, cx| {
3542 workspace.open_path((worktree_id, "2.txt"), true, cx)
3543 })
3544 .await
3545 .unwrap()
3546 .downcast::<Editor>()
3547 .unwrap();
3548
3549 // Client B opens an editor.
3550 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3551 let editor_b1 = workspace_b
3552 .update(cx_b, |workspace, cx| {
3553 workspace.open_path((worktree_id, "1.txt"), true, cx)
3554 })
3555 .await
3556 .unwrap()
3557 .downcast::<Editor>()
3558 .unwrap();
3559
3560 let client_a_id = project_b.read_with(cx_b, |project, _| {
3561 project.collaborators().values().next().unwrap().peer_id
3562 });
3563 let client_b_id = project_a.read_with(cx_a, |project, _| {
3564 project.collaborators().values().next().unwrap().peer_id
3565 });
3566
3567 // When client B starts following client A, all visible view states are replicated to client B.
3568 editor_a1.update(cx_a, |editor, cx| {
3569 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
3570 });
3571 editor_a2.update(cx_a, |editor, cx| {
3572 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
3573 });
3574 workspace_b
3575 .update(cx_b, |workspace, cx| {
3576 workspace
3577 .toggle_follow(&ToggleFollow(client_a_id), cx)
3578 .unwrap()
3579 })
3580 .await
3581 .unwrap();
3582
3583 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
3584 workspace
3585 .active_item(cx)
3586 .unwrap()
3587 .downcast::<Editor>()
3588 .unwrap()
3589 });
3590 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
3591 assert_eq!(
3592 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
3593 Some((worktree_id, "2.txt").into())
3594 );
3595 assert_eq!(
3596 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
3597 vec![2..3]
3598 );
3599 assert_eq!(
3600 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
3601 vec![0..1]
3602 );
3603
3604 // When client A activates a different editor, client B does so as well.
3605 workspace_a.update(cx_a, |workspace, cx| {
3606 workspace.activate_item(&editor_a1, cx)
3607 });
3608 workspace_b
3609 .condition(cx_b, |workspace, cx| {
3610 workspace.active_item(cx).unwrap().id() == editor_b1.id()
3611 })
3612 .await;
3613
3614 // When client A navigates back and forth, client B does so as well.
3615 workspace_a
3616 .update(cx_a, |workspace, cx| {
3617 workspace::Pane::go_back(workspace, None, cx)
3618 })
3619 .await;
3620 workspace_b
3621 .condition(cx_b, |workspace, cx| {
3622 workspace.active_item(cx).unwrap().id() == editor_b2.id()
3623 })
3624 .await;
3625
3626 workspace_a
3627 .update(cx_a, |workspace, cx| {
3628 workspace::Pane::go_forward(workspace, None, cx)
3629 })
3630 .await;
3631 workspace_b
3632 .condition(cx_b, |workspace, cx| {
3633 workspace.active_item(cx).unwrap().id() == editor_b1.id()
3634 })
3635 .await;
3636
3637 // Changes to client A's editor are reflected on client B.
3638 editor_a1.update(cx_a, |editor, cx| {
3639 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
3640 });
3641 editor_b1
3642 .condition(cx_b, |editor, cx| {
3643 editor.selections.ranges(cx) == vec![1..1, 2..2]
3644 })
3645 .await;
3646
3647 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
3648 editor_b1
3649 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
3650 .await;
3651
3652 editor_a1.update(cx_a, |editor, cx| {
3653 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3654 editor.set_scroll_position(vec2f(0., 100.), cx);
3655 });
3656 editor_b1
3657 .condition(cx_b, |editor, cx| {
3658 editor.selections.ranges(cx) == vec![3..3]
3659 })
3660 .await;
3661
3662 // After unfollowing, client B stops receiving updates from client A.
3663 workspace_b.update(cx_b, |workspace, cx| {
3664 workspace.unfollow(&workspace.active_pane().clone(), cx)
3665 });
3666 workspace_a.update(cx_a, |workspace, cx| {
3667 workspace.activate_item(&editor_a2, cx)
3668 });
3669 cx_a.foreground().run_until_parked();
3670 assert_eq!(
3671 workspace_b.read_with(cx_b, |workspace, cx| workspace
3672 .active_item(cx)
3673 .unwrap()
3674 .id()),
3675 editor_b1.id()
3676 );
3677
3678 // Client A starts following client B.
3679 workspace_a
3680 .update(cx_a, |workspace, cx| {
3681 workspace
3682 .toggle_follow(&ToggleFollow(client_b_id), cx)
3683 .unwrap()
3684 })
3685 .await
3686 .unwrap();
3687 assert_eq!(
3688 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
3689 Some(client_b_id)
3690 );
3691 assert_eq!(
3692 workspace_a.read_with(cx_a, |workspace, cx| workspace
3693 .active_item(cx)
3694 .unwrap()
3695 .id()),
3696 editor_a1.id()
3697 );
3698
3699 // Following interrupts when client B disconnects.
3700 client_b.disconnect(&cx_b.to_async()).unwrap();
3701 cx_a.foreground().run_until_parked();
3702 assert_eq!(
3703 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
3704 None
3705 );
3706}
3707
3708#[gpui::test(iterations = 10)]
3709async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3710 cx_a.foreground().forbid_parking();
3711 let fs = FakeFs::new(cx_a.background());
3712
3713 // 2 clients connect to a server.
3714 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3715 let mut client_a = server.create_client(cx_a, "user_a").await;
3716 let mut client_b = server.create_client(cx_b, "user_b").await;
3717 server
3718 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3719 .await;
3720 cx_a.update(editor::init);
3721 cx_b.update(editor::init);
3722
3723 // Client A shares a project.
3724 fs.insert_tree(
3725 "/a",
3726 json!({
3727 "1.txt": "one",
3728 "2.txt": "two",
3729 "3.txt": "three",
3730 "4.txt": "four",
3731 }),
3732 )
3733 .await;
3734 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
3735
3736 // Client B joins the project.
3737 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3738
3739 // Client A opens some editors.
3740 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3741 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
3742 let _editor_a1 = workspace_a
3743 .update(cx_a, |workspace, cx| {
3744 workspace.open_path((worktree_id, "1.txt"), true, cx)
3745 })
3746 .await
3747 .unwrap()
3748 .downcast::<Editor>()
3749 .unwrap();
3750
3751 // Client B opens an editor.
3752 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3753 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
3754 let _editor_b1 = workspace_b
3755 .update(cx_b, |workspace, cx| {
3756 workspace.open_path((worktree_id, "2.txt"), true, cx)
3757 })
3758 .await
3759 .unwrap()
3760 .downcast::<Editor>()
3761 .unwrap();
3762
3763 // Clients A and B follow each other in split panes
3764 workspace_a.update(cx_a, |workspace, cx| {
3765 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
3766 assert_ne!(*workspace.active_pane(), pane_a1);
3767 });
3768 workspace_a
3769 .update(cx_a, |workspace, cx| {
3770 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
3771 workspace
3772 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
3773 .unwrap()
3774 })
3775 .await
3776 .unwrap();
3777 workspace_b.update(cx_b, |workspace, cx| {
3778 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
3779 assert_ne!(*workspace.active_pane(), pane_b1);
3780 });
3781 workspace_b
3782 .update(cx_b, |workspace, cx| {
3783 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
3784 workspace
3785 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
3786 .unwrap()
3787 })
3788 .await
3789 .unwrap();
3790
3791 workspace_a
3792 .update(cx_a, |workspace, cx| {
3793 workspace.activate_next_pane(cx);
3794 assert_eq!(*workspace.active_pane(), pane_a1);
3795 workspace.open_path((worktree_id, "3.txt"), true, cx)
3796 })
3797 .await
3798 .unwrap();
3799 workspace_b
3800 .update(cx_b, |workspace, cx| {
3801 workspace.activate_next_pane(cx);
3802 assert_eq!(*workspace.active_pane(), pane_b1);
3803 workspace.open_path((worktree_id, "4.txt"), true, cx)
3804 })
3805 .await
3806 .unwrap();
3807 cx_a.foreground().run_until_parked();
3808
3809 // Ensure leader updates don't change the active pane of followers
3810 workspace_a.read_with(cx_a, |workspace, _| {
3811 assert_eq!(*workspace.active_pane(), pane_a1);
3812 });
3813 workspace_b.read_with(cx_b, |workspace, _| {
3814 assert_eq!(*workspace.active_pane(), pane_b1);
3815 });
3816
3817 // Ensure peers following each other doesn't cause an infinite loop.
3818 assert_eq!(
3819 workspace_a.read_with(cx_a, |workspace, cx| workspace
3820 .active_item(cx)
3821 .unwrap()
3822 .project_path(cx)),
3823 Some((worktree_id, "3.txt").into())
3824 );
3825 workspace_a.update(cx_a, |workspace, cx| {
3826 assert_eq!(
3827 workspace.active_item(cx).unwrap().project_path(cx),
3828 Some((worktree_id, "3.txt").into())
3829 );
3830 workspace.activate_next_pane(cx);
3831 assert_eq!(
3832 workspace.active_item(cx).unwrap().project_path(cx),
3833 Some((worktree_id, "4.txt").into())
3834 );
3835 });
3836 workspace_b.update(cx_b, |workspace, cx| {
3837 assert_eq!(
3838 workspace.active_item(cx).unwrap().project_path(cx),
3839 Some((worktree_id, "4.txt").into())
3840 );
3841 workspace.activate_next_pane(cx);
3842 assert_eq!(
3843 workspace.active_item(cx).unwrap().project_path(cx),
3844 Some((worktree_id, "3.txt").into())
3845 );
3846 });
3847}
3848
3849#[gpui::test(iterations = 10)]
3850async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3851 cx_a.foreground().forbid_parking();
3852
3853 // 2 clients connect to a server.
3854 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3855 let mut client_a = server.create_client(cx_a, "user_a").await;
3856 let mut client_b = server.create_client(cx_b, "user_b").await;
3857 server
3858 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3859 .await;
3860 cx_a.update(editor::init);
3861 cx_b.update(editor::init);
3862
3863 // Client A shares a project.
3864 let fs = FakeFs::new(cx_a.background());
3865 fs.insert_tree(
3866 "/a",
3867 json!({
3868 "1.txt": "one",
3869 "2.txt": "two",
3870 "3.txt": "three",
3871 }),
3872 )
3873 .await;
3874 let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
3875 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3876
3877 // Client A opens some editors.
3878 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3879 let _editor_a1 = workspace_a
3880 .update(cx_a, |workspace, cx| {
3881 workspace.open_path((worktree_id, "1.txt"), true, cx)
3882 })
3883 .await
3884 .unwrap()
3885 .downcast::<Editor>()
3886 .unwrap();
3887
3888 // Client B starts following client A.
3889 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3890 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
3891 let leader_id = project_b.read_with(cx_b, |project, _| {
3892 project.collaborators().values().next().unwrap().peer_id
3893 });
3894 workspace_b
3895 .update(cx_b, |workspace, cx| {
3896 workspace
3897 .toggle_follow(&ToggleFollow(leader_id), cx)
3898 .unwrap()
3899 })
3900 .await
3901 .unwrap();
3902 assert_eq!(
3903 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3904 Some(leader_id)
3905 );
3906 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
3907 workspace
3908 .active_item(cx)
3909 .unwrap()
3910 .downcast::<Editor>()
3911 .unwrap()
3912 });
3913
3914 // When client B moves, it automatically stops following client A.
3915 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
3916 assert_eq!(
3917 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3918 None
3919 );
3920
3921 workspace_b
3922 .update(cx_b, |workspace, cx| {
3923 workspace
3924 .toggle_follow(&ToggleFollow(leader_id), cx)
3925 .unwrap()
3926 })
3927 .await
3928 .unwrap();
3929 assert_eq!(
3930 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3931 Some(leader_id)
3932 );
3933
3934 // When client B edits, it automatically stops following client A.
3935 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
3936 assert_eq!(
3937 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3938 None
3939 );
3940
3941 workspace_b
3942 .update(cx_b, |workspace, cx| {
3943 workspace
3944 .toggle_follow(&ToggleFollow(leader_id), cx)
3945 .unwrap()
3946 })
3947 .await
3948 .unwrap();
3949 assert_eq!(
3950 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3951 Some(leader_id)
3952 );
3953
3954 // When client B scrolls, it automatically stops following client A.
3955 editor_b2.update(cx_b, |editor, cx| {
3956 editor.set_scroll_position(vec2f(0., 3.), cx)
3957 });
3958 assert_eq!(
3959 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3960 None
3961 );
3962
3963 workspace_b
3964 .update(cx_b, |workspace, cx| {
3965 workspace
3966 .toggle_follow(&ToggleFollow(leader_id), cx)
3967 .unwrap()
3968 })
3969 .await
3970 .unwrap();
3971 assert_eq!(
3972 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3973 Some(leader_id)
3974 );
3975
3976 // When client B activates a different pane, it continues following client A in the original pane.
3977 workspace_b.update(cx_b, |workspace, cx| {
3978 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
3979 });
3980 assert_eq!(
3981 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3982 Some(leader_id)
3983 );
3984
3985 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
3986 assert_eq!(
3987 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
3988 Some(leader_id)
3989 );
3990
3991 // When client B activates a different item in the original pane, it automatically stops following client A.
3992 workspace_b
3993 .update(cx_b, |workspace, cx| {
3994 workspace.open_path((worktree_id, "2.txt"), true, cx)
3995 })
3996 .await
3997 .unwrap();
3998 assert_eq!(
3999 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4000 None
4001 );
4002}
4003
4004#[gpui::test(iterations = 100)]
4005async fn test_random_collaboration(
4006 cx: &mut TestAppContext,
4007 deterministic: Arc<Deterministic>,
4008 rng: StdRng,
4009) {
4010 cx.foreground().forbid_parking();
4011 let max_peers = env::var("MAX_PEERS")
4012 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4013 .unwrap_or(5);
4014 assert!(max_peers <= 5);
4015
4016 let max_operations = env::var("OPERATIONS")
4017 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4018 .unwrap_or(10);
4019
4020 let rng = Arc::new(Mutex::new(rng));
4021
4022 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4023 let host_language_registry = Arc::new(LanguageRegistry::test());
4024
4025 let fs = FakeFs::new(cx.background());
4026 fs.insert_tree("/_collab", json!({"init": ""})).await;
4027
4028 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4029 let db = server.app_state.db.clone();
4030 let host_user_id = db.create_user("host", None, false).await.unwrap();
4031 for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
4032 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4033 server
4034 .app_state
4035 .db
4036 .send_contact_request(guest_user_id, host_user_id)
4037 .await
4038 .unwrap();
4039 server
4040 .app_state
4041 .db
4042 .respond_to_contact_request(host_user_id, guest_user_id, true)
4043 .await
4044 .unwrap();
4045 }
4046
4047 let mut clients = Vec::new();
4048 let mut user_ids = Vec::new();
4049 let mut op_start_signals = Vec::new();
4050
4051 let mut next_entity_id = 100000;
4052 let mut host_cx = TestAppContext::new(
4053 cx.foreground_platform(),
4054 cx.platform(),
4055 deterministic.build_foreground(next_entity_id),
4056 deterministic.build_background(),
4057 cx.font_cache(),
4058 cx.leak_detector(),
4059 next_entity_id,
4060 );
4061 let host = server.create_client(&mut host_cx, "host").await;
4062 let host_project = host_cx.update(|cx| {
4063 Project::local(
4064 true,
4065 host.client.clone(),
4066 host.user_store.clone(),
4067 host.project_store.clone(),
4068 host_language_registry.clone(),
4069 fs.clone(),
4070 cx,
4071 )
4072 });
4073 let host_project_id = host_project
4074 .update(&mut host_cx, |p, _| p.next_remote_id())
4075 .await;
4076
4077 let (collab_worktree, _) = host_project
4078 .update(&mut host_cx, |project, cx| {
4079 project.find_or_create_local_worktree("/_collab", true, cx)
4080 })
4081 .await
4082 .unwrap();
4083 collab_worktree
4084 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4085 .await;
4086
4087 // Set up fake language servers.
4088 let mut language = Language::new(
4089 LanguageConfig {
4090 name: "Rust".into(),
4091 path_suffixes: vec!["rs".to_string()],
4092 ..Default::default()
4093 },
4094 None,
4095 );
4096 let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
4097 name: "the-fake-language-server",
4098 capabilities: lsp::LanguageServer::full_capabilities(),
4099 initializer: Some(Box::new({
4100 let rng = rng.clone();
4101 let fs = fs.clone();
4102 let project = host_project.downgrade();
4103 move |fake_server: &mut FakeLanguageServer| {
4104 fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
4105 Ok(Some(lsp::CompletionResponse::Array(vec![
4106 lsp::CompletionItem {
4107 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4108 range: lsp::Range::new(
4109 lsp::Position::new(0, 0),
4110 lsp::Position::new(0, 0),
4111 ),
4112 new_text: "the-new-text".to_string(),
4113 })),
4114 ..Default::default()
4115 },
4116 ])))
4117 });
4118
4119 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4120 |_, _| async move {
4121 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4122 lsp::CodeAction {
4123 title: "the-code-action".to_string(),
4124 ..Default::default()
4125 },
4126 )]))
4127 },
4128 );
4129
4130 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4131 |params, _| async move {
4132 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4133 params.position,
4134 params.position,
4135 ))))
4136 },
4137 );
4138
4139 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4140 let fs = fs.clone();
4141 let rng = rng.clone();
4142 move |_, _| {
4143 let fs = fs.clone();
4144 let rng = rng.clone();
4145 async move {
4146 let files = fs.files().await;
4147 let mut rng = rng.lock();
4148 let count = rng.gen_range::<usize, _>(1..3);
4149 let files = (0..count)
4150 .map(|_| files.choose(&mut *rng).unwrap())
4151 .collect::<Vec<_>>();
4152 log::info!("LSP: Returning definitions in files {:?}", &files);
4153 Ok(Some(lsp::GotoDefinitionResponse::Array(
4154 files
4155 .into_iter()
4156 .map(|file| lsp::Location {
4157 uri: lsp::Url::from_file_path(file).unwrap(),
4158 range: Default::default(),
4159 })
4160 .collect(),
4161 )))
4162 }
4163 }
4164 });
4165
4166 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
4167 let rng = rng.clone();
4168 let project = project.clone();
4169 move |params, mut cx| {
4170 let highlights = if let Some(project) = project.upgrade(&cx) {
4171 project.update(&mut cx, |project, cx| {
4172 let path = params
4173 .text_document_position_params
4174 .text_document
4175 .uri
4176 .to_file_path()
4177 .unwrap();
4178 let (worktree, relative_path) =
4179 project.find_local_worktree(&path, cx)?;
4180 let project_path =
4181 ProjectPath::from((worktree.read(cx).id(), relative_path));
4182 let buffer = project.get_open_buffer(&project_path, cx)?.read(cx);
4183
4184 let mut highlights = Vec::new();
4185 let highlight_count = rng.lock().gen_range(1..=5);
4186 let mut prev_end = 0;
4187 for _ in 0..highlight_count {
4188 let range =
4189 buffer.random_byte_range(prev_end, &mut *rng.lock());
4190
4191 highlights.push(lsp::DocumentHighlight {
4192 range: range_to_lsp(range.to_point_utf16(buffer)),
4193 kind: Some(lsp::DocumentHighlightKind::READ),
4194 });
4195 prev_end = range.end;
4196 }
4197 Some(highlights)
4198 })
4199 } else {
4200 None
4201 };
4202 async move { Ok(highlights) }
4203 }
4204 });
4205 }
4206 })),
4207 ..Default::default()
4208 });
4209 host_language_registry.add(Arc::new(language));
4210
4211 let op_start_signal = futures::channel::mpsc::unbounded();
4212 user_ids.push(host.current_user_id(&host_cx));
4213 op_start_signals.push(op_start_signal.0);
4214 clients.push(host_cx.foreground().spawn(host.simulate_host(
4215 host_project,
4216 op_start_signal.1,
4217 rng.clone(),
4218 host_cx,
4219 )));
4220
4221 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
4222 rng.lock().gen_range(0..max_operations)
4223 } else {
4224 max_operations
4225 };
4226 let mut available_guests = vec![
4227 "guest-1".to_string(),
4228 "guest-2".to_string(),
4229 "guest-3".to_string(),
4230 "guest-4".to_string(),
4231 ];
4232 let mut operations = 0;
4233 while operations < max_operations {
4234 if operations == disconnect_host_at {
4235 server.disconnect_client(user_ids[0]);
4236 cx.foreground().advance_clock(RECEIVE_TIMEOUT);
4237 drop(op_start_signals);
4238 let mut clients = futures::future::join_all(clients).await;
4239 cx.foreground().run_until_parked();
4240
4241 let (host, mut host_cx, host_err) = clients.remove(0);
4242 if let Some(host_err) = host_err {
4243 log::error!("host error - {:?}", host_err);
4244 }
4245 host.project
4246 .as_ref()
4247 .unwrap()
4248 .read_with(&host_cx, |project, _| assert!(!project.is_shared()));
4249 for (guest, mut guest_cx, guest_err) in clients {
4250 if let Some(guest_err) = guest_err {
4251 log::error!("{} error - {:?}", guest.username, guest_err);
4252 }
4253
4254 let contacts = server
4255 .app_state
4256 .db
4257 .get_contacts(guest.current_user_id(&guest_cx))
4258 .await
4259 .unwrap();
4260 let contacts = server
4261 .store
4262 .read()
4263 .await
4264 .build_initial_contacts_update(contacts)
4265 .contacts;
4266 assert!(!contacts
4267 .iter()
4268 .flat_map(|contact| &contact.projects)
4269 .any(|project| project.id == host_project_id));
4270 guest
4271 .project
4272 .as_ref()
4273 .unwrap()
4274 .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4275 guest_cx.update(|_| drop(guest));
4276 }
4277 host_cx.update(|_| drop(host));
4278
4279 return;
4280 }
4281
4282 let distribution = rng.lock().gen_range(0..100);
4283 match distribution {
4284 0..=19 if !available_guests.is_empty() => {
4285 let guest_ix = rng.lock().gen_range(0..available_guests.len());
4286 let guest_username = available_guests.remove(guest_ix);
4287 log::info!("Adding new connection for {}", guest_username);
4288 next_entity_id += 100000;
4289 let mut guest_cx = TestAppContext::new(
4290 cx.foreground_platform(),
4291 cx.platform(),
4292 deterministic.build_foreground(next_entity_id),
4293 deterministic.build_background(),
4294 cx.font_cache(),
4295 cx.leak_detector(),
4296 next_entity_id,
4297 );
4298 let guest = server.create_client(&mut guest_cx, &guest_username).await;
4299 let guest_project = Project::remote(
4300 host_project_id,
4301 guest.client.clone(),
4302 guest.user_store.clone(),
4303 guest.project_store.clone(),
4304 guest_lang_registry.clone(),
4305 FakeFs::new(cx.background()),
4306 guest_cx.to_async(),
4307 )
4308 .await
4309 .unwrap();
4310 let op_start_signal = futures::channel::mpsc::unbounded();
4311 user_ids.push(guest.current_user_id(&guest_cx));
4312 op_start_signals.push(op_start_signal.0);
4313 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
4314 guest_username.clone(),
4315 guest_project,
4316 op_start_signal.1,
4317 rng.clone(),
4318 guest_cx,
4319 )));
4320
4321 log::info!("Added connection for {}", guest_username);
4322 operations += 1;
4323 }
4324 20..=29 if clients.len() > 1 => {
4325 let guest_ix = rng.lock().gen_range(1..clients.len());
4326 log::info!("Removing guest {}", user_ids[guest_ix]);
4327 let removed_guest_id = user_ids.remove(guest_ix);
4328 let guest = clients.remove(guest_ix);
4329 op_start_signals.remove(guest_ix);
4330 server.forbid_connections();
4331 server.disconnect_client(removed_guest_id);
4332 cx.foreground().advance_clock(RECEIVE_TIMEOUT);
4333 let (guest, mut guest_cx, guest_err) = guest.await;
4334 server.allow_connections();
4335
4336 if let Some(guest_err) = guest_err {
4337 log::error!("{} error - {:?}", guest.username, guest_err);
4338 }
4339 guest
4340 .project
4341 .as_ref()
4342 .unwrap()
4343 .read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4344 for user_id in &user_ids {
4345 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
4346 let contacts = server
4347 .store
4348 .read()
4349 .await
4350 .build_initial_contacts_update(contacts)
4351 .contacts;
4352 for contact in contacts {
4353 if contact.online {
4354 assert_ne!(
4355 contact.user_id, removed_guest_id.0 as u64,
4356 "removed guest is still a contact of another peer"
4357 );
4358 }
4359 for project in contact.projects {
4360 for project_guest_id in project.guests {
4361 assert_ne!(
4362 project_guest_id, removed_guest_id.0 as u64,
4363 "removed guest appears as still participating on a project"
4364 );
4365 }
4366 }
4367 }
4368 }
4369
4370 log::info!("{} removed", guest.username);
4371 available_guests.push(guest.username.clone());
4372 guest_cx.update(|_| drop(guest));
4373
4374 operations += 1;
4375 }
4376 _ => {
4377 while operations < max_operations && rng.lock().gen_bool(0.7) {
4378 op_start_signals
4379 .choose(&mut *rng.lock())
4380 .unwrap()
4381 .unbounded_send(())
4382 .unwrap();
4383 operations += 1;
4384 }
4385
4386 if rng.lock().gen_bool(0.8) {
4387 cx.foreground().run_until_parked();
4388 }
4389 }
4390 }
4391 }
4392
4393 drop(op_start_signals);
4394 let mut clients = futures::future::join_all(clients).await;
4395 cx.foreground().run_until_parked();
4396
4397 let (host_client, mut host_cx, host_err) = clients.remove(0);
4398 if let Some(host_err) = host_err {
4399 panic!("host error - {:?}", host_err);
4400 }
4401 let host_project = host_client.project.as_ref().unwrap();
4402 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
4403 project
4404 .worktrees(cx)
4405 .map(|worktree| {
4406 let snapshot = worktree.read(cx).snapshot();
4407 (snapshot.id(), snapshot)
4408 })
4409 .collect::<BTreeMap<_, _>>()
4410 });
4411
4412 host_client
4413 .project
4414 .as_ref()
4415 .unwrap()
4416 .read_with(&host_cx, |project, cx| project.check_invariants(cx));
4417
4418 for (guest_client, mut guest_cx, guest_err) in clients.into_iter() {
4419 if let Some(guest_err) = guest_err {
4420 panic!("{} error - {:?}", guest_client.username, guest_err);
4421 }
4422 let worktree_snapshots =
4423 guest_client
4424 .project
4425 .as_ref()
4426 .unwrap()
4427 .read_with(&guest_cx, |project, cx| {
4428 project
4429 .worktrees(cx)
4430 .map(|worktree| {
4431 let worktree = worktree.read(cx);
4432 (worktree.id(), worktree.snapshot())
4433 })
4434 .collect::<BTreeMap<_, _>>()
4435 });
4436
4437 assert_eq!(
4438 worktree_snapshots.keys().collect::<Vec<_>>(),
4439 host_worktree_snapshots.keys().collect::<Vec<_>>(),
4440 "{} has different worktrees than the host",
4441 guest_client.username
4442 );
4443 for (id, host_snapshot) in &host_worktree_snapshots {
4444 let guest_snapshot = &worktree_snapshots[id];
4445 assert_eq!(
4446 guest_snapshot.root_name(),
4447 host_snapshot.root_name(),
4448 "{} has different root name than the host for worktree {}",
4449 guest_client.username,
4450 id
4451 );
4452 assert_eq!(
4453 guest_snapshot.entries(false).collect::<Vec<_>>(),
4454 host_snapshot.entries(false).collect::<Vec<_>>(),
4455 "{} has different snapshot than the host for worktree {}",
4456 guest_client.username,
4457 id
4458 );
4459 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
4460 }
4461
4462 guest_client
4463 .project
4464 .as_ref()
4465 .unwrap()
4466 .read_with(&guest_cx, |project, cx| project.check_invariants(cx));
4467
4468 for guest_buffer in &guest_client.buffers {
4469 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
4470 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
4471 project.buffer_for_id(buffer_id, cx).expect(&format!(
4472 "host does not have buffer for guest:{}, peer:{}, id:{}",
4473 guest_client.username, guest_client.peer_id, buffer_id
4474 ))
4475 });
4476 let path =
4477 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
4478
4479 assert_eq!(
4480 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
4481 0,
4482 "{}, buffer {}, path {:?} has deferred operations",
4483 guest_client.username,
4484 buffer_id,
4485 path,
4486 );
4487 assert_eq!(
4488 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
4489 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
4490 "{}, buffer {}, path {:?}, differs from the host's buffer",
4491 guest_client.username,
4492 buffer_id,
4493 path
4494 );
4495 }
4496
4497 guest_cx.update(|_| drop(guest_client));
4498 }
4499
4500 host_cx.update(|_| drop(host_client));
4501}
4502
4503struct TestServer {
4504 peer: Arc<Peer>,
4505 app_state: Arc<AppState>,
4506 server: Arc<Server>,
4507 foreground: Rc<executor::Foreground>,
4508 notifications: mpsc::UnboundedReceiver<()>,
4509 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
4510 forbid_connections: Arc<AtomicBool>,
4511 _test_db: TestDb,
4512}
4513
4514impl TestServer {
4515 async fn start(
4516 foreground: Rc<executor::Foreground>,
4517 background: Arc<executor::Background>,
4518 ) -> Self {
4519 let test_db = TestDb::fake(background);
4520 let app_state = Self::build_app_state(&test_db).await;
4521 let peer = Peer::new();
4522 let notifications = mpsc::unbounded();
4523 let server = Server::new(app_state.clone(), Some(notifications.0));
4524 Self {
4525 peer,
4526 app_state,
4527 server,
4528 foreground,
4529 notifications: notifications.1,
4530 connection_killers: Default::default(),
4531 forbid_connections: Default::default(),
4532 _test_db: test_db,
4533 }
4534 }
4535
4536 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
4537 cx.update(|cx| {
4538 let settings = Settings::test(cx);
4539 cx.set_global(settings);
4540 });
4541
4542 let http = FakeHttpClient::with_404_response();
4543 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
4544 {
4545 user.id
4546 } else {
4547 self.app_state
4548 .db
4549 .create_user(name, None, false)
4550 .await
4551 .unwrap()
4552 };
4553 let client_name = name.to_string();
4554 let mut client = Client::new(http.clone());
4555 let server = self.server.clone();
4556 let db = self.app_state.db.clone();
4557 let connection_killers = self.connection_killers.clone();
4558 let forbid_connections = self.forbid_connections.clone();
4559 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
4560
4561 Arc::get_mut(&mut client)
4562 .unwrap()
4563 .override_authenticate(move |cx| {
4564 cx.spawn(|_| async move {
4565 let access_token = "the-token".to_string();
4566 Ok(Credentials {
4567 user_id: user_id.0 as u64,
4568 access_token,
4569 })
4570 })
4571 })
4572 .override_establish_connection(move |credentials, cx| {
4573 assert_eq!(credentials.user_id, user_id.0 as u64);
4574 assert_eq!(credentials.access_token, "the-token");
4575
4576 let server = server.clone();
4577 let db = db.clone();
4578 let connection_killers = connection_killers.clone();
4579 let forbid_connections = forbid_connections.clone();
4580 let client_name = client_name.clone();
4581 let connection_id_tx = connection_id_tx.clone();
4582 cx.spawn(move |cx| async move {
4583 if forbid_connections.load(SeqCst) {
4584 Err(EstablishConnectionError::other(anyhow!(
4585 "server is forbidding connections"
4586 )))
4587 } else {
4588 let (client_conn, server_conn, killed) =
4589 Connection::in_memory(cx.background());
4590 connection_killers.lock().insert(user_id, killed);
4591 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
4592 cx.background()
4593 .spawn(server.handle_connection(
4594 server_conn,
4595 client_name,
4596 user,
4597 Some(connection_id_tx),
4598 cx.background(),
4599 ))
4600 .detach();
4601 Ok(client_conn)
4602 }
4603 })
4604 });
4605
4606 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
4607 let project_store = cx.add_model(|_| ProjectStore::default());
4608 let app_state = Arc::new(workspace::AppState {
4609 client: client.clone(),
4610 user_store: user_store.clone(),
4611 project_store: project_store.clone(),
4612 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
4613 themes: ThemeRegistry::new((), cx.font_cache()),
4614 fs: FakeFs::new(cx.background()),
4615 build_window_options: || Default::default(),
4616 initialize_workspace: |_, _, _| unimplemented!(),
4617 });
4618
4619 Channel::init(&client);
4620 Project::init(&client);
4621 cx.update(|cx| workspace::init(app_state.clone(), cx));
4622
4623 client
4624 .authenticate_and_connect(false, &cx.to_async())
4625 .await
4626 .unwrap();
4627 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
4628
4629 let client = TestClient {
4630 client,
4631 peer_id,
4632 username: name.to_string(),
4633 user_store,
4634 project_store,
4635 language_registry: Arc::new(LanguageRegistry::test()),
4636 project: Default::default(),
4637 buffers: Default::default(),
4638 };
4639 client.wait_for_current_user(cx).await;
4640 client
4641 }
4642
4643 fn disconnect_client(&self, user_id: UserId) {
4644 self.connection_killers
4645 .lock()
4646 .remove(&user_id)
4647 .unwrap()
4648 .store(true, SeqCst);
4649 }
4650
4651 fn forbid_connections(&self) {
4652 self.forbid_connections.store(true, SeqCst);
4653 }
4654
4655 fn allow_connections(&self) {
4656 self.forbid_connections.store(false, SeqCst);
4657 }
4658
4659 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
4660 while let Some((client_a, cx_a)) = clients.pop() {
4661 for (client_b, cx_b) in &mut clients {
4662 client_a
4663 .user_store
4664 .update(cx_a, |store, cx| {
4665 store.request_contact(client_b.user_id().unwrap(), cx)
4666 })
4667 .await
4668 .unwrap();
4669 cx_a.foreground().run_until_parked();
4670 client_b
4671 .user_store
4672 .update(*cx_b, |store, cx| {
4673 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
4674 })
4675 .await
4676 .unwrap();
4677 }
4678 }
4679 }
4680
4681 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
4682 Arc::new(AppState {
4683 db: test_db.db().clone(),
4684 api_token: Default::default(),
4685 invite_link_prefix: Default::default(),
4686 })
4687 }
4688
4689 async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
4690 self.server.store.read().await
4691 }
4692
4693 async fn condition<F>(&mut self, mut predicate: F)
4694 where
4695 F: FnMut(&Store) -> bool,
4696 {
4697 assert!(
4698 self.foreground.parking_forbidden(),
4699 "you must call forbid_parking to use server conditions so we don't block indefinitely"
4700 );
4701 while !(predicate)(&*self.server.store.read().await) {
4702 self.foreground.start_waiting();
4703 self.notifications.next().await;
4704 self.foreground.finish_waiting();
4705 }
4706 }
4707}
4708
4709impl Deref for TestServer {
4710 type Target = Server;
4711
4712 fn deref(&self) -> &Self::Target {
4713 &self.server
4714 }
4715}
4716
4717impl Drop for TestServer {
4718 fn drop(&mut self) {
4719 self.peer.reset();
4720 }
4721}
4722
4723struct TestClient {
4724 client: Arc<Client>,
4725 username: String,
4726 pub peer_id: PeerId,
4727 pub user_store: ModelHandle<UserStore>,
4728 pub project_store: ModelHandle<ProjectStore>,
4729 language_registry: Arc<LanguageRegistry>,
4730 project: Option<ModelHandle<Project>>,
4731 buffers: HashSet<ModelHandle<language::Buffer>>,
4732}
4733
4734impl Deref for TestClient {
4735 type Target = Arc<Client>;
4736
4737 fn deref(&self) -> &Self::Target {
4738 &self.client
4739 }
4740}
4741
4742struct ContactsSummary {
4743 pub current: Vec<String>,
4744 pub outgoing_requests: Vec<String>,
4745 pub incoming_requests: Vec<String>,
4746}
4747
4748impl TestClient {
4749 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
4750 UserId::from_proto(
4751 self.user_store
4752 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
4753 )
4754 }
4755
4756 async fn wait_for_current_user(&self, cx: &TestAppContext) {
4757 let mut authed_user = self
4758 .user_store
4759 .read_with(cx, |user_store, _| user_store.watch_current_user());
4760 while authed_user.next().await.unwrap().is_none() {}
4761 }
4762
4763 async fn clear_contacts(&self, cx: &mut TestAppContext) {
4764 self.user_store
4765 .update(cx, |store, _| store.clear_contacts())
4766 .await;
4767 }
4768
4769 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
4770 self.user_store.read_with(cx, |store, _| ContactsSummary {
4771 current: store
4772 .contacts()
4773 .iter()
4774 .map(|contact| contact.user.github_login.clone())
4775 .collect(),
4776 outgoing_requests: store
4777 .outgoing_contact_requests()
4778 .iter()
4779 .map(|user| user.github_login.clone())
4780 .collect(),
4781 incoming_requests: store
4782 .incoming_contact_requests()
4783 .iter()
4784 .map(|user| user.github_login.clone())
4785 .collect(),
4786 })
4787 }
4788
4789 async fn build_local_project(
4790 &mut self,
4791 fs: Arc<FakeFs>,
4792 root_path: impl AsRef<Path>,
4793 cx: &mut TestAppContext,
4794 ) -> (ModelHandle<Project>, WorktreeId) {
4795 let project = cx.update(|cx| {
4796 Project::local(
4797 true,
4798 self.client.clone(),
4799 self.user_store.clone(),
4800 self.project_store.clone(),
4801 self.language_registry.clone(),
4802 fs,
4803 cx,
4804 )
4805 });
4806 self.project = Some(project.clone());
4807 let (worktree, _) = project
4808 .update(cx, |p, cx| {
4809 p.find_or_create_local_worktree(root_path, true, cx)
4810 })
4811 .await
4812 .unwrap();
4813 worktree
4814 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4815 .await;
4816 project
4817 .update(cx, |project, _| project.next_remote_id())
4818 .await;
4819 (project, worktree.read_with(cx, |tree, _| tree.id()))
4820 }
4821
4822 async fn build_remote_project(
4823 &mut self,
4824 host_project: &ModelHandle<Project>,
4825 host_cx: &mut TestAppContext,
4826 guest_cx: &mut TestAppContext,
4827 ) -> ModelHandle<Project> {
4828 let host_project_id = host_project
4829 .read_with(host_cx, |project, _| project.next_remote_id())
4830 .await;
4831 let guest_user_id = self.user_id().unwrap();
4832 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
4833 let project_b = guest_cx.spawn(|cx| {
4834 Project::remote(
4835 host_project_id,
4836 self.client.clone(),
4837 self.user_store.clone(),
4838 self.project_store.clone(),
4839 languages,
4840 FakeFs::new(cx.background()),
4841 cx,
4842 )
4843 });
4844 host_cx.foreground().run_until_parked();
4845 host_project.update(host_cx, |project, cx| {
4846 project.respond_to_join_request(guest_user_id, true, cx)
4847 });
4848 let project = project_b.await.unwrap();
4849 self.project = Some(project.clone());
4850 project
4851 }
4852
4853 fn build_workspace(
4854 &self,
4855 project: &ModelHandle<Project>,
4856 cx: &mut TestAppContext,
4857 ) -> ViewHandle<Workspace> {
4858 let (window_id, _) = cx.add_window(|_| EmptyView);
4859 cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
4860 }
4861
4862 async fn simulate_host(
4863 mut self,
4864 project: ModelHandle<Project>,
4865 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
4866 rng: Arc<Mutex<StdRng>>,
4867 mut cx: TestAppContext,
4868 ) -> (Self, TestAppContext, Option<anyhow::Error>) {
4869 async fn simulate_host_internal(
4870 client: &mut TestClient,
4871 project: ModelHandle<Project>,
4872 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
4873 rng: Arc<Mutex<StdRng>>,
4874 cx: &mut TestAppContext,
4875 ) -> anyhow::Result<()> {
4876 let fs = project.read_with(cx, |project, _| project.fs().clone());
4877
4878 cx.update(|cx| {
4879 cx.subscribe(&project, move |project, event, cx| {
4880 if let project::Event::ContactRequestedJoin(user) = event {
4881 log::info!("Host: accepting join request from {}", user.github_login);
4882 project.update(cx, |project, cx| {
4883 project.respond_to_join_request(user.id, true, cx)
4884 });
4885 }
4886 })
4887 .detach();
4888 });
4889
4890 while op_start_signal.next().await.is_some() {
4891 let distribution = rng.lock().gen_range::<usize, _>(0..100);
4892 let files = fs.as_fake().files().await;
4893 match distribution {
4894 0..=19 if !files.is_empty() => {
4895 let path = files.choose(&mut *rng.lock()).unwrap();
4896 let mut path = path.as_path();
4897 while let Some(parent_path) = path.parent() {
4898 path = parent_path;
4899 if rng.lock().gen() {
4900 break;
4901 }
4902 }
4903
4904 log::info!("Host: find/create local worktree {:?}", path);
4905 let find_or_create_worktree = project.update(cx, |project, cx| {
4906 project.find_or_create_local_worktree(path, true, cx)
4907 });
4908 if rng.lock().gen() {
4909 cx.background().spawn(find_or_create_worktree).detach();
4910 } else {
4911 find_or_create_worktree.await?;
4912 }
4913 }
4914 20..=79 if !files.is_empty() => {
4915 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
4916 let file = files.choose(&mut *rng.lock()).unwrap();
4917 let (worktree, path) = project
4918 .update(cx, |project, cx| {
4919 project.find_or_create_local_worktree(file.clone(), true, cx)
4920 })
4921 .await?;
4922 let project_path =
4923 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
4924 log::info!(
4925 "Host: opening path {:?}, worktree {}, relative_path {:?}",
4926 file,
4927 project_path.0,
4928 project_path.1
4929 );
4930 let buffer = project
4931 .update(cx, |project, cx| project.open_buffer(project_path, cx))
4932 .await
4933 .unwrap();
4934 client.buffers.insert(buffer.clone());
4935 buffer
4936 } else {
4937 client
4938 .buffers
4939 .iter()
4940 .choose(&mut *rng.lock())
4941 .unwrap()
4942 .clone()
4943 };
4944
4945 if rng.lock().gen_bool(0.1) {
4946 cx.update(|cx| {
4947 log::info!(
4948 "Host: dropping buffer {:?}",
4949 buffer.read(cx).file().unwrap().full_path(cx)
4950 );
4951 client.buffers.remove(&buffer);
4952 drop(buffer);
4953 });
4954 } else {
4955 buffer.update(cx, |buffer, cx| {
4956 log::info!(
4957 "Host: updating buffer {:?} ({})",
4958 buffer.file().unwrap().full_path(cx),
4959 buffer.remote_id()
4960 );
4961
4962 if rng.lock().gen_bool(0.7) {
4963 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
4964 } else {
4965 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
4966 }
4967 });
4968 }
4969 }
4970 _ => loop {
4971 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
4972 let mut path = PathBuf::new();
4973 path.push("/");
4974 for _ in 0..path_component_count {
4975 let letter = rng.lock().gen_range(b'a'..=b'z');
4976 path.push(std::str::from_utf8(&[letter]).unwrap());
4977 }
4978 path.set_extension("rs");
4979 let parent_path = path.parent().unwrap();
4980
4981 log::info!("Host: creating file {:?}", path,);
4982
4983 if fs.create_dir(&parent_path).await.is_ok()
4984 && fs.create_file(&path, Default::default()).await.is_ok()
4985 {
4986 break;
4987 } else {
4988 log::info!("Host: cannot create file");
4989 }
4990 },
4991 }
4992
4993 cx.background().simulate_random_delay().await;
4994 }
4995
4996 Ok(())
4997 }
4998
4999 let result =
5000 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5001 log::info!("Host done");
5002 self.project = Some(project);
5003 (self, cx, result.err())
5004 }
5005
5006 pub async fn simulate_guest(
5007 mut self,
5008 guest_username: String,
5009 project: ModelHandle<Project>,
5010 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5011 rng: Arc<Mutex<StdRng>>,
5012 mut cx: TestAppContext,
5013 ) -> (Self, TestAppContext, Option<anyhow::Error>) {
5014 async fn simulate_guest_internal(
5015 client: &mut TestClient,
5016 guest_username: &str,
5017 project: ModelHandle<Project>,
5018 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5019 rng: Arc<Mutex<StdRng>>,
5020 cx: &mut TestAppContext,
5021 ) -> anyhow::Result<()> {
5022 while op_start_signal.next().await.is_some() {
5023 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5024 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5025 project
5026 .worktrees(&cx)
5027 .filter(|worktree| {
5028 let worktree = worktree.read(cx);
5029 worktree.is_visible()
5030 && worktree.entries(false).any(|e| e.is_file())
5031 })
5032 .choose(&mut *rng.lock())
5033 }) {
5034 worktree
5035 } else {
5036 cx.background().simulate_random_delay().await;
5037 continue;
5038 };
5039
5040 let (worktree_root_name, project_path) =
5041 worktree.read_with(cx, |worktree, _| {
5042 let entry = worktree
5043 .entries(false)
5044 .filter(|e| e.is_file())
5045 .choose(&mut *rng.lock())
5046 .unwrap();
5047 (
5048 worktree.root_name().to_string(),
5049 (worktree.id(), entry.path.clone()),
5050 )
5051 });
5052 log::info!(
5053 "{}: opening path {:?} in worktree {} ({})",
5054 guest_username,
5055 project_path.1,
5056 project_path.0,
5057 worktree_root_name,
5058 );
5059 let buffer = project
5060 .update(cx, |project, cx| {
5061 project.open_buffer(project_path.clone(), cx)
5062 })
5063 .await?;
5064 log::info!(
5065 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5066 guest_username,
5067 project_path.1,
5068 project_path.0,
5069 worktree_root_name,
5070 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5071 );
5072 client.buffers.insert(buffer.clone());
5073 buffer
5074 } else {
5075 client
5076 .buffers
5077 .iter()
5078 .choose(&mut *rng.lock())
5079 .unwrap()
5080 .clone()
5081 };
5082
5083 let choice = rng.lock().gen_range(0..100);
5084 match choice {
5085 0..=9 => {
5086 cx.update(|cx| {
5087 log::info!(
5088 "{}: dropping buffer {:?}",
5089 guest_username,
5090 buffer.read(cx).file().unwrap().full_path(cx)
5091 );
5092 client.buffers.remove(&buffer);
5093 drop(buffer);
5094 });
5095 }
5096 10..=19 => {
5097 let completions = project.update(cx, |project, cx| {
5098 log::info!(
5099 "{}: requesting completions for buffer {} ({:?})",
5100 guest_username,
5101 buffer.read(cx).remote_id(),
5102 buffer.read(cx).file().unwrap().full_path(cx)
5103 );
5104 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5105 project.completions(&buffer, offset, cx)
5106 });
5107 let completions = cx.background().spawn(async move {
5108 completions
5109 .await
5110 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5111 });
5112 if rng.lock().gen_bool(0.3) {
5113 log::info!("{}: detaching completions request", guest_username);
5114 cx.update(|cx| completions.detach_and_log_err(cx));
5115 } else {
5116 completions.await?;
5117 }
5118 }
5119 20..=29 => {
5120 let code_actions = project.update(cx, |project, cx| {
5121 log::info!(
5122 "{}: requesting code actions for buffer {} ({:?})",
5123 guest_username,
5124 buffer.read(cx).remote_id(),
5125 buffer.read(cx).file().unwrap().full_path(cx)
5126 );
5127 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5128 project.code_actions(&buffer, range, cx)
5129 });
5130 let code_actions = cx.background().spawn(async move {
5131 code_actions
5132 .await
5133 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5134 });
5135 if rng.lock().gen_bool(0.3) {
5136 log::info!("{}: detaching code actions request", guest_username);
5137 cx.update(|cx| code_actions.detach_and_log_err(cx));
5138 } else {
5139 code_actions.await?;
5140 }
5141 }
5142 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5143 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5144 log::info!(
5145 "{}: saving buffer {} ({:?})",
5146 guest_username,
5147 buffer.remote_id(),
5148 buffer.file().unwrap().full_path(cx)
5149 );
5150 (buffer.version(), buffer.save(cx))
5151 });
5152 let save = cx.background().spawn(async move {
5153 let (saved_version, _) = save
5154 .await
5155 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
5156 assert!(saved_version.observed_all(&requested_version));
5157 Ok::<_, anyhow::Error>(())
5158 });
5159 if rng.lock().gen_bool(0.3) {
5160 log::info!("{}: detaching save request", guest_username);
5161 cx.update(|cx| save.detach_and_log_err(cx));
5162 } else {
5163 save.await?;
5164 }
5165 }
5166 40..=44 => {
5167 let prepare_rename = project.update(cx, |project, cx| {
5168 log::info!(
5169 "{}: preparing rename for buffer {} ({:?})",
5170 guest_username,
5171 buffer.read(cx).remote_id(),
5172 buffer.read(cx).file().unwrap().full_path(cx)
5173 );
5174 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5175 project.prepare_rename(buffer, offset, cx)
5176 });
5177 let prepare_rename = cx.background().spawn(async move {
5178 prepare_rename
5179 .await
5180 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
5181 });
5182 if rng.lock().gen_bool(0.3) {
5183 log::info!("{}: detaching prepare rename request", guest_username);
5184 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
5185 } else {
5186 prepare_rename.await?;
5187 }
5188 }
5189 45..=49 => {
5190 let definitions = project.update(cx, |project, cx| {
5191 log::info!(
5192 "{}: requesting definitions for buffer {} ({:?})",
5193 guest_username,
5194 buffer.read(cx).remote_id(),
5195 buffer.read(cx).file().unwrap().full_path(cx)
5196 );
5197 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5198 project.definition(&buffer, offset, cx)
5199 });
5200 let definitions = cx.background().spawn(async move {
5201 definitions
5202 .await
5203 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
5204 });
5205 if rng.lock().gen_bool(0.3) {
5206 log::info!("{}: detaching definitions request", guest_username);
5207 cx.update(|cx| definitions.detach_and_log_err(cx));
5208 } else {
5209 client
5210 .buffers
5211 .extend(definitions.await?.into_iter().map(|loc| loc.buffer));
5212 }
5213 }
5214 50..=54 => {
5215 let highlights = project.update(cx, |project, cx| {
5216 log::info!(
5217 "{}: requesting highlights for buffer {} ({:?})",
5218 guest_username,
5219 buffer.read(cx).remote_id(),
5220 buffer.read(cx).file().unwrap().full_path(cx)
5221 );
5222 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5223 project.document_highlights(&buffer, offset, cx)
5224 });
5225 let highlights = cx.background().spawn(async move {
5226 highlights
5227 .await
5228 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
5229 });
5230 if rng.lock().gen_bool(0.3) {
5231 log::info!("{}: detaching highlights request", guest_username);
5232 cx.update(|cx| highlights.detach_and_log_err(cx));
5233 } else {
5234 highlights.await?;
5235 }
5236 }
5237 55..=59 => {
5238 let search = project.update(cx, |project, cx| {
5239 let query = rng.lock().gen_range('a'..='z');
5240 log::info!("{}: project-wide search {:?}", guest_username, query);
5241 project.search(SearchQuery::text(query, false, false), cx)
5242 });
5243 let search = cx.background().spawn(async move {
5244 search
5245 .await
5246 .map_err(|err| anyhow!("search request failed: {:?}", err))
5247 });
5248 if rng.lock().gen_bool(0.3) {
5249 log::info!("{}: detaching search request", guest_username);
5250 cx.update(|cx| search.detach_and_log_err(cx));
5251 } else {
5252 client.buffers.extend(search.await?.into_keys());
5253 }
5254 }
5255 60..=69 => {
5256 let worktree = project
5257 .read_with(cx, |project, cx| {
5258 project
5259 .worktrees(&cx)
5260 .filter(|worktree| {
5261 let worktree = worktree.read(cx);
5262 worktree.is_visible()
5263 && worktree.entries(false).any(|e| e.is_file())
5264 && worktree.root_entry().map_or(false, |e| e.is_dir())
5265 })
5266 .choose(&mut *rng.lock())
5267 })
5268 .unwrap();
5269 let (worktree_id, worktree_root_name) = worktree
5270 .read_with(cx, |worktree, _| {
5271 (worktree.id(), worktree.root_name().to_string())
5272 });
5273
5274 let mut new_name = String::new();
5275 for _ in 0..10 {
5276 let letter = rng.lock().gen_range('a'..='z');
5277 new_name.push(letter);
5278 }
5279 let mut new_path = PathBuf::new();
5280 new_path.push(new_name);
5281 new_path.set_extension("rs");
5282 log::info!(
5283 "{}: creating {:?} in worktree {} ({})",
5284 guest_username,
5285 new_path,
5286 worktree_id,
5287 worktree_root_name,
5288 );
5289 project
5290 .update(cx, |project, cx| {
5291 project.create_entry((worktree_id, new_path), false, cx)
5292 })
5293 .unwrap()
5294 .await?;
5295 }
5296 _ => {
5297 buffer.update(cx, |buffer, cx| {
5298 log::info!(
5299 "{}: updating buffer {} ({:?})",
5300 guest_username,
5301 buffer.remote_id(),
5302 buffer.file().unwrap().full_path(cx)
5303 );
5304 if rng.lock().gen_bool(0.7) {
5305 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5306 } else {
5307 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5308 }
5309 });
5310 }
5311 }
5312 cx.background().simulate_random_delay().await;
5313 }
5314 Ok(())
5315 }
5316
5317 let result = simulate_guest_internal(
5318 &mut self,
5319 &guest_username,
5320 project.clone(),
5321 op_start_signal,
5322 rng,
5323 &mut cx,
5324 )
5325 .await;
5326 log::info!("{}: done", guest_username);
5327
5328 self.project = Some(project);
5329 (self, cx, result.err())
5330 }
5331}
5332
5333impl Drop for TestClient {
5334 fn drop(&mut self) {
5335 self.client.tear_down();
5336 }
5337}
5338
5339impl Executor for Arc<gpui::executor::Background> {
5340 type Sleep = gpui::executor::Timer;
5341
5342 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
5343 self.spawn(future).detach();
5344 }
5345
5346 fn sleep(&self, duration: Duration) -> Self::Sleep {
5347 self.as_ref().timer(duration)
5348 }
5349}
5350
5351fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
5352 channel
5353 .messages()
5354 .cursor::<()>()
5355 .map(|m| {
5356 (
5357 m.sender.github_login.clone(),
5358 m.body.clone(),
5359 m.is_pending(),
5360 )
5361 })
5362 .collect()
5363}
5364
5365struct EmptyView;
5366
5367impl gpui::Entity for EmptyView {
5368 type Event = ();
5369}
5370
5371impl gpui::View for EmptyView {
5372 fn ui_name() -> &'static str {
5373 "empty view"
5374 }
5375
5376 fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
5377 gpui::Element::boxed(gpui::elements::Empty::new())
5378 }
5379}