Files
zed/crates/project/src/buffer_store.rs
T
Kirill Bulatov 18be72fd68 Make file scanner less eager in non-git-tracked directory trees (#62583)
Fixes https://github.com/zed-industries/zed/issues/35780
Collab schema migration PR:
https://github.com/zed-industries/cloud/pull/3422
The corresponding database schema migration has been created in the
Cloud repo and applied to the production database.

Before, Zed scanned each and every entry in the tree down from the
directory it was opened in, except gitignored files and scan exclusions.
The approach is unchanged, if Zed detects it was open inside a git
repository: e.g. the directory open in Zed contains `.git` directory.

For the rest of the projects, 2 optimizations are made:

* Limit the depth of file scan traversal.
Now, `file_scan_depth` (default `5`) restricts Zed from traversing any
directory that has same number or more segments in its file path.


Such directories behave similar to gitignored directories: their
contents is not available in file finder, project search and project
panel, but can be lazily traversed when the directory is expanded (e.g.
project panel expands it or a nested file is open by path via terminal,
etc.)

To indicate that to the users, a status entry is shown firs time the
limitation is hit in the project:

<img width="858" height="133" alt="image"
src="https://github.com/user-attachments/assets/7da6cfbb-98b4-4cc3-bf2a-8902a9597a15"
/>

* During the scan, any git repositories that are not direct children of
the directory open in Zed (depth >= 2), are traversed and indexed
normally, but their git metadata is never fetched eagerly.

Only when Zed opens a buffer from that repo the git metadata is fetched
and applied.

All that combined now uses a way more moderate amount of CPU and RAM
when opening `~`:

<img width="1717" height="368" alt="Screenshot 2026-08-13 at 17 43 53"
src="https://github.com/user-attachments/assets/ec83e2a9-f7cc-452b-8eb7-af158284ca4e"
/>

File scan inclusions and exclusions are considered still for such
projects.
Set `file_scan_depth` to `0` to enable old behavior.
The setting is supported in the project settings, so custom values can
be set based on the project's structure.

---

Release Notes:

- Fixed Zed using a lot of memory and CPU in large, non-git-tracked,
directory trees
2026-08-13 17:07:06 +00:00

1875 lines
67 KiB
Rust

use crate::{
ProjectPath,
lsp_store::OpenLspBufferHandle,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
use anyhow::{Context as _, Result, anyhow};
use client::Client;
use collections::{HashMap, HashSet, hash_map};
use futures::{Future, FutureExt as _, StreamExt as _, channel::oneshot, future::Shared};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, TaskExt,
WeakEntity,
};
use language::{
Buffer, BufferEvent, Capability, DiskState, File as _, Language, LineEnding, Operation,
language_settings::{AllLanguageSettings, LineEndingSetting},
proto::{
deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version,
split_operations,
},
};
use rpc::{
AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope,
proto::{self, PeerId},
};
use settings::Settings;
use std::{io, sync::Arc, time::Instant};
use text::{BufferId, ReplicaId};
use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath};
use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
/// A set of open buffers.
pub struct BufferStore {
state: BufferStoreState,
#[allow(clippy::type_complexity)]
loading_buffers: HashMap<ProjectPath, Shared<Task<Result<Entity<Buffer>, Arc<anyhow::Error>>>>>,
worktree_store: Entity<WorktreeStore>,
opened_buffers: HashMap<BufferId, OpenBuffer>,
path_to_buffer_id: HashMap<ProjectPath, BufferId>,
downstream_client: Option<(AnyProtoClient, u64)>,
shared_buffers: HashMap<proto::PeerId, HashMap<BufferId, SharedBuffer>>,
non_searchable_buffers: HashSet<BufferId>,
project_search: RemoteProjectSearchState,
}
#[derive(Default)]
struct RemoteProjectSearchState {
// List of ongoing project search chunks from our remote host. Used by the side issuing a search RPC request.
chunks: HashMap<u64, async_channel::Sender<BufferId>>,
// Monotonously-increasing handle to hand out to remote host in order to identify the project search result chunk.
next_id: u64,
// Used by the side running the actual search for match candidates to potentially cancel the search prematurely.
searches_in_progress: HashMap<(PeerId, u64), Task<Result<()>>>,
}
#[derive(Hash, Eq, PartialEq, Clone)]
struct SharedBuffer {
buffer: Entity<Buffer>,
lsp_handle: Option<OpenLspBufferHandle>,
}
enum BufferStoreState {
Local(LocalBufferStore),
Remote(RemoteBufferStore),
}
struct RemoteBufferStore {
shared_with_me: HashSet<Entity<Buffer>>,
upstream_client: AnyProtoClient,
project_id: u64,
loading_remote_buffers_by_id: HashMap<BufferId, Entity<Buffer>>,
remote_buffer_listeners:
HashMap<BufferId, Vec<oneshot::Sender<anyhow::Result<Entity<Buffer>>>>>,
worktree_store: Entity<WorktreeStore>,
}
struct LocalBufferStore {
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, BufferId>,
worktree_store: Entity<WorktreeStore>,
_subscription: Subscription,
}
enum OpenBuffer {
Complete { buffer: WeakEntity<Buffer> },
Operations(Vec<Operation>),
}
pub enum BufferStoreEvent {
BufferAdded(Entity<Buffer>),
SharedBufferClosed(proto::PeerId, BufferId),
BufferDropped(BufferId),
BufferChangedFilePath {
buffer: Entity<Buffer>,
old_file: Option<Arc<dyn language::File>>,
},
}
#[derive(Default, Debug, Clone)]
pub struct ProjectTransaction(pub HashMap<Entity<Buffer>, language::Transaction>);
impl PartialEq for ProjectTransaction {
fn eq(&self, other: &Self) -> bool {
self.0.len() == other.0.len()
&& self.0.iter().all(|(buffer, transaction)| {
other.0.get(buffer).is_some_and(|t| t.id == transaction.id)
})
}
}
impl EventEmitter<BufferStoreEvent> for BufferStore {}
impl RemoteBufferStore {
pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
let (tx, rx) = oneshot::channel();
self.remote_buffer_listeners.entry(id).or_default().push(tx);
cx.spawn(async move |this, cx| {
if let Some(buffer) = this
.read_with(cx, |buffer_store, _| buffer_store.get(id))
.ok()
.flatten()
{
return Ok(buffer);
}
cx.background_spawn(async move { rx.await? }).await
})
}
fn save_remote_buffer(
&self,
buffer_handle: Entity<Buffer>,
new_path: Option<proto::ProjectPath>,
cx: &Context<BufferStore>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id().into();
let version = buffer.version();
let rpc = self.upstream_client.clone();
let project_id = self.project_id;
cx.spawn(async move |_, cx| {
let response = rpc
.request(proto::SaveBuffer {
project_id,
buffer_id,
new_path,
version: serialize_version(&version),
})
.await?;
let version = deserialize_version(&response.version);
let mtime = response.mtime.map(|mtime| mtime.into());
buffer_handle.update(cx, |buffer, cx| {
buffer.did_save(version.clone(), mtime, cx);
});
Ok(())
})
}
pub fn handle_create_buffer_for_peer(
&mut self,
envelope: TypedEnvelope<proto::CreateBufferForPeer>,
replica_id: ReplicaId,
capability: Capability,
cx: &mut Context<BufferStore>,
) -> Result<Option<Entity<Buffer>>> {
match envelope.payload.variant.context("missing variant")? {
proto::create_buffer_for_peer::Variant::State(mut state) => {
let buffer_id = BufferId::new(state.id)?;
let buffer_result = maybe!({
let mut buffer_file = None;
if let Some(file) = state.file.take() {
let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id);
let worktree = self
.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
.with_context(|| {
format!("no worktree found for id {}", file.worktree_id)
})?;
buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?)
as Arc<dyn language::File>);
}
Buffer::from_proto(replica_id, capability, state, buffer_file)
});
match buffer_result {
Ok(buffer) => {
let buffer = cx.new(|_| buffer);
self.loading_remote_buffers_by_id.insert(buffer_id, buffer);
}
Err(error) => {
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
for listener in listeners {
listener.send(Err(anyhow!(error.cloned()))).ok();
}
}
}
}
}
proto::create_buffer_for_peer::Variant::Chunk(chunk) => {
let buffer_id = BufferId::new(chunk.buffer_id)?;
let buffer = self
.loading_remote_buffers_by_id
.get(&buffer_id)
.cloned()
.with_context(|| {
format!(
"received chunk for buffer {} without initial state",
chunk.buffer_id
)
})?;
let result = maybe!({
let operations = chunk
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>>>()?;
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx));
anyhow::Ok(())
});
if let Err(error) = result {
self.loading_remote_buffers_by_id.remove(&buffer_id);
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
for listener in listeners {
listener.send(Err(error.cloned())).ok();
}
}
} else if chunk.is_last {
self.loading_remote_buffers_by_id.remove(&buffer_id);
if self.upstream_client.is_via_collab() {
// retain buffers sent by peers to avoid races.
self.shared_with_me.insert(buffer.clone());
}
if let Some(senders) = self.remote_buffer_listeners.remove(&buffer_id) {
for sender in senders {
sender.send(Ok(buffer.clone())).ok();
}
}
return Ok(Some(buffer));
}
}
}
Ok(None)
}
pub fn incomplete_buffer_ids(&self) -> Vec<BufferId> {
self.loading_remote_buffers_by_id
.keys()
.copied()
.collect::<Vec<_>>()
}
pub fn deserialize_project_transaction(
&self,
message: proto::ProjectTransaction,
push_to_history: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<ProjectTransaction>> {
cx.spawn(async move |this, cx| {
let mut project_transaction = ProjectTransaction::default();
for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions)
{
let buffer_id = BufferId::new(buffer_id)?;
let buffer = this
.update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx))?
.await?;
let transaction = language::proto::deserialize_transaction(transaction)?;
project_transaction.0.insert(buffer, transaction);
}
for (buffer, transaction) in &project_transaction.0 {
buffer
.update(cx, |buffer, _| {
buffer.wait_for_edits(transaction.edit_ids.iter().copied())
})
.await?;
if push_to_history {
buffer.update(cx, |buffer, _| {
buffer.push_transaction(transaction.clone(), Instant::now());
buffer.finalize_last_transaction();
});
}
}
Ok(project_transaction)
})
}
fn open_buffer(
&self,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
let worktree_id = worktree.read(cx).id().to_proto();
let project_id = self.project_id;
let client = self.upstream_client.clone();
cx.spawn(async move |this, cx| {
let response = client
.request(proto::OpenBufferByPath {
project_id,
worktree_id,
path: path.as_unix_str().to_owned(),
})
.await?;
let buffer_id = BufferId::new(response.buffer_id)?;
let buffer = this
.update(cx, {
|this, cx| this.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
Ok(buffer)
})
}
fn create_buffer(
&self,
language: Option<Arc<Language>>,
project_searchable: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
let create = self.upstream_client.request(proto::OpenNewBuffer {
project_id: self.project_id,
});
cx.spawn(async move |this, cx| {
let response = create.await?;
let buffer_id = BufferId::new(response.buffer_id)?;
let buffer = this
.update(cx, |this, cx| {
if !project_searchable {
this.non_searchable_buffers.insert(buffer_id);
}
this.wait_for_remote_buffer(buffer_id, cx)
})?
.await?;
if let Some(language) = language {
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
}
Ok(buffer)
})
}
fn reload_buffers(
&self,
buffers: HashSet<Entity<Buffer>>,
push_to_history: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<ProjectTransaction>> {
let request = self.upstream_client.request(proto::ReloadBuffers {
project_id: self.project_id,
buffer_ids: buffers
.iter()
.map(|buffer| buffer.read(cx).remote_id().to_proto())
.collect(),
});
cx.spawn(async move |this, cx| {
let response = request.await?.transaction.context("missing transaction")?;
this.update(cx, |this, cx| {
this.deserialize_project_transaction(response, push_to_history, cx)
})?
.await
})
}
}
impl LocalBufferStore {
fn save_local_buffer(
&self,
buffer_handle: Entity<Buffer>,
worktree: Entity<Worktree>,
path: Arc<RelPath>,
mut has_changed_file: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let text = buffer.as_rope().clone();
let line_ending = buffer.line_ending();
let encoding = buffer.encoding();
let has_bom = buffer.has_bom();
let version = buffer.version();
let buffer_id = buffer.remote_id();
let file = buffer.file().cloned();
if file
.as_ref()
.is_some_and(|file| file.disk_state() == DiskState::New)
{
has_changed_file = true;
}
let save = worktree.update(cx, |worktree, cx| {
worktree.write_file(path, text, line_ending, encoding, has_bom, cx)
});
cx.spawn(async move |this, cx| {
let new_file = save.await?;
let mtime = new_file.disk_state().mtime();
this.update(cx, |this, cx| {
if let Some((downstream_client, project_id)) = this.downstream_client.clone() {
if has_changed_file {
downstream_client
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id.to_proto(),
file: Some(language::File::to_proto(&*new_file, cx)),
})
.log_err();
}
downstream_client
.send(proto::BufferSaved {
project_id,
buffer_id: buffer_id.to_proto(),
version: serialize_version(&version),
mtime: mtime.map(|time| time.into()),
})
.log_err();
}
})?;
buffer_handle.update(cx, |buffer, cx| {
if has_changed_file {
buffer.file_updated(new_file, cx);
}
buffer.did_save(version.clone(), mtime, cx);
});
Ok(())
})
}
fn subscribe_to_worktree(
&mut self,
worktree: &Entity<Worktree>,
cx: &mut Context<BufferStore>,
) {
cx.subscribe(worktree, |this, worktree, event, cx| {
if worktree.read(cx).is_local()
&& let worktree::Event::UpdatedEntries(changes) = event
{
Self::local_worktree_entries_changed(this, &worktree, changes, cx);
}
})
.detach();
}
fn local_worktree_entries_changed(
this: &mut BufferStore,
worktree_handle: &Entity<Worktree>,
changes: &[(Arc<RelPath>, ProjectEntryId, PathChange)],
cx: &mut Context<BufferStore>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
for (path, entry_id, _) in changes {
Self::local_worktree_entry_changed(
this,
*entry_id,
path,
worktree_handle,
&snapshot,
cx,
);
}
}
fn local_worktree_entry_changed(
this: &mut BufferStore,
entry_id: ProjectEntryId,
path: &Arc<RelPath>,
worktree: &Entity<worktree::Worktree>,
snapshot: &worktree::Snapshot,
cx: &mut Context<BufferStore>,
) -> Option<()> {
let project_path = ProjectPath {
worktree_id: snapshot.id(),
path: path.clone(),
};
let buffer_id = this
.as_local_mut()
.and_then(|local| local.local_buffer_ids_by_entry_id.get(&entry_id))
.copied()
.or_else(|| this.path_to_buffer_id.get(&project_path).copied())?;
let buffer = if let Some(buffer) = this.get(buffer_id) {
Some(buffer)
} else {
this.opened_buffers.remove(&buffer_id);
this.non_searchable_buffers.remove(&buffer_id);
None
};
let buffer = if let Some(buffer) = buffer {
buffer
} else {
this.path_to_buffer_id.remove(&project_path);
let this = this.as_local_mut()?;
this.local_buffer_ids_by_entry_id.remove(&entry_id);
return None;
};
if snapshot.entry_for_id(entry_id).is_none()
&& snapshot.entry_for_path(path.as_ref()).is_none()
&& Self::unloaded_ancestor_hides_path(snapshot, path)
{
let mut refresh = worktree
.read(cx)
.as_local()?
.refresh_entries_for_paths(vec![path.clone()]);
cx.spawn({
let path = path.clone();
let worktree = worktree.clone();
async move |this, cx| {
refresh.next().await;
this.update(cx, |this, cx| {
let snapshot = worktree.read(cx).snapshot();
if Self::unloaded_ancestor_hides_path(&snapshot, &path) {
log::warn!(
"buffer path {path:?} is still hidden by an unloaded directory after a refresh"
);
} else {
Self::local_worktree_entry_changed(
this, entry_id, &path, &worktree, &snapshot, cx,
);
}
})
.ok();
}
})
.detach();
return None;
}
let events = buffer.update(cx, |buffer, cx| {
let file = buffer.file()?;
let old_file = File::from_dyn(Some(file))?;
if old_file.worktree != *worktree {
return None;
}
let snapshot_entry = old_file
.entry_id
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
.or_else(|| snapshot.entry_for_path(old_file.path.as_ref()));
let new_file = if let Some(entry) = snapshot_entry {
File {
disk_state: match entry.mtime {
Some(mtime) => DiskState::Present {
mtime,
size: entry.size,
},
None => old_file.disk_state,
},
is_local: true,
entry_id: Some(entry.id),
path: entry.path.clone(),
worktree: worktree.clone(),
is_private: entry.is_private,
}
} else {
File {
disk_state: DiskState::Deleted,
is_local: true,
entry_id: old_file.entry_id,
path: old_file.path.clone(),
worktree: worktree.clone(),
is_private: old_file.is_private,
}
};
if new_file == *old_file {
return None;
}
let mut events = Vec::new();
if new_file.path != old_file.path {
this.path_to_buffer_id.remove(&ProjectPath {
path: old_file.path.clone(),
worktree_id: old_file.worktree_id(cx),
});
this.path_to_buffer_id.insert(
ProjectPath {
worktree_id: new_file.worktree_id(cx),
path: new_file.path.clone(),
},
buffer_id,
);
events.push(BufferStoreEvent::BufferChangedFilePath {
buffer: cx.entity(),
old_file: buffer.file().cloned(),
});
}
let local = this.as_local_mut()?;
if new_file.entry_id != old_file.entry_id {
if let Some(entry_id) = old_file.entry_id {
local.local_buffer_ids_by_entry_id.remove(&entry_id);
}
if let Some(entry_id) = new_file.entry_id {
local
.local_buffer_ids_by_entry_id
.insert(entry_id, buffer_id);
}
}
if let Some((client, project_id)) = &this.downstream_client {
client
.send(proto::UpdateBufferFile {
project_id: *project_id,
buffer_id: buffer_id.to_proto(),
file: Some(new_file.to_proto(cx)),
})
.ok();
}
buffer.file_updated(Arc::new(new_file), cx);
Some(events)
})?;
for event in events {
cx.emit(event);
}
None
}
fn unloaded_ancestor_hides_path(snapshot: &worktree::Snapshot, path: &RelPath) -> bool {
path.ancestors()
.skip(1)
.find_map(|ancestor| snapshot.entry_for_path(ancestor))
.is_some_and(|entry| entry.kind == worktree::EntryKind::UnloadedDir)
}
fn save_buffer(
&self,
buffer: Entity<Buffer>,
cx: &mut Context<BufferStore>,
) -> Task<Result<()>> {
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
return Task::ready(Err(anyhow!("buffer doesn't have a file")));
};
let worktree = file.worktree.clone();
self.save_local_buffer(buffer, worktree, file.path.clone(), false, cx)
}
fn save_buffer_as(
&self,
buffer: Entity<Buffer>,
path: ProjectPath,
cx: &mut Context<BufferStore>,
) -> Task<Result<()>> {
let Some(worktree) = self
.worktree_store
.read(cx)
.worktree_for_id(path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("no such worktree")));
};
self.save_local_buffer(buffer, worktree, path.path, true, cx)
}
#[ztracing::instrument(skip_all)]
fn open_buffer(
&self,
path: Arc<RelPath>,
worktree: Entity<Worktree>,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx));
cx.spawn(async move |this, cx| {
let path = path.clone();
let buffer = match load_file.await {
Ok(loaded) => {
let is_writable = loaded.is_writable;
let capability = if is_writable {
Capability::ReadWrite
} else {
Capability::Read
};
let reservation = cx.reserve_entity::<Buffer>();
let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
let text_buffer = cx
.background_spawn(async move {
text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text)
})
.await;
cx.insert_entity(reservation, |_| {
let mut buffer = Buffer::build(text_buffer, Some(loaded.file), capability);
buffer.set_encoding(loaded.encoding);
buffer.set_has_bom(loaded.has_bom);
buffer
})
}
Err(error) if is_not_found_error(&error) => cx.new(|cx| {
let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64());
let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, "");
let mut buffer = Buffer::build(
text_buffer,
Some(Arc::new(File {
worktree,
path,
disk_state: DiskState::New,
entry_id: None,
is_local: true,
is_private: false,
})),
Capability::ReadWrite,
);
apply_initial_line_ending(&mut buffer, cx);
buffer
}),
Err(e) => return Err(e),
};
this.update(cx, |this, cx| {
this.add_buffer(buffer.clone(), cx)?;
let buffer_id = buffer.read(cx).remote_id();
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
let project_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
};
let entry_id = file.entry_id;
// Check if the file should be read-only based on settings
let settings = WorktreeSettings::get(Some((&project_path).into()), cx);
let is_read_only = if project_path.path.is_empty() {
settings.is_std_path_read_only(&file.full_path(cx))
} else {
settings.is_path_read_only(&project_path.path)
};
if is_read_only {
buffer.update(cx, |buffer, cx| {
buffer.set_capability(Capability::Read, cx);
});
}
this.path_to_buffer_id.insert(project_path, buffer_id);
let this = this.as_local_mut().unwrap();
if let Some(entry_id) = entry_id {
this.local_buffer_ids_by_entry_id
.insert(entry_id, buffer_id);
}
}
anyhow::Ok(())
})??;
Ok(buffer)
})
}
fn create_buffer(
&self,
language: Option<Arc<Language>>,
project_searchable: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
cx.spawn(async move |buffer_store, cx| {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local("", cx)
.with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx);
apply_initial_line_ending(&mut buffer, cx);
buffer
});
buffer_store.update(cx, |buffer_store, cx| {
buffer_store.add_buffer(buffer.clone(), cx).log_err();
if !project_searchable {
buffer_store
.non_searchable_buffers
.insert(buffer.read(cx).remote_id());
}
})?;
Ok(buffer)
})
}
fn reload_buffers(
&self,
buffers: HashSet<Entity<Buffer>>,
push_to_history: bool,
cx: &mut Context<BufferStore>,
) -> Task<Result<ProjectTransaction>> {
cx.spawn(async move |_, cx| {
let mut project_transaction = ProjectTransaction::default();
for buffer in buffers {
let transaction = buffer.update(cx, |buffer, cx| buffer.reload(cx)).await?;
buffer.update(cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !push_to_history {
buffer.forget_transaction(transaction.id);
}
project_transaction.0.insert(cx.entity(), transaction);
}
});
}
Ok(project_transaction)
})
}
}
impl BufferStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_message_handler(Self::handle_buffer_reloaded);
client.add_entity_message_handler(Self::handle_buffer_saved);
client.add_entity_message_handler(Self::handle_update_buffer_file);
client.add_entity_request_handler(Self::handle_save_buffer);
client.add_entity_request_handler(Self::handle_reload_buffers);
}
/// Creates a buffer store, optionally retaining its buffers.
pub fn local(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {
Self {
state: BufferStoreState::Local(LocalBufferStore {
local_buffer_ids_by_entry_id: Default::default(),
worktree_store: worktree_store.clone(),
_subscription: cx.subscribe(&worktree_store, |this, _, event, cx| {
if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
let this = this.as_local_mut().unwrap();
this.subscribe_to_worktree(worktree, cx);
}
}),
}),
downstream_client: None,
opened_buffers: Default::default(),
path_to_buffer_id: Default::default(),
shared_buffers: Default::default(),
loading_buffers: Default::default(),
non_searchable_buffers: Default::default(),
worktree_store,
project_search: Default::default(),
}
}
pub fn remote(
worktree_store: Entity<WorktreeStore>,
upstream_client: AnyProtoClient,
remote_id: u64,
_cx: &mut Context<Self>,
) -> Self {
Self {
state: BufferStoreState::Remote(RemoteBufferStore {
shared_with_me: Default::default(),
loading_remote_buffers_by_id: Default::default(),
remote_buffer_listeners: Default::default(),
project_id: remote_id,
upstream_client,
worktree_store: worktree_store.clone(),
}),
downstream_client: None,
opened_buffers: Default::default(),
path_to_buffer_id: Default::default(),
loading_buffers: Default::default(),
shared_buffers: Default::default(),
non_searchable_buffers: Default::default(),
worktree_store,
project_search: Default::default(),
}
}
fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {
match &mut self.state {
BufferStoreState::Local(state) => Some(state),
_ => None,
}
}
fn as_remote_mut(&mut self) -> Option<&mut RemoteBufferStore> {
match &mut self.state {
BufferStoreState::Remote(state) => Some(state),
_ => None,
}
}
fn as_remote(&self) -> Option<&RemoteBufferStore> {
match &self.state {
BufferStoreState::Remote(state) => Some(state),
_ => None,
}
}
#[ztracing::instrument(skip_all)]
pub fn open_buffer(
&mut self,
project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.get_by_path(&project_path) {
return Task::ready(Ok(buffer));
}
let task = match self.loading_buffers.entry(project_path.clone()) {
hash_map::Entry::Occupied(e) => e.get().clone(),
hash_map::Entry::Vacant(entry) => {
let path = project_path.path.clone();
let Some(worktree) = self
.worktree_store
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("no such worktree")));
};
let load_buffer = match &self.state {
BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx),
BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx),
};
entry
.insert(
cx.spawn(async move |this, cx| {
let load_result = load_buffer.await;
this.update(cx, |this, _cx| {
// Record the fact that the buffer is no longer loading.
this.loading_buffers.remove(&project_path);
let buffer = load_result.map_err(Arc::new)?;
Ok(buffer)
})?
})
.shared(),
)
.clone()
}
};
cx.background_spawn(async move {
task.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
anyhow!(e.error_code())
} else {
anyhow!("{e}")
}
})
})
}
pub fn create_buffer(
&mut self,
language: Option<Arc<Language>>,
project_searchable: bool,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
match &self.state {
BufferStoreState::Local(this) => this.create_buffer(language, project_searchable, cx),
BufferStoreState::Remote(this) => this.create_buffer(language, project_searchable, cx),
}
}
pub fn save_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
match &mut self.state {
BufferStoreState::Local(this) => this.save_buffer(buffer, cx),
BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx),
}
}
pub fn save_buffer_as(
&mut self,
buffer: Entity<Buffer>,
path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let old_file = buffer.read(cx).file().cloned();
let task = match &self.state {
BufferStoreState::Local(this) => this.save_buffer_as(buffer.clone(), path, cx),
BufferStoreState::Remote(this) => {
this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx)
}
};
cx.spawn(async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
old_file.clone().and_then(|file| {
this.path_to_buffer_id.remove(&ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
})
});
cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file });
})
})
}
fn add_buffer(&mut self, buffer_entity: Entity<Buffer>, cx: &mut Context<Self>) -> Result<()> {
let buffer = buffer_entity.read(cx);
let remote_id = buffer.remote_id();
let path = File::from_dyn(buffer.file()).map(|file| ProjectPath {
path: file.path.clone(),
worktree_id: file.worktree_id(cx),
});
let is_remote = buffer.replica_id().is_remote();
let open_buffer = OpenBuffer::Complete {
buffer: buffer_entity.downgrade(),
};
let handle = cx.entity().downgrade();
buffer_entity.update(cx, move |_, cx| {
cx.on_release(move |buffer, cx| {
handle
.update(cx, |_, cx| {
cx.emit(BufferStoreEvent::BufferDropped(buffer.remote_id()))
})
.ok();
})
.detach()
});
let _expect_path_to_exist;
match self.opened_buffers.entry(remote_id) {
hash_map::Entry::Vacant(entry) => {
entry.insert(open_buffer);
_expect_path_to_exist = false;
}
hash_map::Entry::Occupied(mut entry) => {
if let OpenBuffer::Operations(operations) = entry.get_mut() {
buffer_entity.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx));
} else if entry.get().upgrade().is_some() {
if is_remote {
return Ok(());
} else {
debug_panic!("buffer {remote_id} was already registered");
anyhow::bail!("buffer {remote_id} was already registered");
}
}
entry.insert(open_buffer);
_expect_path_to_exist = true;
}
}
if let Some(path) = path {
self.path_to_buffer_id.insert(path, remote_id);
}
cx.subscribe(&buffer_entity, Self::on_buffer_event).detach();
cx.emit(BufferStoreEvent::BufferAdded(buffer_entity));
Ok(())
}
pub fn buffers(&self) -> impl '_ + Iterator<Item = Entity<Buffer>> {
self.opened_buffers
.values()
.filter_map(|buffer| buffer.upgrade())
}
pub(crate) fn is_searchable(&self, id: &BufferId) -> bool {
!self.non_searchable_buffers.contains(&id)
}
pub fn loading_buffers(
&self,
) -> impl Iterator<Item = (&ProjectPath, impl Future<Output = Result<Entity<Buffer>>>)> {
self.loading_buffers.iter().map(|(path, task)| {
let task = task.clone();
(path, async move {
task.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
anyhow!(e.error_code())
} else {
anyhow!("{e}")
}
})
})
})
}
pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> {
self.path_to_buffer_id.get(project_path)
}
pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> {
self.path_to_buffer_id
.get(path)
.and_then(|buffer_id| self.get(*buffer_id))
}
pub fn get(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
self.opened_buffers.get(&buffer_id)?.upgrade()
}
pub fn get_existing(&self, buffer_id: BufferId) -> Result<Entity<Buffer>> {
self.get(buffer_id)
.with_context(|| format!("unknown buffer id {buffer_id}"))
}
pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
self.get(buffer_id).or_else(|| {
self.as_remote()
.and_then(|remote| remote.loading_remote_buffers_by_id.get(&buffer_id).cloned())
})
}
pub fn buffer_version_info(&self, cx: &App) -> (Vec<proto::BufferVersion>, Vec<BufferId>) {
let buffers = self
.buffers()
.map(|buffer| {
let buffer = buffer.read(cx);
proto::BufferVersion {
id: buffer.remote_id().into(),
version: language::proto::serialize_version(&buffer.version),
}
})
.collect();
let incomplete_buffer_ids = self
.as_remote()
.map(|remote| remote.incomplete_buffer_ids())
.unwrap_or_default();
(buffers, incomplete_buffer_ids)
}
pub fn disconnected_from_host(&mut self, cx: &mut App) {
for open_buffer in self.opened_buffers.values_mut() {
if let Some(buffer) = open_buffer.upgrade() {
buffer.update(cx, |buffer, _| buffer.give_up_waiting());
}
}
for buffer in self.buffers() {
buffer.update(cx, |buffer, cx| {
buffer.set_capability(Capability::ReadOnly, cx)
});
}
if let Some(remote) = self.as_remote_mut() {
// Wake up all futures currently waiting on a buffer to get opened,
// to give them a chance to fail now that we've disconnected.
remote.remote_buffer_listeners.clear()
}
}
pub fn shared(&mut self, remote_id: u64, downstream_client: AnyProtoClient, _cx: &mut App) {
self.downstream_client = Some((downstream_client, remote_id));
}
pub fn unshared(&mut self, _cx: &mut Context<Self>) {
self.downstream_client.take();
self.forget_shared_buffers();
}
pub fn discard_incomplete(&mut self) {
self.opened_buffers
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
}
fn buffer_changed_file(&mut self, buffer: Entity<Buffer>, cx: &mut App) -> Option<()> {
let file = File::from_dyn(buffer.read(cx).file())?;
let remote_id = buffer.read(cx).remote_id();
if let Some(entry_id) = file.entry_id {
if let Some(local) = self.as_local_mut() {
match local.local_buffer_ids_by_entry_id.get(&entry_id) {
Some(_) => {
return None;
}
None => {
local
.local_buffer_ids_by_entry_id
.insert(entry_id, remote_id);
}
}
}
self.path_to_buffer_id.insert(
ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
remote_id,
);
};
Some(())
}
fn on_buffer_event(
&mut self,
buffer: Entity<Buffer>,
event: &BufferEvent,
cx: &mut Context<Self>,
) {
match event {
BufferEvent::FileHandleChanged => {
self.buffer_changed_file(buffer, cx);
}
BufferEvent::Reloaded => {
let Some((downstream_client, project_id)) = self.downstream_client.as_ref() else {
return;
};
let buffer = buffer.read(cx);
downstream_client
.send(proto::BufferReloaded {
project_id: *project_id,
buffer_id: buffer.remote_id().to_proto(),
version: serialize_version(&buffer.version()),
mtime: buffer.saved_mtime().map(|t| t.into()),
line_ending: serialize_line_ending(buffer.line_ending()) as i32,
})
.log_err();
}
BufferEvent::LanguageChanged(_) => {}
_ => {}
}
}
pub async fn handle_update_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let payload = envelope.payload;
let buffer_id = BufferId::new(payload.buffer_id)?;
let ops = payload
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
this.update(&mut cx, |this, cx| {
match this.opened_buffers.entry(buffer_id) {
hash_map::Entry::Occupied(mut e) => match e.get_mut() {
OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops),
OpenBuffer::Complete { buffer, .. } => {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx));
}
}
},
hash_map::Entry::Vacant(e) => {
e.insert(OpenBuffer::Operations(ops));
}
}
Ok(proto::Ack {})
})
}
pub fn register_shared_lsp_handle(
&mut self,
peer_id: proto::PeerId,
buffer_id: BufferId,
handle: OpenLspBufferHandle,
) {
if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id)
&& let Some(buffer) = shared_buffers.get_mut(&buffer_id)
{
buffer.lsp_handle = Some(handle);
return;
}
debug_panic!("tried to register shared lsp handle, but buffer was not shared")
}
pub fn handle_synchronize_buffers(
&mut self,
envelope: TypedEnvelope<proto::SynchronizeBuffers>,
cx: &mut Context<Self>,
client: Arc<Client>,
) -> Result<proto::SynchronizeBuffersResponse> {
let project_id = envelope.payload.project_id;
let mut response = proto::SynchronizeBuffersResponse {
buffers: Default::default(),
};
let Some(guest_id) = envelope.original_sender_id else {
anyhow::bail!("missing original_sender_id on SynchronizeBuffers request");
};
self.shared_buffers.entry(guest_id).or_default().clear();
for buffer in envelope.payload.buffers {
let buffer_id = BufferId::new(buffer.id)?;
let remote_version = language::proto::deserialize_version(&buffer.version);
if let Some(buffer) = self.get(buffer_id) {
self.shared_buffers
.entry(guest_id)
.or_default()
.entry(buffer_id)
.or_insert_with(|| SharedBuffer {
buffer: buffer.clone(),
lsp_handle: None,
});
let buffer = buffer.read(cx);
response.buffers.push(proto::BufferVersion {
id: buffer_id.into(),
version: language::proto::serialize_version(&buffer.version),
});
let operations = buffer.serialize_ops(Some(remote_version), cx);
let client = client.clone();
if let Some(file) = buffer.file() {
client
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id.into(),
file: Some(file.to_proto(cx)),
})
.log_err();
}
// TODO(max): do something
// client
// .send(proto::UpdateStagedText {
// project_id,
// buffer_id: buffer_id.into(),
// diff_base: buffer.diff_base().map(ToString::to_string),
// })
// .log_err();
client
.send(proto::BufferReloaded {
project_id,
buffer_id: buffer_id.into(),
version: language::proto::serialize_version(buffer.saved_version()),
mtime: buffer.saved_mtime().map(|time| time.into()),
line_ending: language::proto::serialize_line_ending(buffer.line_ending())
as i32,
})
.log_err();
cx.background_spawn(
async move {
let operations = operations.await;
for chunk in split_operations(operations) {
client
.request(proto::UpdateBuffer {
project_id,
buffer_id: buffer_id.into(),
operations: chunk,
})
.await?;
}
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}
Ok(response)
}
pub fn handle_create_buffer_for_peer(
&mut self,
envelope: TypedEnvelope<proto::CreateBufferForPeer>,
replica_id: ReplicaId,
capability: Capability,
cx: &mut Context<Self>,
) -> Result<()> {
let remote = self
.as_remote_mut()
.context("buffer store is not a remote")?;
if let Some(buffer) =
remote.handle_create_buffer_for_peer(envelope, replica_id, capability, cx)?
{
self.add_buffer(buffer, cx)?;
}
Ok(())
}
pub async fn handle_update_buffer_file(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateBufferFile>,
mut cx: AsyncApp,
) -> Result<()> {
let buffer_id = envelope.payload.buffer_id;
let buffer_id = BufferId::new(buffer_id)?;
this.update(&mut cx, |this, cx| {
let payload = envelope.payload.clone();
if let Some(buffer) = this.get_possibly_incomplete(buffer_id) {
let file = payload.file.context("invalid file")?;
let worktree = this
.worktree_store
.read(cx)
.worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx)
.context("no such worktree")?;
let file = File::from_proto(file, worktree, cx)?;
let old_file = buffer.update(cx, |buffer, cx| {
let old_file = buffer.file().cloned();
let new_path = file.path.clone();
buffer.file_updated(Arc::new(file), cx);
if old_file.as_ref().is_none_or(|old| *old.path() != new_path) {
Some(old_file)
} else {
None
}
});
if let Some(old_file) = old_file {
cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file });
}
}
if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() {
downstream_client
.send(proto::UpdateBufferFile {
project_id: *project_id,
buffer_id: buffer_id.into(),
file: envelope.payload.file,
})
.log_err();
}
Ok(())
})
}
pub async fn handle_save_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::SaveBuffer>,
mut cx: AsyncApp,
) -> Result<proto::BufferSaved> {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let (buffer, project_id) = this.read_with(&cx, |this, _| {
anyhow::Ok((
this.get_existing(buffer_id)?,
this.downstream_client
.as_ref()
.map(|(_, project_id)| *project_id)
.context("project is not shared")?,
))
})?;
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&envelope.payload.version))
})
.await?;
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
if let Some(new_path) = envelope.payload.new_path
&& let Some(new_path) = ProjectPath::from_proto(new_path)
{
this.update(&mut cx, |this, cx| {
this.save_buffer_as(buffer.clone(), new_path, cx)
})
.await?;
} else {
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))
.await?;
}
Ok(buffer.read_with(&cx, |buffer, _| proto::BufferSaved {
project_id,
buffer_id: buffer_id.into(),
version: serialize_version(buffer.saved_version()),
mtime: buffer.saved_mtime().map(|time| time.into()),
}))
}
pub async fn handle_close_buffer(
this: Entity<Self>,
envelope: TypedEnvelope<proto::CloseBuffer>,
mut cx: AsyncApp,
) -> Result<()> {
let peer_id = envelope.sender_id;
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
this.update(&mut cx, |this, cx| {
if let Some(shared) = this.shared_buffers.get_mut(&peer_id)
&& shared.remove(&buffer_id).is_some()
{
cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id));
if shared.is_empty() {
this.shared_buffers.remove(&peer_id);
}
return;
}
debug_panic!(
"peer_id {} closed buffer_id {} which was either not open or already closed",
peer_id,
buffer_id
)
});
Ok(())
}
pub async fn handle_buffer_saved(
this: Entity<Self>,
envelope: TypedEnvelope<proto::BufferSaved>,
mut cx: AsyncApp,
) -> Result<()> {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let version = deserialize_version(&envelope.payload.version);
let mtime = envelope.payload.mtime.clone().map(|time| time.into());
this.update(&mut cx, move |this, cx| {
if let Some(buffer) = this.get_possibly_incomplete(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.did_save(version, mtime, cx);
});
}
if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() {
downstream_client
.send(proto::BufferSaved {
project_id: *project_id,
buffer_id: buffer_id.into(),
mtime: envelope.payload.mtime,
version: envelope.payload.version,
})
.log_err();
}
});
Ok(())
}
pub async fn handle_buffer_reloaded(
this: Entity<Self>,
envelope: TypedEnvelope<proto::BufferReloaded>,
mut cx: AsyncApp,
) -> Result<()> {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let version = deserialize_version(&envelope.payload.version);
let mtime = envelope.payload.mtime.clone().map(|time| time.into());
let line_ending = deserialize_line_ending(
proto::LineEnding::from_i32(envelope.payload.line_ending)
.context("missing line ending")?,
);
this.update(&mut cx, |this, cx| {
if let Some(buffer) = this.get_possibly_incomplete(buffer_id) {
buffer.update(cx, |buffer, cx| {
buffer.did_reload(version, line_ending, mtime, cx);
});
}
if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() {
downstream_client
.send(proto::BufferReloaded {
project_id: *project_id,
buffer_id: buffer_id.into(),
mtime: envelope.payload.mtime,
version: envelope.payload.version,
line_ending: envelope.payload.line_ending,
})
.log_err();
}
});
Ok(())
}
pub fn reload_buffers(
&self,
buffers: HashSet<Entity<Buffer>>,
push_to_history: bool,
cx: &mut Context<Self>,
) -> Task<Result<ProjectTransaction>> {
if buffers.is_empty() {
return Task::ready(Ok(ProjectTransaction::default()));
}
match &self.state {
BufferStoreState::Local(this) => this.reload_buffers(buffers, push_to_history, cx),
BufferStoreState::Remote(this) => this.reload_buffers(buffers, push_to_history, cx),
}
}
async fn handle_reload_buffers(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ReloadBuffers>,
mut cx: AsyncApp,
) -> Result<proto::ReloadBuffersResponse> {
let sender_id = envelope.original_sender_id().unwrap_or_default();
let reload = this.update(&mut cx, |this, cx| {
let mut buffers = HashSet::default();
for buffer_id in &envelope.payload.buffer_ids {
let buffer_id = BufferId::new(*buffer_id)?;
buffers.insert(this.get_existing(buffer_id)?);
}
anyhow::Ok(this.reload_buffers(buffers, false, cx))
})?;
let project_transaction = reload.await?;
let project_transaction = this.update(&mut cx, |this, cx| {
this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
});
Ok(proto::ReloadBuffersResponse {
transaction: Some(project_transaction),
})
}
pub fn create_buffer_for_peer(
&mut self,
buffer: &Entity<Buffer>,
peer_id: proto::PeerId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let buffer_id = buffer.read(cx).remote_id();
let shared_buffers = self.shared_buffers.entry(peer_id).or_default();
if shared_buffers.contains_key(&buffer_id) {
return Task::ready(Ok(()));
}
shared_buffers.insert(
buffer_id,
SharedBuffer {
buffer: buffer.clone(),
lsp_handle: None,
},
);
let Some((client, project_id)) = self.downstream_client.clone() else {
return Task::ready(Ok(()));
};
cx.spawn(async move |this, cx| {
let Some(buffer) = this.read_with(cx, |this, _| this.get(buffer_id))? else {
return anyhow::Ok(());
};
let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx));
let operations = operations.await;
let state = buffer.update(cx, |buffer, cx| buffer.to_proto(cx));
let initial_state = proto::CreateBufferForPeer {
project_id,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
};
if client.send(initial_state).log_err().is_some() {
let client = client.clone();
cx.background_spawn(async move {
let mut chunks = split_operations(operations).peekable();
while let Some(chunk) = chunks.next() {
let is_last = chunks.peek().is_none();
client.send(proto::CreateBufferForPeer {
project_id,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::Chunk(
proto::BufferChunk {
buffer_id: buffer_id.into(),
operations: chunk,
is_last,
},
)),
})?;
}
anyhow::Ok(())
})
.await
.log_err();
}
Ok(())
})
}
pub fn forget_shared_buffers(&mut self) {
self.shared_buffers.clear();
}
pub fn forget_shared_buffers_for(&mut self, peer_id: &proto::PeerId) {
self.shared_buffers.remove(peer_id);
}
pub fn update_peer_id(&mut self, old_peer_id: &proto::PeerId, new_peer_id: proto::PeerId) {
if let Some(buffers) = self.shared_buffers.remove(old_peer_id) {
self.shared_buffers.insert(new_peer_id, buffers);
}
}
pub fn has_shared_buffers(&self) -> bool {
!self.shared_buffers.is_empty()
}
pub fn create_local_buffer(
&mut self,
text: &str,
language: Option<Arc<Language>>,
project_searchable: bool,
cx: &mut Context<Self>,
) -> Entity<Buffer> {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(text, cx)
.with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx);
apply_initial_line_ending(&mut buffer, cx);
buffer
});
self.add_buffer(buffer.clone(), cx).log_err();
let buffer_id = buffer.read(cx).remote_id();
if !project_searchable {
self.non_searchable_buffers.insert(buffer_id);
}
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
self.path_to_buffer_id.insert(
ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
buffer_id,
);
let this = self
.as_local_mut()
.expect("local-only method called in a non-local context");
if let Some(entry_id) = file.entry_id {
this.local_buffer_ids_by_entry_id
.insert(entry_id, buffer_id);
}
}
buffer
}
pub fn deserialize_project_transaction(
&mut self,
message: proto::ProjectTransaction,
push_to_history: bool,
cx: &mut Context<Self>,
) -> Task<Result<ProjectTransaction>> {
if let Some(this) = self.as_remote_mut() {
this.deserialize_project_transaction(message, push_to_history, cx)
} else {
debug_panic!("not a remote buffer store");
Task::ready(Err(anyhow!("not a remote buffer store")))
}
}
pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
cx: &mut Context<BufferStore>,
) -> Task<Result<Entity<Buffer>>> {
if let Some(this) = self.as_remote_mut() {
this.wait_for_remote_buffer(id, cx)
} else {
debug_panic!("not a remote buffer store");
Task::ready(Err(anyhow!("not a remote buffer store")))
}
}
pub fn serialize_project_transaction_for_peer(
&mut self,
project_transaction: ProjectTransaction,
peer_id: proto::PeerId,
cx: &mut Context<Self>,
) -> proto::ProjectTransaction {
let mut serialized_transaction = proto::ProjectTransaction {
buffer_ids: Default::default(),
transactions: Default::default(),
};
for (buffer, transaction) in project_transaction.0 {
self.create_buffer_for_peer(&buffer, peer_id, cx)
.detach_and_log_err(cx);
serialized_transaction
.buffer_ids
.push(buffer.read(cx).remote_id().into());
serialized_transaction
.transactions
.push(language::proto::serialize_transaction(&transaction));
}
serialized_transaction
}
pub(crate) fn register_project_search_result_handle(
&mut self,
) -> (u64, async_channel::Receiver<BufferId>) {
let (tx, rx) = async_channel::unbounded();
let handle = util::post_inc(&mut self.project_search.next_id);
let _old_entry = self.project_search.chunks.insert(handle, tx);
debug_assert!(_old_entry.is_none());
(handle, rx)
}
pub fn register_ongoing_project_search(
&mut self,
id: (PeerId, u64),
search: Task<anyhow::Result<()>>,
) {
let _old = self.project_search.searches_in_progress.insert(id, search);
debug_assert!(_old.is_none());
}
pub async fn handle_find_search_candidates_cancel(
this: Entity<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidatesCancelled>,
mut cx: AsyncApp,
) -> Result<()> {
let id = (
envelope.original_sender_id.unwrap_or(envelope.sender_id),
envelope.payload.handle,
);
let _ = this.update(&mut cx, |this, _| {
this.project_search.searches_in_progress.remove(&id)
});
Ok(())
}
pub(crate) async fn handle_find_search_candidates_chunk(
this: Entity<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidatesChunk>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
use proto::find_search_candidates_chunk::Variant;
let handle = envelope.payload.handle;
let buffer_ids = match envelope
.payload
.variant
.context("Expected non-null variant")?
{
Variant::Matches(find_search_candidates_matches) => find_search_candidates_matches
.buffer_ids
.into_iter()
.filter_map(|buffer_id| BufferId::new(buffer_id).ok())
.collect::<Vec<_>>(),
Variant::Done(_) => {
this.update(&mut cx, |this, _| {
this.project_search.chunks.remove(&handle)
});
return Ok(proto::Ack {});
}
};
let Some(sender) = this.read_with(&mut cx, |this, _| {
this.project_search.chunks.get(&handle).cloned()
}) else {
return Ok(proto::Ack {});
};
for buffer_id in buffer_ids {
let Ok(_) = sender.send(buffer_id).await else {
this.update(&mut cx, |this, _| {
this.project_search.chunks.remove(&handle)
});
return Ok(proto::Ack {});
};
}
Ok(proto::Ack {})
}
}
impl OpenBuffer {
fn upgrade(&self) -> Option<Entity<Buffer>> {
match self {
OpenBuffer::Complete { buffer, .. } => buffer.upgrade(),
OpenBuffer::Operations(_) => None,
}
}
}
fn is_not_found_error(error: &anyhow::Error) -> bool {
error
.root_cause()
.downcast_ref::<io::Error>()
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}
fn apply_initial_line_ending(buffer: &mut Buffer, cx: &mut Context<Buffer>) {
// Only applies for empty rope or a single line with no trailing newline.
if buffer.max_point().row > 0 {
return;
}
let location = buffer.file().map(|file| settings::SettingsLocation {
worktree_id: file.worktree_id(cx),
path: file.path().as_ref(),
});
let language = buffer.language().map(|l| l.name());
let settings = AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx);
let desired = match settings.line_ending {
LineEndingSetting::Detect => return,
LineEndingSetting::PreferLf | LineEndingSetting::EnforceLf => LineEnding::Unix,
LineEndingSetting::PreferCrlf | LineEndingSetting::EnforceCrlf => LineEnding::Windows,
};
if buffer.line_ending() != desired {
buffer.set_line_ending(desired, cx);
}
}