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:

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 przypadku ant czy maven.

Z trzech powyższych spróbujemy mavena (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ć property netbeans.installation na istniejące IDE NetBeans, dzięki czemu da się uruchomić nasz projekt poprzez mvn 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 manifest
  • OpenIDE-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 akcji
  • TomlFileVisualElement.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: Netbeans: rozpoznany plik *.toml oraz wzbogacony 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:

  1. 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
  2. @MIMEResolver.ExtensionRegistration — powiązanie rozszerzenia pliku z podanym typem mime
  3. @DataObject.Registration — rejestracja określonego typu mime (oraz naszej klasy TomlFileDataObject jako obsługującej ten typ) w wirtualnym systemie plików Netbeans.
  4. @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.
  5. @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 z content)
  • 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: Netbeans: rozpoznany plik *.toml oraz wzbogacony edytor

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 rozszerzamy DefaultLanguageConfig) przypisanej do określonego typu mime (przypisanie poprzez adnotację @LanguageRegistration) - nie znalazłem do tych klas żadnych wygenerowanych javadoc...
  • 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 Lexera 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 jest Netbeans oraz zakomentowany CityLights) ze wskazaniem na plik zawierający domyślną konfigurację kolorów wraz z towarzyszącym mu Bundle zawierającym tłumaczenia
  • OptionsDialog - czyli okno opcji, gdzie do "PreviewExamples" (czyli pliki używane do demonstrowania kolorowania składni) dodajemy nasz example.toml

Po kompilacji i uruchomieniu, w oknie opcji widać, że można ustawić kolorowanie składni dla naszego nowego typu pliku: Netbeans - widok konfiguracji czcionek i kolorów dla plików *.toml

(tak, przykład pliku TOML wziąłem z Wikipedii)

Kilka uwag na koniec

  1. 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 w src/main/resources znajdziecie wpis layer.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>): Netbeans - widok wirtualnego systemu plików

  2. Trzeba uważać z opcją Clean podczas budowania projektów. Netbeans nie jest na tyle sprytne, żeby rozpoznać, że np mamy już uruchomiony projekt, a Clean 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

  1. Tom's Obvious, Minimal Language. oficjalne repo Toml
  2. Rich Client Programming: Plugging Into the NetBeans Platform książka o programowaniu z wykorzystaniem platformy Netbeans
  3. NetBeans api javadoc
  4. Maven Netbeans Module Plugin
  5. File Type Integration Tutorial