Handle buffer diff base updates and file renames properly for SSH projects (#14989)

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-07-23 11:32:37 -07:00 committed by GitHub
parent ec093c390f
commit 38e3182bef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1021 additions and 811 deletions

View file

@ -32,6 +32,7 @@ remote.workspace = true
rpc.workspace = true
settings.workspace = true
smol.workspace = true
util.workspace = true
worktree.workspace = true
[dev-dependencies]

View file

@ -1,7 +1,11 @@
use anyhow::{Context as _, Result};
use anyhow::Result;
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use project::{buffer_store::BufferStore, ProjectPath, WorktreeId, WorktreeSettings};
use project::{
buffer_store::{BufferStore, BufferStoreEvent},
worktree_store::WorktreeStore,
ProjectPath, WorktreeId, WorktreeSettings,
};
use remote::SshSession;
use rpc::{
proto::{self, AnyProtoClient, PeerId},
@ -12,6 +16,7 @@ use std::{
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
};
use util::ResultExt as _;
use worktree::Worktree;
const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
@ -20,7 +25,7 @@ const PROJECT_ID: u64 = 0;
pub struct HeadlessProject {
pub fs: Arc<dyn Fs>,
pub session: AnyProtoClient,
pub worktrees: Vec<Model<Worktree>>,
pub worktree_store: Model<WorktreeStore>,
pub buffer_store: Model<BufferStore>,
pub next_entry_id: Arc<AtomicUsize>,
}
@ -34,27 +39,45 @@ impl HeadlessProject {
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
let this = cx.weak_model();
let worktree_store = cx.new_model(|_| WorktreeStore::new(true));
let buffer_store = cx.new_model(|cx| BufferStore::new(worktree_store.clone(), true, cx));
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
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_update_buffer);
session.add_request_handler(this.clone(), Self::handle_save_buffer);
session.add_request_handler(
worktree_store.downgrade(),
WorktreeStore::handle_create_project_entry,
);
session.add_request_handler(
worktree_store.downgrade(),
WorktreeStore::handle_rename_project_entry,
);
session.add_request_handler(
worktree_store.downgrade(),
WorktreeStore::handle_copy_project_entry,
);
session.add_request_handler(
worktree_store.downgrade(),
WorktreeStore::handle_delete_project_entry,
);
session.add_request_handler(
worktree_store.downgrade(),
WorktreeStore::handle_expand_project_entry,
);
HeadlessProject {
session: session.into(),
fs,
worktrees: Vec::new(),
buffer_store: cx.new_model(|_| BufferStore::new(true)),
worktree_store,
buffer_store,
next_entry_id: Default::default(),
}
}
fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
self.worktrees
.iter()
.find(|worktree| worktree.read(cx).id() == id)
.cloned()
}
pub async fn handle_add_worktree(
this: Model<Self>,
message: TypedEnvelope<proto::AddWorktree>,
@ -74,7 +97,9 @@ impl HeadlessProject {
this.update(&mut cx, |this, cx| {
let session = this.session.clone();
this.worktrees.push(worktree.clone());
this.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.add(&worktree, cx);
});
worktree.update(cx, |worktree, cx| {
worktree.observe_updates(0, cx, move |update| {
session.send(update).ok();
@ -104,19 +129,8 @@ impl HeadlessProject {
envelope: TypedEnvelope<proto::SaveBuffer>,
mut cx: AsyncAppContext,
) -> Result<proto::BufferSaved> {
let (buffer_store, worktree) = this.update(&mut cx, |this, cx| {
let buffer_store = this.buffer_store.clone();
let worktree = if let Some(path) = &envelope.payload.new_path {
Some(
this.worktree_for_id(WorktreeId::from_proto(path.worktree_id), cx)
.context("worktree does not exist")?,
)
} else {
None
};
anyhow::Ok((buffer_store, worktree))
})??;
BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, worktree, envelope, cx).await
let buffer_store = this.update(&mut cx, |this, _| this.buffer_store.clone())?;
BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, envelope, cx).await
}
pub async fn handle_open_buffer_by_path(
@ -126,9 +140,6 @@ impl HeadlessProject {
) -> Result<proto::OpenBufferResponse> {
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
let worktree = this
.worktree_for_id(worktree_id, cx)
.context("no such worktree")?;
let buffer_store = this.buffer_store.clone();
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.open_buffer(
@ -136,7 +147,6 @@ impl HeadlessProject {
worktree_id,
path: PathBuf::from(message.payload.path).into(),
},
worktree,
cx,
)
});
@ -163,4 +173,41 @@ impl HeadlessProject {
buffer_id: buffer_id.to_proto(),
})
}
pub fn on_buffer_store_event(
&mut self,
_: Model<BufferStore>,
event: &BufferStoreEvent,
cx: &mut ModelContext<Self>,
) {
match event {
BufferStoreEvent::LocalBufferUpdated { buffer } => {
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let Some(new_file) = buffer.file() else {
return;
};
self.session
.send(proto::UpdateBufferFile {
project_id: 0,
buffer_id: buffer_id.into(),
file: Some(new_file.to_proto(cx)),
})
.log_err();
}
BufferStoreEvent::DiffBaseUpdated { buffer } => {
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let diff_base = buffer.diff_base();
self.session
.send(proto::UpdateDiffBase {
project_id: 0,
buffer_id: buffer_id.to_proto(),
diff_base: diff_base.map(|b| b.to_string()),
})
.log_err();
}
_ => {}
}
}
}

View file

@ -11,7 +11,6 @@ use std::{env, io, mem, process, sync::Arc};
fn main() {
env::set_var("RUST_BACKTRACE", "1");
env::set_var("RUST_LOG", "remote=trace");
let subcommand = std::env::args().nth(1);
match subcommand.as_deref() {

View file

@ -12,15 +12,23 @@ use serde_json::json;
use settings::SettingsStore;
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 }"
@ -32,6 +40,10 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
}),
)
.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 =
@ -52,6 +64,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
assert_eq!(
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
vec![
Path::new(".git"),
Path::new("README.md"),
Path::new("src"),
Path::new("src/lib.rs"),
@ -69,6 +82,10 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
.unwrap();
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
assert_eq!(
buffer.diff_base().unwrap().to_string(),
"fn one() -> usize { 0 }"
);
let ix = buffer.text().find('1').unwrap();
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
@ -76,7 +93,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
// The user saves the buffer. The new contents are written to the
// remote filesystem.
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
assert_eq!(
@ -98,6 +115,7 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
assert_eq!(
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
vec![
Path::new(".git"),
Path::new("README.md"),
Path::new("src"),
Path::new("src/lib.rs"),
@ -105,6 +123,31 @@ async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppCon
]
);
});
// A file that is currently open in a buffer is renamed.
fs.rename(
"/code/project1/src/lib.rs".as_ref(),
"/code/project1/src/lib2.rs".as_ref(),
Default::default(),
)
.await
.unwrap();
cx.executor().run_until_parked();
buffer.update(cx, |buffer, _| {
assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
});
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
&[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
);
cx.executor().run_until_parked();
buffer.update(cx, |buffer, _| {
assert_eq!(
buffer.diff_base().unwrap().to_string(),
"fn one() -> usize { 100 }"
);
});
}
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {