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()).await;
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
1679 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1680 capabilities: lsp::ServerCapabilities {
1681 completion_provider: Some(lsp::CompletionOptions {
1682 trigger_characters: Some(vec![".".to_string()]),
1683 ..Default::default()
1684 }),
1685 ..Default::default()
1686 },
1687 ..Default::default()
1688 }))
1689 .await;
1690 client_a.language_registry.add(Arc::new(language));
1691
1692 client_a
1693 .fs
1694 .insert_tree(
1695 "/a",
1696 json!({
1697 "main.rs": "fn main() { a }",
1698 "other.rs": "",
1699 }),
1700 )
1701 .await;
1702 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1703 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1704
1705 // Open a file in an editor as the guest.
1706 let buffer_b = project_b
1707 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1708 .await
1709 .unwrap();
1710 let (window_b, _) = cx_b.add_window(|_| EmptyView);
1711 let editor_b = cx_b.add_view(window_b, |cx| {
1712 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
1713 });
1714
1715 let fake_language_server = fake_language_servers.next().await.unwrap();
1716 buffer_b
1717 .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
1718 .await;
1719
1720 // Type a completion trigger character as the guest.
1721 editor_b.update(cx_b, |editor, cx| {
1722 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1723 editor.handle_input(&Input(".".into()), cx);
1724 cx.focus(&editor_b);
1725 });
1726
1727 // Receive a completion request as the host's language server.
1728 // Return some completions from the host's language server.
1729 cx_a.foreground().start_waiting();
1730 fake_language_server
1731 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
1732 assert_eq!(
1733 params.text_document_position.text_document.uri,
1734 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1735 );
1736 assert_eq!(
1737 params.text_document_position.position,
1738 lsp::Position::new(0, 14),
1739 );
1740
1741 Ok(Some(lsp::CompletionResponse::Array(vec![
1742 lsp::CompletionItem {
1743 label: "first_method(…)".into(),
1744 detail: Some("fn(&mut self, B) -> C".into()),
1745 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1746 new_text: "first_method($1)".to_string(),
1747 range: lsp::Range::new(
1748 lsp::Position::new(0, 14),
1749 lsp::Position::new(0, 14),
1750 ),
1751 })),
1752 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1753 ..Default::default()
1754 },
1755 lsp::CompletionItem {
1756 label: "second_method(…)".into(),
1757 detail: Some("fn(&mut self, C) -> D<E>".into()),
1758 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1759 new_text: "second_method()".to_string(),
1760 range: lsp::Range::new(
1761 lsp::Position::new(0, 14),
1762 lsp::Position::new(0, 14),
1763 ),
1764 })),
1765 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1766 ..Default::default()
1767 },
1768 ])))
1769 })
1770 .next()
1771 .await
1772 .unwrap();
1773 cx_a.foreground().finish_waiting();
1774
1775 // Open the buffer on the host.
1776 let buffer_a = project_a
1777 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1778 .await
1779 .unwrap();
1780 buffer_a
1781 .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
1782 .await;
1783
1784 // Confirm a completion on the guest.
1785 editor_b
1786 .condition(&cx_b, |editor, _| editor.context_menu_visible())
1787 .await;
1788 editor_b.update(cx_b, |editor, cx| {
1789 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
1790 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
1791 });
1792
1793 // Return a resolved completion from the host's language server.
1794 // The resolved completion has an additional text edit.
1795 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
1796 |params, _| async move {
1797 assert_eq!(params.label, "first_method(…)");
1798 Ok(lsp::CompletionItem {
1799 label: "first_method(…)".into(),
1800 detail: Some("fn(&mut self, B) -> C".into()),
1801 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1802 new_text: "first_method($1)".to_string(),
1803 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1804 })),
1805 additional_text_edits: Some(vec![lsp::TextEdit {
1806 new_text: "use d::SomeTrait;\n".to_string(),
1807 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1808 }]),
1809 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1810 ..Default::default()
1811 })
1812 },
1813 );
1814
1815 // The additional edit is applied.
1816 buffer_a
1817 .condition(&cx_a, |buffer, _| {
1818 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1819 })
1820 .await;
1821 buffer_b
1822 .condition(&cx_b, |buffer, _| {
1823 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1824 })
1825 .await;
1826}
1827
1828#[gpui::test(iterations = 10)]
1829async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1830 cx_a.foreground().forbid_parking();
1831 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1832 let client_a = server.create_client(cx_a, "user_a").await;
1833 let client_b = server.create_client(cx_b, "user_b").await;
1834 server
1835 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1836 .await;
1837
1838 client_a
1839 .fs
1840 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
1841 .await;
1842 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1843 let buffer_a = project_a
1844 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
1845 .await
1846 .unwrap();
1847
1848 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1849
1850 let buffer_b = cx_b
1851 .background()
1852 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1853 .await
1854 .unwrap();
1855 buffer_b.update(cx_b, |buffer, cx| {
1856 buffer.edit([(4..7, "six")], cx);
1857 buffer.edit([(10..11, "6")], cx);
1858 assert_eq!(buffer.text(), "let six = 6;");
1859 assert!(buffer.is_dirty());
1860 assert!(!buffer.has_conflict());
1861 });
1862 buffer_a
1863 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
1864 .await;
1865
1866 client_a
1867 .fs
1868 .save(
1869 "/a/a.rs".as_ref(),
1870 &Rope::from("let seven = 7;"),
1871 LineEnding::Unix,
1872 )
1873 .await
1874 .unwrap();
1875 buffer_a
1876 .condition(cx_a, |buffer, _| buffer.has_conflict())
1877 .await;
1878 buffer_b
1879 .condition(cx_b, |buffer, _| buffer.has_conflict())
1880 .await;
1881
1882 project_b
1883 .update(cx_b, |project, cx| {
1884 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
1885 })
1886 .await
1887 .unwrap();
1888 buffer_a.read_with(cx_a, |buffer, _| {
1889 assert_eq!(buffer.text(), "let seven = 7;");
1890 assert!(!buffer.is_dirty());
1891 assert!(!buffer.has_conflict());
1892 });
1893 buffer_b.read_with(cx_b, |buffer, _| {
1894 assert_eq!(buffer.text(), "let seven = 7;");
1895 assert!(!buffer.is_dirty());
1896 assert!(!buffer.has_conflict());
1897 });
1898
1899 buffer_a.update(cx_a, |buffer, cx| {
1900 // Undoing on the host is a no-op when the reload was initiated by the guest.
1901 buffer.undo(cx);
1902 assert_eq!(buffer.text(), "let seven = 7;");
1903 assert!(!buffer.is_dirty());
1904 assert!(!buffer.has_conflict());
1905 });
1906 buffer_b.update(cx_b, |buffer, cx| {
1907 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
1908 buffer.undo(cx);
1909 assert_eq!(buffer.text(), "let six = 6;");
1910 assert!(buffer.is_dirty());
1911 assert!(!buffer.has_conflict());
1912 });
1913}
1914
1915#[gpui::test(iterations = 10)]
1916async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1917 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1918 let client_a = server.create_client(cx_a, "user_a").await;
1919 let client_b = server.create_client(cx_b, "user_b").await;
1920 server
1921 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1922 .await;
1923
1924 // Set up a fake language server.
1925 let mut language = Language::new(
1926 LanguageConfig {
1927 name: "Rust".into(),
1928 path_suffixes: vec!["rs".to_string()],
1929 ..Default::default()
1930 },
1931 Some(tree_sitter_rust::language()),
1932 );
1933 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1934 client_a.language_registry.add(Arc::new(language));
1935
1936 // Here we insert a fake tree with a directory that exists on disk. This is needed
1937 // because later we'll invoke a command, which requires passing a working directory
1938 // that points to a valid location on disk.
1939 let directory = env::current_dir().unwrap();
1940 client_a
1941 .fs
1942 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
1943 .await;
1944 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
1945 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1946
1947 let buffer_b = cx_b
1948 .background()
1949 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1950 .await
1951 .unwrap();
1952
1953 let fake_language_server = fake_language_servers.next().await.unwrap();
1954 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
1955 Ok(Some(vec![
1956 lsp::TextEdit {
1957 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
1958 new_text: "h".to_string(),
1959 },
1960 lsp::TextEdit {
1961 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
1962 new_text: "y".to_string(),
1963 },
1964 ]))
1965 });
1966
1967 project_b
1968 .update(cx_b, |project, cx| {
1969 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
1970 })
1971 .await
1972 .unwrap();
1973 assert_eq!(
1974 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
1975 "let honey = \"two\""
1976 );
1977
1978 // Ensure buffer can be formatted using an external command. Notice how the
1979 // host's configuration is honored as opposed to using the guest's settings.
1980 cx_a.update(|cx| {
1981 cx.update_global(|settings: &mut Settings, _| {
1982 settings.language_settings.format_on_save = Some(FormatOnSave::External {
1983 command: "awk".to_string(),
1984 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
1985 });
1986 });
1987 });
1988 project_b
1989 .update(cx_b, |project, cx| {
1990 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
1991 })
1992 .await
1993 .unwrap();
1994 assert_eq!(
1995 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
1996 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
1997 );
1998}
1999
2000#[gpui::test(iterations = 10)]
2001async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2002 cx_a.foreground().forbid_parking();
2003 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2004 let client_a = server.create_client(cx_a, "user_a").await;
2005 let client_b = server.create_client(cx_b, "user_b").await;
2006 server
2007 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2008 .await;
2009
2010 // Set up a fake language server.
2011 let mut language = Language::new(
2012 LanguageConfig {
2013 name: "Rust".into(),
2014 path_suffixes: vec!["rs".to_string()],
2015 ..Default::default()
2016 },
2017 Some(tree_sitter_rust::language()),
2018 );
2019 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2020 client_a.language_registry.add(Arc::new(language));
2021
2022 client_a
2023 .fs
2024 .insert_tree(
2025 "/root",
2026 json!({
2027 "dir-1": {
2028 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
2029 },
2030 "dir-2": {
2031 "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;",
2032 }
2033 }),
2034 )
2035 .await;
2036 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2037 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2038
2039 // Open the file on client B.
2040 let buffer_b = cx_b
2041 .background()
2042 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2043 .await
2044 .unwrap();
2045
2046 // Request the definition of a symbol as the guest.
2047 let fake_language_server = fake_language_servers.next().await.unwrap();
2048 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2049 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2050 lsp::Location::new(
2051 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2052 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2053 ),
2054 )))
2055 });
2056
2057 let definitions_1 = project_b
2058 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
2059 .await
2060 .unwrap();
2061 cx_b.read(|cx| {
2062 assert_eq!(definitions_1.len(), 1);
2063 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2064 let target_buffer = definitions_1[0].target.buffer.read(cx);
2065 assert_eq!(
2066 target_buffer.text(),
2067 "const TWO: usize = 2;\nconst THREE: usize = 3;"
2068 );
2069 assert_eq!(
2070 definitions_1[0].target.range.to_point(target_buffer),
2071 Point::new(0, 6)..Point::new(0, 9)
2072 );
2073 });
2074
2075 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
2076 // the previous call to `definition`.
2077 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2078 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2079 lsp::Location::new(
2080 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2081 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
2082 ),
2083 )))
2084 });
2085
2086 let definitions_2 = project_b
2087 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
2088 .await
2089 .unwrap();
2090 cx_b.read(|cx| {
2091 assert_eq!(definitions_2.len(), 1);
2092 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2093 let target_buffer = definitions_2[0].target.buffer.read(cx);
2094 assert_eq!(
2095 target_buffer.text(),
2096 "const TWO: usize = 2;\nconst THREE: usize = 3;"
2097 );
2098 assert_eq!(
2099 definitions_2[0].target.range.to_point(target_buffer),
2100 Point::new(1, 6)..Point::new(1, 11)
2101 );
2102 });
2103 assert_eq!(
2104 definitions_1[0].target.buffer,
2105 definitions_2[0].target.buffer
2106 );
2107}
2108
2109#[gpui::test(iterations = 10)]
2110async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2111 cx_a.foreground().forbid_parking();
2112 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2113 let client_a = server.create_client(cx_a, "user_a").await;
2114 let client_b = server.create_client(cx_b, "user_b").await;
2115 server
2116 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2117 .await;
2118
2119 // Set up a fake language server.
2120 let mut language = Language::new(
2121 LanguageConfig {
2122 name: "Rust".into(),
2123 path_suffixes: vec!["rs".to_string()],
2124 ..Default::default()
2125 },
2126 Some(tree_sitter_rust::language()),
2127 );
2128 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2129 client_a.language_registry.add(Arc::new(language));
2130
2131 client_a
2132 .fs
2133 .insert_tree(
2134 "/root",
2135 json!({
2136 "dir-1": {
2137 "one.rs": "const ONE: usize = 1;",
2138 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2139 },
2140 "dir-2": {
2141 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
2142 }
2143 }),
2144 )
2145 .await;
2146 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2147 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2148
2149 // Open the file on client B.
2150 let buffer_b = cx_b
2151 .background()
2152 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2153 .await
2154 .unwrap();
2155
2156 // Request references to a symbol as the guest.
2157 let fake_language_server = fake_language_servers.next().await.unwrap();
2158 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
2159 assert_eq!(
2160 params.text_document_position.text_document.uri.as_str(),
2161 "file:///root/dir-1/one.rs"
2162 );
2163 Ok(Some(vec![
2164 lsp::Location {
2165 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2166 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
2167 },
2168 lsp::Location {
2169 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2170 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
2171 },
2172 lsp::Location {
2173 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
2174 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
2175 },
2176 ]))
2177 });
2178
2179 let references = project_b
2180 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
2181 .await
2182 .unwrap();
2183 cx_b.read(|cx| {
2184 assert_eq!(references.len(), 3);
2185 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2186
2187 let two_buffer = references[0].buffer.read(cx);
2188 let three_buffer = references[2].buffer.read(cx);
2189 assert_eq!(
2190 two_buffer.file().unwrap().path().as_ref(),
2191 Path::new("two.rs")
2192 );
2193 assert_eq!(references[1].buffer, references[0].buffer);
2194 assert_eq!(
2195 three_buffer.file().unwrap().full_path(cx),
2196 Path::new("three.rs")
2197 );
2198
2199 assert_eq!(references[0].range.to_offset(&two_buffer), 24..27);
2200 assert_eq!(references[1].range.to_offset(&two_buffer), 35..38);
2201 assert_eq!(references[2].range.to_offset(&three_buffer), 37..40);
2202 });
2203}
2204
2205#[gpui::test(iterations = 10)]
2206async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2207 cx_a.foreground().forbid_parking();
2208 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2209 let client_a = server.create_client(cx_a, "user_a").await;
2210 let client_b = server.create_client(cx_b, "user_b").await;
2211 server
2212 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2213 .await;
2214
2215 client_a
2216 .fs
2217 .insert_tree(
2218 "/root",
2219 json!({
2220 "dir-1": {
2221 "a": "hello world",
2222 "b": "goodnight moon",
2223 "c": "a world of goo",
2224 "d": "world champion of clown world",
2225 },
2226 "dir-2": {
2227 "e": "disney world is fun",
2228 }
2229 }),
2230 )
2231 .await;
2232 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
2233 let (worktree_2, _) = project_a
2234 .update(cx_a, |p, cx| {
2235 p.find_or_create_local_worktree("/root/dir-2", true, cx)
2236 })
2237 .await
2238 .unwrap();
2239 worktree_2
2240 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
2241 .await;
2242
2243 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2244
2245 // Perform a search as the guest.
2246 let results = project_b
2247 .update(cx_b, |project, cx| {
2248 project.search(SearchQuery::text("world", false, false), cx)
2249 })
2250 .await
2251 .unwrap();
2252
2253 let mut ranges_by_path = results
2254 .into_iter()
2255 .map(|(buffer, ranges)| {
2256 buffer.read_with(cx_b, |buffer, cx| {
2257 let path = buffer.file().unwrap().full_path(cx);
2258 let offset_ranges = ranges
2259 .into_iter()
2260 .map(|range| range.to_offset(buffer))
2261 .collect::<Vec<_>>();
2262 (path, offset_ranges)
2263 })
2264 })
2265 .collect::<Vec<_>>();
2266 ranges_by_path.sort_by_key(|(path, _)| path.clone());
2267
2268 assert_eq!(
2269 ranges_by_path,
2270 &[
2271 (PathBuf::from("dir-1/a"), vec![6..11]),
2272 (PathBuf::from("dir-1/c"), vec![2..7]),
2273 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
2274 (PathBuf::from("dir-2/e"), vec![7..12]),
2275 ]
2276 );
2277}
2278
2279#[gpui::test(iterations = 10)]
2280async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2281 cx_a.foreground().forbid_parking();
2282 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2283 let client_a = server.create_client(cx_a, "user_a").await;
2284 let client_b = server.create_client(cx_b, "user_b").await;
2285 server
2286 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2287 .await;
2288
2289 client_a
2290 .fs
2291 .insert_tree(
2292 "/root-1",
2293 json!({
2294 "main.rs": "fn double(number: i32) -> i32 { number + number }",
2295 }),
2296 )
2297 .await;
2298
2299 // Set up a fake language server.
2300 let mut language = Language::new(
2301 LanguageConfig {
2302 name: "Rust".into(),
2303 path_suffixes: vec!["rs".to_string()],
2304 ..Default::default()
2305 },
2306 Some(tree_sitter_rust::language()),
2307 );
2308 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2309 client_a.language_registry.add(Arc::new(language));
2310
2311 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2312 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2313
2314 // Open the file on client B.
2315 let buffer_b = cx_b
2316 .background()
2317 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2318 .await
2319 .unwrap();
2320
2321 // Request document highlights as the guest.
2322 let fake_language_server = fake_language_servers.next().await.unwrap();
2323 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
2324 |params, _| async move {
2325 assert_eq!(
2326 params
2327 .text_document_position_params
2328 .text_document
2329 .uri
2330 .as_str(),
2331 "file:///root-1/main.rs"
2332 );
2333 assert_eq!(
2334 params.text_document_position_params.position,
2335 lsp::Position::new(0, 34)
2336 );
2337 Ok(Some(vec![
2338 lsp::DocumentHighlight {
2339 kind: Some(lsp::DocumentHighlightKind::WRITE),
2340 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
2341 },
2342 lsp::DocumentHighlight {
2343 kind: Some(lsp::DocumentHighlightKind::READ),
2344 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
2345 },
2346 lsp::DocumentHighlight {
2347 kind: Some(lsp::DocumentHighlightKind::READ),
2348 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
2349 },
2350 ]))
2351 },
2352 );
2353
2354 let highlights = project_b
2355 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
2356 .await
2357 .unwrap();
2358 buffer_b.read_with(cx_b, |buffer, _| {
2359 let snapshot = buffer.snapshot();
2360
2361 let highlights = highlights
2362 .into_iter()
2363 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
2364 .collect::<Vec<_>>();
2365 assert_eq!(
2366 highlights,
2367 &[
2368 (lsp::DocumentHighlightKind::WRITE, 10..16),
2369 (lsp::DocumentHighlightKind::READ, 32..38),
2370 (lsp::DocumentHighlightKind::READ, 41..47)
2371 ]
2372 )
2373 });
2374}
2375
2376#[gpui::test(iterations = 10)]
2377async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2378 cx_a.foreground().forbid_parking();
2379 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2380 let client_a = server.create_client(cx_a, "user_a").await;
2381 let client_b = server.create_client(cx_b, "user_b").await;
2382 server
2383 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2384 .await;
2385
2386 client_a
2387 .fs
2388 .insert_tree(
2389 "/root-1",
2390 json!({
2391 "main.rs": "use std::collections::HashMap;",
2392 }),
2393 )
2394 .await;
2395
2396 // Set up a fake language server.
2397 let mut language = Language::new(
2398 LanguageConfig {
2399 name: "Rust".into(),
2400 path_suffixes: vec!["rs".to_string()],
2401 ..Default::default()
2402 },
2403 Some(tree_sitter_rust::language()),
2404 );
2405 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2406 client_a.language_registry.add(Arc::new(language));
2407
2408 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2409 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2410
2411 // Open the file as the guest
2412 let buffer_b = cx_b
2413 .background()
2414 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2415 .await
2416 .unwrap();
2417
2418 // Request hover information as the guest.
2419 let fake_language_server = fake_language_servers.next().await.unwrap();
2420 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
2421 |params, _| async move {
2422 assert_eq!(
2423 params
2424 .text_document_position_params
2425 .text_document
2426 .uri
2427 .as_str(),
2428 "file:///root-1/main.rs"
2429 );
2430 assert_eq!(
2431 params.text_document_position_params.position,
2432 lsp::Position::new(0, 22)
2433 );
2434 Ok(Some(lsp::Hover {
2435 contents: lsp::HoverContents::Array(vec![
2436 lsp::MarkedString::String("Test hover content.".to_string()),
2437 lsp::MarkedString::LanguageString(lsp::LanguageString {
2438 language: "Rust".to_string(),
2439 value: "let foo = 42;".to_string(),
2440 }),
2441 ]),
2442 range: Some(lsp::Range::new(
2443 lsp::Position::new(0, 22),
2444 lsp::Position::new(0, 29),
2445 )),
2446 }))
2447 },
2448 );
2449
2450 let hover_info = project_b
2451 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
2452 .await
2453 .unwrap()
2454 .unwrap();
2455 buffer_b.read_with(cx_b, |buffer, _| {
2456 let snapshot = buffer.snapshot();
2457 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
2458 assert_eq!(
2459 hover_info.contents,
2460 vec![
2461 project::HoverBlock {
2462 text: "Test hover content.".to_string(),
2463 language: None,
2464 },
2465 project::HoverBlock {
2466 text: "let foo = 42;".to_string(),
2467 language: Some("Rust".to_string()),
2468 }
2469 ]
2470 );
2471 });
2472}
2473
2474#[gpui::test(iterations = 10)]
2475async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2476 cx_a.foreground().forbid_parking();
2477 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2478 let client_a = server.create_client(cx_a, "user_a").await;
2479 let client_b = server.create_client(cx_b, "user_b").await;
2480 server
2481 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2482 .await;
2483
2484 // Set up a fake language server.
2485 let mut language = Language::new(
2486 LanguageConfig {
2487 name: "Rust".into(),
2488 path_suffixes: vec!["rs".to_string()],
2489 ..Default::default()
2490 },
2491 Some(tree_sitter_rust::language()),
2492 );
2493 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2494 client_a.language_registry.add(Arc::new(language));
2495
2496 client_a
2497 .fs
2498 .insert_tree(
2499 "/code",
2500 json!({
2501 "crate-1": {
2502 "one.rs": "const ONE: usize = 1;",
2503 },
2504 "crate-2": {
2505 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
2506 },
2507 "private": {
2508 "passwords.txt": "the-password",
2509 }
2510 }),
2511 )
2512 .await;
2513 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
2514 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2515
2516 // Cause the language server to start.
2517 let _buffer = cx_b
2518 .background()
2519 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2520 .await
2521 .unwrap();
2522
2523 let fake_language_server = fake_language_servers.next().await.unwrap();
2524 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
2525 #[allow(deprecated)]
2526 Ok(Some(vec![lsp::SymbolInformation {
2527 name: "TWO".into(),
2528 location: lsp::Location {
2529 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
2530 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2531 },
2532 kind: lsp::SymbolKind::CONSTANT,
2533 tags: None,
2534 container_name: None,
2535 deprecated: None,
2536 }]))
2537 });
2538
2539 // Request the definition of a symbol as the guest.
2540 let symbols = project_b
2541 .update(cx_b, |p, cx| p.symbols("two", cx))
2542 .await
2543 .unwrap();
2544 assert_eq!(symbols.len(), 1);
2545 assert_eq!(symbols[0].name, "TWO");
2546
2547 // Open one of the returned symbols.
2548 let buffer_b_2 = project_b
2549 .update(cx_b, |project, cx| {
2550 project.open_buffer_for_symbol(&symbols[0], cx)
2551 })
2552 .await
2553 .unwrap();
2554 buffer_b_2.read_with(cx_b, |buffer, _| {
2555 assert_eq!(
2556 buffer.file().unwrap().path().as_ref(),
2557 Path::new("../crate-2/two.rs")
2558 );
2559 });
2560
2561 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
2562 let mut fake_symbol = symbols[0].clone();
2563 fake_symbol.path = Path::new("/code/secrets").into();
2564 let error = project_b
2565 .update(cx_b, |project, cx| {
2566 project.open_buffer_for_symbol(&fake_symbol, cx)
2567 })
2568 .await
2569 .unwrap_err();
2570 assert!(error.to_string().contains("invalid symbol signature"));
2571}
2572
2573#[gpui::test(iterations = 10)]
2574async fn test_open_buffer_while_getting_definition_pointing_to_it(
2575 cx_a: &mut TestAppContext,
2576 cx_b: &mut TestAppContext,
2577 mut rng: StdRng,
2578) {
2579 cx_a.foreground().forbid_parking();
2580 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2581 let client_a = server.create_client(cx_a, "user_a").await;
2582 let client_b = server.create_client(cx_b, "user_b").await;
2583 server
2584 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2585 .await;
2586
2587 // Set up a fake language server.
2588 let mut language = Language::new(
2589 LanguageConfig {
2590 name: "Rust".into(),
2591 path_suffixes: vec!["rs".to_string()],
2592 ..Default::default()
2593 },
2594 Some(tree_sitter_rust::language()),
2595 );
2596 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2597 client_a.language_registry.add(Arc::new(language));
2598
2599 client_a
2600 .fs
2601 .insert_tree(
2602 "/root",
2603 json!({
2604 "a.rs": "const ONE: usize = b::TWO;",
2605 "b.rs": "const TWO: usize = 2",
2606 }),
2607 )
2608 .await;
2609 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
2610 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2611
2612 let buffer_b1 = cx_b
2613 .background()
2614 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2615 .await
2616 .unwrap();
2617
2618 let fake_language_server = fake_language_servers.next().await.unwrap();
2619 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2620 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2621 lsp::Location::new(
2622 lsp::Url::from_file_path("/root/b.rs").unwrap(),
2623 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2624 ),
2625 )))
2626 });
2627
2628 let definitions;
2629 let buffer_b2;
2630 if rng.gen() {
2631 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2632 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2633 } else {
2634 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2635 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2636 }
2637
2638 let buffer_b2 = buffer_b2.await.unwrap();
2639 let definitions = definitions.await.unwrap();
2640 assert_eq!(definitions.len(), 1);
2641 assert_eq!(definitions[0].target.buffer, buffer_b2);
2642}
2643
2644#[gpui::test(iterations = 10)]
2645async fn test_collaborating_with_code_actions(
2646 cx_a: &mut TestAppContext,
2647 cx_b: &mut TestAppContext,
2648) {
2649 cx_a.foreground().forbid_parking();
2650 cx_b.update(|cx| editor::init(cx));
2651 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2652 let client_a = server.create_client(cx_a, "user_a").await;
2653 let client_b = server.create_client(cx_b, "user_b").await;
2654 server
2655 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2656 .await;
2657
2658 // Set up a fake language server.
2659 let mut language = Language::new(
2660 LanguageConfig {
2661 name: "Rust".into(),
2662 path_suffixes: vec!["rs".to_string()],
2663 ..Default::default()
2664 },
2665 Some(tree_sitter_rust::language()),
2666 );
2667 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2668 client_a.language_registry.add(Arc::new(language));
2669
2670 client_a
2671 .fs
2672 .insert_tree(
2673 "/a",
2674 json!({
2675 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2676 "other.rs": "pub fn foo() -> usize { 4 }",
2677 }),
2678 )
2679 .await;
2680 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2681
2682 // Join the project as client B.
2683 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2684 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2685 let editor_b = workspace_b
2686 .update(cx_b, |workspace, cx| {
2687 workspace.open_path((worktree_id, "main.rs"), true, cx)
2688 })
2689 .await
2690 .unwrap()
2691 .downcast::<Editor>()
2692 .unwrap();
2693
2694 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2695 fake_language_server
2696 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2697 assert_eq!(
2698 params.text_document.uri,
2699 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2700 );
2701 assert_eq!(params.range.start, lsp::Position::new(0, 0));
2702 assert_eq!(params.range.end, lsp::Position::new(0, 0));
2703 Ok(None)
2704 })
2705 .next()
2706 .await;
2707
2708 // Move cursor to a location that contains code actions.
2709 editor_b.update(cx_b, |editor, cx| {
2710 editor.change_selections(None, cx, |s| {
2711 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
2712 });
2713 cx.focus(&editor_b);
2714 });
2715
2716 fake_language_server
2717 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2718 assert_eq!(
2719 params.text_document.uri,
2720 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2721 );
2722 assert_eq!(params.range.start, lsp::Position::new(1, 31));
2723 assert_eq!(params.range.end, lsp::Position::new(1, 31));
2724
2725 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
2726 lsp::CodeAction {
2727 title: "Inline into all callers".to_string(),
2728 edit: Some(lsp::WorkspaceEdit {
2729 changes: Some(
2730 [
2731 (
2732 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2733 vec![lsp::TextEdit::new(
2734 lsp::Range::new(
2735 lsp::Position::new(1, 22),
2736 lsp::Position::new(1, 34),
2737 ),
2738 "4".to_string(),
2739 )],
2740 ),
2741 (
2742 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2743 vec![lsp::TextEdit::new(
2744 lsp::Range::new(
2745 lsp::Position::new(0, 0),
2746 lsp::Position::new(0, 27),
2747 ),
2748 "".to_string(),
2749 )],
2750 ),
2751 ]
2752 .into_iter()
2753 .collect(),
2754 ),
2755 ..Default::default()
2756 }),
2757 data: Some(json!({
2758 "codeActionParams": {
2759 "range": {
2760 "start": {"line": 1, "column": 31},
2761 "end": {"line": 1, "column": 31},
2762 }
2763 }
2764 })),
2765 ..Default::default()
2766 },
2767 )]))
2768 })
2769 .next()
2770 .await;
2771
2772 // Toggle code actions and wait for them to display.
2773 editor_b.update(cx_b, |editor, cx| {
2774 editor.toggle_code_actions(
2775 &ToggleCodeActions {
2776 deployed_from_indicator: false,
2777 },
2778 cx,
2779 );
2780 });
2781 editor_b
2782 .condition(&cx_b, |editor, _| editor.context_menu_visible())
2783 .await;
2784
2785 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
2786
2787 // Confirming the code action will trigger a resolve request.
2788 let confirm_action = workspace_b
2789 .update(cx_b, |workspace, cx| {
2790 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
2791 })
2792 .unwrap();
2793 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2794 |_, _| async move {
2795 Ok(lsp::CodeAction {
2796 title: "Inline into all callers".to_string(),
2797 edit: Some(lsp::WorkspaceEdit {
2798 changes: Some(
2799 [
2800 (
2801 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2802 vec![lsp::TextEdit::new(
2803 lsp::Range::new(
2804 lsp::Position::new(1, 22),
2805 lsp::Position::new(1, 34),
2806 ),
2807 "4".to_string(),
2808 )],
2809 ),
2810 (
2811 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2812 vec![lsp::TextEdit::new(
2813 lsp::Range::new(
2814 lsp::Position::new(0, 0),
2815 lsp::Position::new(0, 27),
2816 ),
2817 "".to_string(),
2818 )],
2819 ),
2820 ]
2821 .into_iter()
2822 .collect(),
2823 ),
2824 ..Default::default()
2825 }),
2826 ..Default::default()
2827 })
2828 },
2829 );
2830
2831 // After the action is confirmed, an editor containing both modified files is opened.
2832 confirm_action.await.unwrap();
2833 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2834 workspace
2835 .active_item(cx)
2836 .unwrap()
2837 .downcast::<Editor>()
2838 .unwrap()
2839 });
2840 code_action_editor.update(cx_b, |editor, cx| {
2841 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2842 editor.undo(&Undo, cx);
2843 assert_eq!(
2844 editor.text(cx),
2845 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
2846 );
2847 editor.redo(&Redo, cx);
2848 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2849 });
2850}
2851
2852#[gpui::test(iterations = 10)]
2853async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2854 cx_a.foreground().forbid_parking();
2855 cx_b.update(|cx| editor::init(cx));
2856 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2857 let client_a = server.create_client(cx_a, "user_a").await;
2858 let client_b = server.create_client(cx_b, "user_b").await;
2859 server
2860 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2861 .await;
2862
2863 // Set up a fake language server.
2864 let mut language = Language::new(
2865 LanguageConfig {
2866 name: "Rust".into(),
2867 path_suffixes: vec!["rs".to_string()],
2868 ..Default::default()
2869 },
2870 Some(tree_sitter_rust::language()),
2871 );
2872 let mut fake_language_servers = language
2873 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2874 capabilities: lsp::ServerCapabilities {
2875 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
2876 prepare_provider: Some(true),
2877 work_done_progress_options: Default::default(),
2878 })),
2879 ..Default::default()
2880 },
2881 ..Default::default()
2882 }))
2883 .await;
2884 client_a.language_registry.add(Arc::new(language));
2885
2886 client_a
2887 .fs
2888 .insert_tree(
2889 "/dir",
2890 json!({
2891 "one.rs": "const ONE: usize = 1;",
2892 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
2893 }),
2894 )
2895 .await;
2896 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2897 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2898
2899 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2900 let editor_b = workspace_b
2901 .update(cx_b, |workspace, cx| {
2902 workspace.open_path((worktree_id, "one.rs"), true, cx)
2903 })
2904 .await
2905 .unwrap()
2906 .downcast::<Editor>()
2907 .unwrap();
2908 let fake_language_server = fake_language_servers.next().await.unwrap();
2909
2910 // Move cursor to a location that can be renamed.
2911 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
2912 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
2913 editor.rename(&Rename, cx).unwrap()
2914 });
2915
2916 fake_language_server
2917 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
2918 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
2919 assert_eq!(params.position, lsp::Position::new(0, 7));
2920 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
2921 lsp::Position::new(0, 6),
2922 lsp::Position::new(0, 9),
2923 ))))
2924 })
2925 .next()
2926 .await
2927 .unwrap();
2928 prepare_rename.await.unwrap();
2929 editor_b.update(cx_b, |editor, cx| {
2930 let rename = editor.pending_rename().unwrap();
2931 let buffer = editor.buffer().read(cx).snapshot(cx);
2932 assert_eq!(
2933 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
2934 6..9
2935 );
2936 rename.editor.update(cx, |rename_editor, cx| {
2937 rename_editor.buffer().update(cx, |rename_buffer, cx| {
2938 rename_buffer.edit([(0..3, "THREE")], cx);
2939 });
2940 });
2941 });
2942
2943 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
2944 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
2945 });
2946 fake_language_server
2947 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
2948 assert_eq!(
2949 params.text_document_position.text_document.uri.as_str(),
2950 "file:///dir/one.rs"
2951 );
2952 assert_eq!(
2953 params.text_document_position.position,
2954 lsp::Position::new(0, 6)
2955 );
2956 assert_eq!(params.new_name, "THREE");
2957 Ok(Some(lsp::WorkspaceEdit {
2958 changes: Some(
2959 [
2960 (
2961 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
2962 vec![lsp::TextEdit::new(
2963 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2964 "THREE".to_string(),
2965 )],
2966 ),
2967 (
2968 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
2969 vec![
2970 lsp::TextEdit::new(
2971 lsp::Range::new(
2972 lsp::Position::new(0, 24),
2973 lsp::Position::new(0, 27),
2974 ),
2975 "THREE".to_string(),
2976 ),
2977 lsp::TextEdit::new(
2978 lsp::Range::new(
2979 lsp::Position::new(0, 35),
2980 lsp::Position::new(0, 38),
2981 ),
2982 "THREE".to_string(),
2983 ),
2984 ],
2985 ),
2986 ]
2987 .into_iter()
2988 .collect(),
2989 ),
2990 ..Default::default()
2991 }))
2992 })
2993 .next()
2994 .await
2995 .unwrap();
2996 confirm_rename.await.unwrap();
2997
2998 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2999 workspace
3000 .active_item(cx)
3001 .unwrap()
3002 .downcast::<Editor>()
3003 .unwrap()
3004 });
3005 rename_editor.update(cx_b, |editor, cx| {
3006 assert_eq!(
3007 editor.text(cx),
3008 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3009 );
3010 editor.undo(&Undo, cx);
3011 assert_eq!(
3012 editor.text(cx),
3013 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
3014 );
3015 editor.redo(&Redo, cx);
3016 assert_eq!(
3017 editor.text(cx),
3018 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3019 );
3020 });
3021
3022 // Ensure temporary rename edits cannot be undone/redone.
3023 editor_b.update(cx_b, |editor, cx| {
3024 editor.undo(&Undo, cx);
3025 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3026 editor.undo(&Undo, cx);
3027 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3028 editor.redo(&Redo, cx);
3029 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
3030 })
3031}
3032
3033#[gpui::test(iterations = 10)]
3034async fn test_language_server_statuses(
3035 deterministic: Arc<Deterministic>,
3036 cx_a: &mut TestAppContext,
3037 cx_b: &mut TestAppContext,
3038) {
3039 deterministic.forbid_parking();
3040
3041 cx_b.update(|cx| editor::init(cx));
3042 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3043 let client_a = server.create_client(cx_a, "user_a").await;
3044 let client_b = server.create_client(cx_b, "user_b").await;
3045 server
3046 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3047 .await;
3048
3049 // Set up a fake language server.
3050 let mut language = Language::new(
3051 LanguageConfig {
3052 name: "Rust".into(),
3053 path_suffixes: vec!["rs".to_string()],
3054 ..Default::default()
3055 },
3056 Some(tree_sitter_rust::language()),
3057 );
3058 let mut fake_language_servers = language
3059 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3060 name: "the-language-server",
3061 ..Default::default()
3062 }))
3063 .await;
3064 client_a.language_registry.add(Arc::new(language));
3065
3066 client_a
3067 .fs
3068 .insert_tree(
3069 "/dir",
3070 json!({
3071 "main.rs": "const ONE: usize = 1;",
3072 }),
3073 )
3074 .await;
3075 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3076
3077 let _buffer_a = project_a
3078 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3079 .await
3080 .unwrap();
3081
3082 let fake_language_server = fake_language_servers.next().await.unwrap();
3083 fake_language_server.start_progress("the-token").await;
3084 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3085 token: lsp::NumberOrString::String("the-token".to_string()),
3086 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3087 lsp::WorkDoneProgressReport {
3088 message: Some("the-message".to_string()),
3089 ..Default::default()
3090 },
3091 )),
3092 });
3093 deterministic.run_until_parked();
3094 project_a.read_with(cx_a, |project, _| {
3095 let status = project.language_server_statuses().next().unwrap();
3096 assert_eq!(status.name, "the-language-server");
3097 assert_eq!(status.pending_work.len(), 1);
3098 assert_eq!(
3099 status.pending_work["the-token"].message.as_ref().unwrap(),
3100 "the-message"
3101 );
3102 });
3103
3104 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3105 project_b.read_with(cx_b, |project, _| {
3106 let status = project.language_server_statuses().next().unwrap();
3107 assert_eq!(status.name, "the-language-server");
3108 });
3109
3110 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3111 token: lsp::NumberOrString::String("the-token".to_string()),
3112 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3113 lsp::WorkDoneProgressReport {
3114 message: Some("the-message-2".to_string()),
3115 ..Default::default()
3116 },
3117 )),
3118 });
3119 deterministic.run_until_parked();
3120 project_a.read_with(cx_a, |project, _| {
3121 let status = project.language_server_statuses().next().unwrap();
3122 assert_eq!(status.name, "the-language-server");
3123 assert_eq!(status.pending_work.len(), 1);
3124 assert_eq!(
3125 status.pending_work["the-token"].message.as_ref().unwrap(),
3126 "the-message-2"
3127 );
3128 });
3129 project_b.read_with(cx_b, |project, _| {
3130 let status = project.language_server_statuses().next().unwrap();
3131 assert_eq!(status.name, "the-language-server");
3132 assert_eq!(status.pending_work.len(), 1);
3133 assert_eq!(
3134 status.pending_work["the-token"].message.as_ref().unwrap(),
3135 "the-message-2"
3136 );
3137 });
3138}
3139
3140#[gpui::test(iterations = 10)]
3141async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3142 cx_a.foreground().forbid_parking();
3143 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3144 let client_a = server.create_client(cx_a, "user_a").await;
3145 let client_b = server.create_client(cx_b, "user_b").await;
3146
3147 // Create an org that includes these 2 users.
3148 let db = &server.app_state.db;
3149 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3150 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3151 .await
3152 .unwrap();
3153 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
3154 .await
3155 .unwrap();
3156
3157 // Create a channel that includes all the users.
3158 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3159 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3160 .await
3161 .unwrap();
3162 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
3163 .await
3164 .unwrap();
3165 db.create_channel_message(
3166 channel_id,
3167 client_b.current_user_id(&cx_b),
3168 "hello A, it's B.",
3169 OffsetDateTime::now_utc(),
3170 1,
3171 )
3172 .await
3173 .unwrap();
3174
3175 let channels_a =
3176 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3177 channels_a
3178 .condition(cx_a, |list, _| list.available_channels().is_some())
3179 .await;
3180 channels_a.read_with(cx_a, |list, _| {
3181 assert_eq!(
3182 list.available_channels().unwrap(),
3183 &[ChannelDetails {
3184 id: channel_id.to_proto(),
3185 name: "test-channel".to_string()
3186 }]
3187 )
3188 });
3189 let channel_a = channels_a.update(cx_a, |this, cx| {
3190 this.get_channel(channel_id.to_proto(), cx).unwrap()
3191 });
3192 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3193 channel_a
3194 .condition(&cx_a, |channel, _| {
3195 channel_messages(channel)
3196 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3197 })
3198 .await;
3199
3200 let channels_b =
3201 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3202 channels_b
3203 .condition(cx_b, |list, _| list.available_channels().is_some())
3204 .await;
3205 channels_b.read_with(cx_b, |list, _| {
3206 assert_eq!(
3207 list.available_channels().unwrap(),
3208 &[ChannelDetails {
3209 id: channel_id.to_proto(),
3210 name: "test-channel".to_string()
3211 }]
3212 )
3213 });
3214
3215 let channel_b = channels_b.update(cx_b, |this, cx| {
3216 this.get_channel(channel_id.to_proto(), cx).unwrap()
3217 });
3218 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3219 channel_b
3220 .condition(&cx_b, |channel, _| {
3221 channel_messages(channel)
3222 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3223 })
3224 .await;
3225
3226 channel_a
3227 .update(cx_a, |channel, cx| {
3228 channel
3229 .send_message("oh, hi B.".to_string(), cx)
3230 .unwrap()
3231 .detach();
3232 let task = channel.send_message("sup".to_string(), cx).unwrap();
3233 assert_eq!(
3234 channel_messages(channel),
3235 &[
3236 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3237 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3238 ("user_a".to_string(), "sup".to_string(), true)
3239 ]
3240 );
3241 task
3242 })
3243 .await
3244 .unwrap();
3245
3246 channel_b
3247 .condition(&cx_b, |channel, _| {
3248 channel_messages(channel)
3249 == [
3250 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3251 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3252 ("user_a".to_string(), "sup".to_string(), false),
3253 ]
3254 })
3255 .await;
3256
3257 assert_eq!(
3258 server
3259 .store()
3260 .await
3261 .channel(channel_id)
3262 .unwrap()
3263 .connection_ids
3264 .len(),
3265 2
3266 );
3267 cx_b.update(|_| drop(channel_b));
3268 server
3269 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
3270 .await;
3271
3272 cx_a.update(|_| drop(channel_a));
3273 server
3274 .condition(|state| state.channel(channel_id).is_none())
3275 .await;
3276}
3277
3278#[gpui::test(iterations = 10)]
3279async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
3280 cx_a.foreground().forbid_parking();
3281 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3282 let client_a = server.create_client(cx_a, "user_a").await;
3283
3284 let db = &server.app_state.db;
3285 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3286 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3287 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3288 .await
3289 .unwrap();
3290 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3291 .await
3292 .unwrap();
3293
3294 let channels_a =
3295 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3296 channels_a
3297 .condition(cx_a, |list, _| list.available_channels().is_some())
3298 .await;
3299 let channel_a = channels_a.update(cx_a, |this, cx| {
3300 this.get_channel(channel_id.to_proto(), cx).unwrap()
3301 });
3302
3303 // Messages aren't allowed to be too long.
3304 channel_a
3305 .update(cx_a, |channel, cx| {
3306 let long_body = "this is long.\n".repeat(1024);
3307 channel.send_message(long_body, cx).unwrap()
3308 })
3309 .await
3310 .unwrap_err();
3311
3312 // Messages aren't allowed to be blank.
3313 channel_a.update(cx_a, |channel, cx| {
3314 channel.send_message(String::new(), cx).unwrap_err()
3315 });
3316
3317 // Leading and trailing whitespace are trimmed.
3318 channel_a
3319 .update(cx_a, |channel, cx| {
3320 channel
3321 .send_message("\n surrounded by whitespace \n".to_string(), cx)
3322 .unwrap()
3323 })
3324 .await
3325 .unwrap();
3326 assert_eq!(
3327 db.get_channel_messages(channel_id, 10, None)
3328 .await
3329 .unwrap()
3330 .iter()
3331 .map(|m| &m.body)
3332 .collect::<Vec<_>>(),
3333 &["surrounded by whitespace"]
3334 );
3335}
3336
3337#[gpui::test(iterations = 10)]
3338async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3339 cx_a.foreground().forbid_parking();
3340 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3341 let client_a = server.create_client(cx_a, "user_a").await;
3342 let client_b = server.create_client(cx_b, "user_b").await;
3343
3344 let mut status_b = client_b.status();
3345
3346 // Create an org that includes these 2 users.
3347 let db = &server.app_state.db;
3348 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3349 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3350 .await
3351 .unwrap();
3352 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
3353 .await
3354 .unwrap();
3355
3356 // Create a channel that includes all the users.
3357 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3358 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3359 .await
3360 .unwrap();
3361 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
3362 .await
3363 .unwrap();
3364 db.create_channel_message(
3365 channel_id,
3366 client_b.current_user_id(&cx_b),
3367 "hello A, it's B.",
3368 OffsetDateTime::now_utc(),
3369 2,
3370 )
3371 .await
3372 .unwrap();
3373
3374 let channels_a =
3375 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3376 channels_a
3377 .condition(cx_a, |list, _| list.available_channels().is_some())
3378 .await;
3379
3380 channels_a.read_with(cx_a, |list, _| {
3381 assert_eq!(
3382 list.available_channels().unwrap(),
3383 &[ChannelDetails {
3384 id: channel_id.to_proto(),
3385 name: "test-channel".to_string()
3386 }]
3387 )
3388 });
3389 let channel_a = channels_a.update(cx_a, |this, cx| {
3390 this.get_channel(channel_id.to_proto(), cx).unwrap()
3391 });
3392 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3393 channel_a
3394 .condition(&cx_a, |channel, _| {
3395 channel_messages(channel)
3396 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3397 })
3398 .await;
3399
3400 let channels_b =
3401 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3402 channels_b
3403 .condition(cx_b, |list, _| list.available_channels().is_some())
3404 .await;
3405 channels_b.read_with(cx_b, |list, _| {
3406 assert_eq!(
3407 list.available_channels().unwrap(),
3408 &[ChannelDetails {
3409 id: channel_id.to_proto(),
3410 name: "test-channel".to_string()
3411 }]
3412 )
3413 });
3414
3415 let channel_b = channels_b.update(cx_b, |this, cx| {
3416 this.get_channel(channel_id.to_proto(), cx).unwrap()
3417 });
3418 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3419 channel_b
3420 .condition(&cx_b, |channel, _| {
3421 channel_messages(channel)
3422 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3423 })
3424 .await;
3425
3426 // Disconnect client B, ensuring we can still access its cached channel data.
3427 server.forbid_connections();
3428 server.disconnect_client(client_b.current_user_id(&cx_b));
3429 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
3430 while !matches!(
3431 status_b.next().await,
3432 Some(client::Status::ReconnectionError { .. })
3433 ) {}
3434
3435 channels_b.read_with(cx_b, |channels, _| {
3436 assert_eq!(
3437 channels.available_channels().unwrap(),
3438 [ChannelDetails {
3439 id: channel_id.to_proto(),
3440 name: "test-channel".to_string()
3441 }]
3442 )
3443 });
3444 channel_b.read_with(cx_b, |channel, _| {
3445 assert_eq!(
3446 channel_messages(channel),
3447 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3448 )
3449 });
3450
3451 // Send a message from client B while it is disconnected.
3452 channel_b
3453 .update(cx_b, |channel, cx| {
3454 let task = channel
3455 .send_message("can you see this?".to_string(), cx)
3456 .unwrap();
3457 assert_eq!(
3458 channel_messages(channel),
3459 &[
3460 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3461 ("user_b".to_string(), "can you see this?".to_string(), true)
3462 ]
3463 );
3464 task
3465 })
3466 .await
3467 .unwrap_err();
3468
3469 // Send a message from client A while B is disconnected.
3470 channel_a
3471 .update(cx_a, |channel, cx| {
3472 channel
3473 .send_message("oh, hi B.".to_string(), cx)
3474 .unwrap()
3475 .detach();
3476 let task = channel.send_message("sup".to_string(), cx).unwrap();
3477 assert_eq!(
3478 channel_messages(channel),
3479 &[
3480 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3481 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3482 ("user_a".to_string(), "sup".to_string(), true)
3483 ]
3484 );
3485 task
3486 })
3487 .await
3488 .unwrap();
3489
3490 // Give client B a chance to reconnect.
3491 server.allow_connections();
3492 cx_b.foreground().advance_clock(Duration::from_secs(10));
3493
3494 // Verify that B sees the new messages upon reconnection, as well as the message client B
3495 // sent while offline.
3496 channel_b
3497 .condition(&cx_b, |channel, _| {
3498 channel_messages(channel)
3499 == [
3500 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3501 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3502 ("user_a".to_string(), "sup".to_string(), false),
3503 ("user_b".to_string(), "can you see this?".to_string(), false),
3504 ]
3505 })
3506 .await;
3507
3508 // Ensure client A and B can communicate normally after reconnection.
3509 channel_a
3510 .update(cx_a, |channel, cx| {
3511 channel.send_message("you online?".to_string(), cx).unwrap()
3512 })
3513 .await
3514 .unwrap();
3515 channel_b
3516 .condition(&cx_b, |channel, _| {
3517 channel_messages(channel)
3518 == [
3519 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3520 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3521 ("user_a".to_string(), "sup".to_string(), false),
3522 ("user_b".to_string(), "can you see this?".to_string(), false),
3523 ("user_a".to_string(), "you online?".to_string(), false),
3524 ]
3525 })
3526 .await;
3527
3528 channel_b
3529 .update(cx_b, |channel, cx| {
3530 channel.send_message("yep".to_string(), cx).unwrap()
3531 })
3532 .await
3533 .unwrap();
3534 channel_a
3535 .condition(&cx_a, |channel, _| {
3536 channel_messages(channel)
3537 == [
3538 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3539 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3540 ("user_a".to_string(), "sup".to_string(), false),
3541 ("user_b".to_string(), "can you see this?".to_string(), false),
3542 ("user_a".to_string(), "you online?".to_string(), false),
3543 ("user_b".to_string(), "yep".to_string(), false),
3544 ]
3545 })
3546 .await;
3547}
3548
3549#[gpui::test(iterations = 10)]
3550async fn test_contacts(
3551 deterministic: Arc<Deterministic>,
3552 cx_a: &mut TestAppContext,
3553 cx_b: &mut TestAppContext,
3554 cx_c: &mut TestAppContext,
3555) {
3556 cx_a.foreground().forbid_parking();
3557 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3558 let client_a = server.create_client(cx_a, "user_a").await;
3559 let client_b = server.create_client(cx_b, "user_b").await;
3560 let client_c = server.create_client(cx_c, "user_c").await;
3561 server
3562 .make_contacts(vec![
3563 (&client_a, cx_a),
3564 (&client_b, cx_b),
3565 (&client_c, cx_c),
3566 ])
3567 .await;
3568
3569 deterministic.run_until_parked();
3570 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3571 client.user_store.read_with(*cx, |store, _| {
3572 assert_eq!(
3573 contacts(store),
3574 [
3575 ("user_a", true, vec![]),
3576 ("user_b", true, vec![]),
3577 ("user_c", true, vec![])
3578 ],
3579 "{} has the wrong contacts",
3580 client.username
3581 )
3582 });
3583 }
3584
3585 // Share a project as client A.
3586 client_a.fs.create_dir(Path::new("/a")).await.unwrap();
3587 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3588
3589 deterministic.run_until_parked();
3590 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3591 client.user_store.read_with(*cx, |store, _| {
3592 assert_eq!(
3593 contacts(store),
3594 [
3595 ("user_a", true, vec![("a", vec![])]),
3596 ("user_b", true, vec![]),
3597 ("user_c", true, vec![])
3598 ],
3599 "{} has the wrong contacts",
3600 client.username
3601 )
3602 });
3603 }
3604
3605 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3606
3607 deterministic.run_until_parked();
3608 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3609 client.user_store.read_with(*cx, |store, _| {
3610 assert_eq!(
3611 contacts(store),
3612 [
3613 ("user_a", true, vec![("a", vec!["user_b"])]),
3614 ("user_b", true, vec![]),
3615 ("user_c", true, vec![])
3616 ],
3617 "{} has the wrong contacts",
3618 client.username
3619 )
3620 });
3621 }
3622
3623 // Add a local project as client B
3624 client_a.fs.create_dir("/b".as_ref()).await.unwrap();
3625 let (_project_b, _) = client_b.build_local_project("/b", cx_b).await;
3626
3627 deterministic.run_until_parked();
3628 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3629 client.user_store.read_with(*cx, |store, _| {
3630 assert_eq!(
3631 contacts(store),
3632 [
3633 ("user_a", true, vec![("a", vec!["user_b"])]),
3634 ("user_b", true, vec![("b", vec![])]),
3635 ("user_c", true, vec![])
3636 ],
3637 "{} has the wrong contacts",
3638 client.username
3639 )
3640 });
3641 }
3642
3643 project_a
3644 .condition(&cx_a, |project, _| {
3645 project.collaborators().contains_key(&client_b.peer_id)
3646 })
3647 .await;
3648
3649 cx_a.update(move |_| drop(project_a));
3650 deterministic.run_until_parked();
3651 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3652 client.user_store.read_with(*cx, |store, _| {
3653 assert_eq!(
3654 contacts(store),
3655 [
3656 ("user_a", true, vec![]),
3657 ("user_b", true, vec![("b", vec![])]),
3658 ("user_c", true, vec![])
3659 ],
3660 "{} has the wrong contacts",
3661 client.username
3662 )
3663 });
3664 }
3665
3666 server.disconnect_client(client_c.current_user_id(cx_c));
3667 server.forbid_connections();
3668 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
3669 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
3670 client.user_store.read_with(*cx, |store, _| {
3671 assert_eq!(
3672 contacts(store),
3673 [
3674 ("user_a", true, vec![]),
3675 ("user_b", true, vec![("b", vec![])]),
3676 ("user_c", false, vec![])
3677 ],
3678 "{} has the wrong contacts",
3679 client.username
3680 )
3681 });
3682 }
3683 client_c
3684 .user_store
3685 .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
3686
3687 server.allow_connections();
3688 client_c
3689 .authenticate_and_connect(false, &cx_c.to_async())
3690 .await
3691 .unwrap();
3692
3693 deterministic.run_until_parked();
3694 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3695 client.user_store.read_with(*cx, |store, _| {
3696 assert_eq!(
3697 contacts(store),
3698 [
3699 ("user_a", true, vec![]),
3700 ("user_b", true, vec![("b", vec![])]),
3701 ("user_c", true, vec![])
3702 ],
3703 "{} has the wrong contacts",
3704 client.username
3705 )
3706 });
3707 }
3708
3709 fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
3710 user_store
3711 .contacts()
3712 .iter()
3713 .map(|contact| {
3714 let projects = contact
3715 .projects
3716 .iter()
3717 .map(|p| {
3718 (
3719 p.visible_worktree_root_names[0].as_str(),
3720 p.guests.iter().map(|p| p.github_login.as_str()).collect(),
3721 )
3722 })
3723 .collect();
3724 (contact.user.github_login.as_str(), contact.online, projects)
3725 })
3726 .collect()
3727 }
3728}
3729
3730#[gpui::test(iterations = 10)]
3731async fn test_contact_requests(
3732 executor: Arc<Deterministic>,
3733 cx_a: &mut TestAppContext,
3734 cx_a2: &mut TestAppContext,
3735 cx_b: &mut TestAppContext,
3736 cx_b2: &mut TestAppContext,
3737 cx_c: &mut TestAppContext,
3738 cx_c2: &mut TestAppContext,
3739) {
3740 cx_a.foreground().forbid_parking();
3741
3742 // Connect to a server as 3 clients.
3743 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3744 let client_a = server.create_client(cx_a, "user_a").await;
3745 let client_a2 = server.create_client(cx_a2, "user_a").await;
3746 let client_b = server.create_client(cx_b, "user_b").await;
3747 let client_b2 = server.create_client(cx_b2, "user_b").await;
3748 let client_c = server.create_client(cx_c, "user_c").await;
3749 let client_c2 = server.create_client(cx_c2, "user_c").await;
3750
3751 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
3752 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
3753 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
3754
3755 // User A and User C request that user B become their contact.
3756 client_a
3757 .user_store
3758 .update(cx_a, |store, cx| {
3759 store.request_contact(client_b.user_id().unwrap(), cx)
3760 })
3761 .await
3762 .unwrap();
3763 client_c
3764 .user_store
3765 .update(cx_c, |store, cx| {
3766 store.request_contact(client_b.user_id().unwrap(), cx)
3767 })
3768 .await
3769 .unwrap();
3770 executor.run_until_parked();
3771
3772 // All users see the pending request appear in all their clients.
3773 assert_eq!(
3774 client_a.summarize_contacts(&cx_a).outgoing_requests,
3775 &["user_b"]
3776 );
3777 assert_eq!(
3778 client_a2.summarize_contacts(&cx_a2).outgoing_requests,
3779 &["user_b"]
3780 );
3781 assert_eq!(
3782 client_b.summarize_contacts(&cx_b).incoming_requests,
3783 &["user_a", "user_c"]
3784 );
3785 assert_eq!(
3786 client_b2.summarize_contacts(&cx_b2).incoming_requests,
3787 &["user_a", "user_c"]
3788 );
3789 assert_eq!(
3790 client_c.summarize_contacts(&cx_c).outgoing_requests,
3791 &["user_b"]
3792 );
3793 assert_eq!(
3794 client_c2.summarize_contacts(&cx_c2).outgoing_requests,
3795 &["user_b"]
3796 );
3797
3798 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
3799 disconnect_and_reconnect(&client_a, cx_a).await;
3800 disconnect_and_reconnect(&client_b, cx_b).await;
3801 disconnect_and_reconnect(&client_c, cx_c).await;
3802 executor.run_until_parked();
3803 assert_eq!(
3804 client_a.summarize_contacts(&cx_a).outgoing_requests,
3805 &["user_b"]
3806 );
3807 assert_eq!(
3808 client_b.summarize_contacts(&cx_b).incoming_requests,
3809 &["user_a", "user_c"]
3810 );
3811 assert_eq!(
3812 client_c.summarize_contacts(&cx_c).outgoing_requests,
3813 &["user_b"]
3814 );
3815
3816 // User B accepts the request from user A.
3817 client_b
3818 .user_store
3819 .update(cx_b, |store, cx| {
3820 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
3821 })
3822 .await
3823 .unwrap();
3824
3825 executor.run_until_parked();
3826
3827 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
3828 let contacts_b = client_b.summarize_contacts(&cx_b);
3829 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3830 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
3831 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3832 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3833 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
3834
3835 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
3836 let contacts_a = client_a.summarize_contacts(&cx_a);
3837 assert_eq!(contacts_a.current, &["user_a", "user_b"]);
3838 assert!(contacts_a.outgoing_requests.is_empty());
3839 let contacts_a2 = client_a2.summarize_contacts(&cx_a2);
3840 assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
3841 assert!(contacts_a2.outgoing_requests.is_empty());
3842
3843 // Contacts are present upon connecting (tested here via disconnect/reconnect)
3844 disconnect_and_reconnect(&client_a, cx_a).await;
3845 disconnect_and_reconnect(&client_b, cx_b).await;
3846 disconnect_and_reconnect(&client_c, cx_c).await;
3847 executor.run_until_parked();
3848 assert_eq!(
3849 client_a.summarize_contacts(&cx_a).current,
3850 &["user_a", "user_b"]
3851 );
3852 assert_eq!(
3853 client_b.summarize_contacts(&cx_b).current,
3854 &["user_a", "user_b"]
3855 );
3856 assert_eq!(
3857 client_b.summarize_contacts(&cx_b).incoming_requests,
3858 &["user_c"]
3859 );
3860 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3861 assert_eq!(
3862 client_c.summarize_contacts(&cx_c).outgoing_requests,
3863 &["user_b"]
3864 );
3865
3866 // User B rejects the request from user C.
3867 client_b
3868 .user_store
3869 .update(cx_b, |store, cx| {
3870 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
3871 })
3872 .await
3873 .unwrap();
3874
3875 executor.run_until_parked();
3876
3877 // User B doesn't see user C as their contact, and the incoming request from them is removed.
3878 let contacts_b = client_b.summarize_contacts(&cx_b);
3879 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3880 assert!(contacts_b.incoming_requests.is_empty());
3881 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3882 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3883 assert!(contacts_b2.incoming_requests.is_empty());
3884
3885 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
3886 let contacts_c = client_c.summarize_contacts(&cx_c);
3887 assert_eq!(contacts_c.current, &["user_c"]);
3888 assert!(contacts_c.outgoing_requests.is_empty());
3889 let contacts_c2 = client_c2.summarize_contacts(&cx_c2);
3890 assert_eq!(contacts_c2.current, &["user_c"]);
3891 assert!(contacts_c2.outgoing_requests.is_empty());
3892
3893 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
3894 disconnect_and_reconnect(&client_a, cx_a).await;
3895 disconnect_and_reconnect(&client_b, cx_b).await;
3896 disconnect_and_reconnect(&client_c, cx_c).await;
3897 executor.run_until_parked();
3898 assert_eq!(
3899 client_a.summarize_contacts(&cx_a).current,
3900 &["user_a", "user_b"]
3901 );
3902 assert_eq!(
3903 client_b.summarize_contacts(&cx_b).current,
3904 &["user_a", "user_b"]
3905 );
3906 assert!(client_b
3907 .summarize_contacts(&cx_b)
3908 .incoming_requests
3909 .is_empty());
3910 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3911 assert!(client_c
3912 .summarize_contacts(&cx_c)
3913 .outgoing_requests
3914 .is_empty());
3915
3916 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
3917 client.disconnect(&cx.to_async()).unwrap();
3918 client.clear_contacts(cx).await;
3919 client
3920 .authenticate_and_connect(false, &cx.to_async())
3921 .await
3922 .unwrap();
3923 }
3924}
3925
3926#[gpui::test(iterations = 10)]
3927async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3928 cx_a.foreground().forbid_parking();
3929 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3930 let client_a = server.create_client(cx_a, "user_a").await;
3931 let client_b = server.create_client(cx_b, "user_b").await;
3932 server
3933 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3934 .await;
3935 cx_a.update(editor::init);
3936 cx_b.update(editor::init);
3937
3938 client_a
3939 .fs
3940 .insert_tree(
3941 "/a",
3942 json!({
3943 "1.txt": "one",
3944 "2.txt": "two",
3945 "3.txt": "three",
3946 }),
3947 )
3948 .await;
3949 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3950
3951 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3952
3953 // Client A opens some editors.
3954 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3955 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
3956 let editor_a1 = workspace_a
3957 .update(cx_a, |workspace, cx| {
3958 workspace.open_path((worktree_id, "1.txt"), true, cx)
3959 })
3960 .await
3961 .unwrap()
3962 .downcast::<Editor>()
3963 .unwrap();
3964 let editor_a2 = workspace_a
3965 .update(cx_a, |workspace, cx| {
3966 workspace.open_path((worktree_id, "2.txt"), true, cx)
3967 })
3968 .await
3969 .unwrap()
3970 .downcast::<Editor>()
3971 .unwrap();
3972
3973 // Client B opens an editor.
3974 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3975 let editor_b1 = workspace_b
3976 .update(cx_b, |workspace, cx| {
3977 workspace.open_path((worktree_id, "1.txt"), true, cx)
3978 })
3979 .await
3980 .unwrap()
3981 .downcast::<Editor>()
3982 .unwrap();
3983
3984 let client_a_id = project_b.read_with(cx_b, |project, _| {
3985 project.collaborators().values().next().unwrap().peer_id
3986 });
3987 let client_b_id = project_a.read_with(cx_a, |project, _| {
3988 project.collaborators().values().next().unwrap().peer_id
3989 });
3990
3991 // When client B starts following client A, all visible view states are replicated to client B.
3992 editor_a1.update(cx_a, |editor, cx| {
3993 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
3994 });
3995 editor_a2.update(cx_a, |editor, cx| {
3996 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
3997 });
3998 workspace_b
3999 .update(cx_b, |workspace, cx| {
4000 workspace
4001 .toggle_follow(&ToggleFollow(client_a_id), cx)
4002 .unwrap()
4003 })
4004 .await
4005 .unwrap();
4006
4007 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4008 workspace
4009 .active_item(cx)
4010 .unwrap()
4011 .downcast::<Editor>()
4012 .unwrap()
4013 });
4014 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
4015 assert_eq!(
4016 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
4017 Some((worktree_id, "2.txt").into())
4018 );
4019 assert_eq!(
4020 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4021 vec![2..3]
4022 );
4023 assert_eq!(
4024 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4025 vec![0..1]
4026 );
4027
4028 // When client A activates a different editor, client B does so as well.
4029 workspace_a.update(cx_a, |workspace, cx| {
4030 workspace.activate_item(&editor_a1, cx)
4031 });
4032 workspace_b
4033 .condition(cx_b, |workspace, cx| {
4034 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4035 })
4036 .await;
4037
4038 // When client A navigates back and forth, client B does so as well.
4039 workspace_a
4040 .update(cx_a, |workspace, cx| {
4041 workspace::Pane::go_back(workspace, None, cx)
4042 })
4043 .await;
4044 workspace_b
4045 .condition(cx_b, |workspace, cx| {
4046 workspace.active_item(cx).unwrap().id() == editor_b2.id()
4047 })
4048 .await;
4049
4050 workspace_a
4051 .update(cx_a, |workspace, cx| {
4052 workspace::Pane::go_forward(workspace, None, cx)
4053 })
4054 .await;
4055 workspace_b
4056 .condition(cx_b, |workspace, cx| {
4057 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4058 })
4059 .await;
4060
4061 // Changes to client A's editor are reflected on client B.
4062 editor_a1.update(cx_a, |editor, cx| {
4063 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
4064 });
4065 editor_b1
4066 .condition(cx_b, |editor, cx| {
4067 editor.selections.ranges(cx) == vec![1..1, 2..2]
4068 })
4069 .await;
4070
4071 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
4072 editor_b1
4073 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
4074 .await;
4075
4076 editor_a1.update(cx_a, |editor, cx| {
4077 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
4078 editor.set_scroll_position(vec2f(0., 100.), cx);
4079 });
4080 editor_b1
4081 .condition(cx_b, |editor, cx| {
4082 editor.selections.ranges(cx) == vec![3..3]
4083 })
4084 .await;
4085
4086 // After unfollowing, client B stops receiving updates from client A.
4087 workspace_b.update(cx_b, |workspace, cx| {
4088 workspace.unfollow(&workspace.active_pane().clone(), cx)
4089 });
4090 workspace_a.update(cx_a, |workspace, cx| {
4091 workspace.activate_item(&editor_a2, cx)
4092 });
4093 cx_a.foreground().run_until_parked();
4094 assert_eq!(
4095 workspace_b.read_with(cx_b, |workspace, cx| workspace
4096 .active_item(cx)
4097 .unwrap()
4098 .id()),
4099 editor_b1.id()
4100 );
4101
4102 // Client A starts following client B.
4103 workspace_a
4104 .update(cx_a, |workspace, cx| {
4105 workspace
4106 .toggle_follow(&ToggleFollow(client_b_id), cx)
4107 .unwrap()
4108 })
4109 .await
4110 .unwrap();
4111 assert_eq!(
4112 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4113 Some(client_b_id)
4114 );
4115 assert_eq!(
4116 workspace_a.read_with(cx_a, |workspace, cx| workspace
4117 .active_item(cx)
4118 .unwrap()
4119 .id()),
4120 editor_a1.id()
4121 );
4122
4123 // Following interrupts when client B disconnects.
4124 client_b.disconnect(&cx_b.to_async()).unwrap();
4125 cx_a.foreground().run_until_parked();
4126 assert_eq!(
4127 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4128 None
4129 );
4130}
4131
4132#[gpui::test(iterations = 10)]
4133async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4134 cx_a.foreground().forbid_parking();
4135 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4136 let client_a = server.create_client(cx_a, "user_a").await;
4137 let client_b = server.create_client(cx_b, "user_b").await;
4138 server
4139 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4140 .await;
4141 cx_a.update(editor::init);
4142 cx_b.update(editor::init);
4143
4144 // Client A shares a project.
4145 client_a
4146 .fs
4147 .insert_tree(
4148 "/a",
4149 json!({
4150 "1.txt": "one",
4151 "2.txt": "two",
4152 "3.txt": "three",
4153 "4.txt": "four",
4154 }),
4155 )
4156 .await;
4157 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4158
4159 // Client B joins the project.
4160 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4161
4162 // Client A opens some editors.
4163 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4164 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4165 let _editor_a1 = workspace_a
4166 .update(cx_a, |workspace, cx| {
4167 workspace.open_path((worktree_id, "1.txt"), true, cx)
4168 })
4169 .await
4170 .unwrap()
4171 .downcast::<Editor>()
4172 .unwrap();
4173
4174 // Client B opens an editor.
4175 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4176 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4177 let _editor_b1 = workspace_b
4178 .update(cx_b, |workspace, cx| {
4179 workspace.open_path((worktree_id, "2.txt"), true, cx)
4180 })
4181 .await
4182 .unwrap()
4183 .downcast::<Editor>()
4184 .unwrap();
4185
4186 // Clients A and B follow each other in split panes
4187 workspace_a.update(cx_a, |workspace, cx| {
4188 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4189 assert_ne!(*workspace.active_pane(), pane_a1);
4190 });
4191 workspace_a
4192 .update(cx_a, |workspace, cx| {
4193 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
4194 workspace
4195 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4196 .unwrap()
4197 })
4198 .await
4199 .unwrap();
4200 workspace_b.update(cx_b, |workspace, cx| {
4201 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4202 assert_ne!(*workspace.active_pane(), pane_b1);
4203 });
4204 workspace_b
4205 .update(cx_b, |workspace, cx| {
4206 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
4207 workspace
4208 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4209 .unwrap()
4210 })
4211 .await
4212 .unwrap();
4213
4214 workspace_a
4215 .update(cx_a, |workspace, cx| {
4216 workspace.activate_next_pane(cx);
4217 assert_eq!(*workspace.active_pane(), pane_a1);
4218 workspace.open_path((worktree_id, "3.txt"), true, cx)
4219 })
4220 .await
4221 .unwrap();
4222 workspace_b
4223 .update(cx_b, |workspace, cx| {
4224 workspace.activate_next_pane(cx);
4225 assert_eq!(*workspace.active_pane(), pane_b1);
4226 workspace.open_path((worktree_id, "4.txt"), true, cx)
4227 })
4228 .await
4229 .unwrap();
4230 cx_a.foreground().run_until_parked();
4231
4232 // Ensure leader updates don't change the active pane of followers
4233 workspace_a.read_with(cx_a, |workspace, _| {
4234 assert_eq!(*workspace.active_pane(), pane_a1);
4235 });
4236 workspace_b.read_with(cx_b, |workspace, _| {
4237 assert_eq!(*workspace.active_pane(), pane_b1);
4238 });
4239
4240 // Ensure peers following each other doesn't cause an infinite loop.
4241 assert_eq!(
4242 workspace_a.read_with(cx_a, |workspace, cx| workspace
4243 .active_item(cx)
4244 .unwrap()
4245 .project_path(cx)),
4246 Some((worktree_id, "3.txt").into())
4247 );
4248 workspace_a.update(cx_a, |workspace, cx| {
4249 assert_eq!(
4250 workspace.active_item(cx).unwrap().project_path(cx),
4251 Some((worktree_id, "3.txt").into())
4252 );
4253 workspace.activate_next_pane(cx);
4254 assert_eq!(
4255 workspace.active_item(cx).unwrap().project_path(cx),
4256 Some((worktree_id, "4.txt").into())
4257 );
4258 });
4259 workspace_b.update(cx_b, |workspace, cx| {
4260 assert_eq!(
4261 workspace.active_item(cx).unwrap().project_path(cx),
4262 Some((worktree_id, "4.txt").into())
4263 );
4264 workspace.activate_next_pane(cx);
4265 assert_eq!(
4266 workspace.active_item(cx).unwrap().project_path(cx),
4267 Some((worktree_id, "3.txt").into())
4268 );
4269 });
4270}
4271
4272#[gpui::test(iterations = 10)]
4273async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4274 cx_a.foreground().forbid_parking();
4275
4276 // 2 clients connect to a server.
4277 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4278 let client_a = server.create_client(cx_a, "user_a").await;
4279 let client_b = server.create_client(cx_b, "user_b").await;
4280 server
4281 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4282 .await;
4283 cx_a.update(editor::init);
4284 cx_b.update(editor::init);
4285
4286 // Client A shares a project.
4287 client_a
4288 .fs
4289 .insert_tree(
4290 "/a",
4291 json!({
4292 "1.txt": "one",
4293 "2.txt": "two",
4294 "3.txt": "three",
4295 }),
4296 )
4297 .await;
4298 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4299 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4300
4301 // Client A opens some editors.
4302 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4303 let _editor_a1 = workspace_a
4304 .update(cx_a, |workspace, cx| {
4305 workspace.open_path((worktree_id, "1.txt"), true, cx)
4306 })
4307 .await
4308 .unwrap()
4309 .downcast::<Editor>()
4310 .unwrap();
4311
4312 // Client B starts following client A.
4313 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4314 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4315 let leader_id = project_b.read_with(cx_b, |project, _| {
4316 project.collaborators().values().next().unwrap().peer_id
4317 });
4318 workspace_b
4319 .update(cx_b, |workspace, cx| {
4320 workspace
4321 .toggle_follow(&ToggleFollow(leader_id), cx)
4322 .unwrap()
4323 })
4324 .await
4325 .unwrap();
4326 assert_eq!(
4327 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4328 Some(leader_id)
4329 );
4330 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4331 workspace
4332 .active_item(cx)
4333 .unwrap()
4334 .downcast::<Editor>()
4335 .unwrap()
4336 });
4337
4338 // When client B moves, it automatically stops following client A.
4339 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
4340 assert_eq!(
4341 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4342 None
4343 );
4344
4345 workspace_b
4346 .update(cx_b, |workspace, cx| {
4347 workspace
4348 .toggle_follow(&ToggleFollow(leader_id), cx)
4349 .unwrap()
4350 })
4351 .await
4352 .unwrap();
4353 assert_eq!(
4354 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4355 Some(leader_id)
4356 );
4357
4358 // When client B edits, it automatically stops following client A.
4359 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
4360 assert_eq!(
4361 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4362 None
4363 );
4364
4365 workspace_b
4366 .update(cx_b, |workspace, cx| {
4367 workspace
4368 .toggle_follow(&ToggleFollow(leader_id), cx)
4369 .unwrap()
4370 })
4371 .await
4372 .unwrap();
4373 assert_eq!(
4374 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4375 Some(leader_id)
4376 );
4377
4378 // When client B scrolls, it automatically stops following client A.
4379 editor_b2.update(cx_b, |editor, cx| {
4380 editor.set_scroll_position(vec2f(0., 3.), cx)
4381 });
4382 assert_eq!(
4383 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4384 None
4385 );
4386
4387 workspace_b
4388 .update(cx_b, |workspace, cx| {
4389 workspace
4390 .toggle_follow(&ToggleFollow(leader_id), cx)
4391 .unwrap()
4392 })
4393 .await
4394 .unwrap();
4395 assert_eq!(
4396 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4397 Some(leader_id)
4398 );
4399
4400 // When client B activates a different pane, it continues following client A in the original pane.
4401 workspace_b.update(cx_b, |workspace, cx| {
4402 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
4403 });
4404 assert_eq!(
4405 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4406 Some(leader_id)
4407 );
4408
4409 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
4410 assert_eq!(
4411 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4412 Some(leader_id)
4413 );
4414
4415 // When client B activates a different item in the original pane, it automatically stops following client A.
4416 workspace_b
4417 .update(cx_b, |workspace, cx| {
4418 workspace.open_path((worktree_id, "2.txt"), true, cx)
4419 })
4420 .await
4421 .unwrap();
4422 assert_eq!(
4423 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4424 None
4425 );
4426}
4427
4428#[gpui::test(iterations = 10)]
4429async fn test_peers_simultaneously_following_each_other(
4430 deterministic: Arc<Deterministic>,
4431 cx_a: &mut TestAppContext,
4432 cx_b: &mut TestAppContext,
4433) {
4434 deterministic.forbid_parking();
4435
4436 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4437 let client_a = server.create_client(cx_a, "user_a").await;
4438 let client_b = server.create_client(cx_b, "user_b").await;
4439 server
4440 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4441 .await;
4442 cx_a.update(editor::init);
4443 cx_b.update(editor::init);
4444
4445 client_a.fs.insert_tree("/a", json!({})).await;
4446 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
4447 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4448
4449 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4450 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4451
4452 deterministic.run_until_parked();
4453 let client_a_id = project_b.read_with(cx_b, |project, _| {
4454 project.collaborators().values().next().unwrap().peer_id
4455 });
4456 let client_b_id = project_a.read_with(cx_a, |project, _| {
4457 project.collaborators().values().next().unwrap().peer_id
4458 });
4459
4460 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
4461 workspace
4462 .toggle_follow(&ToggleFollow(client_b_id), cx)
4463 .unwrap()
4464 });
4465 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
4466 workspace
4467 .toggle_follow(&ToggleFollow(client_a_id), cx)
4468 .unwrap()
4469 });
4470
4471 futures::try_join!(a_follow_b, b_follow_a).unwrap();
4472 workspace_a.read_with(cx_a, |workspace, _| {
4473 assert_eq!(
4474 workspace.leader_for_pane(&workspace.active_pane()),
4475 Some(client_b_id)
4476 );
4477 });
4478 workspace_b.read_with(cx_b, |workspace, _| {
4479 assert_eq!(
4480 workspace.leader_for_pane(&workspace.active_pane()),
4481 Some(client_a_id)
4482 );
4483 });
4484}
4485
4486#[gpui::test(iterations = 100)]
4487async fn test_random_collaboration(
4488 cx: &mut TestAppContext,
4489 deterministic: Arc<Deterministic>,
4490 rng: StdRng,
4491) {
4492 deterministic.forbid_parking();
4493 let max_peers = env::var("MAX_PEERS")
4494 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4495 .unwrap_or(5);
4496 assert!(max_peers <= 5);
4497
4498 let max_operations = env::var("OPERATIONS")
4499 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4500 .unwrap_or(10);
4501
4502 let rng = Arc::new(Mutex::new(rng));
4503
4504 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4505 let host_language_registry = Arc::new(LanguageRegistry::test());
4506
4507 let fs = FakeFs::new(cx.background());
4508 fs.insert_tree("/_collab", json!({"init": ""})).await;
4509
4510 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4511 let db = server.app_state.db.clone();
4512 let host_user_id = db.create_user("host", None, false).await.unwrap();
4513 let mut available_guests = vec![
4514 "guest-1".to_string(),
4515 "guest-2".to_string(),
4516 "guest-3".to_string(),
4517 "guest-4".to_string(),
4518 ];
4519
4520 for username in &available_guests {
4521 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4522 assert_eq!(*username, format!("guest-{}", guest_user_id));
4523 server
4524 .app_state
4525 .db
4526 .send_contact_request(guest_user_id, host_user_id)
4527 .await
4528 .unwrap();
4529 server
4530 .app_state
4531 .db
4532 .respond_to_contact_request(host_user_id, guest_user_id, true)
4533 .await
4534 .unwrap();
4535 }
4536
4537 let mut clients = Vec::new();
4538 let mut user_ids = Vec::new();
4539 let mut op_start_signals = Vec::new();
4540
4541 let mut next_entity_id = 100000;
4542 let mut host_cx = TestAppContext::new(
4543 cx.foreground_platform(),
4544 cx.platform(),
4545 deterministic.build_foreground(next_entity_id),
4546 deterministic.build_background(),
4547 cx.font_cache(),
4548 cx.leak_detector(),
4549 next_entity_id,
4550 );
4551 let host = server.create_client(&mut host_cx, "host").await;
4552 let host_project = host_cx.update(|cx| {
4553 Project::local(
4554 true,
4555 host.client.clone(),
4556 host.user_store.clone(),
4557 host.project_store.clone(),
4558 host_language_registry.clone(),
4559 fs.clone(),
4560 cx,
4561 )
4562 });
4563 let host_project_id = host_project
4564 .update(&mut host_cx, |p, _| p.next_remote_id())
4565 .await;
4566
4567 let (collab_worktree, _) = host_project
4568 .update(&mut host_cx, |project, cx| {
4569 project.find_or_create_local_worktree("/_collab", true, cx)
4570 })
4571 .await
4572 .unwrap();
4573 collab_worktree
4574 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4575 .await;
4576
4577 // Set up fake language servers.
4578 let mut language = Language::new(
4579 LanguageConfig {
4580 name: "Rust".into(),
4581 path_suffixes: vec!["rs".to_string()],
4582 ..Default::default()
4583 },
4584 None,
4585 );
4586 let _fake_servers = language
4587 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4588 name: "the-fake-language-server",
4589 capabilities: lsp::LanguageServer::full_capabilities(),
4590 initializer: Some(Box::new({
4591 let rng = rng.clone();
4592 let fs = fs.clone();
4593 let project = host_project.downgrade();
4594 move |fake_server: &mut FakeLanguageServer| {
4595 fake_server.handle_request::<lsp::request::Completion, _, _>(
4596 |_, _| async move {
4597 Ok(Some(lsp::CompletionResponse::Array(vec![
4598 lsp::CompletionItem {
4599 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4600 range: lsp::Range::new(
4601 lsp::Position::new(0, 0),
4602 lsp::Position::new(0, 0),
4603 ),
4604 new_text: "the-new-text".to_string(),
4605 })),
4606 ..Default::default()
4607 },
4608 ])))
4609 },
4610 );
4611
4612 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4613 |_, _| async move {
4614 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4615 lsp::CodeAction {
4616 title: "the-code-action".to_string(),
4617 ..Default::default()
4618 },
4619 )]))
4620 },
4621 );
4622
4623 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4624 |params, _| async move {
4625 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4626 params.position,
4627 params.position,
4628 ))))
4629 },
4630 );
4631
4632 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4633 let fs = fs.clone();
4634 let rng = rng.clone();
4635 move |_, _| {
4636 let fs = fs.clone();
4637 let rng = rng.clone();
4638 async move {
4639 let files = fs.files().await;
4640 let mut rng = rng.lock();
4641 let count = rng.gen_range::<usize, _>(1..3);
4642 let files = (0..count)
4643 .map(|_| files.choose(&mut *rng).unwrap())
4644 .collect::<Vec<_>>();
4645 log::info!("LSP: Returning definitions in files {:?}", &files);
4646 Ok(Some(lsp::GotoDefinitionResponse::Array(
4647 files
4648 .into_iter()
4649 .map(|file| lsp::Location {
4650 uri: lsp::Url::from_file_path(file).unwrap(),
4651 range: Default::default(),
4652 })
4653 .collect(),
4654 )))
4655 }
4656 }
4657 });
4658
4659 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
4660 let rng = rng.clone();
4661 let project = project.clone();
4662 move |params, mut cx| {
4663 let highlights = if let Some(project) = project.upgrade(&cx) {
4664 project.update(&mut cx, |project, cx| {
4665 let path = params
4666 .text_document_position_params
4667 .text_document
4668 .uri
4669 .to_file_path()
4670 .unwrap();
4671 let (worktree, relative_path) =
4672 project.find_local_worktree(&path, cx)?;
4673 let project_path =
4674 ProjectPath::from((worktree.read(cx).id(), relative_path));
4675 let buffer =
4676 project.get_open_buffer(&project_path, cx)?.read(cx);
4677
4678 let mut highlights = Vec::new();
4679 let highlight_count = rng.lock().gen_range(1..=5);
4680 let mut prev_end = 0;
4681 for _ in 0..highlight_count {
4682 let range =
4683 buffer.random_byte_range(prev_end, &mut *rng.lock());
4684
4685 highlights.push(lsp::DocumentHighlight {
4686 range: range_to_lsp(range.to_point_utf16(buffer)),
4687 kind: Some(lsp::DocumentHighlightKind::READ),
4688 });
4689 prev_end = range.end;
4690 }
4691 Some(highlights)
4692 })
4693 } else {
4694 None
4695 };
4696 async move { Ok(highlights) }
4697 }
4698 });
4699 }
4700 })),
4701 ..Default::default()
4702 }))
4703 .await;
4704 host_language_registry.add(Arc::new(language));
4705
4706 let op_start_signal = futures::channel::mpsc::unbounded();
4707 user_ids.push(host.current_user_id(&host_cx));
4708 op_start_signals.push(op_start_signal.0);
4709 clients.push(host_cx.foreground().spawn(host.simulate_host(
4710 host_project,
4711 op_start_signal.1,
4712 rng.clone(),
4713 host_cx,
4714 )));
4715
4716 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
4717 rng.lock().gen_range(0..max_operations)
4718 } else {
4719 max_operations
4720 };
4721
4722 let mut operations = 0;
4723 while operations < max_operations {
4724 if operations == disconnect_host_at {
4725 server.disconnect_client(user_ids[0]);
4726 deterministic.advance_clock(RECEIVE_TIMEOUT);
4727 drop(op_start_signals);
4728
4729 deterministic.start_waiting();
4730 let mut clients = futures::future::join_all(clients).await;
4731 deterministic.finish_waiting();
4732 deterministic.run_until_parked();
4733
4734 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
4735 if let Some(host_err) = host_err {
4736 log::error!("host error - {:?}", host_err);
4737 }
4738 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
4739 for (guest, guest_project, mut guest_cx, guest_err) in clients {
4740 if let Some(guest_err) = guest_err {
4741 log::error!("{} error - {:?}", guest.username, guest_err);
4742 }
4743
4744 let contacts = server
4745 .app_state
4746 .db
4747 .get_contacts(guest.current_user_id(&guest_cx))
4748 .await
4749 .unwrap();
4750 let contacts = server
4751 .store
4752 .lock()
4753 .await
4754 .build_initial_contacts_update(contacts)
4755 .contacts;
4756 assert!(!contacts
4757 .iter()
4758 .flat_map(|contact| &contact.projects)
4759 .any(|project| project.id == host_project_id));
4760 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4761 guest_cx.update(|_| drop((guest, guest_project)));
4762 }
4763 host_cx.update(|_| drop((host, host_project)));
4764
4765 return;
4766 }
4767
4768 let distribution = rng.lock().gen_range(0..100);
4769 match distribution {
4770 0..=19 if !available_guests.is_empty() => {
4771 let guest_ix = rng.lock().gen_range(0..available_guests.len());
4772 let guest_username = available_guests.remove(guest_ix);
4773 log::info!("Adding new connection for {}", guest_username);
4774 next_entity_id += 100000;
4775 let mut guest_cx = TestAppContext::new(
4776 cx.foreground_platform(),
4777 cx.platform(),
4778 deterministic.build_foreground(next_entity_id),
4779 deterministic.build_background(),
4780 cx.font_cache(),
4781 cx.leak_detector(),
4782 next_entity_id,
4783 );
4784
4785 deterministic.start_waiting();
4786 let guest = server.create_client(&mut guest_cx, &guest_username).await;
4787 let guest_project = Project::remote(
4788 host_project_id,
4789 guest.client.clone(),
4790 guest.user_store.clone(),
4791 guest.project_store.clone(),
4792 guest_lang_registry.clone(),
4793 FakeFs::new(cx.background()),
4794 guest_cx.to_async(),
4795 )
4796 .await
4797 .unwrap();
4798 deterministic.finish_waiting();
4799
4800 let op_start_signal = futures::channel::mpsc::unbounded();
4801 user_ids.push(guest.current_user_id(&guest_cx));
4802 op_start_signals.push(op_start_signal.0);
4803 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
4804 guest_username.clone(),
4805 guest_project,
4806 op_start_signal.1,
4807 rng.clone(),
4808 guest_cx,
4809 )));
4810
4811 log::info!("Added connection for {}", guest_username);
4812 operations += 1;
4813 }
4814 20..=29 if clients.len() > 1 => {
4815 let guest_ix = rng.lock().gen_range(1..clients.len());
4816 log::info!("Removing guest {}", user_ids[guest_ix]);
4817 let removed_guest_id = user_ids.remove(guest_ix);
4818 let guest = clients.remove(guest_ix);
4819 op_start_signals.remove(guest_ix);
4820 server.forbid_connections();
4821 server.disconnect_client(removed_guest_id);
4822 deterministic.advance_clock(RECEIVE_TIMEOUT);
4823 deterministic.start_waiting();
4824 log::info!("Waiting for guest {} to exit...", removed_guest_id);
4825 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
4826 deterministic.finish_waiting();
4827 server.allow_connections();
4828
4829 if let Some(guest_err) = guest_err {
4830 log::error!("{} error - {:?}", guest.username, guest_err);
4831 }
4832 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4833 for user_id in &user_ids {
4834 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
4835 let contacts = server
4836 .store
4837 .lock()
4838 .await
4839 .build_initial_contacts_update(contacts)
4840 .contacts;
4841 for contact in contacts {
4842 if contact.online {
4843 assert_ne!(
4844 contact.user_id, removed_guest_id.0 as u64,
4845 "removed guest is still a contact of another peer"
4846 );
4847 }
4848 for project in contact.projects {
4849 for project_guest_id in project.guests {
4850 assert_ne!(
4851 project_guest_id, removed_guest_id.0 as u64,
4852 "removed guest appears as still participating on a project"
4853 );
4854 }
4855 }
4856 }
4857 }
4858
4859 log::info!("{} removed", guest.username);
4860 available_guests.push(guest.username.clone());
4861 guest_cx.update(|_| drop((guest, guest_project)));
4862
4863 operations += 1;
4864 }
4865 _ => {
4866 while operations < max_operations && rng.lock().gen_bool(0.7) {
4867 op_start_signals
4868 .choose(&mut *rng.lock())
4869 .unwrap()
4870 .unbounded_send(())
4871 .unwrap();
4872 operations += 1;
4873 }
4874
4875 if rng.lock().gen_bool(0.8) {
4876 deterministic.run_until_parked();
4877 }
4878 }
4879 }
4880 }
4881
4882 drop(op_start_signals);
4883 deterministic.start_waiting();
4884 let mut clients = futures::future::join_all(clients).await;
4885 deterministic.finish_waiting();
4886 deterministic.run_until_parked();
4887
4888 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
4889 if let Some(host_err) = host_err {
4890 panic!("host error - {:?}", host_err);
4891 }
4892 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
4893 project
4894 .worktrees(cx)
4895 .map(|worktree| {
4896 let snapshot = worktree.read(cx).snapshot();
4897 (snapshot.id(), snapshot)
4898 })
4899 .collect::<BTreeMap<_, _>>()
4900 });
4901
4902 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
4903
4904 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
4905 if let Some(guest_err) = guest_err {
4906 panic!("{} error - {:?}", guest_client.username, guest_err);
4907 }
4908 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
4909 project
4910 .worktrees(cx)
4911 .map(|worktree| {
4912 let worktree = worktree.read(cx);
4913 (worktree.id(), worktree.snapshot())
4914 })
4915 .collect::<BTreeMap<_, _>>()
4916 });
4917
4918 assert_eq!(
4919 worktree_snapshots.keys().collect::<Vec<_>>(),
4920 host_worktree_snapshots.keys().collect::<Vec<_>>(),
4921 "{} has different worktrees than the host",
4922 guest_client.username
4923 );
4924 for (id, host_snapshot) in &host_worktree_snapshots {
4925 let guest_snapshot = &worktree_snapshots[id];
4926 assert_eq!(
4927 guest_snapshot.root_name(),
4928 host_snapshot.root_name(),
4929 "{} has different root name than the host for worktree {}",
4930 guest_client.username,
4931 id
4932 );
4933 assert_eq!(
4934 guest_snapshot.entries(false).collect::<Vec<_>>(),
4935 host_snapshot.entries(false).collect::<Vec<_>>(),
4936 "{} has different snapshot than the host for worktree {}",
4937 guest_client.username,
4938 id
4939 );
4940 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
4941 }
4942
4943 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
4944
4945 for guest_buffer in &guest_client.buffers {
4946 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
4947 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
4948 project.buffer_for_id(buffer_id, cx).expect(&format!(
4949 "host does not have buffer for guest:{}, peer:{}, id:{}",
4950 guest_client.username, guest_client.peer_id, buffer_id
4951 ))
4952 });
4953 let path =
4954 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
4955
4956 assert_eq!(
4957 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
4958 0,
4959 "{}, buffer {}, path {:?} has deferred operations",
4960 guest_client.username,
4961 buffer_id,
4962 path,
4963 );
4964 assert_eq!(
4965 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
4966 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
4967 "{}, buffer {}, path {:?}, differs from the host's buffer",
4968 guest_client.username,
4969 buffer_id,
4970 path
4971 );
4972 }
4973
4974 guest_cx.update(|_| drop((guest_project, guest_client)));
4975 }
4976
4977 host_cx.update(|_| drop((host_client, host_project)));
4978}
4979
4980struct TestServer {
4981 peer: Arc<Peer>,
4982 app_state: Arc<AppState>,
4983 server: Arc<Server>,
4984 foreground: Rc<executor::Foreground>,
4985 notifications: mpsc::UnboundedReceiver<()>,
4986 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
4987 forbid_connections: Arc<AtomicBool>,
4988 _test_db: TestDb,
4989}
4990
4991impl TestServer {
4992 async fn start(
4993 foreground: Rc<executor::Foreground>,
4994 background: Arc<executor::Background>,
4995 ) -> Self {
4996 let test_db = TestDb::fake(background.clone());
4997 let app_state = Self::build_app_state(&test_db).await;
4998 let peer = Peer::new();
4999 let notifications = mpsc::unbounded();
5000 let server = Server::new(app_state.clone(), Some(notifications.0));
5001 Self {
5002 peer,
5003 app_state,
5004 server,
5005 foreground,
5006 notifications: notifications.1,
5007 connection_killers: Default::default(),
5008 forbid_connections: Default::default(),
5009 _test_db: test_db,
5010 }
5011 }
5012
5013 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
5014 cx.update(|cx| {
5015 let mut settings = Settings::test(cx);
5016 settings.projects_online_by_default = false;
5017 cx.set_global(settings);
5018 });
5019
5020 let http = FakeHttpClient::with_404_response();
5021 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
5022 {
5023 user.id
5024 } else {
5025 self.app_state
5026 .db
5027 .create_user(name, None, false)
5028 .await
5029 .unwrap()
5030 };
5031 let client_name = name.to_string();
5032 let mut client = Client::new(http.clone());
5033 let server = self.server.clone();
5034 let db = self.app_state.db.clone();
5035 let connection_killers = self.connection_killers.clone();
5036 let forbid_connections = self.forbid_connections.clone();
5037 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
5038
5039 Arc::get_mut(&mut client)
5040 .unwrap()
5041 .set_id(user_id.0 as usize)
5042 .override_authenticate(move |cx| {
5043 cx.spawn(|_| async move {
5044 let access_token = "the-token".to_string();
5045 Ok(Credentials {
5046 user_id: user_id.0 as u64,
5047 access_token,
5048 })
5049 })
5050 })
5051 .override_establish_connection(move |credentials, cx| {
5052 assert_eq!(credentials.user_id, user_id.0 as u64);
5053 assert_eq!(credentials.access_token, "the-token");
5054
5055 let server = server.clone();
5056 let db = db.clone();
5057 let connection_killers = connection_killers.clone();
5058 let forbid_connections = forbid_connections.clone();
5059 let client_name = client_name.clone();
5060 let connection_id_tx = connection_id_tx.clone();
5061 cx.spawn(move |cx| async move {
5062 if forbid_connections.load(SeqCst) {
5063 Err(EstablishConnectionError::other(anyhow!(
5064 "server is forbidding connections"
5065 )))
5066 } else {
5067 let (client_conn, server_conn, killed) =
5068 Connection::in_memory(cx.background());
5069 connection_killers.lock().insert(user_id, killed);
5070 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
5071 cx.background()
5072 .spawn(server.handle_connection(
5073 server_conn,
5074 client_name,
5075 user,
5076 Some(connection_id_tx),
5077 cx.background(),
5078 ))
5079 .detach();
5080 Ok(client_conn)
5081 }
5082 })
5083 });
5084
5085 let fs = FakeFs::new(cx.background());
5086 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
5087 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
5088 let app_state = Arc::new(workspace::AppState {
5089 client: client.clone(),
5090 user_store: user_store.clone(),
5091 project_store: project_store.clone(),
5092 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
5093 themes: ThemeRegistry::new((), cx.font_cache()),
5094 fs: fs.clone(),
5095 build_window_options: || Default::default(),
5096 initialize_workspace: |_, _, _| unimplemented!(),
5097 });
5098
5099 Channel::init(&client);
5100 Project::init(&client);
5101 cx.update(|cx| workspace::init(app_state.clone(), cx));
5102
5103 client
5104 .authenticate_and_connect(false, &cx.to_async())
5105 .await
5106 .unwrap();
5107 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
5108
5109 let client = TestClient {
5110 client,
5111 peer_id,
5112 username: name.to_string(),
5113 user_store,
5114 project_store,
5115 fs,
5116 language_registry: Arc::new(LanguageRegistry::test()),
5117 buffers: Default::default(),
5118 };
5119 client.wait_for_current_user(cx).await;
5120 client
5121 }
5122
5123 fn disconnect_client(&self, user_id: UserId) {
5124 self.connection_killers
5125 .lock()
5126 .remove(&user_id)
5127 .unwrap()
5128 .store(true, SeqCst);
5129 }
5130
5131 fn forbid_connections(&self) {
5132 self.forbid_connections.store(true, SeqCst);
5133 }
5134
5135 fn allow_connections(&self) {
5136 self.forbid_connections.store(false, SeqCst);
5137 }
5138
5139 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
5140 while let Some((client_a, cx_a)) = clients.pop() {
5141 for (client_b, cx_b) in &mut clients {
5142 client_a
5143 .user_store
5144 .update(cx_a, |store, cx| {
5145 store.request_contact(client_b.user_id().unwrap(), cx)
5146 })
5147 .await
5148 .unwrap();
5149 cx_a.foreground().run_until_parked();
5150 client_b
5151 .user_store
5152 .update(*cx_b, |store, cx| {
5153 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5154 })
5155 .await
5156 .unwrap();
5157 }
5158 }
5159 }
5160
5161 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
5162 Arc::new(AppState {
5163 db: test_db.db().clone(),
5164 api_token: Default::default(),
5165 invite_link_prefix: Default::default(),
5166 })
5167 }
5168
5169 async fn condition<F>(&mut self, mut predicate: F)
5170 where
5171 F: FnMut(&Store) -> bool,
5172 {
5173 assert!(
5174 self.foreground.parking_forbidden(),
5175 "you must call forbid_parking to use server conditions so we don't block indefinitely"
5176 );
5177 while !(predicate)(&*self.server.store.lock().await) {
5178 self.foreground.start_waiting();
5179 self.notifications.next().await;
5180 self.foreground.finish_waiting();
5181 }
5182 }
5183}
5184
5185impl Deref for TestServer {
5186 type Target = Server;
5187
5188 fn deref(&self) -> &Self::Target {
5189 &self.server
5190 }
5191}
5192
5193impl Drop for TestServer {
5194 fn drop(&mut self) {
5195 self.peer.reset();
5196 }
5197}
5198
5199struct TestClient {
5200 client: Arc<Client>,
5201 username: String,
5202 pub peer_id: PeerId,
5203 pub user_store: ModelHandle<UserStore>,
5204 pub project_store: ModelHandle<ProjectStore>,
5205 language_registry: Arc<LanguageRegistry>,
5206 fs: Arc<FakeFs>,
5207 buffers: HashSet<ModelHandle<language::Buffer>>,
5208}
5209
5210impl Deref for TestClient {
5211 type Target = Arc<Client>;
5212
5213 fn deref(&self) -> &Self::Target {
5214 &self.client
5215 }
5216}
5217
5218struct ContactsSummary {
5219 pub current: Vec<String>,
5220 pub outgoing_requests: Vec<String>,
5221 pub incoming_requests: Vec<String>,
5222}
5223
5224impl TestClient {
5225 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5226 UserId::from_proto(
5227 self.user_store
5228 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5229 )
5230 }
5231
5232 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5233 let mut authed_user = self
5234 .user_store
5235 .read_with(cx, |user_store, _| user_store.watch_current_user());
5236 while authed_user.next().await.unwrap().is_none() {}
5237 }
5238
5239 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5240 self.user_store
5241 .update(cx, |store, _| store.clear_contacts())
5242 .await;
5243 }
5244
5245 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
5246 self.user_store.read_with(cx, |store, _| ContactsSummary {
5247 current: store
5248 .contacts()
5249 .iter()
5250 .map(|contact| contact.user.github_login.clone())
5251 .collect(),
5252 outgoing_requests: store
5253 .outgoing_contact_requests()
5254 .iter()
5255 .map(|user| user.github_login.clone())
5256 .collect(),
5257 incoming_requests: store
5258 .incoming_contact_requests()
5259 .iter()
5260 .map(|user| user.github_login.clone())
5261 .collect(),
5262 })
5263 }
5264
5265 async fn build_local_project(
5266 &self,
5267 root_path: impl AsRef<Path>,
5268 cx: &mut TestAppContext,
5269 ) -> (ModelHandle<Project>, WorktreeId) {
5270 let project = cx.update(|cx| {
5271 Project::local(
5272 true,
5273 self.client.clone(),
5274 self.user_store.clone(),
5275 self.project_store.clone(),
5276 self.language_registry.clone(),
5277 self.fs.clone(),
5278 cx,
5279 )
5280 });
5281 let (worktree, _) = project
5282 .update(cx, |p, cx| {
5283 p.find_or_create_local_worktree(root_path, true, cx)
5284 })
5285 .await
5286 .unwrap();
5287 worktree
5288 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
5289 .await;
5290 project
5291 .update(cx, |project, _| project.next_remote_id())
5292 .await;
5293 (project, worktree.read_with(cx, |tree, _| tree.id()))
5294 }
5295
5296 async fn build_remote_project(
5297 &self,
5298 host_project: &ModelHandle<Project>,
5299 host_cx: &mut TestAppContext,
5300 guest_cx: &mut TestAppContext,
5301 ) -> ModelHandle<Project> {
5302 let host_project_id = host_project
5303 .read_with(host_cx, |project, _| project.next_remote_id())
5304 .await;
5305 let guest_user_id = self.user_id().unwrap();
5306 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
5307 let project_b = guest_cx.spawn(|cx| {
5308 Project::remote(
5309 host_project_id,
5310 self.client.clone(),
5311 self.user_store.clone(),
5312 self.project_store.clone(),
5313 languages,
5314 FakeFs::new(cx.background()),
5315 cx,
5316 )
5317 });
5318 host_cx.foreground().run_until_parked();
5319 host_project.update(host_cx, |project, cx| {
5320 project.respond_to_join_request(guest_user_id, true, cx)
5321 });
5322 let project = project_b.await.unwrap();
5323 project
5324 }
5325
5326 fn build_workspace(
5327 &self,
5328 project: &ModelHandle<Project>,
5329 cx: &mut TestAppContext,
5330 ) -> ViewHandle<Workspace> {
5331 let (window_id, _) = cx.add_window(|_| EmptyView);
5332 cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
5333 }
5334
5335 async fn simulate_host(
5336 mut self,
5337 project: ModelHandle<Project>,
5338 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5339 rng: Arc<Mutex<StdRng>>,
5340 mut cx: TestAppContext,
5341 ) -> (
5342 Self,
5343 ModelHandle<Project>,
5344 TestAppContext,
5345 Option<anyhow::Error>,
5346 ) {
5347 async fn simulate_host_internal(
5348 client: &mut TestClient,
5349 project: ModelHandle<Project>,
5350 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5351 rng: Arc<Mutex<StdRng>>,
5352 cx: &mut TestAppContext,
5353 ) -> anyhow::Result<()> {
5354 let fs = project.read_with(cx, |project, _| project.fs().clone());
5355
5356 cx.update(|cx| {
5357 cx.subscribe(&project, move |project, event, cx| {
5358 if let project::Event::ContactRequestedJoin(user) = event {
5359 log::info!("Host: accepting join request from {}", user.github_login);
5360 project.update(cx, |project, cx| {
5361 project.respond_to_join_request(user.id, true, cx)
5362 });
5363 }
5364 })
5365 .detach();
5366 });
5367
5368 while op_start_signal.next().await.is_some() {
5369 let distribution = rng.lock().gen_range::<usize, _>(0..100);
5370 let files = fs.as_fake().files().await;
5371 match distribution {
5372 0..=19 if !files.is_empty() => {
5373 let path = files.choose(&mut *rng.lock()).unwrap();
5374 let mut path = path.as_path();
5375 while let Some(parent_path) = path.parent() {
5376 path = parent_path;
5377 if rng.lock().gen() {
5378 break;
5379 }
5380 }
5381
5382 log::info!("Host: find/create local worktree {:?}", path);
5383 let find_or_create_worktree = project.update(cx, |project, cx| {
5384 project.find_or_create_local_worktree(path, true, cx)
5385 });
5386 if rng.lock().gen() {
5387 cx.background().spawn(find_or_create_worktree).detach();
5388 } else {
5389 find_or_create_worktree.await?;
5390 }
5391 }
5392 20..=79 if !files.is_empty() => {
5393 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5394 let file = files.choose(&mut *rng.lock()).unwrap();
5395 let (worktree, path) = project
5396 .update(cx, |project, cx| {
5397 project.find_or_create_local_worktree(file.clone(), true, cx)
5398 })
5399 .await?;
5400 let project_path =
5401 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
5402 log::info!(
5403 "Host: opening path {:?}, worktree {}, relative_path {:?}",
5404 file,
5405 project_path.0,
5406 project_path.1
5407 );
5408 let buffer = project
5409 .update(cx, |project, cx| project.open_buffer(project_path, cx))
5410 .await
5411 .unwrap();
5412 client.buffers.insert(buffer.clone());
5413 buffer
5414 } else {
5415 client
5416 .buffers
5417 .iter()
5418 .choose(&mut *rng.lock())
5419 .unwrap()
5420 .clone()
5421 };
5422
5423 if rng.lock().gen_bool(0.1) {
5424 cx.update(|cx| {
5425 log::info!(
5426 "Host: dropping buffer {:?}",
5427 buffer.read(cx).file().unwrap().full_path(cx)
5428 );
5429 client.buffers.remove(&buffer);
5430 drop(buffer);
5431 });
5432 } else {
5433 buffer.update(cx, |buffer, cx| {
5434 log::info!(
5435 "Host: updating buffer {:?} ({})",
5436 buffer.file().unwrap().full_path(cx),
5437 buffer.remote_id()
5438 );
5439
5440 if rng.lock().gen_bool(0.7) {
5441 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5442 } else {
5443 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5444 }
5445 });
5446 }
5447 }
5448 _ => loop {
5449 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
5450 let mut path = PathBuf::new();
5451 path.push("/");
5452 for _ in 0..path_component_count {
5453 let letter = rng.lock().gen_range(b'a'..=b'z');
5454 path.push(std::str::from_utf8(&[letter]).unwrap());
5455 }
5456 path.set_extension("rs");
5457 let parent_path = path.parent().unwrap();
5458
5459 log::info!("Host: creating file {:?}", path,);
5460
5461 if fs.create_dir(&parent_path).await.is_ok()
5462 && fs.create_file(&path, Default::default()).await.is_ok()
5463 {
5464 break;
5465 } else {
5466 log::info!("Host: cannot create file");
5467 }
5468 },
5469 }
5470
5471 cx.background().simulate_random_delay().await;
5472 }
5473
5474 Ok(())
5475 }
5476
5477 let result =
5478 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5479 log::info!("Host done");
5480 (self, project, cx, result.err())
5481 }
5482
5483 pub async fn simulate_guest(
5484 mut self,
5485 guest_username: String,
5486 project: ModelHandle<Project>,
5487 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5488 rng: Arc<Mutex<StdRng>>,
5489 mut cx: TestAppContext,
5490 ) -> (
5491 Self,
5492 ModelHandle<Project>,
5493 TestAppContext,
5494 Option<anyhow::Error>,
5495 ) {
5496 async fn simulate_guest_internal(
5497 client: &mut TestClient,
5498 guest_username: &str,
5499 project: ModelHandle<Project>,
5500 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5501 rng: Arc<Mutex<StdRng>>,
5502 cx: &mut TestAppContext,
5503 ) -> anyhow::Result<()> {
5504 while op_start_signal.next().await.is_some() {
5505 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5506 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5507 project
5508 .worktrees(&cx)
5509 .filter(|worktree| {
5510 let worktree = worktree.read(cx);
5511 worktree.is_visible()
5512 && worktree.entries(false).any(|e| e.is_file())
5513 })
5514 .choose(&mut *rng.lock())
5515 }) {
5516 worktree
5517 } else {
5518 cx.background().simulate_random_delay().await;
5519 continue;
5520 };
5521
5522 let (worktree_root_name, project_path) =
5523 worktree.read_with(cx, |worktree, _| {
5524 let entry = worktree
5525 .entries(false)
5526 .filter(|e| e.is_file())
5527 .choose(&mut *rng.lock())
5528 .unwrap();
5529 (
5530 worktree.root_name().to_string(),
5531 (worktree.id(), entry.path.clone()),
5532 )
5533 });
5534 log::info!(
5535 "{}: opening path {:?} in worktree {} ({})",
5536 guest_username,
5537 project_path.1,
5538 project_path.0,
5539 worktree_root_name,
5540 );
5541 let buffer = project
5542 .update(cx, |project, cx| {
5543 project.open_buffer(project_path.clone(), cx)
5544 })
5545 .await?;
5546 log::info!(
5547 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5548 guest_username,
5549 project_path.1,
5550 project_path.0,
5551 worktree_root_name,
5552 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5553 );
5554 client.buffers.insert(buffer.clone());
5555 buffer
5556 } else {
5557 client
5558 .buffers
5559 .iter()
5560 .choose(&mut *rng.lock())
5561 .unwrap()
5562 .clone()
5563 };
5564
5565 let choice = rng.lock().gen_range(0..100);
5566 match choice {
5567 0..=9 => {
5568 cx.update(|cx| {
5569 log::info!(
5570 "{}: dropping buffer {:?}",
5571 guest_username,
5572 buffer.read(cx).file().unwrap().full_path(cx)
5573 );
5574 client.buffers.remove(&buffer);
5575 drop(buffer);
5576 });
5577 }
5578 10..=19 => {
5579 let completions = project.update(cx, |project, cx| {
5580 log::info!(
5581 "{}: requesting completions for buffer {} ({:?})",
5582 guest_username,
5583 buffer.read(cx).remote_id(),
5584 buffer.read(cx).file().unwrap().full_path(cx)
5585 );
5586 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5587 project.completions(&buffer, offset, cx)
5588 });
5589 let completions = cx.background().spawn(async move {
5590 completions
5591 .await
5592 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5593 });
5594 if rng.lock().gen_bool(0.3) {
5595 log::info!("{}: detaching completions request", guest_username);
5596 cx.update(|cx| completions.detach_and_log_err(cx));
5597 } else {
5598 completions.await?;
5599 }
5600 }
5601 20..=29 => {
5602 let code_actions = project.update(cx, |project, cx| {
5603 log::info!(
5604 "{}: requesting code actions for buffer {} ({:?})",
5605 guest_username,
5606 buffer.read(cx).remote_id(),
5607 buffer.read(cx).file().unwrap().full_path(cx)
5608 );
5609 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5610 project.code_actions(&buffer, range, cx)
5611 });
5612 let code_actions = cx.background().spawn(async move {
5613 code_actions
5614 .await
5615 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5616 });
5617 if rng.lock().gen_bool(0.3) {
5618 log::info!("{}: detaching code actions request", guest_username);
5619 cx.update(|cx| code_actions.detach_and_log_err(cx));
5620 } else {
5621 code_actions.await?;
5622 }
5623 }
5624 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5625 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5626 log::info!(
5627 "{}: saving buffer {} ({:?})",
5628 guest_username,
5629 buffer.remote_id(),
5630 buffer.file().unwrap().full_path(cx)
5631 );
5632 (buffer.version(), buffer.save(cx))
5633 });
5634 let save = cx.background().spawn(async move {
5635 let (saved_version, _, _) = save
5636 .await
5637 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
5638 assert!(saved_version.observed_all(&requested_version));
5639 Ok::<_, anyhow::Error>(())
5640 });
5641 if rng.lock().gen_bool(0.3) {
5642 log::info!("{}: detaching save request", guest_username);
5643 cx.update(|cx| save.detach_and_log_err(cx));
5644 } else {
5645 save.await?;
5646 }
5647 }
5648 40..=44 => {
5649 let prepare_rename = project.update(cx, |project, cx| {
5650 log::info!(
5651 "{}: preparing rename for buffer {} ({:?})",
5652 guest_username,
5653 buffer.read(cx).remote_id(),
5654 buffer.read(cx).file().unwrap().full_path(cx)
5655 );
5656 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5657 project.prepare_rename(buffer, offset, cx)
5658 });
5659 let prepare_rename = cx.background().spawn(async move {
5660 prepare_rename
5661 .await
5662 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
5663 });
5664 if rng.lock().gen_bool(0.3) {
5665 log::info!("{}: detaching prepare rename request", guest_username);
5666 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
5667 } else {
5668 prepare_rename.await?;
5669 }
5670 }
5671 45..=49 => {
5672 let definitions = project.update(cx, |project, cx| {
5673 log::info!(
5674 "{}: requesting definitions for buffer {} ({:?})",
5675 guest_username,
5676 buffer.read(cx).remote_id(),
5677 buffer.read(cx).file().unwrap().full_path(cx)
5678 );
5679 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5680 project.definition(&buffer, offset, cx)
5681 });
5682 let definitions = cx.background().spawn(async move {
5683 definitions
5684 .await
5685 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
5686 });
5687 if rng.lock().gen_bool(0.3) {
5688 log::info!("{}: detaching definitions request", guest_username);
5689 cx.update(|cx| definitions.detach_and_log_err(cx));
5690 } else {
5691 client.buffers.extend(
5692 definitions.await?.into_iter().map(|loc| loc.target.buffer),
5693 );
5694 }
5695 }
5696 50..=54 => {
5697 let highlights = project.update(cx, |project, cx| {
5698 log::info!(
5699 "{}: requesting highlights for buffer {} ({:?})",
5700 guest_username,
5701 buffer.read(cx).remote_id(),
5702 buffer.read(cx).file().unwrap().full_path(cx)
5703 );
5704 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5705 project.document_highlights(&buffer, offset, cx)
5706 });
5707 let highlights = cx.background().spawn(async move {
5708 highlights
5709 .await
5710 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
5711 });
5712 if rng.lock().gen_bool(0.3) {
5713 log::info!("{}: detaching highlights request", guest_username);
5714 cx.update(|cx| highlights.detach_and_log_err(cx));
5715 } else {
5716 highlights.await?;
5717 }
5718 }
5719 55..=59 => {
5720 let search = project.update(cx, |project, cx| {
5721 let query = rng.lock().gen_range('a'..='z');
5722 log::info!("{}: project-wide search {:?}", guest_username, query);
5723 project.search(SearchQuery::text(query, false, false), cx)
5724 });
5725 let search = cx.background().spawn(async move {
5726 search
5727 .await
5728 .map_err(|err| anyhow!("search request failed: {:?}", err))
5729 });
5730 if rng.lock().gen_bool(0.3) {
5731 log::info!("{}: detaching search request", guest_username);
5732 cx.update(|cx| search.detach_and_log_err(cx));
5733 } else {
5734 client.buffers.extend(search.await?.into_keys());
5735 }
5736 }
5737 60..=69 => {
5738 let worktree = project
5739 .read_with(cx, |project, cx| {
5740 project
5741 .worktrees(&cx)
5742 .filter(|worktree| {
5743 let worktree = worktree.read(cx);
5744 worktree.is_visible()
5745 && worktree.entries(false).any(|e| e.is_file())
5746 && worktree.root_entry().map_or(false, |e| e.is_dir())
5747 })
5748 .choose(&mut *rng.lock())
5749 })
5750 .unwrap();
5751 let (worktree_id, worktree_root_name) = worktree
5752 .read_with(cx, |worktree, _| {
5753 (worktree.id(), worktree.root_name().to_string())
5754 });
5755
5756 let mut new_name = String::new();
5757 for _ in 0..10 {
5758 let letter = rng.lock().gen_range('a'..='z');
5759 new_name.push(letter);
5760 }
5761 let mut new_path = PathBuf::new();
5762 new_path.push(new_name);
5763 new_path.set_extension("rs");
5764 log::info!(
5765 "{}: creating {:?} in worktree {} ({})",
5766 guest_username,
5767 new_path,
5768 worktree_id,
5769 worktree_root_name,
5770 );
5771 project
5772 .update(cx, |project, cx| {
5773 project.create_entry((worktree_id, new_path), false, cx)
5774 })
5775 .unwrap()
5776 .await?;
5777 }
5778 _ => {
5779 buffer.update(cx, |buffer, cx| {
5780 log::info!(
5781 "{}: updating buffer {} ({:?})",
5782 guest_username,
5783 buffer.remote_id(),
5784 buffer.file().unwrap().full_path(cx)
5785 );
5786 if rng.lock().gen_bool(0.7) {
5787 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5788 } else {
5789 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5790 }
5791 });
5792 }
5793 }
5794 cx.background().simulate_random_delay().await;
5795 }
5796 Ok(())
5797 }
5798
5799 let result = simulate_guest_internal(
5800 &mut self,
5801 &guest_username,
5802 project.clone(),
5803 op_start_signal,
5804 rng,
5805 &mut cx,
5806 )
5807 .await;
5808 log::info!("{}: done", guest_username);
5809
5810 (self, project, cx, result.err())
5811 }
5812}
5813
5814impl Drop for TestClient {
5815 fn drop(&mut self) {
5816 self.client.tear_down();
5817 }
5818}
5819
5820impl Executor for Arc<gpui::executor::Background> {
5821 type Sleep = gpui::executor::Timer;
5822
5823 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
5824 self.spawn(future).detach();
5825 }
5826
5827 fn sleep(&self, duration: Duration) -> Self::Sleep {
5828 self.as_ref().timer(duration)
5829 }
5830}
5831
5832fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
5833 channel
5834 .messages()
5835 .cursor::<()>()
5836 .map(|m| {
5837 (
5838 m.sender.github_login.clone(),
5839 m.body.clone(),
5840 m.is_pending(),
5841 )
5842 })
5843 .collect()
5844}
5845
5846struct EmptyView;
5847
5848impl gpui::Entity for EmptyView {
5849 type Event = ();
5850}
5851
5852impl gpui::View for EmptyView {
5853 fn ui_name() -> &'static str {
5854 "empty view"
5855 }
5856
5857 fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
5858 gpui::Element::boxed(gpui::elements::Empty::new())
5859 }
5860}