#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]use std::collections::{HashMap, HashSet};use std::path::{Path, PathBuf};use std::rc::Rc;use std::sync::Arc;use std::time::Duration; use bimap::BiMap;use futures_util::stream::AbortHandle;use lsp::types::TextDocumentContentChangeEvent;use lsp::{LspManagerModel, LspServerLogLevel, LspServerModel};use remote_server::manager::RemoteServerManager;use string_offset::{ByteOffset, CharOffset};use vec1::vec1;use warp_core::features::FeatureFlag;use warp_core::safe_error;use warp_editor::content::buffer::{Buffer, ToBufferCharOffset};use warp_editor::content::diff::{TextDiff, text_diff};use warp_editor::content::edit::PreciseDelta;use warp_editor::content::version::BufferVersion;use warp_util::content_version::ContentVersion;use warp_util::file::{FileId, FileLoadError, FileSaveError};use warp_util::host_id::HostId;use warp_util::remote_path::RemotePath;use warp_util::standardized_path::StandardizedPath;use warpui::r#async::Timer;use warpui::{Entity, ModelContext, ModelHandle, SingletonEntity, WeakModelHandle}; use super::buffer_location::{LocalOrRemotePath, SyncClock}; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use lsp::LspManagerModelEvent; use warp_files::{FileModelEvent, FileModel}; use warp_editor::content::text::IndentBehavior; use warp_editor::content::text::IndentUnit; use warp_editor::content::buffer::EditOrigin; }} /// State for a shared buffer including the file ID and buffer handle.#[derive(Debug, Clone)]pub struct BufferState { pub file_id: FileId, pub buffer: ModelHandle<Buffer>,} impl BufferState { pub fn new(file_id: FileId, buffer: ModelHandle<Buffer>) -> Self { Self { file_id, buffer } }} /// Tracks an active background diff parsing operation.struct PendingDiffParse { abort_handle: AbortHandle,} /// How long to wait after the last keystroke before sending a batched/// `BufferEdit` to the remote server. Long enough to coalesce rapid/// keystrokes, short enough for the remote view to feel responsive.const REMOTE_EDIT_DEBOUNCE: Duration = Duration::from_millis(200); /// Accumulates incremental edits for a single remote buffer during a/// debounce window before sending them as a single `BufferEdit` message.struct PendingEditBatch { /// The server version known when the first edit in this batch was captured. expected_server_version: u64, /// Accumulated `TextEdit`s — each edit's offsets reference the buffer state /// AFTER all previous edits in this batch have been applied. edits: Vec<remote_server::proto::TextEdit>, /// The client version to send (updated on each append). latest_client_version: ContentVersion, /// Handle to cancel the debounce timer when a new edit arrives or the /// batch is flushed/discarded. debounce_timer: Option<AbortHandle>,} impl PendingEditBatch { /// Flush this batch: send accumulated edits as a single `BufferEdit` /// to the remote server and cancel the debounce timer. /// /// Note: `send_buffer_edit` uses best-effort `try_send` on an unbounded /// channel, so it can only fail if the connection is closed (in which /// case the subsequent `save_buffer` would also fail). fn flush(self, client: &remote_server::client::RemoteServerClient, path: &str) { if let Some(timer) = &self.debounce_timer { timer.abort(); } if self.edits.is_empty() { return; } log::debug!( "[remote-buffer] Flushing batched BufferEdit: path={path} \ expected_sv={} new_cv={} edit_count={}", self.expected_server_version, self.latest_client_version.as_u64(), self.edits.len() ); client.send_buffer_edit( path.to_string(), self.expected_server_version, self.latest_client_version.as_u64(), self.edits, ); } /// Discard this batch without sending, cancelling the debounce timer. fn discard(self) { if let Some(timer) = &self.debounce_timer { timer.abort(); log::debug!( "[remote-buffer] Discarded pending batch: \ expected_sv={} edit_count={}", self.expected_server_version, self.edits.len() ); } }} /// Describes the backing store for a buffer's content.enum BufferSource { /// Backed by the local filesystem (existing behavior). Local { base_content_version: Option<ContentVersion>, /// The first ever content version when the file was loaded. /// Used to avoid sending a spurious didChange to LSP on initial load. initial_content_version: Option<ContentVersion>, }, /// Backed by a remote filesystem over the remote server protocol. Remote { remote_path: RemotePath, /// `None` while waiting for the `OpenBufferResponse`; `Some` once loaded. sync_clock: Option<SyncClock>, /// Pending batched edits awaiting the debounce timer. `None` when idle. pending_batch: Option<PendingEditBatch>, }, /// Local file managed by the remote-server daemon. /// Owns the SyncClock for version tracking. Connection tracking /// is handled by ServerModel, not here — the buffer is a file-level /// concept shared across connections. ServerLocal { sync_clock: SyncClock, base_content_version: Option<ContentVersion>, initial_content_version: Option<ContentVersion>, },} struct InternalBufferState { buffer: WeakModelHandle<Buffer>, /// Tracks the latest buffer version we've attempted to sync with LSP. /// Used to detect if previous versions were synced successfully. latest_buffer_version: Option<usize>, /// Tracks any active background diff parsing for auto-reload. pending_diff_parse: Option<PendingDiffParse>, source: BufferSource,} impl InternalBufferState { /// Returns the base content version for local buffers, `None` for remote. /// /// Remote buffers return `None` because they don't use the file-watcher /// auto-reload path (which is `local_fs`-only). Version tracking for /// remote buffers is handled by `SyncClock` instead. fn base_content_version(&self) -> Option<ContentVersion> { match &self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => *base_content_version, BufferSource::Remote { .. } => None, } } /// Sets the base content version. Applicable to Local and ServerLocal buffers. fn set_base_content_version(&mut self, version: ContentVersion) { match &mut self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => { *base_content_version = Some(version); } BufferSource::Remote { .. } => {} } } /// Returns the initial content version for local/server-local buffers, /// `None` for remote. /// /// Remote buffers return `None` because the initial-version guard is only /// needed to avoid a spurious LSP `didChange` on first load. Remote /// buffers don't interact with the local LSP (it runs on the server side), /// so no guard is necessary. fn initial_content_version(&self) -> Option<ContentVersion> { match &self.source { BufferSource::Local { initial_content_version, .. } | BufferSource::ServerLocal { initial_content_version, .. } => *initial_content_version, BufferSource::Remote { .. } => None, } } /// Sets the initial content version. Applicable to Local and ServerLocal buffers. fn set_initial_content_version(&mut self, version: ContentVersion) { match &mut self.source { BufferSource::Local { initial_content_version, .. } | BufferSource::ServerLocal { initial_content_version, .. } => { *initial_content_version = Some(version); } BufferSource::Remote { .. } => {} } } /// Whether this buffer has been loaded (has content). fn is_loaded(&self) -> bool { match &self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => base_content_version.is_some(), // Remote buffers are loaded once the OpenBufferResponse arrives // and populates the sync clock. BufferSource::Remote { sync_clock, .. } => sync_clock.is_some(), } }} pub enum GlobalBufferModelEvent { BufferLoaded { file_id: FileId, content_version: ContentVersion, }, FailedToLoad { file_id: FileId, error: Rc<FileLoadError>, }, BufferUpdatedFromFileEvent { file_id: FileId, success: bool, content_version: ContentVersion, }, FileSaved { file_id: FileId, content_version: ContentVersion, }, FailedToSave { file_id: FileId, error: Arc<FileSaveError>, }, /// A remote buffer update conflicted with local edits. /// The UI should present a resolution dialog. RemoteBufferConflict { file_id: FileId }, /// A server-local buffer was updated from a file-watcher event. /// Carries the incremental diff edits for the ServerModel to push /// to connected clients as `BufferUpdatedPush`. ServerLocalBufferUpdated { file_id: FileId, /// Incremental edits with 1-indexed character offsets (matching `CharOffset`). edits: Vec<CharOffsetEdit>, new_server_version: ContentVersion, expected_client_version: ContentVersion, },} impl GlobalBufferModelEvent { pub fn file_id(&self) -> FileId { match self { GlobalBufferModelEvent::BufferLoaded { file_id, .. } | GlobalBufferModelEvent::FailedToLoad { file_id, .. } | GlobalBufferModelEvent::BufferUpdatedFromFileEvent { file_id, .. } | GlobalBufferModelEvent::FileSaved { file_id, .. } | GlobalBufferModelEvent::FailedToSave { file_id, .. } | GlobalBufferModelEvent::RemoteBufferConflict { file_id, .. } | GlobalBufferModelEvent::ServerLocalBufferUpdated { file_id, .. } => *file_id, } }} /// A text edit using 1-indexed character offsets (matching `CharOffset`).////// Used to carry incremental edits in `ServerLocalBufferUpdated` events/// and `handle_buffer_updated_push` without coupling `GlobalBufferModel`/// to proto types. Offsets use the same 1-indexed coordinate system as/// the buffer's `CharOffset`, so no conversion is needed at the boundary.pub struct CharOffsetEdit { pub start: CharOffset, pub end: CharOffset, pub text: String,} /// Global singleton model for managing shared buffers across editors.////// This allows multiple editors to share the same buffer when editing the same file,/// enabling consistent content synchronization and more efficient memory usage.pub struct GlobalBufferModel { location_to_id: BiMap<LocalOrRemotePath, FileId>, buffers: HashMap<FileId, InternalBufferState>,} impl GlobalBufferModel { pub fn new(_ctx: &mut ModelContext<Self>) -> Self { #[cfg(feature = "local_fs")] _ctx.subscribe_to_model(&FileModel::handle(_ctx), Self::handle_file_model_events); #[cfg(feature = "local_fs")] _ctx.subscribe_to_model( &LspManagerModel::handle(_ctx), Self::handle_lsp_manager_events, ); // Subscribe to remote buffer updates from the RemoteServerManager. #[cfg(feature = "local_tty")] if FeatureFlag::SshRemoteServer.is_enabled() { use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent}; let mgr = RemoteServerManager::handle(_ctx); _ctx.subscribe_to_model(&mgr, |me, _, event, ctx| match event { RemoteServerManagerEvent::BufferUpdated { host_id, path, new_server_version, expected_client_version, edits, } => { let char_edits: Vec<_> = edits .iter() .map(|e| CharOffsetEdit { start: CharOffset::from(e.start_offset as usize), end: CharOffset::from(e.end_offset as usize), text: e.text.clone(), }) .collect(); me.handle_buffer_updated_push( host_id, path, *new_server_version, *expected_client_version, &char_edits, ctx, ); } RemoteServerManagerEvent::BufferConflictDetected { host_id, path } => { me.handle_buffer_conflict_detected(host_id, path, ctx); } _ => {} }); } Self { location_to_id: BiMap::new(), buffers: HashMap::new(), } } /// Scan through all buffers and deallocate any that are no longer in use. pub fn remove_deallocated_buffers(&mut self, ctx: &mut ModelContext<Self>) { let ids_to_remove: HashSet<FileId> = self .buffers .iter() .filter_map(|(id, state)| { if state.buffer.upgrade(ctx).is_none() { Some(*id) } else { None } }) .collect(); if ids_to_remove.is_empty() { return; } // Collect paths for didClose before removing entries. let paths_to_close: Vec<PathBuf> = ids_to_remove .iter() .filter_map(|id| match self.location_to_id.get_by_right(id) { Some(LocalOrRemotePath::Local(path)) => Some(path.clone()), Some(LocalOrRemotePath::Remote(_)) | None => None, }) .collect(); for path in &paths_to_close { self.close_document_with_lsp(path, ctx); } for id in &ids_to_remove { self.location_to_id.remove_by_right(id); } for id in ids_to_remove { self.buffers.remove(&id); #[cfg(feature = "local_fs")] { let file_model = FileModel::handle(ctx); file_model.update(ctx, |file_model, ctx| { file_model.cancel(id); file_model.unsubscribe(id, ctx); }); } } } pub fn buffer_loaded(&self, file_id: FileId) -> bool { self.buffers .get(&file_id) .map(|state| state.is_loaded()) .unwrap_or(false) } fn cleanup_file_id(&mut self, file_id: FileId, _ctx: &mut ModelContext<Self>) { // Send didClose before removing the entry. if let Some((LocalOrRemotePath::Local(path), _)) = self.location_to_id.remove_by_right(&file_id) { self.close_document_with_lsp(&path, _ctx); } self.buffers.remove(&file_id); #[cfg(feature = "local_fs")] { let file_model = FileModel::handle(_ctx); file_model.update(_ctx, |file_model, ctx| { file_model.cancel(file_id); file_model.unsubscribe(file_id, ctx); }); } } /// Returns the buffer handle if it is 1) still exists + active 2) loaded. fn buffer_handle_for_id( &mut self, file_id: FileId, ctx: &mut ModelContext<Self>, ) -> Option<ModelHandle<Buffer>> { let state = self.buffers.get(&file_id)?; // If the buffer hasn't been loaded yet, don't return a model handle. if !state.is_loaded() { log::info!("Cannot return handle for unloaded buffers"); return None; } match state.buffer.upgrade(ctx) { Some(handle) => Some(handle), None => { // Clean up deallocated buffers. self.cleanup_file_id(file_id, ctx); None } } } /// Once we finish reading the file's content from the disk, populate the buffer with the content. /// For initial load (is_loaded_from_file_system == true), this is synchronous. /// For auto-reload (is_loaded_from_file_system == false), this spawns a background task for diff computation. /// Exposed as `pub(crate)` so tests can populate buffer content /// without going through the async `FileModel` load path. pub(crate) fn populate_buffer_with_read_content( &mut self, file_id: FileId, content: &str, base_version: ContentVersion, new_version: ContentVersion, is_initial_load: bool, ctx: &mut ModelContext<Self>, ) { let Some(state) = self.buffers.get_mut(&file_id) else { return; }; let Some(buffer) = state.buffer.upgrade(ctx) else { self.cleanup_file_id(file_id, ctx); log::warn!("Cannot populate buffer with content due to deallocated model handle"); return;Lines 1–500 of 2477. Load more
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))] use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use bimap::BiMap; use futures_util::stream::AbortHandle; use lsp::types::TextDocumentContentChangeEvent; use lsp::{LspManagerModel, LspServerLogLevel, LspServerModel}; use remote_server::manager::RemoteServerManager; use string_offset::{ByteOffset, CharOffset}; use vec1::vec1; use warp_core::features::FeatureFlag; use warp_core::safe_error; use warp_editor::content::buffer::{Buffer, ToBufferCharOffset}; use warp_editor::content::diff::{TextDiff, text_diff}; use warp_editor::content::edit::PreciseDelta; use warp_editor::content::version::BufferVersion; use warp_util::content_version::ContentVersion; use warp_util::file::{FileId, FileLoadError, FileSaveError}; use warp_util::host_id::HostId; use warp_util::remote_path::RemotePath; use warp_util::standardized_path::StandardizedPath; use warpui::r#async::Timer; use warpui::{Entity, ModelContext, ModelHandle, SingletonEntity, WeakModelHandle}; use super::buffer_location::{LocalOrRemotePath, SyncClock}; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use lsp::LspManagerModelEvent; use warp_files::{FileModelEvent, FileModel}; use warp_editor::content::text::IndentBehavior; use warp_editor::content::text::IndentUnit; use warp_editor::content::buffer::EditOrigin; } } /// State for a shared buffer including the file ID and buffer handle. #[derive(Debug, Clone)] pub struct BufferState { pub file_id: FileId, pub buffer: ModelHandle<Buffer>, } impl BufferState { pub fn new(file_id: FileId, buffer: ModelHandle<Buffer>) -> Self { Self { file_id, buffer } } } /// Tracks an active background diff parsing operation. struct PendingDiffParse { abort_handle: AbortHandle, } /// How long to wait after the last keystroke before sending a batched /// `BufferEdit` to the remote server. Long enough to coalesce rapid /// keystrokes, short enough for the remote view to feel responsive. const REMOTE_EDIT_DEBOUNCE: Duration = Duration::from_millis(200); /// Accumulates incremental edits for a single remote buffer during a /// debounce window before sending them as a single `BufferEdit` message. struct PendingEditBatch { /// The server version known when the first edit in this batch was captured. expected_server_version: u64, /// Accumulated `TextEdit`s — each edit's offsets reference the buffer state /// AFTER all previous edits in this batch have been applied. edits: Vec<remote_server::proto::TextEdit>, /// The client version to send (updated on each append). latest_client_version: ContentVersion, /// Handle to cancel the debounce timer when a new edit arrives or the /// batch is flushed/discarded. debounce_timer: Option<AbortHandle>, } impl PendingEditBatch { /// Flush this batch: send accumulated edits as a single `BufferEdit` /// to the remote server and cancel the debounce timer. /// /// Note: `send_buffer_edit` uses best-effort `try_send` on an unbounded /// channel, so it can only fail if the connection is closed (in which /// case the subsequent `save_buffer` would also fail). fn flush(self, client: &remote_server::client::RemoteServerClient, path: &str) { if let Some(timer) = &self.debounce_timer { timer.abort(); } if self.edits.is_empty() { return; } log::debug!( "[remote-buffer] Flushing batched BufferEdit: path={path} \ expected_sv={} new_cv={} edit_count={}", self.expected_server_version, self.latest_client_version.as_u64(), self.edits.len() ); client.send_buffer_edit( path.to_string(), self.expected_server_version, self.latest_client_version.as_u64(), self.edits, ); } /// Discard this batch without sending, cancelling the debounce timer. fn discard(self) { if let Some(timer) = &self.debounce_timer { timer.abort(); log::debug!( "[remote-buffer] Discarded pending batch: \ expected_sv={} edit_count={}", self.expected_server_version, self.edits.len() ); } } } /// Describes the backing store for a buffer's content. enum BufferSource { /// Backed by the local filesystem (existing behavior). Local { base_content_version: Option<ContentVersion>, /// The first ever content version when the file was loaded. /// Used to avoid sending a spurious didChange to LSP on initial load. initial_content_version: Option<ContentVersion>, }, /// Backed by a remote filesystem over the remote server protocol. Remote { remote_path: RemotePath, /// `None` while waiting for the `OpenBufferResponse`; `Some` once loaded. sync_clock: Option<SyncClock>, /// Pending batched edits awaiting the debounce timer. `None` when idle. pending_batch: Option<PendingEditBatch>, }, /// Local file managed by the remote-server daemon. /// Owns the SyncClock for version tracking. Connection tracking /// is handled by ServerModel, not here — the buffer is a file-level /// concept shared across connections. ServerLocal { sync_clock: SyncClock, base_content_version: Option<ContentVersion>, initial_content_version: Option<ContentVersion>, }, } struct InternalBufferState { buffer: WeakModelHandle<Buffer>, /// Tracks the latest buffer version we've attempted to sync with LSP. /// Used to detect if previous versions were synced successfully. latest_buffer_version: Option<usize>, /// Tracks any active background diff parsing for auto-reload. pending_diff_parse: Option<PendingDiffParse>, source: BufferSource, } impl InternalBufferState { /// Returns the base content version for local buffers, `None` for remote. /// /// Remote buffers return `None` because they don't use the file-watcher /// auto-reload path (which is `local_fs`-only). Version tracking for /// remote buffers is handled by `SyncClock` instead. fn base_content_version(&self) -> Option<ContentVersion> { match &self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => *base_content_version, BufferSource::Remote { .. } => None, } } /// Sets the base content version. Applicable to Local and ServerLocal buffers. fn set_base_content_version(&mut self, version: ContentVersion) { match &mut self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => { *base_content_version = Some(version); } BufferSource::Remote { .. } => {} } } /// Returns the initial content version for local/server-local buffers, /// `None` for remote. /// /// Remote buffers return `None` because the initial-version guard is only /// needed to avoid a spurious LSP `didChange` on first load. Remote /// buffers don't interact with the local LSP (it runs on the server side), /// so no guard is necessary. fn initial_content_version(&self) -> Option<ContentVersion> { match &self.source { BufferSource::Local { initial_content_version, .. } | BufferSource::ServerLocal { initial_content_version, .. } => *initial_content_version, BufferSource::Remote { .. } => None, } } /// Sets the initial content version. Applicable to Local and ServerLocal buffers. fn set_initial_content_version(&mut self, version: ContentVersion) { match &mut self.source { BufferSource::Local { initial_content_version, .. } | BufferSource::ServerLocal { initial_content_version, .. } => { *initial_content_version = Some(version); } BufferSource::Remote { .. } => {} } } /// Whether this buffer has been loaded (has content). fn is_loaded(&self) -> bool { match &self.source { BufferSource::Local { base_content_version, .. } | BufferSource::ServerLocal { base_content_version, .. } => base_content_version.is_some(), // Remote buffers are loaded once the OpenBufferResponse arrives // and populates the sync clock. BufferSource::Remote { sync_clock, .. } => sync_clock.is_some(), } } } pub enum GlobalBufferModelEvent { BufferLoaded { file_id: FileId, content_version: ContentVersion, }, FailedToLoad { file_id: FileId, error: Rc<FileLoadError>, }, BufferUpdatedFromFileEvent { file_id: FileId, success: bool, content_version: ContentVersion, }, FileSaved { file_id: FileId, content_version: ContentVersion, }, FailedToSave { file_id: FileId, error: Arc<FileSaveError>, }, /// A remote buffer update conflicted with local edits. /// The UI should present a resolution dialog. RemoteBufferConflict { file_id: FileId }, /// A server-local buffer was updated from a file-watcher event. /// Carries the incremental diff edits for the ServerModel to push /// to connected clients as `BufferUpdatedPush`. ServerLocalBufferUpdated { file_id: FileId, /// Incremental edits with 1-indexed character offsets (matching `CharOffset`). edits: Vec<CharOffsetEdit>, new_server_version: ContentVersion, expected_client_version: ContentVersion, }, } impl GlobalBufferModelEvent { pub fn file_id(&self) -> FileId { match self { GlobalBufferModelEvent::BufferLoaded { file_id, .. } | GlobalBufferModelEvent::FailedToLoad { file_id, .. } | GlobalBufferModelEvent::BufferUpdatedFromFileEvent { file_id, .. } | GlobalBufferModelEvent::FileSaved { file_id, .. } | GlobalBufferModelEvent::FailedToSave { file_id, .. } | GlobalBufferModelEvent::RemoteBufferConflict { file_id, .. } | GlobalBufferModelEvent::ServerLocalBufferUpdated { file_id, .. } => *file_id, } } } /// A text edit using 1-indexed character offsets (matching `CharOffset`). /// /// Used to carry incremental edits in `ServerLocalBufferUpdated` events /// and `handle_buffer_updated_push` without coupling `GlobalBufferModel` /// to proto types. Offsets use the same 1-indexed coordinate system as /// the buffer's `CharOffset`, so no conversion is needed at the boundary. pub struct CharOffsetEdit { pub start: CharOffset, pub end: CharOffset, pub text: String, } /// Global singleton model for managing shared buffers across editors. /// /// This allows multiple editors to share the same buffer when editing the same file, /// enabling consistent content synchronization and more efficient memory usage. pub struct GlobalBufferModel { location_to_id: BiMap<LocalOrRemotePath, FileId>, buffers: HashMap<FileId, InternalBufferState>, } impl GlobalBufferModel { pub fn new(_ctx: &mut ModelContext<Self>) -> Self { #[cfg(feature = "local_fs")] _ctx.subscribe_to_model(&FileModel::handle(_ctx), Self::handle_file_model_events); #[cfg(feature = "local_fs")] _ctx.subscribe_to_model( &LspManagerModel::handle(_ctx), Self::handle_lsp_manager_events, ); // Subscribe to remote buffer updates from the RemoteServerManager. #[cfg(feature = "local_tty")] if FeatureFlag::SshRemoteServer.is_enabled() { use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent}; let mgr = RemoteServerManager::handle(_ctx); _ctx.subscribe_to_model(&mgr, |me, _, event, ctx| match event { RemoteServerManagerEvent::BufferUpdated { host_id, path, new_server_version, expected_client_version, edits, } => { let char_edits: Vec<_> = edits .iter() .map(|e| CharOffsetEdit { start: CharOffset::from(e.start_offset as usize), end: CharOffset::from(e.end_offset as usize), text: e.text.clone(), }) .collect(); me.handle_buffer_updated_push( host_id, path, *new_server_version, *expected_client_version, &char_edits, ctx, ); } RemoteServerManagerEvent::BufferConflictDetected { host_id, path } => { me.handle_buffer_conflict_detected(host_id, path, ctx); } _ => {} }); } Self { location_to_id: BiMap::new(), buffers: HashMap::new(), } } /// Scan through all buffers and deallocate any that are no longer in use. pub fn remove_deallocated_buffers(&mut self, ctx: &mut ModelContext<Self>) { let ids_to_remove: HashSet<FileId> = self .buffers .iter() .filter_map(|(id, state)| { if state.buffer.upgrade(ctx).is_none() { Some(*id) } else { None } }) .collect(); if ids_to_remove.is_empty() { return; } // Collect paths for didClose before removing entries. let paths_to_close: Vec<PathBuf> = ids_to_remove .iter() .filter_map(|id| match self.location_to_id.get_by_right(id) { Some(LocalOrRemotePath::Local(path)) => Some(path.clone()), Some(LocalOrRemotePath::Remote(_)) | None => None, }) .collect(); for path in &paths_to_close { self.close_document_with_lsp(path, ctx); } for id in &ids_to_remove { self.location_to_id.remove_by_right(id); } for id in ids_to_remove { self.buffers.remove(&id); #[cfg(feature = "local_fs")] { let file_model = FileModel::handle(ctx); file_model.update(ctx, |file_model, ctx| { file_model.cancel(id); file_model.unsubscribe(id, ctx); }); } } } pub fn buffer_loaded(&self, file_id: FileId) -> bool { self.buffers .get(&file_id) .map(|state| state.is_loaded()) .unwrap_or(false) } fn cleanup_file_id(&mut self, file_id: FileId, _ctx: &mut ModelContext<Self>) { // Send didClose before removing the entry. if let Some((LocalOrRemotePath::Local(path), _)) = self.location_to_id.remove_by_right(&file_id) { self.close_document_with_lsp(&path, _ctx); } self.buffers.remove(&file_id); #[cfg(feature = "local_fs")] { let file_model = FileModel::handle(_ctx); file_model.update(_ctx, |file_model, ctx| { file_model.cancel(file_id); file_model.unsubscribe(file_id, ctx); }); } } /// Returns the buffer handle if it is 1) still exists + active 2) loaded. fn buffer_handle_for_id( &mut self, file_id: FileId, ctx: &mut ModelContext<Self>, ) -> Option<ModelHandle<Buffer>> { let state = self.buffers.get(&file_id)?; // If the buffer hasn't been loaded yet, don't return a model handle. if !state.is_loaded() { log::info!("Cannot return handle for unloaded buffers"); return None; } match state.buffer.upgrade(ctx) { Some(handle) => Some(handle), None => { // Clean up deallocated buffers. self.cleanup_file_id(file_id, ctx); None } } } /// Once we finish reading the file's content from the disk, populate the buffer with the content. /// For initial load (is_loaded_from_file_system == true), this is synchronous. /// For auto-reload (is_loaded_from_file_system == false), this spawns a background task for diff computation. /// Exposed as `pub(crate)` so tests can populate buffer content /// without going through the async `FileModel` load path. pub(crate) fn populate_buffer_with_read_content( &mut self, file_id: FileId, content: &str, base_version: ContentVersion, new_version: ContentVersion, is_initial_load: bool, ctx: &mut ModelContext<Self>, ) { let Some(state) = self.buffers.get_mut(&file_id) else { return; }; let Some(buffer) = state.buffer.upgrade(ctx) else { self.cleanup_file_id(file_id, ctx); log::warn!("Cannot populate buffer with content due to deallocated model handle"); return;