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::{Buffer, LanguageRegistry};
8use node_runtime::FakeNodeRuntime;
9use project::{
10 search::{SearchQuery, SearchResult},
11 Project,
12};
13use remote::SshSession;
14use serde_json::json;
15use settings::SettingsStore;
16use smol::stream::StreamExt;
17use std::{path::Path, sync::Arc};
18
19#[gpui::test]
20async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
21 let (project, _headless, fs) = init_test(cx, server_cx).await;
22 let (worktree, _) = project
23 .update(cx, |project, cx| {
24 project.find_or_create_worktree("/code/project1", true, cx)
25 })
26 .await
27 .unwrap();
28
29 // The client sees the worktree's contents.
30 cx.executor().run_until_parked();
31 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
32 worktree.update(cx, |worktree, _cx| {
33 assert_eq!(
34 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
35 vec![
36 Path::new(".git"),
37 Path::new("README.md"),
38 Path::new("src"),
39 Path::new("src/lib.rs"),
40 ]
41 );
42 });
43
44 // The user opens a buffer in the remote worktree. The buffer's
45 // contents are loaded from the remote filesystem.
46 let buffer = project
47 .update(cx, |project, cx| {
48 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
49 })
50 .await
51 .unwrap();
52 buffer.update(cx, |buffer, cx| {
53 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
54 assert_eq!(
55 buffer.diff_base().unwrap().to_string(),
56 "fn one() -> usize { 0 }"
57 );
58 let ix = buffer.text().find('1').unwrap();
59 buffer.edit([(ix..ix + 1, "100")], None, cx);
60 });
61
62 // The user saves the buffer. The new contents are written to the
63 // remote filesystem.
64 project
65 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
66 .await
67 .unwrap();
68 assert_eq!(
69 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
70 "fn one() -> usize { 100 }"
71 );
72
73 // A new file is created in the remote filesystem. The user
74 // sees the new file.
75 fs.save(
76 "/code/project1/src/main.rs".as_ref(),
77 &"fn main() {}".into(),
78 Default::default(),
79 )
80 .await
81 .unwrap();
82 cx.executor().run_until_parked();
83 worktree.update(cx, |worktree, _cx| {
84 assert_eq!(
85 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
86 vec![
87 Path::new(".git"),
88 Path::new("README.md"),
89 Path::new("src"),
90 Path::new("src/lib.rs"),
91 Path::new("src/main.rs"),
92 ]
93 );
94 });
95
96 // A file that is currently open in a buffer is renamed.
97 fs.rename(
98 "/code/project1/src/lib.rs".as_ref(),
99 "/code/project1/src/lib2.rs".as_ref(),
100 Default::default(),
101 )
102 .await
103 .unwrap();
104 cx.executor().run_until_parked();
105 buffer.update(cx, |buffer, _| {
106 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
107 });
108
109 fs.set_index_for_repo(
110 Path::new("/code/project1/.git"),
111 &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
112 );
113 cx.executor().run_until_parked();
114 buffer.update(cx, |buffer, _| {
115 assert_eq!(
116 buffer.diff_base().unwrap().to_string(),
117 "fn one() -> usize { 100 }"
118 );
119 });
120}
121
122#[gpui::test]
123async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
124 let (project, headless, _) = init_test(cx, server_cx).await;
125
126 project
127 .update(cx, |project, cx| {
128 project.find_or_create_worktree("/code/project1", true, cx)
129 })
130 .await
131 .unwrap();
132
133 cx.run_until_parked();
134
135 async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
136 let mut receiver = project.update(&mut cx, |project, cx| {
137 project.search(
138 SearchQuery::text(
139 "project",
140 false,
141 true,
142 false,
143 Default::default(),
144 Default::default(),
145 None,
146 )
147 .unwrap(),
148 cx,
149 )
150 });
151
152 let first_response = receiver.next().await.unwrap();
153 let SearchResult::Buffer { buffer, .. } = first_response else {
154 panic!("incorrect result");
155 };
156 buffer.update(&mut cx, |buffer, cx| {
157 assert_eq!(
158 buffer.file().unwrap().full_path(cx).to_string_lossy(),
159 "project1/README.md"
160 )
161 });
162
163 assert!(receiver.next().await.is_none());
164 buffer
165 }
166
167 let buffer = do_search(&project, cx.clone()).await;
168
169 // test that the headless server is tracking which buffers we have open correctly.
170 cx.run_until_parked();
171 headless.update(server_cx, |headless, cx| {
172 assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
173 });
174 do_search(&project, cx.clone()).await;
175
176 cx.update(|_| {
177 drop(buffer);
178 });
179 cx.run_until_parked();
180 headless.update(server_cx, |headless, cx| {
181 assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
182 });
183
184 do_search(&project, cx.clone()).await;
185}
186
187fn init_logger() {
188 if std::env::var("RUST_LOG").is_ok() {
189 env_logger::try_init().ok();
190 }
191}
192
193async fn init_test(
194 cx: &mut TestAppContext,
195 server_cx: &mut TestAppContext,
196) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
197 let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
198 init_logger();
199
200 let fs = FakeFs::new(server_cx.executor());
201 fs.insert_tree(
202 "/code",
203 json!({
204 "project1": {
205 ".git": {},
206 "README.md": "# project 1",
207 "src": {
208 "lib.rs": "fn one() -> usize { 1 }"
209 }
210 },
211 "project2": {
212 "README.md": "# project 2",
213 },
214 }),
215 )
216 .await;
217 fs.set_index_for_repo(
218 Path::new("/code/project1/.git"),
219 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
220 );
221
222 server_cx.update(HeadlessProject::init);
223 let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
224 let project = build_project(client_ssh, cx);
225
226 project
227 .update(cx, {
228 let headless = headless.clone();
229 |_, cx| cx.on_release(|_, _| drop(headless))
230 })
231 .detach();
232 (project, headless, fs)
233}
234
235fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
236 cx.update(|cx| {
237 let settings_store = SettingsStore::test(cx);
238 cx.set_global(settings_store);
239 });
240
241 let client = cx.update(|cx| {
242 Client::new(
243 Arc::new(FakeSystemClock::default()),
244 FakeHttpClient::with_404_response(),
245 cx,
246 )
247 });
248
249 let node = FakeNodeRuntime::new();
250 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
251 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
252 let fs = FakeFs::new(cx.executor());
253 cx.update(|cx| {
254 Project::init(&client, cx);
255 language::init(cx);
256 });
257
258 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
259}