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::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, _, _) = 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 let mut receiver = project.update(cx, |project, cx| {
136 project.search(
137 SearchQuery::text(
138 "project",
139 false,
140 true,
141 false,
142 Default::default(),
143 Default::default(),
144 )
145 .unwrap(),
146 cx,
147 )
148 });
149
150 let first_response = receiver.next().await.unwrap();
151 let SearchResult::Buffer { buffer, .. } = first_response else {
152 panic!("incorrect result");
153 };
154 buffer.update(cx, |buffer, cx| {
155 assert_eq!(
156 buffer.file().unwrap().full_path(cx).to_string_lossy(),
157 "project1/README.md"
158 )
159 });
160
161 assert!(receiver.next().await.is_none());
162}
163
164fn init_logger() {
165 if std::env::var("RUST_LOG").is_ok() {
166 env_logger::try_init().ok();
167 }
168}
169
170async fn init_test(
171 cx: &mut TestAppContext,
172 server_cx: &mut TestAppContext,
173) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
174 let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
175 init_logger();
176
177 let fs = FakeFs::new(server_cx.executor());
178 fs.insert_tree(
179 "/code",
180 json!({
181 "project1": {
182 ".git": {},
183 "README.md": "# project 1",
184 "src": {
185 "lib.rs": "fn one() -> usize { 1 }"
186 }
187 },
188 "project2": {
189 "README.md": "# project 2",
190 },
191 }),
192 )
193 .await;
194 fs.set_index_for_repo(
195 Path::new("/code/project1/.git"),
196 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
197 );
198
199 server_cx.update(HeadlessProject::init);
200 let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
201 let project = build_project(client_ssh, cx);
202
203 project
204 .update(cx, {
205 let headless = headless.clone();
206 |_, cx| cx.on_release(|_, _| drop(headless))
207 })
208 .detach();
209 (project, headless, fs)
210}
211
212fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
213 cx.update(|cx| {
214 let settings_store = SettingsStore::test(cx);
215 cx.set_global(settings_store);
216 });
217
218 let client = cx.update(|cx| {
219 Client::new(
220 Arc::new(FakeSystemClock::default()),
221 FakeHttpClient::with_404_response(),
222 cx,
223 )
224 });
225
226 let node = FakeNodeRuntime::new();
227 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
228 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
229 let fs = FakeFs::new(cx.executor());
230 cx.update(|cx| {
231 Project::init(&client, cx);
232 language::init(cx);
233 });
234
235 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
236}