SSH remote search (#16915)

Co-Authored-By: Max <max@zed.dev>

Release Notes:

- ssh remoting: add project search

---------

Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Conrad Irwin 2024-08-26 14:47:02 -06:00 committed by GitHub
parent 0332eaf797
commit ef22372f0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 866 additions and 572 deletions

View file

@ -1,8 +1,9 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use project::{
buffer_store::{BufferStore, BufferStoreEvent},
search::SearchQuery,
worktree_store::WorktreeStore,
ProjectPath, WorktreeId, WorktreeSettings,
};
@ -49,6 +50,7 @@ impl HeadlessProject {
session.add_request_handler(this.clone(), Self::handle_list_remote_directory);
session.add_request_handler(this.clone(), Self::handle_add_worktree);
session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
session.add_request_handler(this.clone(), Self::handle_find_search_candidates);
session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_blame_buffer);
session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_update_buffer);
@ -160,6 +162,49 @@ impl HeadlessProject {
})
}
pub async fn handle_find_search_candidates(
this: Model<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidates>,
mut cx: AsyncAppContext,
) -> Result<proto::FindSearchCandidatesResponse> {
let message = envelope.payload;
let query = SearchQuery::from_proto(
message
.query
.ok_or_else(|| anyhow!("missing query field"))?,
)?;
let mut results = this.update(&mut cx, |this, cx| {
this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
})
})?;
let mut response = proto::FindSearchCandidatesResponse {
buffer_ids: Vec::new(),
};
let (buffer_store, client) = this.update(&mut cx, |this, _| {
(this.buffer_store.clone(), this.session.clone())
})?;
while let Some(buffer) = results.next().await {
let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
response.buffer_ids.push(buffer_id.to_proto());
BufferStore::create_buffer_for_peer(
buffer_store.clone(),
PEER_ID,
buffer_id,
PROJECT_ID,
client.clone(),
&mut cx,
)
.await?;
}
Ok(response)
}
pub async fn handle_list_remote_directory(
this: Model<Self>,
envelope: TypedEnvelope<proto::ListRemoteDirectory>,

View file

@ -1,55 +1,24 @@
use crate::headless_project::HeadlessProject;
use client::{Client, UserStore};
use clock::FakeSystemClock;
use fs::{FakeFs, Fs as _};
use fs::{FakeFs, Fs};
use gpui::{Context, Model, TestAppContext};
use http_client::FakeHttpClient;
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
use project::Project;
use project::{
search::{SearchQuery, SearchResult},
Project,
};
use remote::SshSession;
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{path::Path, sync::Arc};
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
}
#[gpui::test]
async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
init_logger();
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
"project2": {
"README.md": "# project 2",
},
}),
)
.await;
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
&[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
);
server_cx.update(HeadlessProject::init);
let _headless_project =
server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
let project = build_project(client_ssh, cx);
let (project, _headless, fs) = init_test(cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
@ -150,6 +119,96 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
});
}
#[gpui::test]
async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (project, _, _) = init_test(cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
cx.run_until_parked();
let mut receiver = project.update(cx, |project, cx| {
project.search(
SearchQuery::text(
"project",
false,
true,
false,
Default::default(),
Default::default(),
)
.unwrap(),
cx,
)
});
let first_response = receiver.next().await.unwrap();
let SearchResult::Buffer { buffer, .. } = first_response else {
panic!("incorrect result");
};
buffer.update(cx, |buffer, cx| {
assert_eq!(
buffer.file().unwrap().full_path(cx).to_string_lossy(),
"project1/README.md"
)
});
assert!(receiver.next().await.is_none());
}
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
}
async fn init_test(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
init_logger();
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
"project2": {
"README.md": "# project 2",
},
}),
)
.await;
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
&[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
);
server_cx.update(HeadlessProject::init);
let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
let project = build_project(client_ssh, cx);
project
.update(cx, {
let headless = headless.clone();
|_, cx| cx.on_release(|_, _| drop(headless))
})
.detach();
(project, headless, fs)
}
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);