Skip to main content

DocumentTracker

Struct DocumentTracker 

Source
pub struct DocumentTracker { /* private fields */ }
Expand description

Tracks document state across the workspace.

Every method takes &self: the document map and the per-path locks used by Self::ensure_open are both interior-mutable, so a single tracker can be shared behind a plain Arc<DocumentTracker> with no outer lock. See Self::ensure_open for the concurrency contract this maintains.

Implementations§

Source§

impl DocumentTracker

Source

pub fn new( limits: ResourceLimits, extension_map: HashMap<String, String>, ) -> Self

Create a new document tracker with custom limits and extension mappings.

Source

pub fn is_open(&self, path: &Path) -> bool

Check if a document is currently open.

Source

pub fn get(&self, path: &Path) -> Option<DocumentState>

Get a clone of the state of an open document.

Source

pub fn line_text(&self, path: &Path, line: u32) -> Option<String>

Text of the 0-based line’th line of path’s currently tracked content, or None if the document is not open or has no such line.

Reads the in-memory content mcpls already sent the server via didOpen/didChange – cheaper than a disk read (no I/O, no re-scanning the whole file) and more correct when disk and server state have diverged (e.g. an edit not yet flushed to disk).

Source

pub fn len(&self) -> usize

Get the number of open documents.

Source

pub fn is_empty(&self) -> bool

Check if there are no open documents.

Source

pub fn open(&self, path: PathBuf, content: String) -> Result<Uri>

Open a document and track its state.

Returns the document URI for use in LSP requests.

§Errors

Returns an error if:

  • Document limit is exceeded
  • File size limit is exceeded
Source

pub fn update(&self, path: &Path, content: String) -> Option<i32>

Update a document’s content and increment its version.

Returns None if the document is not open. The updated content has no known disk provenance, so the next ensure_open call on this path will always re-verify by content compare rather than trusting a stat.

Source

pub fn close(&self, path: &Path) -> Option<DocumentState>

Close a document and remove it from tracking.

Returns the document state if it was open.

Source

pub fn close_all(&self) -> Vec<DocumentState>

Close all documents.

Source

pub fn open_paths(&self) -> Vec<PathBuf>

Snapshot of the filesystem paths of all currently open documents.

Source

pub fn forget_server(&self, server: &ServerId)

Forget server’s last-synced version for every currently open document, so the next ensure_open call sends didOpen again instead of didChange.

Called after server is respawned: the fresh process has no memory of any document the old one had open, so this tracker’s per-server sync history for it must be forgotten too, or ensure_open would wrongly send didChange for a document the new process never saw.

Also bumps server’s sync generation. Clearing synced alone is not enough: a call already in flight against the old (dead) connection when this runs can still have its didOpen/didChange notify “succeed” (LspClient::notify only enqueues onto a channel – a dead process is not observed by the send itself), and would otherwise re-insert a stale entry after this method has already cleared it. ensure_open captures the generation before starting and discards its synced write if the generation moved in the meantime, closing that race regardless of exactly when the notify “succeeds”.

Source

pub async fn ensure_open( &self, path: &Path, server: &ServerId, lsp_client: &LspClient, ) -> Result<Uri>

Ensure a document is open for server, opening it lazily if necessary, and resynchronize it with disk and with server if either has fallen behind.

A single path can be synced to several servers independently (e.g. hover routed to one server, diagnostics to another, for the same language) – this call syncs only the one server it is for. Internally it runs in two phases:

Disk phase: stats the file on every call (a cheap syscall, never debounced) to detect external changes – git checkout/stash, formatters, or edits made by the MCP host itself outside mcpls – and re-reads its content when the stat indicates a possible change (see DiskSync for the settled/debounce rules). This phase never skips the per-server sync check below, even when it takes a fast path that skips the disk read: a second server that has never seen this document must still receive didOpen even if the file has not changed since a first server was opened on it.

Sync phase: compares server’s last-synced version (tracked via DocumentState::synced_version) against the version decided by the disk phase, and sends exactly one of didOpen (server has never seen this document), didChange (server is behind), or nothing (server is already caught up). A didChange is always a single full-replacement notification (a TextDocumentContentChangeEvent with range: None, which per the LSP spec means “this is the entire new document content”); mcpls does not consult the server’s negotiated TextDocumentSyncKind (LspClient has no access to ServerCapabilities at this layer) – full-replacement is accepted in practice by rust-analyzer, pyright, tsserver, gopls and clangd, but is the first place to look if a future maintainer sees sync errors from a new server. The document is never closed and reopened on a change, so get_cached_diagnostics keeps serving the last-known diagnostics until the server re-publishes – there is no transient empty window.

st.version/st.content/st.disk/synced[server] are all committed only after the notification succeeds. A server that is never asked again never catches up to a later edit – which is correct, since a server that is never asked never needs the content.

Two cases fall outside the disk-change-detection mechanism entirely:

  • A tool that restores a file with an mtime and size identical to the last ones observed (e.g. tar x, rsync -a, cp -p) is indistinguishable from “unchanged”, however long ago that snapshot was taken – not just within the racy detection window. Once a snapshot is mtime_settled, restoring its exact (mtime, size) retakes the fast path forever. Closing this would require hashing content on every access.
  • workspace_symbol_search is served from the LSP server’s own index and is unaffected by this per-document mechanism for files mcpls has never opened.
§Concurrency

Calls for the same path are serialized against each other (via lock_path), so no two such calls can observe or mutate that path’s state concurrently – this is what prevents duplicate didOpen/didChange notifications for the same document. Calls for different paths run fully concurrently: neither the per-path lock nor the short, synchronous locks used to touch the shared document map are ever held across this call’s disk I/O or LSP notify.

§Errors

Returns an error if:

  • The file cannot be stat’d or read from disk
  • The didOpen/didChange notification fails to send
  • Resource limits are exceeded

Trait Implementations§

Source§

impl Debug for DocumentTracker

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more