Zum Inhalt
Kuonen.Studio

Erweiterungen schreiben

Manifest, Befehle, Ansichten, Sprachen, Aufgaben, Startprofile und Debugger.

Eine Erweiterung ist eine .NET-DLL mit einem Manifest erweiterung.json daneben. Sie läuft nicht in der IDE, sondern im Erweiterungs-Host, einem eigenen Prozess; stürzt sie ab, startet die IDE den Host neu. Die IDE liest nur das Manifest, bis ein Aktivierungsereignis eintritt: Menüs, Leisten, Kürzel und Werkzeugfenster stehen also, ohne dass die Erweiterung läuft.

Mitgelieferte Erweiterungen liegen neben dem Programm in erweiterungen/<id>/, eigene im Konfigurationsordner unter erweiterungen/<id>/.

Projekt

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>
  <ItemGroup>
    <!-- Das SDK kommt zur Laufzeit vom Host: nicht mitliefern. -->
    <ProjectReference Include="…/Kuonen.Studio.Erweiterung.csproj" Private="false" ExcludeAssets="runtime" />
    <None Include="erweiterung.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
</Project>

Einstieg

[assembly: StudioErweiterung(typeof(MeineErweiterung))]

public sealed class MeineErweiterung : IErweiterung
{
    public Task AktiviereAsync(ErweiterungsKontext kontext)
    {
        kontext.RegistriereBefehl("meine.hallo", async _ =>
        {
            kontext.Schreibe("Meine", "Hallo aus dem Host");
            var wahl = await kontext.ZeigeMeldungAsync(MeldungsArt.Info, "Weiter?", "Ja", "Nein");
        });
        kontext.RegistriereBaum("meine.baum", new MeinBaum());
        return Task.CompletedTask;
    }
}

Der Kontext bietet RegistriereBefehl, RegistriereBaum, BaumGeaendert, FuehreIdeBefehlAusAsync (etwa studio.dateiOeffnen mit einem Pfad), Schreibe (Ausgabefenster), ZeigeMeldungAsync, SetzeStatus, WaehleOrdnerAsync, FrageAsync, Arbeitsordner und ArbeitsordnerGeaendert. Console.WriteLine stört nichts: es landet im Kanal „Erweiterungs-Host“ der Ausgabe.

Manifest

{
  "id": "kuonen.dateien",
  "name": "Datei-Explorer",
  "version": "1.0.0",
  "einstieg": "Kuonen.Studio.Erweiterungen.Dateien.dll",
  "aktivierung": ["beimStart", "befehl:dateien.ordnerOeffnen", "ansicht:dateien.explorer"],
  "beitraege": {
    "befehle":    [{ "id": "dateien.ordnerOeffnen", "titel": "Ordner öffnen…", "kategorie": "Datei", "symbol": "ordner" }],
    "menues":     [{ "ort": "hauptmenue/datei", "befehl": "dateien.ordnerOeffnen", "gruppe": "1_oeffnen@2" }],
    "leisten":    [{ "ort": "standard", "befehl": "dateien.ordnerOeffnen" }],
    "kuerzel":    [{ "befehl": "dateien.ordnerOeffnen", "taste": "Strg+K Strg+O" }],
    "ansichten":  [{ "id": "dateien.explorer", "titel": "Projektmappen-Explorer", "ort": "links", "art": "baum" }],
    "einstellungen": [{ "schluessel": "dateien.versteckteZeigen", "typ": "bool", "vorgabe": false, "titel": "Versteckte Dateien zeigen" }]
  }
}
  • id: Buchstaben, Ziffern, _ und einzelne . oder - dazwischen; die Erweiterungs-Id wird klein gespeichert, Befehls- und Ansichts-Ids behalten ihre Schreibweise.
  • einstieg: Dateiname der DLL im Ordner, ohne Pfad.
  • aktivierung: beimStart, befehl:<id>, ansicht:<id> oder *. Ein Befehl aktiviert seine Erweiterung beim ersten Aufruf auch ohne Eintrag.
  • menues.ort: hauptmenue/<menü>[/<untermenü>…] mit den Menüs datei, bearbeiten, ansicht, git, projekt, erstellen, debuggen, test, analysieren, extras, erweiterungen, fenster, hilfe; ein unbekanntes Menü erscheint vor „Hilfe“. gruppe <gruppe>@<rang> ordnet; zwischen Gruppen steht ein Trenner, ohne Gruppe geht es ans Ende.
  • kuerzel.taste: bis zu zwei Akkorde, Umschalttasten Strg/Ctrl, Umschalt/Shift, Alt, Meta/Cmd. Ein Kürzel gehört der Erweiterung, die es zuerst bringt; spätere werden in der Ausgabe („Erweiterungen“) gemeldet.
  • ansichten.ort: links, rechts, unten, dokument; art baum (nicht im Dokument) oder kxaml.

Grenzen: Manifest höchstens 1 MiB, je Liste höchstens 1000 Einträge. Ein Fehler in einem Beitrag lässt nur diesen weg.

Oberfläche als KXAML

Eine Ansicht der Art kxaml liefert KXAML-Text und Daten; die IDE baut daraus Controls von Kuonen.UI (KXAML zur Laufzeit, siehe UI/docs/kxaml-laufzeit.md). Als Werkzeugfenster (ort: links, rechts, unten) lädt sie beim Start; als Dokument (ort: dokument) öffnet die Erweiterung sie selbst.

"ansichten": [{ "id": "mein.formular", "titel": "Formular", "ort": "dokument", "art": "kxaml" }]
kontext.RegistriereKxaml("mein.formular", () => new Formular());   // je offene Ansicht ein Anbieter
await kontext.OeffneDokumentAsync("mein.formular", "Kunde Anna", argument: "anna");

sealed class Formular : IKxamlAnbieter
{
    public string Kxaml => """
        <StackPanel xmlns="https://ui.kuonen.org/kxaml" xmlns:x="https://ui.kuonen.org/kxaml/x" Margin="8">
          <TextBox Text="{Binding name, Mode=TwoWay}"/>
          <Button x:Name="knopf" Content="Grüssen" Click="gruessen"/>
          <TextBlock Text="{Binding gruss}"/>
        </StackPanel>
        """;

    public void Fuelle(AnsichtsDaten daten, string? argument) => daten.Setze("name", argument ?? "");

    public void BeiEreignis(string handler, string? element, AnsichtsDaten daten)
    {
        if (handler == "gruessen") daten.Setze("gruss", "Hallo " + daten.Lies("name"));
    }
}
  • Daten: ein Baum aus Objekten (Schlüssel → Wert) und Listen mit Text, Wahrheitswerten und Zahlen. daten.Setze("kunde.name", "Anna") ändert sie hier und in der IDE; Eingaben des Benutzers über Mode=TwoWay kommen von selbst hinein (BeiEingabe meldet sie) und werden nicht zurückgeschickt.
  • Ereignisse: Click="gruessen" ruft BeiEreignis("gruessen", x:Name, daten).
  • KXAML: Elemente, Eigenschaften, angehängte Eigenschaften, Eigenschaftselemente, {Binding pfad[, Mode=…]}, {x:Null}, x:Name; keine Styles, Vorlagen, Ressourcen und kein x:Class. Fehler stehen in der Ausgabe „Erweiterungen“ mit Zeile und Spalte; was sich bauen lässt, wird gebaut.
  • Grenzen: KXAML 1 Mi Zeichen, Daten 4 MiB, Tiefe 32, 100 000 Werte, 256 offene Ansichten. Stürzt der Host ab, laden offene Ansichten nach dem Neustart neu (Fuelle läuft dann wieder).

Bedingungen

Menüs, Leisten und Kürzel tragen wenn, Befehle freigabe (wie when und enablement in VS Code):

"befehle": [{ "id": "mein.senden", "titel": "Senden", "freigabe": "mein.erweiterung.verbunden" }],
"menues":  [{ "ort": "hauptmenue/extras", "befehl": "mein.senden", "wenn": "editorOffen && dateiendung == '.cs'" }],
"kuerzel": [{ "befehl": "mein.senden", "taste": "Strg+Umschalt+S", "wenn": "dateiendung == '.cs'" }]
  • Ausdrücke: Schlüssel, 'Text', true, false, Zahlen, ==, !=, !, &&, ||, Klammern; höchstens 512 Zeichen und 16 Stufen. Ein unbekannter Schlüssel ist leer (falsch). Ein falscher Ausdruck lässt nur seinen Eintrag weg.
  • Schlüssel der IDE: editorOffen, dateiendung (klein, mit Punkt), ansichtFokus, arbeitsordnerOffen, knotenKontext (im Kontextmenü eines Baumknotens).
  • Eigene Schlüssel setzt die Erweiterung mit kontext.SetzeKontext("mein.erweiterung.verbunden", true); sie müssen mit der eigenen Kennung und einem Punkt beginnen.
  • Dieselbe Taste darf mehrere Befehle mit verschiedenen Bedingungen tragen; es gilt der zuletzt angemeldete, dessen Bedingung stimmt. Ein Befehl, dessen Freigabe nicht gilt, steht grau im Menü, fehlt in der Befehlspalette und läuft nicht.

Kontextmenüs

Orte kontext/ansicht/<ansicht> (Baumansicht) und kontext/editor. Im Kontextmenü eines Baumknotens ist knotenKontext der Kontext des Knotens (new BaumKnoten(…, Kontext: "datei")); der Befehl bekommt die Kennung des Knotens vor seinen Argumenten. Ohne passende Einträge öffnet sich kein Menü.

{ "ort": "kontext/ansicht/dateien.explorer", "befehl": "dateien.pfadKopieren", "wenn": "knotenKontext == 'datei'" }

Sprachen

Eine Erweiterung meldet Sprachen im Manifest an; die IDE kennt selbst keine. Öffnet der Benutzer eine Datei mit einer der Endungen, löst die IDE einmal sprache:<id> aus. Die Erweiterung sucht dann ihren Sprachserver und lässt ihn von der IDE starten; die IDE spricht selbst LSP mit ihm (Diagnosen, Vorschläge, Parameterhilfe, Kurzinfo, Gehe zu Definition, Umbenennen, Formatieren, semantische Färbung, Fehlerliste).

"aktivierung": ["sprache:python"],
"beitraege": {
  "sprachen": [ { "id": "python", "name": "Python", "endungen": ["py"],
                  "regeln": { "schluesselwoerter": ["def", "class", "return"], "typen": ["int", "str"], "zeilenkommentar": "#",
                              "zeichenketten": [ { "begrenzer": "\"\"\"", "mehrereZeilen": true }, { "begrenzer": "'" } ] } } ]
}
  • id: a–z, 0–9, . + -, höchstens 40 Zeichen; endungen ohne Punkt, höchstens 32.
  • hervorhebung: eine eingebaute (csharp, java, javascript, typescript, json, xml, html, css, yaml, sql); sonst regeln (höchstens 5000 Wörter, Begrenzer bis 8 Zeichen). Fehlerhafte Einträge fallen einzeln weg.
  • Melden zwei Erweiterungen dieselbe Endung oder Sprache, gewinnt die zuerst geladene.
public async Task AktiviereAsync(ErweiterungsKontext k)
{
    string? pfad = await k.LiesEinstellungAsync("python.sprachserver.pfad") ?? Beschaffung.SucheImPfad("pylsp");
    if (pfad is null) return;
    k.SprachserverZustand += (sprache, zustand) => { /* bereit, neugestartet, angehalten */ };
    k.BeiSprachserverMeldung("python", "eigene/meldung", wert => { });
    string? fehler = await k.StarteSprachserverAsync("python", new SprachserverStart(pfad, ["--stdio"]) { Optionen = new Dictionary<string, object?> { ["x"] = 1 } });
}
  • Die IDE startet nur Server für Sprachen aus dem eigenen Manifest, startet einen abgestürzten neu (höchstens 3-mal in 60 s, dann angehalten bis „Extras > Sprachserver neu starten“) und meldet offene Dateien danach wieder.
  • MeldeSprachserver reicht sprachspezifische Meldungen weiter (etwa Roslyns solution/open); Lebenslauf, textDocument/… und $/… schickt nur die IDE. Meldungen des Servers, die die IDE nicht selbst verarbeitet, kommen über BeiSprachserverMeldung.
  • Beschaffung hilft beim Suchen und Laden: SucheImPfad, NeuesterOrdner (etwa in ~/.vscode/extensions), LadeOrdner, LadeHerunterAsync (Prüfsumme SHA-256, geprüftes Entpacken von ZIP und TAR.GZ, nichts bleibt halb liegen) und FrageHerunterladenAsync.
  • Mitgeliefert: KXAML (eigener Server), C# (Roslyn), Java (Eclipse JDT LS), JavaScript und TypeScript (typescript-language-server). Sie suchen in Einstellung, PATH und den Erweiterungen von VS Code und laden sonst nach Rückfrage (Marc 2026-10-31: suchen, sonst laden); Java- und Node-Laufzeiten lädt die IDE nicht.

Aufgaben und Startprofile

Erweiterungen liefern Aufgaben (wie tasks in VS Code: Erstellen, Neu erstellen, Bereinigen, Testen, Sonstiges) und Startprofile (was Strg+F5 startet). Die IDE führt sie selbst aus: jede Aufgabe als eigenen Prozess, die Ausgabe im Kanal „Erstellen“, Fehler nach den Fehlermustern in der Fehlerliste. Abbrechen (Strg+Pause) beendet den ganzen Prozessbaum.

"aktivierung": ["arbeitsbereich:*.sln", "arbeitsbereich:**/*.csproj", "arbeitsbereich:package.json"]
  • arbeitsbereich:<muster> (wie workspaceContains) aktiviert die Erweiterung, wenn der Arbeitsordner eine passende Datei enthält. Ohne „/“ passt das Muster auf den Dateinamen in jeder Tiefe, mit „/“ auf den Pfad ab dem Arbeitsordner (** für beliebig viele Ordner). Gesucht wird bis Tiefe 4, ohne bin, obj, node_modules und .git, höchstens 10 000 Einträge.
k.RegistriereAufgaben(_ => Task.FromResult<IReadOnlyList<Aufgabe>>(
[
    new Aufgabe("bauen", "Alles erstellen", AufgabenGruppen.Erstellen, "dotnet", ["build", "-nologo"])
        { Vorgabe = true, Arbeitsordner = k.Arbeitsordner, Fehlermuster = [new Fehlermuster("$msbuild")] },
]));
k.RegistriereStartprofile(_ => Task.FromResult<IReadOnlyList<Startprofil>>(
[
    new Startprofil("start", "Programm", "dotnet", ["run", "--no-build"]) { VorherAufgabe = "bauen" }, // ImTerminal: Vorgabe true
]));
k.AufgabenGeaendert(); // nach einer Änderung (etwa an der Projektdatei): die IDE holt neu
  • Gruppen: erstellen (Strg+Umschalt+B), neuErstellen, bereinigen, testen (Strg+R A), sonstige (nur über „Erstellen > Aufgabe ausführen…“). Vorgabe wählt die Aufgabe einer Gruppe; ohne Vorgabe fragt die IDE.
  • Fehlermuster: eingebaut $msbuild, $tsc, $javac, $maven, $gradle (Formate von Werkzeugen, nicht von Sprachen), sonst ein regulärer Ausdruck mit den Gruppen datei, zeile, spalte, schwere, code, meldung. Eigene Ausdrücke laufen ohne Rückverfolgung mit 100 ms Zeitgrenze; relative Pfade gelten ab Basisordner bzw. dem Arbeitsordner der Aufgabe. Fehler eines Laufs ersetzen die des vorigen Laufs derselben Aufgabe, nicht die der Sprachserver.
  • Startprofile: erst VorherAufgabe (scheitert sie, startet nichts), dann das Programm in einem eigenen Terminalreiter (ImTerminal) bzw. mit Ausgabe im Kanal „Programm“. Die Startleiste merkt das gewählte Profil je Arbeitsordner (.kstudio/einstellungen.json, starten.profil).
  • Grenzen: je Erweiterung 1000 Aufgaben und 200 Profile, 64 Argumente; die Ausgabe höchstens 10 000 Zeilen je Minute (danach ein Hinweis), 5000 Fehler je Lauf. Unbrauchbares fällt einzeln weg; der Grund steht im Kanal „Erweiterungen“.
  • Hilfen im SDK: DateiBeobachter (Dateien im Arbeitsordner, gebündelt gemeldet) und AufgabenQuelle<T> (Ordner lesen, daraus Aufgaben und Profile, bei Änderungen neu).
  • Mitgeliefert: C# (Projektmappen, dotnet, Baum „Projektmappe“), TypeScript (package.json, npm), Java (Maven, Gradle).
  • Die IDE-Befehle aufgaben.ausfuehren und starten.profilWaehlen nehmen [erweiterung, kennung] (etwa für ein Kontextmenü „Erstellen“ an einem Projekt).

Debugger

Debugger kommen über das Debug Adapter Protocol (DAP). Die IDE spricht DAP selbst (Haltepunkte, Schritte, Aufrufliste, Variablen, Auswerten); eine Erweiterung meldet nur die Art des Debuggers an und liefert auf Anfrage den Adapter.

"aktivierung": ["debuggen:coreclr"],
"beitraege": {
  "debugger": [ { "typ": "coreclr", "titel": ".NET (netcoredbg)", "sprachen": ["csharp"] } ]
}
k.RegistriereStartprofile(_ => Task.FromResult<IReadOnlyList<Startprofil>>(
[
    new Startprofil("start", "Programm", "dotnet", ["run", "--no-build"])
    {
        VorherAufgabe = "bauen",
        Debuggen = DebugKonfiguration.Aus("coreclr", "launch", new Dictionary<string, object?> { ["projekt"] = projekt, ["cwd"] = ordner }),
    },
]));
k.RegistriereDebugAdapter("coreclr", async (konfiguration, abbruch) =>
    new DebugAdapterStart(netcoredbg, ["--interpreter=vscode"]) { Konfiguration = MitDll(konfiguration) }); // null: kein Adapter
  • Ablauf von F5:
    1. die VorherAufgabe des Profils (scheitert sie, startet nichts),
    2. debuggen:<typ> aktiviert die Erweiterung, die die Art anmeldet,
    3. ihr Anbieter liefert den Adapter (er darf suchen, laden und die Konfiguration auflösen, etwa den Pfad der DLL),
    4. die IDE startet ihn und schickt initialize, launch/attach, beim Ereignis initialized die Haltepunkte und configurationDone.
  • Adapter über stdio oder Port: DebugAdapterStart(Programm, Argumente) spricht über stdin/stdout; mit Port verbindet sich die IDE mit 127.0.0.1:Port (10 s lang versucht), ein leeres Programm heisst „lauscht schon“ (etwa java-debug in jdtls). stderr des Adapters und die Ausgaben des Programms stehen gedrosselt im Kanal „Debuggen“.
  • Rückfragen des Adapters: runInTerminal startet das Programm in einem Terminalreiter; startDebugging öffnet eine Kind-Sitzung über dieselbe Verbindungsart (bei einem Port eine neue Verbindung, wie js-debug es braucht).
  • sprachen begrenzt, für welche Dateien Haltepunkte an diesen Adapter gehen (leer: alle).
  • Ohne Debuggen startet F5 das Profil ohne Debugger und sagt das in der Statusleiste.
  • Den Sprachserver fragen: FrageSprachserverAsync(sprache, "workspace/executeCommand", wert) (nur diese Methode), etwa für vscode.java.startDebugSession bei jdtls.
  • Grenzen: 16 Debugger je Erweiterung, Argumente einer Konfiguration höchstens 1 MiB; vom Adapter höchstens 1000 Rahmen, 10 000 Variablen je Abruf, Texte 64 KiB (siehe Kuonen.Global.Dap). Hängt oder stirbt der Adapter, endet die Sitzung mit einer Meldung; Beenden (Umschalt+F5) tötet nach 3 s den Prozessbaum.
  • Mitgeliefert:
    • C#: netcoredbg, gesucht in Einstellung und PATH, sonst von GitHub geladen. vsdbg darf nur mit Microsoft-Produkten benutzt werden.
    • JavaScript/TypeScript: js-debug als DAP-Server auf einem Port, gesucht oder von GitHub geladen.
    • Java: java-debug als Zusatz von jdtls, aus der VS-Code-Erweiterung „Debugger for Java“ oder der Einstellung.

Pakete

Ein Paket .kstudioext ist ein ZIP mit erweiterung.json an der Wurzel (ErweiterungsPaket.Packe(ordner, ziel)). Installiert wird über „Erweiterungen > Erweiterungen verwalten“ (Strg+Umschalt+X) oder „Erweiterung aus Datei installieren…“ in den Benutzerordner (<Konfiguration>/erweiterungen/<id>); dort lassen sich Erweiterungen auch aus- und einschalten (<Konfiguration>/erweiterungen.json) und installierte entfernen. Änderungen wirken nach „Neu laden“.

  • Abgelehnt werden Pfade ausserhalb des Pakets, Verweise, mehr als 2000 Einträge, mehr als 256 MiB entpackt oder 64 MiB je Eintrag, ein fehlendes oder ungültiges Manifest und die Kennung einer mitgelieferten Erweiterung (die lässt sich nur ausschalten).
  • Eine neue Fassung ersetzt die alte erst, wenn alles geprüft ist; scheitert der Wechsel, bleibt die alte.