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üsdatei,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; artbaum(nicht im Dokument) oderkxaml.
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 überMode=TwoWaykommen von selbst hinein (BeiEingabemeldet sie) und werden nicht zurückgeschickt. - Ereignisse:
Click="gruessen"ruftBeiEreignis("gruessen", x:Name, daten). - KXAML: Elemente, Eigenschaften, angehängte Eigenschaften, Eigenschaftselemente,
{Binding pfad[, Mode=…]},{x:Null},x:Name; keine Styles, Vorlagen, Ressourcen und keinx: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 (
Fuellelä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;endungenohne Punkt, höchstens 32.hervorhebung: eine eingebaute (csharp,java,javascript,typescript,json,xml,html,css,yaml,sql); sonstregeln(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.
MeldeSprachserverreicht sprachspezifische Meldungen weiter (etwa Roslynssolution/open); Lebenslauf,textDocument/…und$/…schickt nur die IDE. Meldungen des Servers, die die IDE nicht selbst verarbeitet, kommen überBeiSprachserverMeldung.Beschaffunghilft 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) undFrageHerunterladenAsync.- 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>(wieworkspaceContains) 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, ohnebin,obj,node_modulesund.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…“).Vorgabewä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 Gruppendatei,zeile,spalte,schwere,code,meldung. Eigene Ausdrücke laufen ohne Rückverfolgung mit 100 ms Zeitgrenze; relative Pfade gelten abBasisordnerbzw. 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) undAufgabenQuelle<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.ausfuehrenundstarten.profilWaehlennehmen[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:
- die
VorherAufgabedes Profils (scheitert sie, startet nichts), debuggen:<typ>aktiviert die Erweiterung, die die Art anmeldet,- ihr Anbieter liefert den Adapter (er darf suchen, laden und die Konfiguration auflösen, etwa den Pfad der DLL),
- die IDE startet ihn und schickt
initialize,launch/attach, beim Ereignisinitializeddie Haltepunkte undconfigurationDone.
- die
- Adapter über stdio oder Port:
DebugAdapterStart(Programm, Argumente)spricht über stdin/stdout; mitPortverbindet sich die IDE mit127.0.0.1:Port(10 s lang versucht), ein leeresProgrammheisst „lauscht schon“ (etwa java-debug in jdtls). stderr des Adapters und die Ausgaben des Programms stehen gedrosselt im Kanal „Debuggen“. - Rückfragen des Adapters:
runInTerminalstartet das Programm in einem Terminalreiter;startDebuggingöffnet eine Kind-Sitzung über dieselbe Verbindungsart (bei einem Port eine neue Verbindung, wie js-debug es braucht). sprachenbegrenzt, für welche Dateien Haltepunkte an diesen Adapter gehen (leer: alle).- Ohne
Debuggenstartet F5 das Profil ohne Debugger und sagt das in der Statusleiste. - Den Sprachserver fragen:
FrageSprachserverAsync(sprache, "workspace/executeCommand", wert)(nur diese Methode), etwa fürvscode.java.startDebugSessionbei 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.vsdbgdarf nur mit Microsoft-Produkten benutzt werden. - JavaScript/TypeScript:
js-debugals DAP-Server auf einem Port, gesucht oder von GitHub geladen. - Java:
java-debugals Zusatz von jdtls, aus der VS-Code-Erweiterung „Debugger for Java“ oder der Einstellung.
- C#:
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.