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