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