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