Projekt na (niejeden) weekend: Netbeans i TOML (początek)
TOML to Tom's Obvious, Minimal Language. - specyfikacja plików konfiguracyjnych, wykorzystywanych (między innymi) przez Rust'owe cargo
. Netbeans oczywiście nie ma
wsparcia dla tego formatu. A co gdyby dodać go samemu...?
Zamiast wstępu
Zanim zacznę kilka słów wyjaśnienia, żeby ograniczyć oczekiwania ;) Napisanie pełnego wsparcia (dowolnego) języka to praca nie na jeden weekend. W związku z tym, to co powstanie w ramach moich eksperymentów trzeba traktować właśnie jako... eksperyment, a artykuły jako formę notatek/tutoriali na temat tworzenia wtyczek obsługi języków dla Netbeans. Czas pokaże co z tego wyjdzie i w jakim stanie będzie kod.
Obsługa języków (wtyczki)
Netbeans umożliwia tworzenie dwóch rodzajów wtyczek językowych:
- tradycyjnych
- od maja 2019 uproszczonych, opartych na gramatykach textmate - wystarczy dodać plik z gramatyką, zarejestrować i mamy gotowe kolorowanie składni! PR z funkcjonalnością: Support for TextMate injection grammars added oraz przykład dla Markdown (więcej tu plików z licencją niż kodu...)
Ja spróbuję przejść drogę tradycyjną (z nadzieją, że uda się napisać coś więcej niż tylko kolorowanie składni).
TOML vs INI
pliki TOML przypominają INI. Główna różnica jest taka, że w przypadku tego pierwszego format pliku jest określony w specyfikacji, a w przypadku plików INI nie, co powoduje, że te same dane zapisane przez różne programy mogą wyglądać inaczej...
Moduły Netbeans
Netbeans to nie tylko IDE ale cała platforma do tworzenia aplikacji. Samo IDE jest po prostu jedną z wielu aplikacji opartych na tej platformie. Moduły Netbeans można oczywiście tworzyć zupełnie dowolnie o ile zostaną spakowane do odpowiedniego formatu (nbm
). Jeśli nie chcemy robić wszystkiego ręcznie do wyboru mamy:
ant
(wbudowane w Netbeans) - czyli pierwszy i nadal główny system budowania Netbeans, tzw.premium support
- możemy liczyć na pełne wsparcie, na możliwość wybierania zależności w ładnym okienku UI, etc etc.maven
(wbudowane w Netbeans) - Netbeans posiada całkiem niezłe wsparcie dla mavena: gotowe szablony do tworzenia modułów, oraz np. UI do wyszukiwania zależności po tekście (przydatne jeśli np. chcemy użyć jakiejś klasy, ale nie wiemy który artefakt ją zawiera...).gradle
(zewnętrzne)- Radim Kubacki podczas swojej pracy dla Gradleware stworzył wtyczkę do budowania modułów Netbeans. Aktualnie jednak wtyczka nie jest aktywnie rozwijana (autor ogranicza się do wprowadzania zmian od innych, ew zapewnienia zgodności z nowymi wersjami gradle), a samo wsparcie nigdy nie otrzymało takich narzędzi UI jak w przypadkuant
czymaven
.
Z trzech powyższych spróbujemy maven
a (liczę na prostsze uaktualnianie zależności w przyszłości i mimo wszystko prostszy system budowania... także poza infrastrukturą Netbans)
Tworzenie modułu
Moduł można oczywiście wyklikać
z IDE: File
-> New Project
-> Java with Maven
-> NetBeans module
. Można też skorzystać z archetypu
mavena:
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.apache.netbeans.archetypes -DarchetypeArtifactId=nbm-archetype -DarchetypeVersion=1.17 \
-DartifactId=toml \
-DgroupId=io.gitlab.ihsahn.netbeans \
-Dversion=1.0-SNAPSHOT \
-DpackageName=io.gitlab.ihsahn.netbeans.toml \
-DnetbeansVersion=RELEASE112
Jeśli chcemy, aby dało się uruchamiać/debugować nasz moduł trzeba dodać/uzupełnić profil w
~/.m2/settings.xml
i ustawić propertynetbeans.installation
na istniejące IDE NetBeans, dzięki czemu da się uruchomić nasz projekt poprzezmvn org.apache.netbeans.utilities:nbm-maven-plugin:run-ide
(albo z IDE)
settings.xml z profilem:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<profiles>
<profile>
<id>netbeans-ide</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<netbeans.installation>/snap/netbeans/current/netbeans</netbeans.installation>
</properties>
</profile>
</profiles>
</settings>
Manifest modułu
Moduły nbm
to oczywiście nic innego jak zwykłe jar
w odpowiedniej strukturze oraz z odpowiednim plikiem manifestu. Plik manifestu znajduje się w ${project}/src/main/nbm/manifest.mf
. Najważniejsze wpisy to:
Manifest-Version: 1.0
- określa standard pliku manifestOpenIDE-Module-Localizing-Bundle: io/gitlab/ihsahn/netbeans/Bundle.properties
wskazuje na plik (properties
) z tłumaczeniami dla modułu.
Pozostałe wpisy albo dodamy później albo od razu jako tłumaczenia do io/gitlab/ihsahn/netbeans/Bundle.properties
:
# Localized module labels. Defaults taken from POM (<name>, <description>, <groupId>) if unset.
OpenIDE-Module-Name=TOML Files support
OpenIDE-Module-Short-Description=Editing support for TOML files.
OpenIDE-Module-Long-Description=\
Editing support for TOML files.
OpenIDE-Module-Display-Category=Editing
Wirtualny system plików
NetBeans opiera się na wirtualnym systemie plików - wszystkie elementy w Projects
, Files
i Services
są reprezentowane przez obiekty w tym systemie plików. Akcje w menu i ich położenie to również odpowiednie wpisy w tym systemie plików. Historycznie każdy z modułów dostarczał swój plik layer.xml
z definicją swojej części systemu plików, a platforma podczas startu łączyła te pliki w jedną całość i wyświetlała/operowała na tak powstałym wirtualnym systemie plików. W nowych wersjach NetBeans część operacji została udostępiona jako adnotacje co upraszcza tworzenie modułów (nie trzeba już znać dokładnego rozkładu systemu plików, żeby się w niego wpiąć).
Wtyczka
Rozpoznawanie typu pliku
Do rozpoznawania nowego typu pliku (*.toml
w naszym przypadku) potrzeba oczywiście obiektu, który będzie go reprezentował w wirtualnym systemie plików. Klasa taka to pochodna DataObject.
Najprościej skorzystać z kreatora wbudowanego w IDE: File
-> New File
-> Module Development
-> File Type
. Podajemy typ mime
dla naszego pliku (według specyfikacji application/toml
), rozszerzenie (toml
), ikonę (16x16, png lub gif) oraz prefix nazw dla plików (ja użyłem TomlFile
). Zaletą kreatora oprócz wygenerowania plików z kodem, jest także to, że uzupełnia pom.xml o brakujące zależności.
IDE wygeneruje nam w źródłach:
package-info.java
z rejestracją szablonu (do tematu wrócę potem)TomlFileDataObject.java
- klasę rejestrującą nowy typ mime razem z rozszerzeniem oraz definicjami akcjiTomlFileVisualElement.java
- dodatkowy widok pliku — po otwarciu będzie dostępny w formie dodatkowej zakładki edytora (np. edytor plików*.properties
pozwala w ten sposób edytować pliki w tabelce)
oraz w resources
plik szablonu TomlFileTemplate.toml
.
Po przebudowaniu i uruchomieniu Netbeans powinien rozpoznawać typ pliku, oznaczać go ikoną, a po otwarciu prezentować nasz "wzbogacony" o część wizualną edytor:
Nie przewiduję potrzeby dodatkowej wizualnej edycji plików *.toml
więc TomlFileVisualElement.java
zostaje usunięty (zniknie też zakładka "Visual" widoczna powyżej w edytorze pliku).
TomlFileDataObject
import java.io.IOException;
import org.netbeans.core.spi.multiview.MultiViewElement;
import org.netbeans.core.spi.multiview.text.MultiViewEditorElement;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.MIMEResolver;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectExistsException;
import org.openide.loaders.MultiDataObject;
import org.openide.loaders.MultiFileLoader;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.TopComponent;
@Messages({
"LBL_TomlFile_LOADER=Files of TomlFile"
})
@MIMEResolver.ExtensionRegistration(
displayName = "#LBL_TomlFile_LOADER",
mimeType = "application/toml",
extension = {"toml"}
)
@DataObject.Registration(
mimeType = "application/toml",
iconBase = "io/gitlab/ihsahn/netbeans/toml_16x16.png",
displayName = "#LBL_TomlFile_LOADER",
position = 300
)
@ActionReferences({
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.OpenAction"),
position = 100,
separatorAfter = 200
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "Edit", id = "org.openide.actions.CutAction"),
position = 300
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "Edit", id = "org.openide.actions.CopyAction"),
position = 400,
separatorAfter = 500
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "Edit", id = "org.openide.actions.DeleteAction"),
position = 600
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.RenameAction"),
position = 700,
separatorAfter = 800
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.SaveAsTemplateAction"),
position = 900,
separatorAfter = 1000
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.FileSystemAction"),
position = 1100,
separatorAfter = 1200
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.ToolsAction"),
position = 1300
),
@ActionReference(
path = "Loaders/application/toml/Actions",
id = @ActionID(category = "System", id = "org.openide.actions.PropertiesAction"),
position = 1400
)
})
public class TomlFileDataObject extends MultiDataObject {
public TomlFileDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
super(pf, loader);
registerEditor("application/toml", true);
}
@Override
protected int associateLookup() {
return 1;
}
@MultiViewElement.Registration(
displayName = "#LBL_TomlFile_EDITOR",
iconBase = "io/gitlab/ihsahn/netbeans/toml_16x16.png",
mimeType = "application/toml",
persistenceType = TopComponent.PERSISTENCE_ONLY_OPENED,
preferredID = "TomlFile",
position = 1000
)
@Messages("LBL_TomlFile_EDITOR=Source")
public static MultiViewEditorElement createEditor(Lookup lkp) {
return new MultiViewEditorElement(lkp);
}
}
Jak widać plik nie jest zbyt duży i zawiera:
- kilka
@Messages
czyli skrócona definicja klucza dla tzw Resource Bundle — dzięki temu nie musimy osobno edytować plików properties (zwłaszcza, jeśli nie przygotowujemy żadnej innej wersji językowej!) - wszystko jest w jednym miejscu w kodzie @MIMEResolver.ExtensionRegistration
— powiązanie rozszerzenia pliku z podanym typem mime@DataObject.Registration
— rejestracja określonego typu mime (oraz naszej klasy TomlFileDataObject jako obsługującej ten typ) w wirtualnym systemie plików Netbeans.@ActionReferences
zawierające listę@ActionReference
— czyli rejestracja podstawowych akcji w wirtualnym systemie plików. Jak widać po kodzie, akcje są rejestrowane we własnej strukturze, która odpowiada typowi mime obsługiwanych plików a wywoływane będą domyślne implementacje dostępne w IDE.@MultiViewElement.Registration
— rejestracja metody tworzącej widokźródła
edytora.
Szablon
Wspominałem wcześniej, że rejestracja szablonu odbywa się w package-info.java
:
@TemplateRegistration(folder = "Other", content = "TomlFileTemplate.toml",
displayName = "Toml File", requireProject = false)
package io.gitlab.ihsahn.netbeans;
import org.netbeans.api.templates.TemplateRegistration;
Rejestracja odbywa się poprzez adnotację @TemplateRegistration
.
W najprostszej wersji — podajemy nazwę folderu, w którym znajdzie się szablon (folder
), oraz wskazujemy plik szablonu (content
). Ja dodałem jeszcze dwa
parametry:
displayName
— czyli nazwa, pod jaką wyświetli się szablon (jeśli nie będzie podana zostanie użyta nazwa zcontent
)requireProject
— określa czy szablon może zostać wykorzystany, jeśli IDE nie ma otwartego żadnego projektu.
Szablon pojawi się w oknie tworzenia nowego pliku:
Implementacja kolorowania składni (ogólnie)
Tokenizacja
Podstawą kolorowania składni jest tokenizacja
- podział tekstu z pliku na elementy oddzielone separatorami (spacja, znak nowej linii, etc) oraz przypisanie rozpoznanych elementów do grup. Z punktu widzenia NetBeans potrzebujemy:
- klasy implementującej interfejs
GsfLanguage
(w praktyce rozszerzamyDefaultLanguageConfig
) przypisanej do określonego typumime
(przypisanie poprzez adnotację@LanguageRegistration
) - nie znalazłem do tych klas żadnych wygenerowanychjavadoc
... - listy tokenów (najprościej zrobić je jako
enum
) wraz z ich kategoriami, gdzie każdy token musi implementować interfejs TokenId - klasy rozszerzającej LanguageHierarchy, która zwróci listę wszystkich możliwych
tokenów
dla danego języka oraz Lexer , który zajmie się właściwą tokenizacją. - wyżej wymieniony Lexer
Idąc od końca: Lexer
.
Ponieważ nie mam pewności czy kod, który piszę zadziała, na razie większość kodu Lexer
a pożyczam z SimplePlainLexer:
import org.netbeans.api.lexer.Token;
import org.netbeans.spi.lexer.Lexer;
import org.netbeans.spi.lexer.LexerInput;
import org.netbeans.spi.lexer.LexerRestartInfo;
import org.netbeans.spi.lexer.TokenFactory;
public class TomlLexer implements Lexer<TomlTokenId> {
private final LexerInput input;
private final TokenFactory<TomlTokenId> tokenFactory;
private static final int INIT = 0;
private static final int IN_WORD = 1;
private static final int IN_WHITESPACE = 2;
private int state = INIT;
public TomlLexer(LexerRestartInfo<TomlTokenId> info) {
this.input = info.input();
this.tokenFactory = info.tokenFactory();
}
@Override
public Token<TomlTokenId> nextToken() {
while (true) {
int ch = input.read();
switch (state) {
case INIT:
if (ch == LexerInput.EOF) {
return null;
} else if (Character.isWhitespace((char) ch)) { // start of whitespace
state = IN_WHITESPACE;
} else { // start of word
state = IN_WORD;
}
break;
case IN_WORD:
while (true) {
if (ch == LexerInput.EOF || Character.isWhitespace((char) ch)) {
if (ch != LexerInput.EOF) { // no backup of EOF
input.backup(1);
}
state = INIT;
return tokenFactory.createToken(TomlTokenId.TOKEN);
}
ch = input.read();
}
case IN_WHITESPACE:
while (true) {
if (ch == LexerInput.EOF || !Character.isWhitespace((char) ch)) {
if (ch != LexerInput.EOF) { // no backup of EOF
input.backup(1);
}
state = INIT;
return tokenFactory.createToken(TomlTokenId.WHITESPACE);
}
ch = input.read();
}
default:
throw new IllegalStateException();
}
}
}
@Override
public Object state() {
return null; //no specific state
}
@Override
public void release() {
//nothing to release
}
}
jak widać Lexer
dokona podziału tylko na dwa rodzaje tokenów: WHITESPACE
czyli wszystko co oddziela wyrazy i TOKEN
czyli sam wyraz/znak etc. Na początek do testów to wystarczy :)
Odpowiednio do tego TomlTokenId
:
import org.netbeans.api.lexer.TokenId;
public enum TomlTokenId implements TokenId {
WHITESPACE, TOKEN;
@Override
public String primaryCategory() {
return name();
}
}
konsekwentnie z powyższym definiujemy tylko dwie kategorie o nazwach takich samych jak tokeny, które do nich należą.
Implementacja LanguageHierarchy
:
public class TomlLanguageHierarchy extends LanguageHierarchy<TomlTokenId> {
private static final Collection<TomlTokenId> TOKEN_IDS = unmodifiableSet(EnumSet.allOf(TomlTokenId.class));
@Override
protected Collection<TomlTokenId> createTokenIds() {
return TOKEN_IDS;
}
@Override
protected Lexer<TomlTokenId> createLexer(LexerRestartInfo<TomlTokenId> info) {
return new TomlLexer(info);
}
@Override
protected String mimeType() {
return "application/toml";
}
}
oraz klasa konfiguracji oraz rejestracji języka:
import org.netbeans.api.lexer.Language;
import org.netbeans.modules.csl.spi.DefaultLanguageConfig;
import org.netbeans.modules.csl.spi.LanguageRegistration;
@LanguageRegistration(mimeType = "application/toml")
public class TomlLanguage extends DefaultLanguageConfig {
public static final TomlLanguageHierarchy LANGUAGE_HIERARCHY = new TomlLanguageHierarchy();
@Override
public Language<TomlTokenId> getLexerLanguage() {
return LANGUAGE_HIERARCHY.language();
}
@Override
public String getPreferredExtension() {
return "toml";
}
@Override
public String getDisplayName() {
return "Toml Language";
}
}
i wymagana zależność:
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-netbeans-modules-csl-api</artifactId>
<version>${netbeans.version}</version>
</dependency>
Niestety wygląda na to, że któraś z klas (annotation procesor?) potrzebuje do kompilacji jeszcze MarkProviderCreator
na claspath:
Fatal error compiling: java.lang.NoClassDefFoundError: org/netbeans/modules/editor/errorstripe/privatespi/MarkProviderCreator: org.netbeans.modules.editor.errorstripe.privatespi.MarkProviderCreator -> [Help 1]
dodajemy brakującą zależność:
<dependency>
<groupId>org.netbeans.modules</groupId>
<artifactId>org-netbeans-modules-editor-errorstripe</artifactId>
<version>${netbeans.version}</version>
<scope>compile</scope>
</dependency>
...i jeśli wszystko poszło dobrze, to ... kod powinien działać bez zmian (edycja pliku) :) Jedyna różnica w stosunku do poprzedniego stanu, to fakt, że do tokenizacji używane są teraz dostarczone przez nas klasy (można to zweryfikować np debuggerem).
Konfiguracja kolorów
Domyślną konfigurację kolorów przygotowujemy w pliku xml, określonym: http://www.netbeans.org/dtds/EditorFontsColors-1_1.dtd , konfiguracja odbywa się dla każdej kategorii tokena.
/src/main/resources/io/gitlab/ihsahn/netbeans/TomlDefaultFontsAndColors.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontscolors PUBLIC "-//NetBeans//DTD Editor Fonts and Colors settings 1.1//EN"
"http://www.netbeans.org/dtds/EditorFontsColors-1_1.dtd">
<fontscolors>
<fontcolor name="WHITESPACE" foreColor="blue" default="default"/>
<fontcolor name="TOKEN" foreColor="orange" default="default"/>
</fontscolors>
Dobrze jest od razu dodać tłumaczenia nazw kategorii do
Bundle.properties
:
# Token categories as displayed in fonts and colors configuration
WHITESPACE=whitespace
TOKEN=non-whitespace
Przygotowany wcześniej plik z konfiguracją trzeba umieścić w wirtualnym systemie plików NetBeans - niestety nie ma tutaj wsparcia adnotacji, więc zmiany trzeba zrobić we wcześniej omawianym pliku layer.xml
.
Po pierwsze, trzeba zdefiniować w manifeście modułu lokalizację pliku, w którym moduł definiuje swoją część konfiguracji systemu plików:
OpenIDE-Module-Layer: io/gitlab/ihsahn/netbeans/layer.xml
Po drugie trzeba przygotować sam plik:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN"
"http://www.netbeans.org/dtds/filesystem-1_2.dtd">
<filesystem>
<folder name="Editors">
<folder name="application">
<folder name="toml">
<attr name="displayName" bundlevalue="io.gitlab.ihsahn.netbeans.Bundle#Editors/application/toml"/>
<folder name="FontsColors">
<folder name="NetBeans"> <!-- Nazwa profilu kolorów -->
<folder name="Defaults">
<file name="io-gitlab-ihsahn-netbeans-toml-editor-default-colors.xml" url="TomlDefaultFontsAndColors.xml">
<attr name="SystemFileSystem.localizingBundle" stringvalue="io.gitlab.ihsahn.netbeans.Bundle"/>
</file>
</folder>
</folder>
<!-- <folder name="CityLights">
<folder name="Defaults">
<file name="org-netbeans-modules-db-sql-editor-token-colorings.xml" url="FontsAndColors.xml">
<attr name="SystemFileSystem.localizingBundle" stringvalue="com.mycompany.toml_maven.Bundle"/>
</file>
</folder>
</folder>-->
</folder> <!--- FontsAndColors -->
</folder> <!-- toml -->
</folder> <!-- application -->
</folder> <!-- Editors -->
<folder name="OptionsDialog">
<folder name="PreviewExamples">
<folder name="application">
<file name="toml" url="example.toml"/>
</folder>
</folder> <!-- PreviewExamples -->
</folder> <!-- OptionsDialog -->
</filesystem>
Plik zawiera dwa wpisy/foldery:
Editors
- gdzie w obrębie naszego typu mime musimy zdefiniować:- atrybut
displayName
- (wskazujący na bundle oraz klucz) - to, co będzie się wyświetlać w oknie konfiguracji kolorowania składni jako nazwa typu plików FontsColors
- podfoldery (po jednym dla każdego z profilów kolorów, które chcemy obsłużyć, w przykładzie powyżej jestNetbeans
oraz zakomentowanyCityLights
) ze wskazaniem na plik zawierający domyślną konfigurację kolorów wraz z towarzyszącym muBundle
zawierającym tłumaczenia
- atrybut
OptionsDialog
- czyli okno opcji, gdzie do "PreviewExamples" (czyli pliki używane do demonstrowania kolorowania składni) dodajemy naszexample.toml
Po kompilacji i uruchomieniu, w oknie opcji widać, że można ustawić kolorowanie składni dla naszego nowego typu pliku:
(tak, przykład pliku TOML wziąłem z Wikipedii)
Kilka uwag na koniec
-
Jeżeli chcecie zobaczyć, jak dokładnie wygląda wasza definicja wirtualnego systemu plików (np. co się wygenerowało z adnotacji), to w projekcie w grupie
Other Sources
wsrc/main/resources
znajdziecie wpislayer.xml
pod którym będzie widać zarówno to co zdefiniowaliście (<this layer>
) jak i to jak wpasowuje się w ten dostarczony z IDE (<this layer in context>
): -
Trzeba uważać z opcją
Clean
podczas budowania projektów. Netbeans nie jest na tyle sprytne, żeby rozpoznać, że np mamy już uruchomiony projekt, aClean
wykonane podczas działania programu, powoduje dużo dziwnych komunikatów (w tej uruchomionej instancji).
Podsumowanie
To co do tej pory udało się zrobić, to jeszcze nie jest pełnoprawna wtyczka obsługująca TOML, za to aktualny stan nadaje się jako bardzo dobra baza (szablon) do tworzenia wtyczek obsługujących inne formaty plików. Przy następnej okazji zajmę się prawdziwym (z uwzględnieniem struktury TOML) kolorowaniem składni.
Repozytorium z aktualnym kodem: https://gitlab.com/ihsahn/netbeans-toml
Literatura
- Tom's Obvious, Minimal Language. oficjalne repo Toml
- Rich Client Programming: Plugging Into the NetBeans Platform książka o programowaniu z wykorzystaniem platformy Netbeans
- NetBeans api javadoc
- Maven Netbeans Module Plugin
- File Type Integration Tutorial