remote_editing_tests.rs

  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}