Detailed changes
@@ -122,13 +122,9 @@ impl ItemView for Editor {
}
fn title(&self, cx: &AppContext) -> String {
- let filename = self
- .buffer()
- .read(cx)
- .file(cx)
- .and_then(|file| file.file_name());
- if let Some(name) = filename {
- name.to_string_lossy().into()
+ let file = self.buffer().read(cx).file(cx);
+ if let Some(file) = file {
+ file.file_name(cx).to_string_lossy().into()
} else {
"untitled".into()
}
@@ -156,21 +156,20 @@ pub enum Event {
}
pub trait File {
+ fn as_local(&self) -> Option<&dyn LocalFile>;
+
fn mtime(&self) -> SystemTime;
/// Returns the path of this file relative to the worktree's root directory.
fn path(&self) -> &Arc<Path>;
- /// Returns the absolute path of this file.
- fn abs_path(&self) -> Option<PathBuf>;
-
/// Returns the path of this file relative to the worktree's parent directory (this means it
/// includes the name of the worktree's root folder).
- fn full_path(&self) -> PathBuf;
+ fn full_path(&self, cx: &AppContext) -> PathBuf;
/// Returns the last component of this handle's absolute path. If this handle refers to the root
/// of its worktree, then this method will return the name of the worktree itself.
- fn file_name(&self) -> Option<OsString>;
+ fn file_name(&self, cx: &AppContext) -> OsString;
fn is_deleted(&self) -> bool;
@@ -182,8 +181,6 @@ pub trait File {
cx: &mut MutableAppContext,
) -> Task<Result<(clock::Global, SystemTime)>>;
- fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>>;
-
fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
-> Option<Task<Result<()>>>;
@@ -192,6 +189,23 @@ pub trait File {
fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext);
fn as_any(&self) -> &dyn Any;
+
+ fn to_proto(&self) -> rpc::proto::File;
+}
+
+pub trait LocalFile: File {
+ /// Returns the absolute path of this file.
+ fn abs_path(&self, cx: &AppContext) -> PathBuf;
+
+ fn load(&self, cx: &AppContext) -> Task<Result<String>>;
+
+ fn buffer_reloaded(
+ &self,
+ buffer_id: u64,
+ version: &clock::Global,
+ mtime: SystemTime,
+ cx: &mut MutableAppContext,
+ );
}
pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
@@ -348,6 +362,7 @@ impl Buffer {
pub fn to_proto(&self) -> proto::Buffer {
proto::Buffer {
id: self.remote_id(),
+ file: self.file.as_ref().map(|f| f.to_proto()),
visible_text: self.text.text(),
deleted_text: self.text.deleted_text(),
undo_map: self
@@ -457,7 +472,7 @@ impl Buffer {
if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() {
let server = server.clone();
- let abs_path = file.abs_path().unwrap();
+ let abs_path = file.as_local().unwrap().abs_path(cx);
let version = self.version();
cx.spawn(|this, mut cx| async move {
let edits = server
@@ -619,7 +634,7 @@ impl Buffer {
None
};
- self.update_language_server();
+ self.update_language_server(cx);
}
pub fn did_save(
@@ -634,7 +649,11 @@ impl Buffer {
if let Some(new_file) = new_file {
self.file = Some(new_file);
}
- if let Some(state) = &self.language_server {
+ if let Some((state, local_file)) = &self
+ .language_server
+ .as_ref()
+ .zip(self.file.as_ref().and_then(|f| f.as_local()))
+ {
cx.background()
.spawn(
state
@@ -642,10 +661,7 @@ impl Buffer {
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document: lsp::TextDocumentIdentifier {
- uri: lsp::Url::from_file_path(
- self.file.as_ref().unwrap().abs_path().unwrap(),
- )
- .unwrap(),
+ uri: lsp::Url::from_file_path(local_file.abs_path(cx)).unwrap(),
},
text: None,
},
@@ -656,14 +672,33 @@ impl Buffer {
cx.emit(Event::Saved);
}
+ pub fn did_reload(
+ &mut self,
+ version: clock::Global,
+ mtime: SystemTime,
+ cx: &mut ModelContext<Self>,
+ ) {
+ self.saved_mtime = mtime;
+ self.saved_version = version;
+ if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
+ file.buffer_reloaded(self.remote_id(), &self.saved_version, self.saved_mtime, cx);
+ }
+ cx.emit(Event::Reloaded);
+ cx.notify();
+ }
+
pub fn file_updated(
&mut self,
new_file: Box<dyn File>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<()>> {
- let old_file = self.file.as_ref()?;
+ ) -> Task<()> {
+ let old_file = if let Some(file) = self.file.as_ref() {
+ file
+ } else {
+ return Task::ready(());
+ };
let mut file_changed = false;
- let mut task = None;
+ let mut task = Task::ready(());
if new_file.path() != old_file.path() {
file_changed = true;
@@ -682,10 +717,12 @@ impl Buffer {
file_changed = true;
if !self.is_dirty() {
- task = Some(cx.spawn(|this, mut cx| {
+ task = cx.spawn(|this, mut cx| {
async move {
let new_text = this.read_with(&cx, |this, cx| {
- this.file.as_ref().and_then(|file| file.load_local(cx))
+ this.file
+ .as_ref()
+ .and_then(|file| file.as_local().map(|f| f.load(cx)))
});
if let Some(new_text) = new_text {
let new_text = new_text.await?;
@@ -694,9 +731,7 @@ impl Buffer {
.await;
this.update(&mut cx, |this, cx| {
if this.apply_diff(diff, cx) {
- this.saved_version = this.version();
- this.saved_mtime = new_mtime;
- cx.emit(Event::Reloaded);
+ this.did_reload(this.version(), new_mtime, cx);
}
});
}
@@ -704,7 +739,7 @@ impl Buffer {
}
.log_err()
.map(drop)
- }));
+ });
}
}
}
@@ -1226,7 +1261,7 @@ impl Buffer {
self.set_active_selections(Arc::from([]), cx);
}
- fn update_language_server(&mut self) {
+ fn update_language_server(&mut self, cx: &AppContext) {
let language_server = if let Some(language_server) = self.language_server.as_mut() {
language_server
} else {
@@ -1235,9 +1270,8 @@ impl Buffer {
let abs_path = self
.file
.as_ref()
- .map_or(Path::new("/").to_path_buf(), |file| {
- file.abs_path().unwrap()
- });
+ .and_then(|f| f.as_local())
+ .map_or(Path::new("/").to_path_buf(), |file| file.abs_path(cx));
let version = post_inc(&mut language_server.next_version);
let snapshot = LanguageServerSnapshot {
@@ -1381,7 +1415,7 @@ impl Buffer {
}
self.reparse(cx);
- self.update_language_server();
+ self.update_language_server(cx);
cx.emit(Event::Edited);
if !was_dirty {
@@ -1,7 +1,7 @@
[package]
name = "project"
version = "0.1.0"
-edition = "2018"
+edition = "2021"
[lib]
path = "src/project.rs"
@@ -298,6 +298,8 @@ impl Project {
Self::handle_disk_based_diagnostics_updated,
),
client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
+ client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer_file),
+ client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_reloaded),
client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
],
client,
@@ -624,7 +626,7 @@ impl Project {
) -> Option<()> {
let (path, full_path) = {
let file = buffer.read(cx).file()?;
- (file.path().clone(), file.full_path())
+ (file.path().clone(), file.full_path(cx))
};
// If the buffer has a language, set it and start/assign the language server
@@ -938,7 +940,7 @@ impl Project {
let buffer_abs_path;
if let Some(file) = File::from_dyn(buffer.file()) {
worktree = file.worktree.clone();
- buffer_abs_path = file.abs_path();
+ buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
} else {
return Task::ready(Err(anyhow!("buffer does not belong to any worktree")));
};
@@ -1136,10 +1138,12 @@ impl Project {
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
- cx.subscribe(&worktree, |this, worktree, _, cx| {
- this.update_open_buffers(worktree, cx)
- })
- .detach();
+ if worktree.read(cx).is_local() {
+ cx.subscribe(&worktree, |this, worktree, _, cx| {
+ this.update_local_worktree_buffers(worktree, cx);
+ })
+ .detach();
+ }
let push_weak_handle = {
let worktree = worktree.read(cx);
@@ -1161,14 +1165,12 @@ impl Project {
cx.notify();
}
- fn update_open_buffers(
+ fn update_local_worktree_buffers(
&mut self,
worktree_handle: ModelHandle<Worktree>,
cx: &mut ModelContext<Self>,
) {
- let local = worktree_handle.read(cx).is_local();
let snapshot = worktree_handle.read(cx).snapshot();
- let worktree_path = snapshot.abs_path();
let mut buffers_to_delete = Vec::new();
for (buffer_id, buffer) in &self.open_buffers {
if let OpenBuffer::Loaded(buffer) = buffer {
@@ -1184,8 +1186,7 @@ impl Project {
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
{
File {
- is_local: local,
- worktree_path: worktree_path.clone(),
+ is_local: true,
entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
@@ -1195,8 +1196,7 @@ impl Project {
snapshot.entry_for_path(old_file.path().as_ref())
{
File {
- is_local: local,
- worktree_path: worktree_path.clone(),
+ is_local: true,
entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
@@ -1204,8 +1204,7 @@ impl Project {
}
} else {
File {
- is_local: local,
- worktree_path: worktree_path.clone(),
+ is_local: true,
entry_id: None,
path: old_file.path().clone(),
mtime: old_file.mtime(),
@@ -1213,9 +1212,18 @@ impl Project {
}
};
- if let Some(task) = buffer.file_updated(Box::new(new_file), cx) {
- task.detach();
+ if let Some(project_id) = self.remote_id() {
+ let client = self.client.clone();
+ let message = proto::UpdateBufferFile {
+ project_id,
+ buffer_id: *buffer_id as u64,
+ file: Some(new_file.to_proto()),
+ };
+ cx.foreground()
+ .spawn(async move { client.send(message).await })
+ .detach_and_log_err(cx);
}
+ buffer.file_updated(Box::new(new_file), cx).detach();
}
});
} else {
@@ -1496,6 +1504,31 @@ impl Project {
Ok(())
}
+ pub fn handle_update_buffer_file(
+ &mut self,
+ envelope: TypedEnvelope<proto::UpdateBufferFile>,
+ _: Arc<Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let payload = envelope.payload.clone();
+ let buffer_id = payload.buffer_id as usize;
+ let file = payload.file.ok_or_else(|| anyhow!("invalid file"))?;
+ let worktree = self
+ .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx)
+ .ok_or_else(|| anyhow!("no such worktree"))?;
+ let file = File::from_proto(file, worktree.clone(), cx)?;
+ let buffer = self
+ .open_buffers
+ .get_mut(&buffer_id)
+ .and_then(|b| b.upgrade(cx))
+ .ok_or_else(|| anyhow!("no such buffer"))?;
+ buffer.update(cx, |buffer, cx| {
+ buffer.file_updated(Box::new(file), cx).detach();
+ });
+
+ Ok(())
+ }
+
pub fn handle_save_buffer(
&mut self,
envelope: TypedEnvelope<proto::SaveBuffer>,
@@ -1662,6 +1695,37 @@ impl Project {
Ok(())
}
+ pub fn handle_buffer_reloaded(
+ &mut self,
+ envelope: TypedEnvelope<proto::BufferReloaded>,
+ _: Arc<Client>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ let payload = envelope.payload.clone();
+ let buffer = self
+ .open_buffers
+ .get(&(payload.buffer_id as usize))
+ .and_then(|buf| {
+ if let OpenBuffer::Loaded(buffer) = buf {
+ buffer.upgrade(cx)
+ } else {
+ None
+ }
+ });
+ if let Some(buffer) = buffer {
+ buffer.update(cx, |buffer, cx| {
+ let version = payload.version.try_into()?;
+ let mtime = payload
+ .mtime
+ .ok_or_else(|| anyhow!("missing mtime"))?
+ .into();
+ buffer.did_reload(version, mtime, cx);
+ Result::<_, anyhow::Error>::Ok(())
+ })?;
+ }
+ Ok(())
+ }
+
pub fn match_paths<'a>(
&self,
query: &'a str,
@@ -2185,8 +2249,13 @@ mod tests {
cx.update(|cx| {
let target_buffer = definition.target_buffer.read(cx);
assert_eq!(
- target_buffer.file().unwrap().abs_path(),
- Some(dir.path().join("a.rs"))
+ target_buffer
+ .file()
+ .unwrap()
+ .as_local()
+ .unwrap()
+ .abs_path(cx),
+ dir.path().join("a.rs")
);
assert_eq!(definition.target_range.to_offset(target_buffer), 9..10);
assert_eq!(
@@ -2432,7 +2501,7 @@ mod tests {
.as_remote_mut()
.unwrap()
.snapshot
- .apply_update(update_message)
+ .apply_remote_update(update_message)
.unwrap();
assert_eq!(
@@ -30,7 +30,7 @@ use std::{
ffi::{OsStr, OsString},
fmt,
future::Future,
- ops::Deref,
+ ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
@@ -54,9 +54,9 @@ pub enum Worktree {
}
pub struct LocalWorktree {
- snapshot: Snapshot,
+ snapshot: LocalSnapshot,
config: WorktreeConfig,
- background_snapshot: Arc<Mutex<Snapshot>>,
+ background_snapshot: Arc<Mutex<LocalSnapshot>>,
last_scan_state_rx: watch::Receiver<ScanState>,
_background_scanner_task: Option<Task<()>>,
poll_task: Option<Task<()>>,
@@ -85,15 +85,34 @@ pub struct RemoteWorktree {
#[derive(Clone)]
pub struct Snapshot {
id: WorktreeId,
- scan_id: usize,
- abs_path: Arc<Path>,
root_name: String,
root_char_bag: CharBag,
- ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
+}
+
+#[derive(Clone)]
+pub struct LocalSnapshot {
+ abs_path: Arc<Path>,
+ scan_id: usize,
+ ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
removed_entry_ids: HashMap<u64, usize>,
next_entry_id: Arc<AtomicUsize>,
+ snapshot: Snapshot,
+}
+
+impl Deref for LocalSnapshot {
+ type Target = Snapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.snapshot
+ }
+}
+
+impl DerefMut for LocalSnapshot {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.snapshot
+ }
}
#[derive(Clone, Debug)]
@@ -112,7 +131,7 @@ enum Registration {
struct ShareState {
project_id: u64,
- snapshots_tx: Sender<Snapshot>,
+ snapshots_tx: Sender<LocalSnapshot>,
_maintain_remote_snapshot: Option<Task<()>>,
}
@@ -158,7 +177,7 @@ impl Worktree {
let (tree, scan_states_tx) = LocalWorktree::new(client, path, weak, fs.clone(), cx).await?;
tree.update(cx, |tree, cx| {
let tree = tree.as_local_mut().unwrap();
- let abs_path = tree.snapshot.abs_path.clone();
+ let abs_path = tree.abs_path().clone();
let background_snapshot = tree.background_snapshot.clone();
let background = cx.background().clone();
tree._background_scanner_task = Some(cx.background().spawn(async move {
@@ -233,15 +252,10 @@ impl Worktree {
cx.add_model(|cx: &mut ModelContext<Worktree>| {
let snapshot = Snapshot {
id: WorktreeId(remote_id as usize),
- scan_id: 0,
- abs_path: Path::new("").into(),
root_name,
root_char_bag,
- ignores: Default::default(),
entries_by_path,
entries_by_id,
- removed_entry_ids: Default::default(),
- next_entry_id: Default::default(),
};
let (updates_tx, mut updates_rx) = postage::mpsc::channel(64);
@@ -251,7 +265,7 @@ impl Worktree {
.spawn(async move {
while let Some(update) = updates_rx.recv().await {
let mut snapshot = snapshot_tx.borrow().clone();
- if let Err(error) = snapshot.apply_update(update) {
+ if let Err(error) = snapshot.apply_remote_update(update) {
log::error!("error applying worktree update: {}", error);
}
*snapshot_tx.borrow_mut() = snapshot;
@@ -328,7 +342,7 @@ impl Worktree {
pub fn snapshot(&self) -> Snapshot {
match self {
- Worktree::Local(worktree) => worktree.snapshot(),
+ Worktree::Local(worktree) => worktree.snapshot().snapshot,
Worktree::Remote(worktree) => worktree.snapshot(),
}
}
@@ -469,28 +483,28 @@ impl LocalWorktree {
let (scan_states_tx, scan_states_rx) = smol::channel::unbounded();
let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning);
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
- let mut snapshot = Snapshot {
- id: WorktreeId::from_usize(cx.model_id()),
- scan_id: 0,
+ let mut snapshot = LocalSnapshot {
abs_path,
- root_name: root_name.clone(),
- root_char_bag,
+ scan_id: 0,
ignores: Default::default(),
- entries_by_path: Default::default(),
- entries_by_id: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id: Arc::new(next_entry_id),
+ snapshot: Snapshot {
+ id: WorktreeId::from_usize(cx.model_id()),
+ root_name: root_name.clone(),
+ root_char_bag,
+ entries_by_path: Default::default(),
+ entries_by_id: Default::default(),
+ },
};
if let Some(metadata) = metadata {
- snapshot.insert_entry(
- Entry::new(
- path.into(),
- &metadata,
- &snapshot.next_entry_id,
- snapshot.root_char_bag,
- ),
- fs.as_ref(),
+ let entry = Entry::new(
+ path.into(),
+ &metadata,
+ &snapshot.next_entry_id,
+ snapshot.root_char_bag,
);
+ snapshot.insert_entry(entry, fs.as_ref());
}
let tree = Self {
@@ -543,6 +557,22 @@ impl LocalWorktree {
Ok((tree, scan_states_tx))
}
+ pub fn abs_path(&self) -> &Arc<Path> {
+ &self.abs_path
+ }
+
+ pub fn contains_abs_path(&self, path: &Path) -> bool {
+ path.starts_with(&self.abs_path)
+ }
+
+ fn absolutize(&self, path: &Path) -> PathBuf {
+ if path.file_name().is_some() {
+ self.abs_path.join(path)
+ } else {
+ self.abs_path.to_path_buf()
+ }
+ }
+
pub fn authorized_logins(&self) -> Vec<String> {
self.config.collaborators.clone()
}
@@ -624,14 +654,13 @@ impl LocalWorktree {
}
}
- pub fn snapshot(&self) -> Snapshot {
+ pub fn snapshot(&self) -> LocalSnapshot {
self.snapshot.clone()
}
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
let handle = cx.handle();
let path = Arc::from(path);
- let worktree_path = self.abs_path.clone();
let abs_path = self.absolutize(&path);
let background_snapshot = self.background_snapshot.clone();
let fs = self.fs.clone();
@@ -644,7 +673,6 @@ impl LocalWorktree {
File {
entry_id: Some(entry.id),
worktree: handle,
- worktree_path,
path: entry.path,
mtime: entry.mtime,
is_local: true,
@@ -664,19 +692,16 @@ impl LocalWorktree {
let text = buffer.as_rope().clone();
let version = buffer.version();
let save = self.save(path, text, cx);
- cx.spawn(|this, mut cx| async move {
+ let handle = cx.handle();
+ cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?;
- let file = this.update(&mut cx, |this, cx| {
- let this = this.as_local_mut().unwrap();
- File {
- entry_id: Some(entry.id),
- worktree: cx.handle(),
- worktree_path: this.abs_path.clone(),
- path: entry.path,
- mtime: entry.mtime,
- is_local: true,
- }
- });
+ let file = File {
+ entry_id: Some(entry.id),
+ worktree: handle,
+ path: entry.path,
+ mtime: entry.mtime,
+ is_local: true,
+ };
buffer_handle.update(&mut cx, |buffer, cx| {
buffer.did_save(version, file.mtime, Some(Box::new(file)), cx);
@@ -755,7 +780,8 @@ impl LocalWorktree {
let snapshot = self.snapshot();
let rpc = self.client.clone();
let worktree_id = cx.model_id() as u64;
- let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::<Snapshot>();
+ let (snapshots_to_send_tx, snapshots_to_send_rx) =
+ smol::channel::unbounded::<LocalSnapshot>();
let maintain_remote_snapshot = cx.background().spawn({
let rpc = rpc.clone();
let snapshot = snapshot.clone();
@@ -811,15 +837,9 @@ impl RemoteWorktree {
let replica_id = self.replica_id;
let project_id = self.project_id;
let remote_worktree_id = self.id();
- let root_path = self.snapshot.abs_path.clone();
let path: Arc<Path> = Arc::from(path);
let path_string = path.to_string_lossy().to_string();
cx.spawn_weak(move |this, mut cx| async move {
- let entry = this
- .upgrade(&cx)
- .ok_or_else(|| anyhow!("worktree was closed"))?
- .read_with(&cx, |tree, _| tree.entry_for_path(&path).cloned())
- .ok_or_else(|| anyhow!("file does not exist"))?;
let response = rpc
.request(proto::OpenBuffer {
project_id,
@@ -831,18 +851,15 @@ impl RemoteWorktree {
let this = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("worktree was closed"))?;
- let file = File {
- entry_id: Some(entry.id),
- worktree: this.clone(),
- worktree_path: root_path,
- path: entry.path,
- mtime: entry.mtime,
- is_local: false,
- };
- let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?;
- Ok(cx.add_model(|cx| {
- Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx).unwrap()
- }))
+ let mut remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?;
+ let file = remote_buffer
+ .file
+ .take()
+ .map(|proto| cx.read(|cx| File::from_proto(proto, this.clone(), cx)))
+ .transpose()?
+ .map(|file| Box::new(file) as Box<dyn language::File>);
+
+ Ok(cx.add_model(|cx| Buffer::from_proto(replica_id, remote_buffer, file, cx).unwrap()))
})
}
@@ -976,10 +993,7 @@ impl Snapshot {
}
}
- pub(crate) fn apply_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
- self.scan_id += 1;
- let scan_id = self.scan_id;
-
+ pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
for entry_id in update.removed_entries {
@@ -1000,7 +1014,7 @@ impl Snapshot {
id: entry.id,
path: entry.path.clone(),
is_ignored: entry.is_ignored,
- scan_id,
+ scan_id: 0,
}));
entries_by_path_edits.push(Edit::Insert(entry));
}
@@ -1087,22 +1101,6 @@ impl Snapshot {
}
}
- pub fn contains_abs_path(&self, path: &Path) -> bool {
- path.starts_with(&self.abs_path)
- }
-
- fn absolutize(&self, path: &Path) -> PathBuf {
- if path.file_name().is_some() {
- self.abs_path.join(path)
- } else {
- self.abs_path.to_path_buf()
- }
- }
-
- pub fn abs_path(&self) -> &Arc<Path> {
- &self.abs_path
- }
-
pub fn root_entry(&self) -> Option<&Entry> {
self.entry_for_path("")
}
@@ -1132,7 +1130,9 @@ impl Snapshot {
pub fn inode_for_path(&self, path: impl AsRef<Path>) -> Option<u64> {
self.entry_for_path(path.as_ref()).map(|e| e.inode)
}
+}
+impl LocalSnapshot {
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
let abs_path = self.abs_path.join(&entry.path);
@@ -1154,12 +1154,13 @@ impl Snapshot {
self.reuse_entry_id(&mut entry);
self.entries_by_path.insert_or_replace(entry.clone(), &());
+ let scan_id = self.scan_id;
self.entries_by_id.insert_or_replace(
PathEntry {
id: entry.id,
path: entry.path.clone(),
is_ignored: entry.is_ignored,
- scan_id: self.scan_id,
+ scan_id,
},
&(),
);
@@ -1315,7 +1316,7 @@ impl Deref for Worktree {
}
impl Deref for LocalWorktree {
- type Target = Snapshot;
+ type Target = LocalSnapshot;
fn deref(&self) -> &Self::Target {
&self.snapshot
@@ -1354,11 +1355,18 @@ pub struct File {
pub path: Arc<Path>,
pub mtime: SystemTime,
pub(crate) entry_id: Option<usize>,
- pub(crate) worktree_path: Arc<Path>,
pub(crate) is_local: bool,
}
impl language::File for File {
+ fn as_local(&self) -> Option<&dyn language::LocalFile> {
+ if self.is_local {
+ Some(self)
+ } else {
+ None
+ }
+ }
+
fn mtime(&self) -> SystemTime {
self.mtime
}
@@ -1367,30 +1375,20 @@ impl language::File for File {
&self.path
}
- fn abs_path(&self) -> Option<PathBuf> {
- if self.is_local {
- Some(self.worktree_path.join(&self.path))
- } else {
- None
- }
- }
-
- fn full_path(&self) -> PathBuf {
+ fn full_path(&self, cx: &AppContext) -> PathBuf {
let mut full_path = PathBuf::new();
- if let Some(worktree_name) = self.worktree_path.file_name() {
- full_path.push(worktree_name);
- }
+ full_path.push(self.worktree.read(cx).root_name());
full_path.push(&self.path);
full_path
}
/// Returns the last component of this handle's absolute path. If this handle refers to the root
/// of its worktree, then this method will return the name of the worktree itself.
- fn file_name<'a>(&'a self) -> Option<OsString> {
+ fn file_name(&self, cx: &AppContext) -> OsString {
self.path
.file_name()
- .or_else(|| self.worktree_path.file_name())
- .map(Into::into)
+ .map(|name| name.into())
+ .unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name))
}
fn is_deleted(&self) -> bool {
@@ -1444,16 +1442,6 @@ impl language::File for File {
})
}
- fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>> {
- let worktree = self.worktree.read(cx).as_local()?;
- let abs_path = worktree.absolutize(&self.path);
- let fs = worktree.fs.clone();
- Some(
- cx.background()
- .spawn(async move { fs.load(&abs_path).await }),
- )
- }
-
fn format_remote(
&self,
buffer_id: u64,
@@ -1504,9 +1492,83 @@ impl language::File for File {
fn as_any(&self) -> &dyn Any {
self
}
+
+ fn to_proto(&self) -> rpc::proto::File {
+ rpc::proto::File {
+ worktree_id: self.worktree.id() as u64,
+ entry_id: self.entry_id.map(|entry_id| entry_id as u64),
+ path: self.path.to_string_lossy().into(),
+ mtime: Some(self.mtime.into()),
+ }
+ }
+}
+
+impl language::LocalFile for File {
+ fn abs_path(&self, cx: &AppContext) -> PathBuf {
+ self.worktree
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .abs_path
+ .join(&self.path)
+ }
+
+ fn load(&self, cx: &AppContext) -> Task<Result<String>> {
+ let worktree = self.worktree.read(cx).as_local().unwrap();
+ let abs_path = worktree.absolutize(&self.path);
+ let fs = worktree.fs.clone();
+ cx.background()
+ .spawn(async move { fs.load(&abs_path).await })
+ }
+
+ fn buffer_reloaded(
+ &self,
+ buffer_id: u64,
+ version: &clock::Global,
+ mtime: SystemTime,
+ cx: &mut MutableAppContext,
+ ) {
+ let worktree = self.worktree.read(cx).as_local().unwrap();
+ if let Some(project_id) = worktree.share.as_ref().map(|share| share.project_id) {
+ let rpc = worktree.client.clone();
+ let message = proto::BufferReloaded {
+ project_id,
+ buffer_id,
+ version: version.into(),
+ mtime: Some(mtime.into()),
+ };
+ cx.background()
+ .spawn(async move { rpc.send(message).await })
+ .detach_and_log_err(cx);
+ }
+ }
}
impl File {
+ pub fn from_proto(
+ proto: rpc::proto::File,
+ worktree: ModelHandle<Worktree>,
+ cx: &AppContext,
+ ) -> Result<Self> {
+ let worktree_id = worktree
+ .read(cx)
+ .as_remote()
+ .ok_or_else(|| anyhow!("not remote"))?
+ .id();
+
+ if worktree_id.to_proto() != proto.worktree_id {
+ return Err(anyhow!("worktree id does not match file"));
+ }
+
+ Ok(Self {
+ worktree,
+ path: Path::new(&proto.path).into(),
+ mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
+ entry_id: proto.entry_id.map(|entry_id| entry_id as usize),
+ is_local: false,
+ })
+ }
+
pub fn from_dyn(file: Option<&dyn language::File>) -> Option<&Self> {
file.and_then(|f| f.as_any().downcast_ref())
}
@@ -1690,14 +1752,14 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
struct BackgroundScanner {
fs: Arc<dyn Fs>,
- snapshot: Arc<Mutex<Snapshot>>,
+ snapshot: Arc<Mutex<LocalSnapshot>>,
notify: Sender<ScanState>,
executor: Arc<executor::Background>,
}
impl BackgroundScanner {
fn new(
- snapshot: Arc<Mutex<Snapshot>>,
+ snapshot: Arc<Mutex<LocalSnapshot>>,
notify: Sender<ScanState>,
fs: Arc<dyn Fs>,
executor: Arc<executor::Background>,
@@ -1714,7 +1776,7 @@ impl BackgroundScanner {
self.snapshot.lock().abs_path.clone()
}
- fn snapshot(&self) -> Snapshot {
+ fn snapshot(&self) -> LocalSnapshot {
self.snapshot.lock().clone()
}
@@ -2057,7 +2119,7 @@ impl BackgroundScanner {
.await;
}
- async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &Snapshot) {
+ async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
let mut ignore_stack = job.ignore_stack;
if let Some((ignore, _)) = snapshot.ignores.get(&job.path) {
ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
@@ -2101,7 +2163,7 @@ impl BackgroundScanner {
async fn refresh_entry(
fs: &dyn Fs,
- snapshot: &Mutex<Snapshot>,
+ snapshot: &Mutex<LocalSnapshot>,
path: Arc<Path>,
abs_path: &Path,
) -> Result<Entry> {
@@ -2169,7 +2231,7 @@ impl WorktreeHandle for ModelHandle<Worktree> {
use smol::future::FutureExt;
let filename = "fs-event-sentinel";
- let root_path = cx.read(|cx| self.read(cx).abs_path.clone());
+ let root_path = cx.read(|cx| self.read(cx).as_local().unwrap().abs_path().clone());
let tree = self.clone();
async move {
std::fs::write(root_path.join(filename), "").unwrap();
@@ -2521,17 +2583,19 @@ mod tests {
let (notify_tx, _notify_rx) = smol::channel::unbounded();
let fs = Arc::new(RealFs);
let next_entry_id = Arc::new(AtomicUsize::new(0));
- let mut initial_snapshot = Snapshot {
- id: WorktreeId::from_usize(0),
- scan_id: 0,
+ let mut initial_snapshot = LocalSnapshot {
abs_path: root_dir.path().into(),
- entries_by_path: Default::default(),
- entries_by_id: Default::default(),
+ scan_id: 0,
removed_entry_ids: Default::default(),
ignores: Default::default(),
- root_name: Default::default(),
- root_char_bag: Default::default(),
next_entry_id: next_entry_id.clone(),
+ snapshot: Snapshot {
+ id: WorktreeId::from_usize(0),
+ entries_by_path: Default::default(),
+ entries_by_id: Default::default(),
+ root_name: Default::default(),
+ root_char_bag: Default::default(),
+ },
};
initial_snapshot.insert_entry(
Entry::new(
@@ -2612,7 +2676,7 @@ mod tests {
let update = scanner
.snapshot()
.build_update(&prev_snapshot, 0, 0, include_ignored);
- prev_snapshot.apply_update(update).unwrap();
+ prev_snapshot.apply_remote_update(update).unwrap();
assert_eq!(
prev_snapshot.to_vec(true),
scanner.snapshot().to_vec(include_ignored)
@@ -2767,7 +2831,7 @@ mod tests {
.collect()
}
- impl Snapshot {
+ impl LocalSnapshot {
fn check_invariants(&self) {
let mut files = self.files(true, 0);
let mut visible_files = self.files(false, 0);
@@ -33,25 +33,27 @@ message Envelope {
OpenBufferResponse open_buffer_response = 25;
CloseBuffer close_buffer = 26;
UpdateBuffer update_buffer = 27;
- SaveBuffer save_buffer = 28;
- BufferSaved buffer_saved = 29;
- FormatBuffer format_buffer = 30;
-
- GetChannels get_channels = 31;
- GetChannelsResponse get_channels_response = 32;
- JoinChannel join_channel = 33;
- JoinChannelResponse join_channel_response = 34;
- LeaveChannel leave_channel = 35;
- SendChannelMessage send_channel_message = 36;
- SendChannelMessageResponse send_channel_message_response = 37;
- ChannelMessageSent channel_message_sent = 38;
- GetChannelMessages get_channel_messages = 39;
- GetChannelMessagesResponse get_channel_messages_response = 40;
-
- UpdateContacts update_contacts = 41;
-
- GetUsers get_users = 42;
- GetUsersResponse get_users_response = 43;
+ UpdateBufferFile update_buffer_file = 28;
+ SaveBuffer save_buffer = 29;
+ BufferSaved buffer_saved = 30;
+ BufferReloaded buffer_reloaded = 31;
+ FormatBuffer format_buffer = 32;
+
+ GetChannels get_channels = 33;
+ GetChannelsResponse get_channels_response = 34;
+ JoinChannel join_channel = 35;
+ JoinChannelResponse join_channel_response = 36;
+ LeaveChannel leave_channel = 37;
+ SendChannelMessage send_channel_message = 38;
+ SendChannelMessageResponse send_channel_message_response = 39;
+ ChannelMessageSent channel_message_sent = 40;
+ GetChannelMessages get_channel_messages = 41;
+ GetChannelMessagesResponse get_channel_messages_response = 42;
+
+ UpdateContacts update_contacts = 43;
+
+ GetUsers get_users = 44;
+ GetUsersResponse get_users_response = 45;
}
}
@@ -88,9 +90,9 @@ message JoinProject {
}
message JoinProjectResponse {
- uint32 replica_id = 2;
- repeated Worktree worktrees = 3;
- repeated Collaborator collaborators = 4;
+ uint32 replica_id = 1;
+ repeated Worktree worktrees = 2;
+ repeated Collaborator collaborators = 3;
}
message LeaveProject {
@@ -150,7 +152,13 @@ message CloseBuffer {
message UpdateBuffer {
uint64 project_id = 1;
uint64 buffer_id = 2;
- repeated Operation operations = 4;
+ repeated Operation operations = 3;
+}
+
+message UpdateBufferFile {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ File file = 3;
}
message SaveBuffer {
@@ -165,6 +173,13 @@ message BufferSaved {
Timestamp mtime = 4;
}
+message BufferReloaded {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ repeated VectorClockEntry version = 3;
+ Timestamp mtime = 4;
+}
+
message FormatBuffer {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -177,11 +192,11 @@ message UpdateDiagnosticSummary {
}
message DiagnosticSummary {
- string path = 3;
- uint32 error_count = 4;
- uint32 warning_count = 5;
- uint32 info_count = 6;
- uint32 hint_count = 7;
+ string path = 1;
+ uint32 error_count = 2;
+ uint32 warning_count = 3;
+ uint32 info_count = 4;
+ uint32 hint_count = 5;
}
message DiskBasedDiagnosticsUpdating {
@@ -270,6 +285,13 @@ message Worktree {
bool weak = 5;
}
+message File {
+ uint64 worktree_id = 1;
+ optional uint64 entry_id = 2;
+ string path = 3;
+ Timestamp mtime = 4;
+}
+
message Entry {
uint64 id = 1;
bool is_dir = 2;
@@ -282,15 +304,16 @@ message Entry {
message Buffer {
uint64 id = 1;
- string visible_text = 2;
- string deleted_text = 3;
- repeated BufferFragment fragments = 4;
- repeated UndoMapEntry undo_map = 5;
- repeated VectorClockEntry version = 6;
- repeated SelectionSet selections = 7;
- repeated Diagnostic diagnostics = 8;
- uint32 lamport_timestamp = 9;
- repeated Operation deferred_operations = 10;
+ optional File file = 2;
+ string visible_text = 3;
+ string deleted_text = 4;
+ repeated BufferFragment fragments = 5;
+ repeated UndoMapEntry undo_map = 6;
+ repeated VectorClockEntry version = 7;
+ repeated SelectionSet selections = 8;
+ repeated Diagnostic diagnostics = 9;
+ uint32 lamport_timestamp = 10;
+ repeated Operation deferred_operations = 11;
}
message BufferFragment {
@@ -299,7 +322,7 @@ message BufferFragment {
uint32 lamport_timestamp = 3;
uint32 insertion_offset = 4;
uint32 len = 5;
- bool visible = 6;
+ bool visible = 6;
repeated VectorClockEntry deletions = 7;
repeated VectorClockEntry max_undos = 8;
}
@@ -383,8 +406,8 @@ message Operation {
message UpdateSelections {
uint32 replica_id = 1;
- uint32 lamport_timestamp = 3;
- repeated Selection selections = 4;
+ uint32 lamport_timestamp = 2;
+ repeated Selection selections = 3;
}
}
@@ -122,6 +122,7 @@ macro_rules! entity_messages {
messages!(
Ack,
AddProjectCollaborator,
+ BufferReloaded,
BufferSaved,
ChannelMessageSent,
CloseBuffer,
@@ -157,6 +158,7 @@ messages!(
UnregisterWorktree,
UnshareProject,
UpdateBuffer,
+ UpdateBufferFile,
UpdateContacts,
UpdateDiagnosticSummary,
UpdateWorktree,
@@ -183,6 +185,7 @@ request_messages!(
entity_messages!(
project_id,
AddProjectCollaborator,
+ BufferReloaded,
BufferSaved,
CloseBuffer,
DiskBasedDiagnosticsUpdated,
@@ -197,6 +200,7 @@ entity_messages!(
UnregisterWorktree,
UnshareProject,
UpdateBuffer,
+ UpdateBufferFile,
UpdateDiagnosticSummary,
UpdateWorktree,
);
@@ -77,6 +77,8 @@ impl Server {
.add_handler(Server::open_buffer)
.add_handler(Server::close_buffer)
.add_handler(Server::update_buffer)
+ .add_handler(Server::update_buffer_file)
+ .add_handler(Server::buffer_reloaded)
.add_handler(Server::buffer_saved)
.add_handler(Server::save_buffer)
.add_handler(Server::format_buffer)
@@ -704,6 +706,38 @@ impl Server {
Ok(())
}
+ async fn update_buffer_file(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::UpdateBufferFile>,
+ ) -> tide::Result<()> {
+ let receiver_ids = self
+ .state()
+ .project_connection_ids(request.payload.project_id, request.sender_id)
+ .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+ broadcast(request.sender_id, receiver_ids, |connection_id| {
+ self.peer
+ .forward_send(request.sender_id, connection_id, request.payload.clone())
+ })
+ .await?;
+ Ok(())
+ }
+
+ async fn buffer_reloaded(
+ self: Arc<Server>,
+ request: TypedEnvelope<proto::BufferReloaded>,
+ ) -> tide::Result<()> {
+ let receiver_ids = self
+ .state()
+ .project_connection_ids(request.payload.project_id, request.sender_id)
+ .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?;
+ broadcast(request.sender_id, receiver_ids, |connection_id| {
+ self.peer
+ .forward_send(request.sender_id, connection_id, request.payload.clone())
+ })
+ .await?;
+ Ok(())
+ }
+
async fn buffer_saved(
self: Arc<Server>,
request: TypedEnvelope<proto::BufferSaved>,
@@ -1470,9 +1504,7 @@ mod tests {
.condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
.await;
buffer_b
- .condition(&mut cx_b, |buf, _| {
- dbg!(buf.text()) == "i-am-c, i-am-b, i-am-a"
- })
+ .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
.await;
buffer_c
.condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
@@ -1490,7 +1522,10 @@ mod tests {
buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
- // Make changes on host's file system, see those changes on the guests.
+ // Make changes on host's file system, see those changes on guest worktrees.
+ fs.rename("/a/file1".as_ref(), "/a/file1-renamed".as_ref())
+ .await
+ .unwrap();
fs.rename("/a/file2".as_ref(), "/a/file3".as_ref())
.await
.unwrap();
@@ -1498,18 +1533,29 @@ mod tests {
.await
.unwrap();
+ worktree_a
+ .condition(&cx_a, |tree, _| tree.file_count() == 4)
+ .await;
worktree_b
.condition(&cx_b, |tree, _| tree.file_count() == 4)
.await;
worktree_c
.condition(&cx_c, |tree, _| tree.file_count() == 4)
.await;
+ worktree_a.read_with(&cx_a, |tree, _| {
+ assert_eq!(
+ tree.paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ &[".zed.toml", "file1-renamed", "file3", "file4"]
+ )
+ });
worktree_b.read_with(&cx_b, |tree, _| {
assert_eq!(
tree.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
- &[".zed.toml", "file1", "file3", "file4"]
+ &[".zed.toml", "file1-renamed", "file3", "file4"]
)
});
worktree_c.read_with(&cx_c, |tree, _| {
@@ -1517,9 +1563,26 @@ mod tests {
tree.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
- &[".zed.toml", "file1", "file3", "file4"]
+ &[".zed.toml", "file1-renamed", "file3", "file4"]
)
});
+
+ // Ensure buffer files are updated as well.
+ buffer_a
+ .condition(&cx_a, |buf, _| {
+ buf.file().unwrap().path().to_str() == Some("file1-renamed")
+ })
+ .await;
+ buffer_b
+ .condition(&cx_b, |buf, _| {
+ buf.file().unwrap().path().to_str() == Some("file1-renamed")
+ })
+ .await;
+ buffer_c
+ .condition(&cx_c, |buf, _| {
+ buf.file().unwrap().path().to_str() == Some("file1-renamed")
+ })
+ .await;
}
#[gpui::test]
@@ -1615,6 +1678,88 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_buffer_reloading(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+ cx_a.foreground().forbid_parking();
+ let lang_registry = Arc::new(LanguageRegistry::new());
+ let fs = Arc::new(FakeFs::new());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start(cx_a.foreground()).await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+ // Share a project as client A
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#,
+ "a.txt": "a-contents",
+ }),
+ )
+ .await;
+
+ let project_a = cx_a.update(|cx| {
+ Project::local(
+ client_a.clone(),
+ client_a.user_store.clone(),
+ lang_registry.clone(),
+ fs.clone(),
+ cx,
+ )
+ });
+ let (worktree_a, _) = project_a
+ .update(&mut cx_a, |p, cx| {
+ p.find_or_create_local_worktree("/dir", false, cx)
+ })
+ .await
+ .unwrap();
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
+ let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
+ project_a
+ .update(&mut cx_a, |p, cx| p.share(cx))
+ .await
+ .unwrap();
+
+ // Join that project as client B
+ let project_b = Project::remote(
+ project_id,
+ client_b.clone(),
+ client_b.user_store.clone(),
+ lang_registry.clone(),
+ fs.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+ let _worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
+
+ // Open a buffer as client B
+ let buffer_b = project_b
+ .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .await
+ .unwrap();
+ buffer_b.read_with(&cx_b, |buf, _| {
+ assert!(!buf.is_dirty());
+ assert!(!buf.has_conflict());
+ });
+
+ fs.save(Path::new("/dir/a.txt"), &"new contents".into())
+ .await
+ .unwrap();
+ buffer_b
+ .condition(&cx_b, |buf, _| {
+ buf.text() == "new contents" && !buf.is_dirty()
+ })
+ .await;
+ buffer_b.read_with(&cx_b, |buf, _| {
+ assert!(!buf.has_conflict());
+ });
+ }
+
#[gpui::test]
async fn test_editing_while_guest_opens_buffer(
mut cx_a: TestAppContext,