Skip to content

Language intelligence (diagnostics, hover, signature help, symbols)

A small, backend-neutral contract family for language intelligence — the data a provider feeds to the editor's code-intelligence surfaces. It follows the project's rule "plugins contribute data, core renders": the squiggles, the hover popup and caret jumps live in the editor view and are never exposed to plugins; a plugin only supplies POD/std::string data, so the same contract can later be fulfilled by a Python backend.

The first consumer is the LSP plugin, but the channels are generic — a linter or spell-checker can publish diagnostics the same way.

What is not here

Go-to-definition, find-references, rename and formatting need no API of their own — they only navigate or edit text, which IEditorService already expresses (caret, openFile, setCaret, setText), so they stay in-plugin commands. Completion reuses the existing ICompletionRegistry. A new contract is added only when there is a core rendering/picker surface and potentially several providers — which is the case for diagnostics, hover, signature help, and symbols.

Diagnostics — push

A provider pushes diagnostics whenever it has them (it decides when results are ready); the editor renders them as squiggly underlines. Reach the sink via IPluginContext::diagnostics(). The value types are plain POD (PluginApi/DiagnosticTypes.h):

enum class DiagnosticSeverity { Error, Warning, Information, Hint };

struct Diagnostic {
    MatchRange         range;   // UTF-8 byte range in the document
    DiagnosticSeverity severity = DiagnosticSeverity::Error;
    std::string        message;
    std::string        code;
};

struct IDiagnosticsSink {       // PluginApi/IDiagnosticsSink.h
    virtual void publishDiagnostics(DocumentId doc, std::string_view source,
                                    const std::vector<Diagnostic>& diags) = 0;
    virtual void clearDiagnostics(DocumentId doc) = 0;
};
  • source is a per-(doc, source) replace key. Publishing for a source replaces that source's previous diagnostics on the document, so multiple producers (LSP, spell-check, build-log) coexist without clobbering each other.
  • Publish an empty vector for a source to retract its diagnostics.
  • Severities render with distinct colours (error/warning/info/hint); the editor also widens very short ranges so a single-character marker stays visible.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

The host always provides a valid sink (a no-op when the editor has no diagnostics surface), so you never need to null-check diagnostics().

Hover — async pull

Hover is driven by the editor (mouse dwell over a symbol). A plugin registers a provider that answers asynchronously; reach the registry via IPluginContext::hover() (PluginApi/IHoverRegistry.h):

struct HoverRequest { DocumentId doc; std::int64_t position; }; // byte offset
using  HoverReply  = std::function<void(std::string markup)>;   // empty = nothing

struct IHoverProvider {
    virtual void requestHover(const HoverRequest&, HoverReply reply) = 0;
};

struct IHoverRegistry {
    virtual HoverToken registerProvider(IHoverProvider& provider) = 0;
};
  • registerProvider returns a HoverToken (RAII); destroy it in shutdown() to unregister.
  • On dwell the host queries every registered provider and shows the first non-empty reply; replies that arrive after the user moved on are dropped.
  • The reply is plain markup text and is rendered as Markdown in a popup, so headings, bold and code blocks work — keep the contract string-in / string-out.
class MyHover final : public IHoverProvider {
public:
    void requestHover(const HoverRequest& r, HoverReply reply) override {
        // ...look up info for r.doc at byte offset r.position, possibly async...
        reply("**foo** — a thing\n\n```cpp\nint foo();\n```");
    }
};
// in initialize():  m_token = ctx.hover().registerProvider(m_hover);

Signature help — async pull

Like hover, but driven by typing a call's ( or a ,. A provider answers the active signature; the host renders it as a call tip with the active parameter highlighted. Reach the registry via IPluginContext::signatureHelp() (PluginApi/ISignatureHelpRegistry.h):

struct SignatureHelpRequest { DocumentId doc; std::int64_t position; std::string languageId; };
struct SignatureHelp { std::string signature; std::int32_t activeStart, activeEnd; }; // byte range in `signature`
using  SignatureHelpReply = std::function<void(SignatureHelp)>;     // empty signature = nothing

struct ISignatureHelpProvider {
    virtual void requestSignatureHelp(const SignatureHelpRequest&, SignatureHelpReply) = 0;
};
struct ISignatureHelpRegistry { virtual SignatureHelpToken registerProvider(ISignatureHelpProvider&) = 0; };

The host shows the first non-empty reply; [activeStart, activeEnd) is the byte span of the active parameter within signature, used for the highlight.

Symbols — async pull (go-to-symbol)

A provider enumerates a document's symbols (and searches the workspace); the host renders the picker (the Command Palette's Ctrl+Shift+O / Ctrl+T modes) and any plugin can pull the aggregated symbols — the Function List dock does, making this a genuinely multi-consumer contract. Reach it via IPluginContext::symbols() (PluginApi/ISymbolRegistry.h):

struct SymbolLocation { std::string path; std::int32_t line, column; }; // path empty = request doc; column = byte offset in line
struct SymbolInfo { std::string name; std::int32_t kind; std::string containerName; SymbolLocation location; }; // kind = LSP SymbolKind (1..26)
using  SymbolReply = std::function<void(std::vector<SymbolInfo>)>;

struct ISymbolProvider {
    virtual void requestDocumentSymbols(const DocumentSymbolRequest&, SymbolReply) = 0;  // { DocumentId doc; std::string languageId; }
    virtual void requestWorkspaceSymbols(const WorkspaceSymbolRequest&, SymbolReply) = 0; // { std::string query; }
};
struct ISymbolRegistry {
    virtual SymbolToken registerProvider(ISymbolProvider&) = 0;                          // producer side
    // consumer side (pull) — used by the Command Palette and the Function List dock:
    virtual void requestDocumentSymbols(const DocumentSymbolRequest&, SymbolReply) const = 0;
    virtual void requestWorkspaceSymbols(const WorkspaceSymbolRequest&, SymbolReply) const = 0;
};
  • Locations are expressed as the host navigates: a filesystem path (empty means the request's document) plus a 0-based line and a byte column within it — no further conversion needed at the call site.
  • The host delivers the first non-empty symbol list, or an empty list once every provider has answered empty (so a waiting picker is always told something).
  • Consumer side: requestDocumentSymbols / requestWorkspaceSymbols on the registry are the pull counterpart of registerProvider — a plugin calls them to read what other plugins (e.g. a language server) contribute. The reply fires once, on the UI thread, possibly later (async). The Function List dock uses this ask-and-fall-back: it shows its own regex outline instantly and, if a non-empty reply arrives, upgrades to it; an empty reply never clears the baseline. Guard a late reply against teardown and against a newer request (doc id + generation).

Status

All four contracts (diagnostics, hover, signature help, symbols) were added pre-release without bumping MTE_PLUGIN_API_VERSION. Their first consumer is the LSP plugin; the symbol contract is intentionally multi-consumer (the Command Palette today, the Function List dock next).