Das 22. Plugin hat ein Wochenende gedauert. Das sagt fast alles darüber, warum geteilte Infrastruktur in der Plugin-Entwicklung wichtig ist.
Der Unfall, der Architektur wurde
Ich hatte nicht vor, ein Framework zu bauen. Ich habe ein Freemarker-Plugin gebaut, weil ich eins brauchte. Es hatte einen Lexer, einen Parser, einen PSI-Baum, Completion, Navigation und eine Handvoll Inspections. Standard-Plugin-Architektur.
Als ich mit dem Julia-Plugin angefangen habe, fiel mir auf, dass ich dieselbe Completion-Infrastruktur nochmal schrieb. Dasselbe Settings-Framework. Denselben Test-Harness. Denselben Marketplace-Integrationscode. Also habe ich die gemeinsamen Teile in ein Shared Module gezogen, und das Julia-Plugin wurde eine Grammatik plus Custom Inspections on top.
Das war der Moment, in dem es ein Framework wurde, auch wenn ich es erst sechs Plugins später so genannt habe.
Was geteilt wird
Das Framework übernimmt alles, was nicht sprachspezifisch ist. Completion-Provider-Registrierung, Reference-Resolution-Patterns, Settings-Persistenz, die Run-Configuration-UI, Color-Scheme-Integration, Error Recovery im Parser und die Test-Infrastruktur.
Jedes neue Sprachplugin bringt seine eigene BNF-Grammatik, seine eigenen Lexer-Regeln und seine eigenen Inspections mit. Alles andere wird geerbt. Das Framework stellt abstrakte Basisklassen für die gängigen Patterns bereit - “so deklariert ein Sprachplugin seinen Dateityp, so liefert es Completion-Items, so hebt es Fehler hervor” - und jede Sprache füllt die Spezifika aus.
Die Grammatik-Überraschungen
Jede Sprache hat mindestens eine Sache, die den Parser wütend macht.
Julia hat Macros, die den AST zur Parse-Zeit transformieren. Man kann Julia nicht vollständig parsen, ohne Macros auszuführen, was bedeutet, dass das IDE-Plugin fundierte Vermutungen anstellen muss.
Haskell hat layout-sensitives Parsing - Einrückung verändert die Bedeutung des Codes. Der Lexer muss Einrückungslevel tracken und synthetische Tokens emittieren. Es funktioniert, aber die Implementierung ist nicht hübsch.
ABAP ist case-insensitiv mit Keywords, die sich mit Identifiern überlappen, auf eine Art, die einem PL/1-Designer unangenehm wäre. Der Lexer braucht Disambiguierungsregeln, die sich anfühlen, als gehörten sie in einen Natural-Language-Processor.
Solidity war tatsächlich einfach zu parsen. Die Überraschungen steckten in den Inspections - Reentrancy-Schwachstellen zu erkennen erfordert Verständnis des Kontrollflusses über externe Calls hinweg, und das ist mehr Analyse als Parsing.
Was das Framework nicht löst
Das Framework macht neue Plugins schnell zu bauen. Es macht sie nicht schnell zu warten. Jede Sprache entwickelt sich unabhängig - Julia bekommt neue Syntax, Solidity neue EVM-Features, ABAP neue Open-SQL-Konstrukte. Die Grammatik-Updates sind Pro-Plugin-Arbeit, und sie hören nie auf. Wie diese laufenden Kosten aussehen, habe ich in Was der Unterhalt von 22 Plugins umfasst beschrieben.
Zweiundzwanzig Sprachen auf einer Engine. Ich würde es wieder so machen. Ich hätte es nur nicht geglaubt, wenn mir jemand gesagt hätte, wohin das führt, als ich mit dem Freemarker-Lexer angefangen habe.