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;
};
sourceis 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;
};
registerProviderreturns aHoverToken(RAII); destroy it inshutdown()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-basedlineand a bytecolumnwithin 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/requestWorkspaceSymbolson the registry are the pull counterpart ofregisterProvider— 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).