1use crate::headless_project::HeadlessProject;
2use client::{Client, UserStore};
3use clock::FakeSystemClock;
4use fs::{FakeFs, Fs as _};
5use gpui::{Context, Model, TestAppContext};
6use http_client::FakeHttpClient;
7use language::LanguageRegistry;
8use node_runtime::FakeNodeRuntime;
9use project::Project;
10use remote::SshSession;
11use serde_json::json;
12use settings::SettingsStore;
13use std::{path::Path, sync::Arc};
14
15fn init_logger() {
16 if std::env::var("RUST_LOG").is_ok() {
17 env_logger::try_init().ok();
18 }
19}
20
21#[gpui::test]
22async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
23 let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
24 init_logger();
25
26 let fs = FakeFs::new(server_cx.executor());
27 fs.insert_tree(
28 "/code",
29 json!({
30 "project1": {
31 ".git": {},
32 "README.md": "# project 1",
33 "src": {
34 "lib.rs": "fn one() -> usize { 1 }"
35 }
36 },
37 "project2": {
38 "README.md": "# project 2",
39 },
40 }),
41 )
42 .await;
43 fs.set_index_for_repo(
44 Path::new("/code/project1/.git"),
45 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
46 );
47
48 server_cx.update(HeadlessProject::init);
49 let _headless_project =
50 server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
51
52 let project = build_project(client_ssh, cx);
53 let (worktree, _) = project
54 .update(cx, |project, cx| {
55 project.find_or_create_worktree("/code/project1", true, cx)
56 })
57 .await
58 .unwrap();
59
60 // The client sees the worktree's contents.
61 cx.executor().run_until_parked();
62 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
63 worktree.update(cx, |worktree, _cx| {
64 assert_eq!(
65 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
66 vec![
67 Path::new(".git"),
68 Path::new("README.md"),
69 Path::new("src"),
70 Path::new("src/lib.rs"),
71 ]
72 );
73 });
74
75 // The user opens a buffer in the remote worktree. The buffer's
76 // contents are loaded from the remote filesystem.
77 let buffer = project
78 .update(cx, |project, cx| {
79 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
80 })
81 .await
82 .unwrap();
83 buffer.update(cx, |buffer, cx| {
84 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
85 assert_eq!(
86 buffer.diff_base().unwrap().to_string(),
87 "fn one() -> usize { 0 }"
88 );
89 let ix = buffer.text().find('1').unwrap();
90 buffer.edit([(ix..ix + 1, "100")], None, cx);
91 });
92
93 // The user saves the buffer. The new contents are written to the
94 // remote filesystem.
95 project
96 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
97 .await
98 .unwrap();
99 assert_eq!(
100 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
101 "fn one() -> usize { 100 }"
102 );
103
104 // A new file is created in the remote filesystem. The user
105 // sees the new file.
106 fs.save(
107 "/code/project1/src/main.rs".as_ref(),
108 &"fn main() {}".into(),
109 Default::default(),
110 )
111 .await
112 .unwrap();
113 cx.executor().run_until_parked();
114 worktree.update(cx, |worktree, _cx| {
115 assert_eq!(
116 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
117 vec![
118 Path::new(".git"),
119 Path::new("README.md"),
120 Path::new("src"),
121 Path::new("src/lib.rs"),
122 Path::new("src/main.rs"),
123 ]
124 );
125 });
126
127 // A file that is currently open in a buffer is renamed.
128 fs.rename(
129 "/code/project1/src/lib.rs".as_ref(),
130 "/code/project1/src/lib2.rs".as_ref(),
131 Default::default(),
132 )
133 .await
134 .unwrap();
135 cx.executor().run_until_parked();
136 buffer.update(cx, |buffer, _| {
137 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
138 });
139
140 fs.set_index_for_repo(
141 Path::new("/code/project1/.git"),
142 &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
143 );
144 cx.executor().run_until_parked();
145 buffer.update(cx, |buffer, _| {
146 assert_eq!(
147 buffer.diff_base().unwrap().to_string(),
148 "fn one() -> usize { 100 }"
149 );
150 });
151}
152
153fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
154 cx.update(|cx| {
155 let settings_store = SettingsStore::test(cx);
156 cx.set_global(settings_store);
157 });
158
159 let client = cx.update(|cx| {
160 Client::new(
161 Arc::new(FakeSystemClock::default()),
162 FakeHttpClient::with_404_response(),
163 cx,
164 )
165 });
166
167 let node = FakeNodeRuntime::new();
168 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
169 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
170 let fs = FakeFs::new(cx.executor());
171 cx.update(|cx| {
172 Project::init(&client, cx);
173 language::init(cx);
174 });
175
176 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
177}