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