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