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
impl DocumentTracker
Sourcepub fn new(
limits: ResourceLimits,
extension_map: HashMap<String, String>,
) -> Self
pub fn new( limits: ResourceLimits, extension_map: HashMap<String, String>, ) -> Self
Create a new document tracker with custom limits and extension mappings.
Sourcepub fn get(&self, path: &Path) -> Option<DocumentState>
pub fn get(&self, path: &Path) -> Option<DocumentState>
Get a clone of the state of an open document.
Sourcepub fn line_text(&self, path: &Path, line: u32) -> Option<String>
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).
Sourcepub fn open(&self, path: PathBuf, content: String) -> Result<Uri>
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
Sourcepub fn update(&self, path: &Path, content: String) -> Option<i32>
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.
Sourcepub fn close(&self, path: &Path) -> Option<DocumentState>
pub fn close(&self, path: &Path) -> Option<DocumentState>
Close a document and remove it from tracking.
Returns the document state if it was open.
Sourcepub fn close_all(&self) -> Vec<DocumentState>
pub fn close_all(&self) -> Vec<DocumentState>
Close all documents.
Sourcepub fn open_paths(&self) -> Vec<PathBuf>
pub fn open_paths(&self) -> Vec<PathBuf>
Snapshot of the filesystem paths of all currently open documents.
Sourcepub fn forget_server(&self, server: &ServerId)
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”.
Sourcepub async fn ensure_open(
&self,
path: &Path,
server: &ServerId,
lsp_client: &LspClient,
) -> Result<Uri>
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 ismtime_settled, restoring its exact(mtime, size)retakes the fast path forever. Closing this would require hashing content on every access. workspace_symbol_searchis 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/didChangenotification fails to send - Resource limits are exceeded