Przejdź do treści

Inteligencja językowa (diagnostyka, hover, signature help, symbole)

Mała, niezależna od backendu rodzina kontraktów dla inteligencji językowej — danych, które provider dostarcza do powierzchni code intelligence edytora. Realizuje projektową zasadę „wtyczki dostarczają dane, rdzeń renderuje": podkreślenia, popup hovera i skoki karetki żyją w widoku edytora i nigdy nie są udostępniane wtyczkom; wtyczka dostarcza wyłącznie dane POD/std::string, więc ten sam kontrakt może później realizować backend Pythona.

Pierwszym konsumentem jest wtyczka LSP, ale kanały są generyczne — linter czy sprawdzanie pisowni mogą publikować diagnostykę w ten sam sposób.

Czego tu nie ma

Go-to-definition, find-references, zmiana nazwy i formatowanie nie potrzebują żadnego własnego API — tylko nawigują lub edytują tekst, co IEditorService już wyraża (caret, openFile, setCaret, setText), więc pozostają poleceniami wewnątrz wtyczki. Uzupełnianie wykorzystuje istniejący ICompletionRegistry. Nowy kontrakt dodawany jest tylko wtedy, gdy istnieje rdzenna powierzchnia renderowania/wyboru i potencjalnie kilku providerów — a tak jest w przypadku diagnostyki, hovera, signature help i symboli.

Diagnostyka — push

Provider pcha (push) diagnostykę, kiedy tylko ją ma (sam decyduje, kiedy wyniki są gotowe); edytor renderuje ją jako faliste podkreślenia. Dostęp do ujścia przez IPluginContext::diagnostics(). Typy wartości to zwykłe 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 to klucz podmiany per (doc, source). Publikacja dla danego źródła zastępuje jego poprzednią diagnostykę na dokumencie, więc wielu producentów (LSP, sprawdzanie pisowni, log builda) współistnieje bez nadpisywania się nawzajem.
  • Opublikuj pusty wektor dla źródła, aby wycofać jego diagnostykę.
  • Poziomy ważności renderowane są odrębnymi kolorami (error/warning/info/hint); edytor poszerza też bardzo krótkie zakresy, aby znacznik jednoznakowy pozostał widoczny.
ctx.diagnostics().publishDiagnostics(doc, "org.mycompany.linter", {
    { byteRange, DiagnosticSeverity::Warning, "unused variable", "W001" },
});

Host zawsze dostarcza prawidłowe ujście (no-op, gdy edytor nie ma powierzchni diagnostyki), więc nigdy nie musisz sprawdzać diagnostics() pod kątem null.

Hover — asynchroniczny pull

Hover jest sterowany przez edytor (zatrzymanie myszy nad symbolem). Wtyczka rejestruje providera, który odpowiada asynchronicznie; dostęp do rejestru przez 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,
                                        std::int32_t    priority) = 0;
};

namespace hover_priority {
    inline constexpr std::int32_t kFallback      = 0;
    inline constexpr std::int32_t kLanguage      = 100;
    inline constexpr std::int32_t kActiveSession = 1000;
}
  • registerProvider zwraca HoverToken (RAII); zniszcz go w shutdown(), aby się wyrejestrować.
  • Każdy provider rejestruje się z priorytetem. Wybierz pasmo, które mówi, co odpowiadasz, a nie jak ważny się wydajesz: kFallback dla informacji na poziomie słowa lub tekstu (definicja ze słownika, cel adresu URL), kLanguage dla informacji językowej o symbolu (hover serwera języka, reguła lintera), kActiveSession dla informacji, która musi wygrywać, dopóki jest aktywna (wartość pod kursorem w debuggerze). Wartości między pasmami (kLanguage + 10) są dozwolone, gdy musisz ustawić się względem znanego sąsiada.
  • Przy zatrzymaniu kursora host pyta wszystkich providerów naraz i pokazuje niepustą odpowiedź providera o najwyższym priorytecie; przy równych priorytetach wygrywa provider zarejestrowany wcześniej. Odpowiedź o niższym priorytecie, która przyjdzie pierwsza, czeka, aż każdy wyższy provider odpowie pusto. Gdy wszyscy odpowiedzą pusto, nic nie jest pokazywane.
  • Wolny provider nie może wstrzymywać podpowiedzi w nieskończoność: 300 ms po zapytaniu pokazywana jest najlepsza odpowiedź otrzymana do tej pory, a późniejsze odpowiedzi na to zapytanie są odrzucane. Provider kActiveSession musi więc odpowiedzieć pusto od razu, gdy nie ma nic do powiedzenia (brak sesji) — inaczej opóźnia każdy hover w edytorze nawet o cały ten czas.
  • Odpowiedzi przychodzące po tym, jak użytkownik przeszedł dalej, są odrzucane.
  • Odpowiedź to zwykły tekst z formatowaniem, renderowany w popupie jako Markdown, więc nagłówki, pogrubienia i bloki kodu działają — kontrakt pozostaje string na wejściu / string na wyjściu.
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, hover_priority::kLanguage);

Signature help — asynchroniczny pull

Jak hover, ale wyzwalany wpisaniem ( wywołania lub ,. Provider odpowiada aktywną sygnaturą; host renderuje ją jako call tip z podświetlonym aktywnym parametrem. Dostęp do rejestru przez 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; };

Host pokazuje pierwszą niepustą odpowiedź; [activeStart, activeEnd) to bajtowy zakres aktywnego parametru wewnątrz signature, używany do podświetlenia.

Symbole — asynchroniczny pull (go-to-symbol)

Provider wylicza symbole dokumentu (i przeszukuje przestrzeń roboczą); host renderuje picker (tryby Ctrl+Shift+O / Ctrl+T palety poleceń), a każda wtyczka może pobrać (pull) zagregowane symbole — robi to dok Function List, co czyni ten kontrakt autentycznie wielokonsumenckim. Dostęp przez 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;
};
  • Lokalizacje wyrażane są tak, jak nawiguje host: path w systemie plików (puste oznacza dokument z żądania) plus liczony od 0 line i bajtowy column w jego obrębie — w miejscu wywołania nie trzeba już nic konwertować.
  • Host dostarcza pierwszą niepustą listę symboli, albo pustą listę, gdy wszyscy providerzy odpowiedzieli pusto (więc czekający picker zawsze dostaje jakąś odpowiedź).
  • Strona konsumenta: requestDocumentSymbols / requestWorkspaceSymbols na rejestrze to pull-owy odpowiednik registerProvider — wtyczka wywołuje je, aby odczytać to, co dostarczają inne wtyczki (np. serwer języka). Odpowiedź przychodzi raz, w wątku UI, być może później (asynchronicznie). Dok Function List używa tego w trybie zapytaj-i-wróć-do-bazy: natychmiast pokazuje własny zarys oparty na regexach, a jeśli przyjdzie niepusta odpowiedź, przechodzi na nią; pusta odpowiedź nigdy nie czyści linii bazowej. Zabezpiecz spóźnioną odpowiedź przed sprzątaniem obiektów i przed nowszym żądaniem (id dokumentu + numer generacji).

Języki — wyliczanie, przełączanie, wnoszenie

Kolorowanie składni to funkcja core: host ma własny katalog colorizerów (dostarczone pliki colorizers/*.json plus pliki użytkownika), rozpoznaje język dokumentu po ścieżce i pierwszej linii i koloruje widok bez żadnej wtyczki. ctx.languages() (ILanguageRegistry) pozwala wtyczce się dołączyć:

// Enumeration -- e.g. to fill a per-language settings dropdown.
std::vector<LanguageInfo> langs = ctx.languages().availableLanguages(); // {id, name}, by name

// Per-document state. languageFor() reports the same id as
// IEditorService::colorizerId(); setLanguageFor() takes exactly the path
// the Language menu takes ("" clears colouring; false = unknown id / doc).
std::string id = ctx.languages().languageFor(doc);
bool ok        = ctx.languages().setLanguageFor(doc, "json");
bool redone    = ctx.languages().autoDetectFor(doc);   // undo a manual override

// Contribution: a whole language definition, in the same shape as a
// shipped colorizer file -- backend lexer NAME, style NAMES -> category,
// keyword groups by the lexer's own slot description. Never engine ids.
LanguageDefinition def;
def.id = "mylang";  def.name = "My Language";  def.lexer = "cpp";
def.extensions = {"mylang"};
def.keywords   = {{"Primary keywords and identifiers", "if else while"}};
def.styles     = {{"SCE_C_COMMENT", SyntaxCategory::Comment},
                  {"SCE_C_STRING",  SyntaxCategory::String}};
m_lang = ctx.languages().registerLanguage(def);          // LanguageToken (RAII)

Wniesiony język zachowuje się dokładnie jak dostarczony: host rozwiązuje nazwy względem metadanych leksera backendu, a aktywny motyw dostarcza kolory. Rejestracja id, które już istnieje, zastępuje je na czas życia tokenu; gdy token umiera, wyparta definicja wraca, a otwarte dokumenty używające tego id są kolorowane ponownie — wtyczka może więc nadpisać dostarczony język bez trwałego uszkodzenia katalogu. Wkłady przeżywają też przeładowanie katalogu wywoływane przez Preferencje ▸ Zastosuj albo zmianę motywu. Host nie zyskuje nowych lekserów backendu w czasie działania — lexer musi wskazywać istniejący. Zawsze poprawny: host bez warstwy kolorowania dostarcza no-op, który nic nie wylicza i odrzuca każdą zmianę.

Status

Wszystkie cztery kontrakty (diagnostyka, hover, signature help, symbole) zostały dodane przed wydaniem bez podbijania MTE_PLUGIN_API_VERSION; powierzchnia języków (ILanguageRegistry) doszła tą samą drogą. Ich pierwszym konsumentem jest wtyczka LSP; kontrakt symboli jest celowo wielokonsumencki (dziś paleta poleceń, następny w kolejce dok Function List).