Architecture diagrams

ASCII diagrams for the major lattice subsystems. Renders cleanly in the in-editor :help view (which can't render Mermaid / images) and in any markdown viewer. Optimised for "look at it once, get the shape, then read the spec." For depth, follow the links to design.md / mode-architecture.md / etc.


1. Three-layer architecture

The paramount goal is non-blocking: input (UI) and computation (parsing, completion, LSP, plugin work) live in different layers and communicate by typed messages.

        ┌─────────────────────────────────────────────────┐
        │                  UI LAYER                       │
        │            (single thread, ~120Hz)              │
        │                                                 │
        │   keystroke → input.rs → CommandInvocation      │
        │                              │                  │
        │   render ←── App ←─ effects ─┘                  │
        │     │                                           │
        │     └── ratatui paints frame                    │
        └────────────────────────┬────────────────────────┘
                                 │ typed events
                                 │ (publish_typed / subscribe_typed)
                                 ▼
        ┌─────────────────────────────────────────────────┐
        │                 CORE LAYER                      │
        │           (multi-thread, tokio)                 │
        │                                                 │
        │   • document actor (rope ops, snapshot)         │
        │   • event bus (typed pub/sub)                   │
        │   • config registry (typed options)             │
        │   • mode registry, keymap registry              │
        │   • LSP supervisor / actor / client             │
        │   • completion sources, snippet host            │
        └────────────────────────┬────────────────────────┘
                                 │ WIT calls (Component Model)
                                 ▼
        ┌─────────────────────────────────────────────────┐
        │              PLUGIN LAYER (Phase 7+)            │
        │      (one wasmtime::Store per plugin task)      │
        │                                                 │
        │   capability-gated, fuel-limited, isolated.     │
        │   user's init.rs compiled to WASM lives here.   │
        └─────────────────────────────────────────────────┘

Read: design.md §3 (architectural overview), §5.5 (plugin subsystem), §5.7 (async runtime).


2. Threading model

   ┌─────────────────┐         ┌──────────────────────────┐
   │    UI thread    │         │   LSP runtime (tokio)    │
   │                 │         │                          │
   │  read keys      │         │  ┌──────────────────┐    │
   │       │         │  spawn  │  │  one task per    │    │
   │       ▼         ├────────►│  │  in-flight       │    │
   │  dispatch       │         │  │  request         │    │
   │       │         │         │  └──────────────────┘    │
   │       ▼         │         │                          │
   │  apply edits    │         │  ┌──────────────────┐    │
   │       │         │         │  │ LspMode::        │    │
   │       ▼         │         │  │  on_activate     │    │
   │  render frame   │◄────────┤  │  → attach_handle │    │
   │                 │  drain  │  └──────────────────┘    │
   │                 │  result │                          │
   └─────────────────┘         └──────────────────────────┘
        │                              ▲
        │                              │
        │   typed events on the bus    │
        ▼                              │
   ┌─────────────────────────────────────────────────────┐
   │                Event Bus (Arc<EventBus>)            │
   │   publish_typed::<T>(event) ─→ subscribe_typed::<T> │
   └─────────────────────────────────────────────────────┘
                          ▲
                          │
   ┌──────────────────────┴───────────────────────────────┐
   │             Plugin tasks (one per plugin)            │
   │      (each owns its own wasmtime::Store)             │
   └──────────────────────────────────────────────────────┘

UI thread does no I/O, no parsing, no shaping. Anything expensive runs on the LSP runtime (spawn_on_lsp_runtime) or the dedicated tokio thread pool. Results return via typed channels; the UI thread drains on the next frame.

Read: design.md §5.7 (async runtime), lsp-architecture.md §2 (supervisor / actor split).


3. Buffer + pane model

Everything is a buffer (DESIGN.md §5.9). Documents, file trees, oil listings, help docs, LSP logs all share one registry.

                ┌──────────────────────────────┐
                │       BufferRegistry         │
                │                              │
                │   BufferId ─→ BufferEntry    │
                │                              │
                │   • #1  doc:src/main.rs      │
                │   • #2  filetree:/src        │
                │   • #3  help:options         │
                │   • #4  lsp-log:rust         │
                │   • #5  oil:/Users/x/proj    │
                └──────────────────────────────┘
                          ▲
                          │ buffer_id
                          │
        ┌─────────────────┼─────────────────┐
        │                 │                 │
        ▼                 ▼                 ▼
  ┌──────────┐      ┌──────────┐      ┌──────────┐
  │  Pane A  │      │  Pane B  │      │  Pane C  │
  │  active  │      │          │      │          │
  │          │      │          │      │          │
  │ buf #1   │      │ buf #2   │      │ buf #3   │
  │ cursor   │      │ cursor   │      │ cursor   │
  │ scroll   │      │ scroll   │      │ scroll   │
  └──────────┘      └──────────┘      └──────────┘

Each pane carries its own (cursor, scroll). Same buffer can
appear in multiple panes; their cursors are independent.

       active_modes[buffer_id] = ActiveModes { major, minors }
       buffer_locals[buffer_id] = BufferLocals (typed map)

       resolved_options[buffer_id] = ResolvedOptions
            ├── modal-state layer
            ├── buffer-local layer
            ├── minor mode contributions (in activation order)
            ├── major mode contribution
            ├── typed-option (`:set`)
            └── default

:bn / :bp / :ls / :bd / :b N operate over the registry uniformly; panes are the rendering surface, buffers are the content.

Read: design.md §5.1 (buffer / document model), §5.9 (UI components), mode-architecture.md §6.1 (option resolution layer stack).


4. Mode resolution layer stack

For every option read on every buffer, the resolver walks the layers highest-priority-first. The first non-empty layer wins.

   read: app.resolved_option::<Number>(buffer_id)
                    │
                    ▼
   ┌────────────────────────────────────────────────┐
   │  Layer 1: modal-state (M.7+ wires this)        │  ←  highest priority
   │           e.g. read-only when in Visual?       │
   ├────────────────────────────────────────────────┤
   │  Layer 2: buffer-local (`:setlocal`)           │
   │           per-buffer explicit overrides        │
   ├────────────────────────────────────────────────┤
   │  Layer 3: active minors (in activation order)  │
   │           last-activated wins on tie           │
   │           e.g. `lsp-mode` contributes nothing  │
   │                `line-numbers-mode` ⇒ Number=t  │
   ├────────────────────────────────────────────────┤
   │  Layer 4: active major                         │
   │           e.g. help-mode ⇒ ReadOnly=true       │
   ├────────────────────────────────────────────────┤
   │  Layer 5: typed-option (`:set`)                │
   │           the global default the user set      │
   ├────────────────────────────────────────────────┤
   │  Layer 6: built-in default                     │
   │           Number = true (etc.)                 │  ←  lowest priority
   └────────────────────────────────────────────────┘
                    │
                    ▼
              resolved value cached per buffer
              (rebuilt on mode toggle / `:set` write)

:describe-option-resolution <name> walks this stack at runtime and shows which layer "won" — useful when a mode contribution shadows a :set write.

Read: mode-architecture.md §6.1 + §6.3 (resolution mechanism + caching).


5. Event bus + typed events

                  ┌────────────────────────────┐
                  │      Arc<EventBus>         │
                  │                            │
                  │  by_kind:    HashMap...    │
                  │  typed_subs: HashMap...    │
                  │  wildcard:   Vec<...>      │
                  └────────────────────────────┘
                       ▲                 │
                       │                 │
         publish_typed │                 │ subscribe_typed
                       │                 ▼
   ┌────────────────────┐         ┌──────────────────────┐
   │ Publisher:         │         │ Subscriber task:     │
   │                    │         │                      │
   │  bus.publish_typed │         │  loop {              │
   │   ::<LspBuffer-    │         │    let evt = rx      │
   │     Attached>(e)   │         │      .recv().await;  │
   │                    │         │    handle(evt);      │
   └────────────────────┘         │  }                   │
                                  └──────────────────────┘

Typed-event surface:

  • Event trait (event_registry.rs)
  • register_event! macro registers a descriptor
  • EVENT_DESCRIPTORS linkme slice (for :describe-events)
  • each event is a struct: LspBufferAttached, LspDocument-
    Changed, OptionChanged, MajorEntered, MinorActivated, ...

Source crates own their events:

  lattice-protocol  → core (DocumentOpened, etc.)
  lattice-lsp        → LspBuffer{Attached,Detached}, LspLog-
                       Pushed, LspDocumentChanged
  lattice-mode       → MajorEntered, MinorActivated, ...

Read: design.md §5.10 (event system + hooks).


6. LSP request lifecycle (hover example)

   user presses K (Normal mode)
        │
        ▼
   keymap_normal: K → Action::LspHoverRequest
        │
        ▼
   App::do_lsp_hover_request:
        ├── State A/B check: focused popup? → focus or no-op
        ├── cancel any in-flight hover token
        ├── M.6.2 sub-mode gate: check_lsp_sub_mode_gate(
        │      lsp-hover-mode, "lsp-hover-mode")
        │     │
        │     │ if umbrella off: echo "lsp-mode disabled"; bail
        │     │ if sub-mode off: echo "lsp-hover-mode disabled"; bail
        │     ▼
        ├── resolve buffer URI
        ├── spawn task on LSP runtime:
        │     ├── lsp.servers_for(uri) → Vec<ServerHandle>
        │     ├── for each handle (cancel-aware):
        │     │     if handle.capabilities().supports_hover():
        │     │       send hover request, await response
        │     │       send result on tx (mpsc channel)
        │     └── ...
        │
        ▼
   keystroke handler returns instantly. UI thread is free.
        │
        ▼ next frame's drain pump
   App::drain_pending_hover:
        ├── try_recv from rx
        ├── compose markdown body
        ├── App::do_open_hover (display_buffer with Hover category)
        │     → resolves to FloatingPopup(CursorAnchored) by
        │        default (or `hover.display` typed option)
        ▼
   render frame paints the popup

The cancel-stale-work step precedes the gate so gate state flips also flip the relay. Token is checked at every async boundary; a stale popup never lands over the new cursor.

Read: lsp-architecture.md §7 (request lifecycle), mode-architecture.md §M.6 (sub-mode gating).


7. Insert completion pipeline

   user types in Insert mode
        │
        ▼
   InsertCompletion sources fire (gated on completion-mode):

   ┌──────────────────────┐  ┌──────────────────────┐
   │  gen:lsp-completion  │  │   gen:snippet         │
   │   (from lsp-actor)   │  │  (from snippet host)  │
   │   priority 200       │  │   priority 150        │
   └──────────┬───────────┘  └──────────┬────────────┘
              │                         │
              └────────────┬────────────┘
                           │
   ┌──────────────────────┐│   ┌──────────────────────┐
   │  gen:buffer-words    ││   │  gen:tree-sitter-     │
   │   (rope scan)        ││   │   symbol              │
   │   priority 100       ││   │   priority 80         │
   └──────────┬───────────┘│   └──────────┬────────────┘
              │            │              │
              └────────────┼──────────────┘
                           ▼
                ┌────────────────────┐
                │  per-language      │
                │  source filter     │
                │  (TOML / :set)     │
                └─────────┬──────────┘
                          ▼
                ┌────────────────────┐
                │   matcher + ranker │
                │  (priority + score)│
                └─────────┬──────────┘
                          ▼
                ┌────────────────────┐
                │   completion popup │
                │   (cursor-anchored)│
                └────────────────────┘

Selection in popup → accept (Tab / Enter) → insert candidate
                  → close popup → fire `OnTypeFormatting` for
                    a configured trigger char (M.6.2 gates on
                    `lsp-format-mode`).

Read: insert-completion.md (the canonical spec; sources, ranking, popup, ghost text, all here).


See also