1use crate::headless_project::HeadlessProject;
2use client::{Client, UserStore};
3use clock::FakeSystemClock;
4use fs::{FakeFs, Fs};
5use gpui::{Context, Model, TestAppContext};
6use http_client::FakeHttpClient;
7use language::{
8 language_settings::{all_language_settings, AllLanguageSettings},
9 Buffer, LanguageRegistry,
10};
11use node_runtime::FakeNodeRuntime;
12use project::{
13 search::{SearchQuery, SearchResult},
14 Project,
15};
16use remote::SshSession;
17use serde_json::json;
18use settings::{Settings, SettingsLocation, SettingsStore};
19use smol::stream::StreamExt;
20use std::{path::Path, sync::Arc};
21
22#[gpui::test]
23async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
24 let (project, _headless, fs) = init_test(cx, server_cx).await;
25 let (worktree, _) = project
26 .update(cx, |project, cx| {
27 project.find_or_create_worktree("/code/project1", true, cx)
28 })
29 .await
30 .unwrap();
31
32 // The client sees the worktree's contents.
33 cx.executor().run_until_parked();
34 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
35 worktree.update(cx, |worktree, _cx| {
36 assert_eq!(
37 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
38 vec![
39 Path::new("README.md"),
40 Path::new("src"),
41 Path::new("src/lib.rs"),
42 ]
43 );
44 });
45
46 // The user opens a buffer in the remote worktree. The buffer's
47 // contents are loaded from the remote filesystem.
48 let buffer = project
49 .update(cx, |project, cx| {
50 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
51 })
52 .await
53 .unwrap();
54 buffer.update(cx, |buffer, cx| {
55 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
56 assert_eq!(
57 buffer.diff_base().unwrap().to_string(),
58 "fn one() -> usize { 0 }"
59 );
60 let ix = buffer.text().find('1').unwrap();
61 buffer.edit([(ix..ix + 1, "100")], None, cx);
62 });
63
64 // The user saves the buffer. The new contents are written to the
65 // remote filesystem.
66 project
67 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
68 .await
69 .unwrap();
70 assert_eq!(
71 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
72 "fn one() -> usize { 100 }"
73 );
74
75 // A new file is created in the remote filesystem. The user
76 // sees the new file.
77 fs.save(
78 "/code/project1/src/main.rs".as_ref(),
79 &"fn main() {}".into(),
80 Default::default(),
81 )
82 .await
83 .unwrap();
84 cx.executor().run_until_parked();
85 worktree.update(cx, |worktree, _cx| {
86 assert_eq!(
87 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
88 vec![
89 Path::new("README.md"),
90 Path::new("src"),
91 Path::new("src/lib.rs"),
92 Path::new("src/main.rs"),
93 ]
94 );
95 });
96
97 // A file that is currently open in a buffer is renamed.
98 fs.rename(
99 "/code/project1/src/lib.rs".as_ref(),
100 "/code/project1/src/lib2.rs".as_ref(),
101 Default::default(),
102 )
103 .await
104 .unwrap();
105 cx.executor().run_until_parked();
106 buffer.update(cx, |buffer, _| {
107 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
108 });
109
110 fs.set_index_for_repo(
111 Path::new("/code/project1/.git"),
112 &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
113 );
114 cx.executor().run_until_parked();
115 buffer.update(cx, |buffer, _| {
116 assert_eq!(
117 buffer.diff_base().unwrap().to_string(),
118 "fn one() -> usize { 100 }"
119 );
120 });
121}
122
123#[gpui::test]
124async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
125 let (project, headless, _) = init_test(cx, server_cx).await;
126
127 project
128 .update(cx, |project, cx| {
129 project.find_or_create_worktree("/code/project1", true, cx)
130 })
131 .await
132 .unwrap();
133
134 cx.run_until_parked();
135
136 async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
137 let mut receiver = project.update(&mut cx, |project, cx| {
138 project.search(
139 SearchQuery::text(
140 "project",
141 false,
142 true,
143 false,
144 Default::default(),
145 Default::default(),
146 None,
147 )
148 .unwrap(),
149 cx,
150 )
151 });
152
153 let first_response = receiver.next().await.unwrap();
154 let SearchResult::Buffer { buffer, .. } = first_response else {
155 panic!("incorrect result");
156 };
157 buffer.update(&mut cx, |buffer, cx| {
158 assert_eq!(
159 buffer.file().unwrap().full_path(cx).to_string_lossy(),
160 "project1/README.md"
161 )
162 });
163
164 assert!(receiver.next().await.is_none());
165 buffer
166 }
167
168 let buffer = do_search(&project, cx.clone()).await;
169
170 // test that the headless server is tracking which buffers we have open correctly.
171 cx.run_until_parked();
172 headless.update(server_cx, |headless, cx| {
173 assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
174 });
175 do_search(&project, cx.clone()).await;
176
177 cx.update(|_| {
178 drop(buffer);
179 });
180 cx.run_until_parked();
181 headless.update(server_cx, |headless, cx| {
182 assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
183 });
184
185 do_search(&project, cx.clone()).await;
186}
187
188#[gpui::test]
189async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
190 let (project, headless, fs) = init_test(cx, server_cx).await;
191
192 cx.update_global(|settings_store: &mut SettingsStore, cx| {
193 settings_store.set_user_settings(
194 r#"{"languages":{"Rust":{"language_servers":["custom-rust-analyzer"]}}}"#,
195 cx,
196 )
197 })
198 .unwrap();
199
200 cx.run_until_parked();
201
202 server_cx.read(|cx| {
203 assert_eq!(
204 AllLanguageSettings::get_global(cx)
205 .language(Some("Rust"))
206 .language_servers,
207 ["custom-rust-analyzer".into()]
208 )
209 });
210
211 fs.insert_tree("/code/project1/.zed", json!({
212 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
213 })).await;
214
215 let worktree_id = project
216 .update(cx, |project, cx| {
217 project.find_or_create_worktree("/code/project1", true, cx)
218 })
219 .await
220 .unwrap()
221 .0
222 .read_with(cx, |worktree, _| worktree.id());
223
224 let buffer = project
225 .update(cx, |project, cx| {
226 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
227 })
228 .await
229 .unwrap();
230 cx.run_until_parked();
231
232 server_cx.read(|cx| {
233 let worktree_id = headless
234 .read(cx)
235 .worktree_store
236 .read(cx)
237 .worktrees()
238 .next()
239 .unwrap()
240 .read(cx)
241 .id();
242 assert_eq!(
243 AllLanguageSettings::get(
244 Some(SettingsLocation {
245 worktree_id,
246 path: Path::new("src/lib.rs")
247 }),
248 cx
249 )
250 .language(Some("Rust"))
251 .language_servers,
252 ["override-rust-analyzer".into()]
253 )
254 });
255
256 cx.read(|cx| {
257 let file = buffer.read(cx).file();
258 assert_eq!(
259 all_language_settings(file, cx)
260 .language(Some("Rust"))
261 .language_servers,
262 ["override-rust-analyzer".into()]
263 )
264 });
265}
266
267fn init_logger() {
268 if std::env::var("RUST_LOG").is_ok() {
269 env_logger::try_init().ok();
270 }
271}
272
273async fn init_test(
274 cx: &mut TestAppContext,
275 server_cx: &mut TestAppContext,
276) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
277 let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
278 init_logger();
279
280 let fs = FakeFs::new(server_cx.executor());
281 fs.insert_tree(
282 "/code",
283 json!({
284 "project1": {
285 ".git": {},
286 "README.md": "# project 1",
287 "src": {
288 "lib.rs": "fn one() -> usize { 1 }"
289 }
290 },
291 "project2": {
292 "README.md": "# project 2",
293 },
294 }),
295 )
296 .await;
297 fs.set_index_for_repo(
298 Path::new("/code/project1/.git"),
299 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
300 );
301
302 server_cx.update(HeadlessProject::init);
303 let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
304 let project = build_project(client_ssh, cx);
305
306 project
307 .update(cx, {
308 let headless = headless.clone();
309 |_, cx| cx.on_release(|_, _| drop(headless))
310 })
311 .detach();
312 (project, headless, fs)
313}
314
315fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
316 cx.update(|cx| {
317 let settings_store = SettingsStore::test(cx);
318 cx.set_global(settings_store);
319 });
320
321 let client = cx.update(|cx| {
322 Client::new(
323 Arc::new(FakeSystemClock::default()),
324 FakeHttpClient::with_404_response(),
325 cx,
326 )
327 });
328
329 let node = FakeNodeRuntime::new();
330 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
331 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
332 let fs = FakeFs::new(cx.executor());
333 cx.update(|cx| {
334 Project::init(&client, cx);
335 language::init(cx);
336 });
337
338 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
339}