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