Expand description
Document synchronisation: didOpen / didChange (incremental
or full) / didClose. One DocSync is owned per actor;
it shadows every buffer the server cares about with a string
mirror so we can translate Position { line, byte } to the
negotiated LSP encoding without re-querying the editor’s rope.
§Pure state, separated I/O
DocSync holds no ServerHandle and performs no I/O. Every
mutating method either updates the mirror in place (no return
value) or returns the LSP params the caller should ship over
the wire. The owning actor sends the params via its writer
task. This keeps the type:
- Standalone-testable. Tests construct a
DocSync, callrecord_edit/take_flush_payloadetc., and assert on the returned params. No mock server needed. - Lock-free at the call site. The actor mutates its own
DocSyncwithout contending with the supervisor or the UI thread. - Encoding-explicit.
record_editand the flush helpers take&Capabilitiesso the converter knows whether to walk utf-16 columns. Pre-this-refactor it pulled this off aServerHandleborrow; explicit parameter is cleaner.
§Lifecycle
editor DocSync actor (sends)
------ ------- -------------
open file -----> open(uri, lang, text) --> didOpen
apply_edit Ok -----> record_edit(caps, uri, edit)
[mirror updated, change queued]
...50ms idle...
flush(uri) -----> take_flush_payload(caps, uri) -> didChange
bdelete -----> close(uri) --> didCloserecord_edit does not eagerly produce a flush payload. The
editor batches edits between flushes; one keystroke commits
one edit but generates one queued change event, sized down
to the affected range. The flush cadence is the actor’s
choice; the per-actor select! loop sets ~50ms idle as the
default (matching common LSP client conventions).
§Sync mode honour
Incremental: queued change events are sent verbatim.Full: queued events are dropped; the entire post-edit mirror text is sent as one change. Most modern servers (rust-analyzer, pyright, gopls, clangd, tsserver) advertise Incremental; Full is the LSP 3.0 fallback.None: didChange is a no-op. Some servers prefer pull-based diagnostics and don’t want continuous text sync.
§Mirror cost
One String per attached buffer per server. For a typical
editor session with a handful of open files this is a few MB
at worst. Not a ropey::Rope because the LSP layer doesn’t
benefit from O(log n) edits – we only ever splice one
contiguous region per edit, and indexing into a String by
line is O(n) but rare (only the affected lines of the
edit).
Structs§
- Close
Payloads - What
DocSync::closereturns: an optional finaldidChange(any pending edits the actor should flush before announcing close) plus thedidCloseitself. The actor sends them in order so the server’s last view of the doc matches the editor’s. - DocSync
- Per-actor document-sync state. Pure state + pure methods –
no I/O, no
ServerHandledependency. The owning actor sends the LSP params returned byopen/take_flush_payload/closeover the wire via its writer task.
Functions§
- uri_
from_ str - Helper for callers: parse a filesystem path into an LSP
Uri. Re-exported here so consumers don’t need to import the internalactor::uri_from_pathhelper.