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