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