2477 lines · 95.5 KB · Rust
RawDownload
#![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.