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_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 )
146 .unwrap(),
147 cx,
148 )
149 });
150
151 let first_response = receiver.next().await.unwrap();
152 let SearchResult::Buffer { buffer, .. } = first_response else {
153 panic!("incorrect result");
154 };
155 buffer.update(&mut cx, |buffer, cx| {
156 assert_eq!(
157 buffer.file().unwrap().full_path(cx).to_string_lossy(),
158 "project1/README.md"
159 )
160 });
161
162 assert!(receiver.next().await.is_none());
163 buffer
164 }
165
166 let buffer = do_search(&project, cx.clone()).await;
167
168 // test that the headless server is tracking which buffers we have open correctly.
169 cx.run_until_parked();
170 headless.update(server_cx, |headless, cx| {
171 assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
172 });
173 do_search(&project, cx.clone()).await;
174
175 cx.update(|_| {
176 drop(buffer);
177 });
178 cx.run_until_parked();
179 headless.update(server_cx, |headless, cx| {
180 assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
181 });
182
183 do_search(&project, cx.clone()).await;
184}
185
186fn init_logger() {
187 if std::env::var("RUST_LOG").is_ok() {
188 env_logger::try_init().ok();
189 }
190}
191
192async fn init_test(
193 cx: &mut TestAppContext,
194 server_cx: &mut TestAppContext,
195) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
196 let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
197 init_logger();
198
199 let fs = FakeFs::new(server_cx.executor());
200 fs.insert_tree(
201 "/code",
202 json!({
203 "project1": {
204 ".git": {},
205 "README.md": "# project 1",
206 "src": {
207 "lib.rs": "fn one() -> usize { 1 }"
208 }
209 },
210 "project2": {
211 "README.md": "# project 2",
212 },
213 }),
214 )
215 .await;
216 fs.set_index_for_repo(
217 Path::new("/code/project1/.git"),
218 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
219 );
220
221 server_cx.update(HeadlessProject::init);
222 let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
223 let project = build_project(client_ssh, cx);
224
225 project
226 .update(cx, {
227 let headless = headless.clone();
228 |_, cx| cx.on_release(|_, _| drop(headless))
229 })
230 .detach();
231 (project, headless, fs)
232}
233
234fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
235 cx.update(|cx| {
236 let settings_store = SettingsStore::test(cx);
237 cx.set_global(settings_store);
238 });
239
240 let client = cx.update(|cx| {
241 Client::new(
242 Arc::new(FakeSystemClock::default()),
243 FakeHttpClient::with_404_response(),
244 cx,
245 )
246 });
247
248 let node = FakeNodeRuntime::new();
249 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
250 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
251 let fs = FakeFs::new(cx.executor());
252 cx.update(|cx| {
253 Project::init(&client, cx);
254 language::init(cx);
255 });
256
257 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
258}