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