Skip to main content

Module sync

Module sync 

Source
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, call record_edit / take_flush_payload etc., and assert on the returned params. No mock server needed.
  • Lock-free at the call site. The actor mutates its own DocSync without contending with the supervisor or the UI thread.
  • Encoding-explicit. record_edit and the flush helpers take &Capabilities so the converter knows whether to walk utf-16 columns. Pre-this-refactor it pulled this off a ServerHandle borrow; 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)             -->  didClose

record_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§

ClosePayloads
What DocSync::close returns: an optional final didChange (any pending edits the actor should flush before announcing close) plus the didClose itself. 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 ServerHandle dependency. The owning actor sends the LSP params returned by open / take_flush_payload / close over 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 internal actor::uri_from_path helper.