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