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