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_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}