Guide: Add a new language to Moonshine IDE - Moonshine-IDE/Moonshine-IDE GitHub Wiki

Add a new language to Moonshine IDE

The purpose of this document is to explain to a Moonshine IDE contributor how to add a new programming language to Moonshine IDE, including displaying syntax, adding a new project type, launching a language server for code intelligence, and running a compiler or other command line tools.

This is not a trivial set of tasks, so expect this document to be long and complex. Additionally, it may not necessarily be written in an order where someone can follow along step by step. For best understanding, read the document in its entirety at least once before starting implementation. Be ready to refer back to it frequently, and expect to jump around a bit to recall all of the necessary details.

Finally, it is also worth remembering that every language has its own unique features and quirks, so there are likely to be important requirements for a particular language that this document may not cover because it's not particularly relevant to other languages. In other words, expect a certain amount of new development where this document simply cannot guide you.

Notes

  • In this document's example code, XYZ is often used in class, method, and variable names. Consider this a generic placeholder for the actual name of the new language being implemented.

  • Moonshine IDE originally targeted the ActionScript/Flash ecosystem only. It now supports a variety of languages, such as Java, Groovy, and Haxe. Certain features of Moonshine IDE have not yet been cleanly refactored into separate plugins for each language. There are a few remaining places containing if/else chains or switch statements used to identify the current language. This is admittedly not ideal, and it will be refactored in the future to make the code less tightly-coupled. One of the purposes of writing this document, besides teaching someone to add a new language, is to create a record of where exactly all of the remaming unrefactored code can be found.

Plugins

Adding a new language to Moonshine IDE involves creating one or more plugins. Moonshine IDE supports organizing code into plugins to make it possible to add new features and languages while keeping parts of the code separated and organized in a way that makes the whole IDE easier to maintain for all contributors.

Generally, each language has at least three plugins:

  • Syntax plugin: manages highlighting keywords and other language constants with different colors in the editor.
  • Project plugin: manages opening, saving, and configuring project settings for the language. There's also a related section for specifying the file format for a new project type.
  • Language server plugin: manages launching and communicating with the language's language server. A language server provides code intelligence features, such as completion, hover documentation, function signature help, goto definition, and more.

For languages that need to be compiled, a fourth plugin may be necessary:

  • Build plugin: manages compiling, cleaning, and other build tasks.

As the introduction to this document warns, there are some other features currently handled in legacy code that is currently shared by all languages, instead of being cleanly separated into the each language's own plugins.

  • File templates and project templates are handled by TemplatingPlugin for all languages, which uses TemplatingHelper and ProjectTemplateType.
  • UtilsCore.setProjectMenuType() checks the ProjectVO subclass at runtime to return a ProjectMenuTypes constant. This is used to determine which menu items to display in the Project menu.
  • IFlexCoreBridge.getWindowsMenu() uses ProjectMenuTypes to determine if certain menu items should be enabled or disabled.
  • CreateProject also uses ProjectMenuTypes.

Syntax plugin

A language's syntax plugin allows an editor to display the language's keywords and other syntax highlighted in different colors.

Line parser

Most languages can extend LineParser as a base class. Some more complex languages (types of markup that contains scripts, for instance — such as HTML and JS, or MXML and AS3) may need to extend ContextSwitchLineParser instead.

class XYZLineParser extends LineParser {
    public function new() {
        super();
    }
}

In the line parser, define constants for various types of syntax, such as strings, keywords, comments, or regular expressions. Each type of syntax should have a unique integer value that is greater than 0 (the value of 0 is reserved). Every language's syntax is different, of course, so the number of constants, and their meanings, will vary from language to language. Additionally, the integer constant used for one language's type of syntax isn't necessarily the same as the integer constant for similar syntax. In other words, line comments are 0x3 in the example below, but that doesn't mean that they must always be 0x3 for every language.

public static final XYZ_CODE:Int = 0x1;
public static final XYZ_STRING:Int = 0x2;
public static final XYZ_SINGLE_LINE_COMMENT:Int = 0x3;
public static final XYZ_MULTI_LINE_COMMENT:Int = 0x4;
public static final XYZ_KEYWORD:Int = 0x5;
public static final XYZ_VARIABLE_KEYWORD:Int = 0x6;
public static final XYZ_FUNCTION_KEYWORD:Int = 0x7;
public static final XYZ_CLASS_KEYWORD:Int = 0x8;

There should be at least one fallback constant for code that receives no special highlighting. In the code above, this constant is named XYZ_CODE, and it has the value of 0x1. In the constructor, it should be passed to the _defaultContext and context member variables. The _defaultContext is the fallback value when no syntax matches, while context is the initial context when the parser initially begins work on a new file (The initial context is allowed to be different from the default, but it is often the same).

A line parser uses regular expressions to identify language syntax, such as keywords, comments, strings, numeric values, etc. These are stored in member variables, which are assigned in the constructor.

The wordBoundaries member variable is used to determine where to split the code into separate words. Usually, the regular expression in the example below will work well for most languages, but it may need some tweaking, depending on the punctuation used by the language for operators and other language syntax:

wordBoundaries = ~/([\s,(){}\[\]\-+*%\/="'~!&|<>?:;.]+)/;

The patterns member variable defines regular expressions used to identify specific types of syntax, such as strings and comments. It does not include keywords, which are defined separately.

patterns = [
    // "
    new LineParserPattern(XYZ_STRING, ~/^"(?:\\\\|\\"|[^\n])*?(?:"|\\\n|(?=\n))/),
    // //
    new LineParserPattern(XYZ_SINGLE_LINE_COMMENT, ~/^\/\/.*/),
    // /*
    new LineParserPattern(XYZ_MULTI_LINE_COMMENT, ~/^\/\*.*?(?:\*\/|\n)/),
];

Some patterns may continue indefinitely until an end pattern is recognized, and these should be included in the endPatterns member variable.

endPatterns = [
    new LineParserPattern(XYZ_STRING, ~/(?:^|[^\\])("|(?=\n))/),
    new LineParserPattern(XYZ_MULTI_LINE_COMMENT, ~/\*\//),
];

Language keywords are passed to the keywords member variable, which is a Map<Int, Array<String>>.

keywords = [
    XYZ_KEYWORD => [
        'if', 'else', 'for'
    ],
    HX_VARIABLE_KEYWORD => ['var'],
    HX_FUNCTION_KEYWORD => ['function'],
    HX_CLASS_KEYWORD => ['class'],
];

The integer keys are one of the constants that were defined previously. The values are arrays string keywords.

By using a map, certain keywords can be given different colors than other keywords. If all keywords should have the same formatting, then this map is certainly allowed to contain one key-value pair only.

Syntax format builder

Next, create a syntax format builder for the language. It will map syntax identified by the ILineParser to TextFormat objects that are used by the TextEditor component for coloring/styling code.

class XYZSyntaxFormatBuilder {
    private var _colorSettings:SyntaxColorSettings;
    private var _fontSettings:SyntaxFontSettings;

    public function new() {}

    public function setColorSettings(settings:SyntaxColorSettings):HaxeSyntaxFormatBuilder {
        _colorSettings = settings;
        return this;
    }

    public function setFontSettings(settings:SyntaxFontSettings):HaxeSyntaxFormatBuilder {
        _fontSettings = settings;
        return this;
    }

    public function build():Map<Int, TextFormat> {
        var formats:Map<Int, TextFormat> = [];
        // TODO: map XYZLineParser constants to TextFormats
        return formats;
    }

    private function getTextFormat(fontColor:UInt):TextFormat {
        var format = new TextFormat(_fontSettings.fontFamily, _fontSettings.fontSize, fontColor);
        format.tabStops = _fontSettings.tabStops;
        return format;
    }
}

The setColorSettings() and setFontSettings() methods are used to pass in instances of the SyntaxColorSettings and SyntaxFontSettings classes, respectively. These are used to provide colors and font styles needed to create the TextFormat objects.

Inside the build() method, create mappings between ILineParser constants and TextFormat instances.

The first constant is 0, which is reserved by the text editor for marking syntax as unrecognized, or otherwise invalid. If there's a bug in the line parser, this is a good way to detect the issue. The SyntaxColorSettings object has an invalidColor field that is usually red or another bright color that contrasts well with the other colors.

formats.set(0 /* default, parser fault */, getTextFormat(_colorSettings.invalidColor));

For the rest of the formats, the constants from XYZLineParser are used, along with the built-in SyntaxColorSettings color values. If none of the color values feel like an exact match, use whichever one seems most appropriate. If none are even remotely close enough, consider opening a feature request to add a new field to SyntaxColorSettings.

formats.set(XYZLineParser.XYZ_CODE, getTextFormat(_colorSettings.foregroundColor));
formats.set(XYZLineParser.XYZ_STRING, getTextFormat(_colorSettings.stringColor));
formats.set(XYZLineParser.XYZ_SINGLE_LINE_COMMENT, getTextFormat(_colorSettings.commentColor));
formats.set(XYZLineParser.XYZ_MULTI_LINE_COMMENT, getTextFormat(_colorSettings.commentColor));
formats.set(XYZLineParser.XYZ_KEYWORD, getTextFormat(_colorSettings.keywordColor));
formats.set(XYZLineParser.XYZ_VARIABLE_KEYWORD, getTextFormat(_colorSettings.fieldKeywordColor));
formats.set(XYZLineParser.XYZ_FUNCTION_KEYWORD, getTextFormat(_colorSettings.methodKeywordColor));
formats.set(XYZLineParser.XYZ_CLASS_KEYWORD, getTextFormat(_colorSettings.typeKeywordColor));

Syntax plugin implementation

The syntax plugin should extend PluginBase. A few getters should always be overridden when creating a new plugin.

package actionScripts.plugin.syntax
{
    import actionScripts.plugin.PluginBase;
    import actionScripts.valueObjects.ConstantsCoreVO;

    public class XYZSyntaxPlugin extends PluginBase
    {
        override public function get name():String
        {
            return "XYZ Syntax Plugin";
        }

        override public function get author():String
        {
            return ConstantsCoreVO.MOONSHINE_IDE_LABEL + " Project Team";
        }

        override public function get description():String
        {
            return "Provides highlighting for XYZ.";
        }

        public function XYZSyntaxPlugin()
        {
            super();
        }
    }
}

The syntax plugin should listen for a few events when it activates, and it should remove them when deactivated.

  • EditorPluginEvent.EVENT_EDITOR_OPEN is dispatched when any new editor is opened by Moonshine. Check its file extension to see if the syntax plugin should handle the new editor or ignore it. When the editor contains a file with the correct language, the plugin will apply font and color styles, and it will also initialize "smart" editor features such as bracket identification, auto-closing character pairs, and strings to use for single-line and multi-line comments (all of these features are optional, and some languages may not support one or more of them).

  • TextEditorSettingsEvent.SYNTAX_COLOR_SCHEME_CHANGE is dispatched when Moonshine changes its color scheme in the global settings (some of the built-in color schemes include light, dark, or monokai). When the color scheme changes, the syntax plugin should update the text editor's styles to match.

  • TextEditorSettingsEvent.FONT_SIZE_CHANGE is dispatched when Moonshine changes the editor font size. The syntax plugin should update the text editor's styles to match.

override public function activate():void
{ 
    super.activate();
    dispatcher.addEventListener(EditorPluginEvent.EVENT_EDITOR_OPEN, handleEditorOpen);
    dispatcher.addEventListener(TextEditorSettingsEvent.SYNTAX_COLOR_SCHEME_CHANGE, handleSyntaxColorChange);
    dispatcher.addEventListener(TextEditorSettingsEvent.FONT_SIZE_CHANGE, handleFontSizeChange);
}

override public function deactivate():void
{ 
    super.deactivate();
    dispatcher.removeEventListener(EditorPluginEvent.EVENT_EDITOR_OPEN, handleEditorOpen);
    dispatcher.removeEventListener(TextEditorSettingsEvent.SYNTAX_COLOR_SCHEME_CHANGE, handleSyntaxColorChange);
    dispatcher.removeEventListener(TextEditorSettingsEvent.FONT_SIZE_CHANGE, handleFontSizeChange);
}

The listener for EditorPluginEvent.EVENT_EDITOR_OPEN needs to check the file extension of the file open in the text editor to determine if it should apply syntax highlighting to the editor.

private function handleEditorOpen(event:EditorPluginEvent):void
{
    if (isExpectedType(event.fileExtension))
    {
        var textEditor:TextEditor = event.editor;
        initializeTextEditor(textEditor);
    }
}

To implement the isExpectedType() method, a constant for the language's file extension will be needed.

private static const FILE_EXTENSION_XYZ:String = "xyz";

Then, the implementation of isExpectedType() checks if the file extension of the opened editor matches.

private function isExpectedType(type:String):Boolean
{
    return type == FILE_EXTENSION_XYZ;
}

Some languages may require multiple file extensions. Even if the language has only one file extension, it's a good idea to add isExpectedType() instead of using if (type == FILE_EXTENSION_XYZ). This not only ensures a certain consistency among all of Moonshine's syntax plugins, but it also allows additional file extensions for a particular language to be more easily added in the future, if necessary.

If the file extension matches, the initializeTextEditor() method is called. It may be implemented like this:

private function initializeTextEditor(textEditor:TextEditor):void
{
    var colorSettings:SyntaxColorSettings = getColorSettings();
    applyFontAndSyntaxSettings(textEditor, colorSettings);
    textEditor.brackets = null;
    textEditor.autoClosingPairs = null;
    textEditor.lineComment = null;
    textEditor.blockComment = null;
}

This not only applies syntax highlighting, but it also customizes other smart language features. Let's look at colors and fonts first. The getColorSettings() method returns the colors for the currently selected color scheme in Moonshine's settings. This is exactly the same in all syntax plugins (and probably should be refactored into a subclass or utility function).

private function getColorSettings():SyntaxColorSettings
{
    var colorSettings:SyntaxColorSettings;
    switch (model.syntaxColorScheme)
    {
        case TextEditorPlugin.SYNTAX_COLOR_SCHEME_DARK:
            return SyntaxColorSettings.defaultDark();
        case TextEditorPlugin.SYNTAX_COLOR_SCHEME_MONOKAI:
            return SyntaxColorSettings.monokai();
        default: // light
            return SyntaxColorSettings.defaultLight();
    }
}

The initializeTextEditor() method passes the text editor and the SyntaxColorSettings created above to applyFontAndSyntaxSettings().

private function applyFontAndSyntaxSettings(textEditor:TextEditor, colorSettings:SyntaxColorSettings):void
{
    var formatBuilder:XYZSyntaxFormatBuilder = new XYZSyntaxFormatBuilder();
    formatBuilder.setFontSettings(new SyntaxFontSettings(Settings.font.defaultFontFamily, Settings.font.defaultFontSize));
    formatBuilder.setColorSettings(colorSettings);
    var formats:IMap = formatBuilder.build();
    textEditor.setParserAndTextStyles(new XYZLineParser(), formats);
    textEditor.embedFonts = Settings.font.defaultFontEmbedded;
}

This method uses the XYZLineParser and XYZSyntaxFormatBuilder that were created earlier.

In initializeTextEditor(), brackets, auto-closing pairs, and comments may be set to null, but generally, these values should be configured for each language, to enable useful smart features in Moonshine's text editor.

For a typical C-style programming language, brackets will include {}, [], and (), line comments will start with // and block comments will be surrounded by /* and */. Auto-closing pairs will include all of the brackets, but also, any quotes used by the language (commonly, " and '). Some languages may have different ways to handle brackets, comments, and strings, so be sure to set these values appropriately.

textEditor.brackets = [["{", "}"], ["[", "]"], ["(", ")"]];
textEditor.autoClosingPairs = [
    new AutoClosingPair("{", "}"),
    new AutoClosingPair("[", "]"),
    new AutoClosingPair("(", ")"),
    new AutoClosingPair("'", "'"),
    new AutoClosingPair("\"", "\"")
];
textEditor.lineComment = "//";
textEditor.blockComment = ["/*", "*/"];

Earlier, in activate(), listeners were added for TextEditorSettingsEvent.SYNTAX_COLOR_SCHEME_CHANGE and TextEditorSettingsEvent.FONT_SIZE_CHANGE. The implementation of these listener methods should be very similar for both:

private function handleSyntaxColorChange(event:TextEditorSettingsEvent):void
{
    applyFontAndSyntaxSettingsToAll();
}

private function handleFontSizeChange(event:TextEditorSettingsEvent):void
{
    applyFontAndSyntaxSettingsToAll();
}

The applyFontAndSyntaxSettingsToAll() method should look like this:

private function applyFontAndSyntaxSettingsToAll():void
{
    var colorSettings:SyntaxColorSettings = getColorSettings();
    var editorCount:int = model.editors.length;
    for(var i:int = 0; i < editorCount; i++)
    {
        var editor:BasicTextEditor = model.editors.getItemAt(i) as BasicTextEditor;
        if (!editor)
        {
            continue;
        }
        if (editor.currentFile && isExpectedType(editor.currentFile.fileBridge.extension))
        {
            applySyntaxColorSettings(editor.getEditorComponent(), colorSettings);
        }
    }
}

It loops through all of Moonshine's open editors, and if the editor's current file extension matches (by calling isExpectedType(), which was implemented previously), then it calls applySyntaxColorSettings(), which was also implemented previously.

File Templates

Warning: File templates are currently managed by TemplatingPlugin for all languages. This is one of the places where Moonshine still needs a bit of refactoring to decouple this functionality into separate plugins for each language.

An object-oriented language will probably need templates for a class and an interface, at the minimum. Some OOP languages may have more types that might be considered just as prominent (such as enums or structures), and it's certainly possible to add more file templates for those too. For scripts and other languages where types are not commonly placed into seperate files, a generic XYZ File.xyz template may be more appropriate instead. It depends on the language. The examples below show how to add class and interface templates, but the example code is easy to modify to add more or different file templates.

A template file will include tokens, which will dynamically replaced when the file is created. Tokens start with the $ character and are usually followed by a word or phrase in camel-case.

The following is the contents of the Java Class.java.template template file:

package $packageName;

$imports
$modifierA $modifierB class $fileName $extends $implements
{
    public $fileName()
    {
    }
}

It contains several tokens that Moonshine IDE will replace when creating a concrete class, including:

  • $packageName is the package where the class is located. For instance, com.example.
  • $imports is a list of imports that should be added for the superclass or any implemented interfaces.
  • $modifierA, $modifierB, and $modifierC (the last one isn't used in this template) are used for keywords that modify the class, such as public and final for Java.
  • $fileName is the name of the file, without the extension. It is also used for the class name.
  • $extends will contain the extends keyword and the superclass, if there is a superclass.
  • $implements will contain the implements keyword and interfaces, if any were chosen by the user.

The following is the contents of the Java Interface.java.template template file:

package $packageName;

$imports
$modifierA interface $fileName $extends
{
    
}

It's very similar, but fewer tokens are required.

Every language has different syntax, so it is up to the implementer to create appropriate templates for the language being added. However, since most OOP languages are very similar, it's often possible to copy and modify these templates to create XYZ Class.xyz.template and XYZ Interface.xyz.template using different syntax.

In TemplatingPlugin, add member variables to store the file location of the file templates.

private var templateXYZClass:FileLocation;
private var templateXYZInterface:FileLocation;

Then, in TemplatingPlugin.readTemplates(), assign those static variables:

files = templatesDir.resolvePath("files/XYZ Class.hx.template");
if (!files.fileBridge.isHidden && !files.fileBridge.isDirectory)
   templateXYZClass = files;

files = templatesDir.resolvePath("files/XYZ Interface.hx.template");
if (!files.fileBridge.isHidden && !files.fileBridge.isDirectory)
    templateXYZInterface = files;

These values will be passed to the NewXYZFilePopup, which we'll create in the next section.

Create a new file named NewXYZFilePopup.mxml in ide/MoonshineSharedCore/src/components/controls/popup/newFile/. This will be used to create a view that allows a user to customize the file.

Copy the contents from NewJavaFilePopup.mxml and replace references to Java with the new language name, such as XYZ. Further changes may be necessary, but this is a good starting point.

In TemplatingPlugin, add a member variable for the new file view:

private var newXYZComponentPopup:NewXYZFilePopup;

In `TemplatingPlugin.handleNewTemplateFile(), add one or more new cases for the new language.

case "XYZ Class":
    openXYZTypeChoose(event, false);
    break;
case "XYZ Interface":
    openXYZTypeChoose(event, true);
    break;

Copy the entire TemplatingPlugin.openJavaTypeChoose and TemplatingPlugin.handleJavaPopupClose methods, and replace references to Java with the new language name, such as XYZ.

In TemplatingPlugin.onNewFileCreateRequest(), how the package/module name in the new language is replaced may need to be customized.

Project Templates

  • The Moonshine-IDE repo includes a number of project templates in _ide/MoonshineSharedCore/src/elements/templates/projects/.

  • Create a directory with the name of the project template. For instance, if the new language is named XYZ, an appropriate name might be XYZ Project.

  • Inside the new directory, create a file inside the template directory named $Settings.xyzproj.template (replace xyzproj with the project file extension that is used by the new language).

In this case $Settings in the file name is a special token that will be replaced dynamically by Moonshine when the new project is created.

Using the XML project file format designed for the new language, populate this file with sensible defaults. Values that need to be populated dynamically (such as the name of the project, or specific file paths, or any other configuration options available to the user) may include tokens starting with the $ character as placeholders. Later, we'll create a view that Moonshine will display for the user to customize these values, and we'll write the code that populates the values of these tokens. As an example, AS3 and Haxe projects have a $SourcePath token that Moonshine populates with a specific path at the time of project creation.

<classpaths>
  <class path="$SourcePath" />
</classpaths>
  • Inside the project template directory, create a subdirectory for the project's primary classpath. For many languages, it might be called src, but some languages may have a different naming convention (aim to follow the convention that each particular language community prefers).

  • Inside the classpath directory, create a file named $ProjectName.xyz.template. Replace xyz with the appropriate file extension for source files in the new language. $ProjectName is another token that Moonshine typically uses. However, some languages may have different naming conventions for the main class or file. For instance, maybe it should always be called Main.xyz. That's fine. Again, the aim is to follow common naming conventions for each particular language.

Populate this file with an appropriate Hello World style of content for the new language. For a simple Haxe project, that looks like this (notice the use of the $ProjectName token again):

class $ProjectName
{
    static function main():Void {
        trace("Hello, Haxe!");
    } 
}

In src/elements/templates/projects, create a file named XYZ Project.xml and add the following content.

<?xml version="1.0" encoding="UTF-8" ?>
<templates>
    <template displayHome="true">
        <title>XYZ Project</title>
        <homeTitle>XYZ Project</homeTitle>
        <name>XYZ Project</name>
        <icon>XYZ Project.png</icon>
        <description>Create a XYZ project.</description>
    </template>
</templates>

If there are multiple project templates for the language, add additional <template> sections.

Next, in src/elements/templates/projects/icons, add a file named XYZ Project.png containing an appropriate icon for the language. The icon should be a single color, like the other icons. Notice that XYZ Project.png is referenced in the <icon> field above.

Project File Format

Adding a new language to Moonshine involves creating a file format for the projects using that language.

Moonshine IDE's existing project files are derived from from project files created by the FlashDevelop and HaxeDevelop editors. Generally, Moonshine IDE can open existing projects from these editors, but Moonshine may also extend these formats with additional fields that don't exist in the other editors.

For a new language, The file extension should start with an abbreviation of the language (or the full name, if short enough) followed by proj. ActionScript 3.0 is typically abbreviated as AS3 and The Haxe file extension is .hx, so these result in .as3proj and .hxproj, respectively.

These existing project files contain XML. Generally, when adding a new language, developers are encouraged to create a similar format to the existing project types, for consistency.

Include the XML header. Then, the root element should be <project>.

<?xml version="1.0" encoding="utf-8"?>
<project>
</project>

If the new language's project file format ever needs to change in the future, it may be appropriate to add a version number at that time, like <project version="2"/>. However, for the first version, the version may be omitted.

Most compilers allow developers to specify a directory containing source files for the language, commonly called a classpath.

<?xml version="1.0" encoding="utf-8"?>
<project>
  <classpaths>
    <class path="../OtherProject/src"/>
    <class path="src"/>
  </classpaths>
</project>

If Moonshine should expose compiler or linker options for the user to configure, include a <build> element. Each option should be exposed as an <option> element with the option name specified using an attribute, formatted in camel-case.

Boolean values should use True and False with the first letter capitalized. Strings and numeric values need only be valid XML values. For instance, if a string contains the & character, it will be encoded as &amp;.

If Moonshine exposes a free-form field for more advanced options, it is common to add a final <option additional=""/> element

<?xml version="1.0" encoding="utf-8"?>
<project>
  <build>
    <option optionOne="False" />
    <option anotherOption="123" />
    <option theThirdOption="string" />
    <option additional="--abc --def=true" />
  </build>
</project>

Reading and writing this file format is done by XYZImporter and XYZExporter classes. The XML is parsed by the importer to populate the XYZProjectVO. The XYZProjectVO is serialized by the exporter into XML.

See the existing importers and exporters for details:

Java

Haxe

Grails

Project plugin

A project plugin adds a project type to Moonshine for the new language.

Extend LanguageServerProjectVO for the new project type, if the project will have a language server. Otherwise, extend ProjectVO.

package actionScripts.plugin.xyz.xyzproject
{
    public XYZProjectVO extends LanguageServerProjectVO
    {
        public function XYZProjectVO(folder:FileLocation, projectName:String = null, updateToTreeView:Boolean = true) 
        {
            super(folder, projectName, updateToTreeView);
        }
    }
}

When the user right-clicks a project in the file view and chooses Settings, Moonshine will generate a page of settings for the project. The items in this settings list come from the project's getSettings() override.

override public function getSettings():Vector.<SettingsWrapper>
{
    var settings:Vector.<SettingsWrapper> = Vector.<SettingsWrapper>([
        new SettingsWrapper("Paths",
            new <ISetting>[
                // add project settings here
            ]
        )
    ]);
    return settings;
}

When the user saves the project's settings, Moonshine calls the project's saveSettings() override. This should export the project (write the .xyzproj file).

override public function saveSettings():void
{
    XYZExporter.export(this);
}

When the user deletes a project from the file view, Moonshine calls the getProjectFilesToDelete() override.

override public function getProjectFilesToDelete():Array
{
    var filesList:Array = [];
    // add project files to delete here
    return filesList;
}

The project plugin should extend PluginBase and implement IProjectTypePlugin.

package actionScripts.plugin.xyz.xyzproject
{
    import actionScripts.plugin.IProjectTypePlugin;
    import actionScripts.plugin.PluginBase;
    import actionScripts.valueObjects.ConstantsCoreVO;

    public class XYZProjectPlugin extends PluginBase implements IProjectTypePlugin
    {
        override public function get name():String
        {
            return "XYZ Project Plugin";
        }

        override public function get author():String
        {
            return ConstantsCoreVO.MOONSHINE_IDE_LABEL + " Project Team";
        }

        override public function get description():String
        {
            return "XYZ project importing, exporting & scaffolding.";
        }
    }
}

The project plugin should listen for at least one event, NewProjectEvent.CREATE_NEW_PROJECT.

override public function activate():void
{
    super.activate();
    dispatcher.addEventListener(NewProjectEvent.CREATE_NEW_PROJECT, createNewProjectHandler);
}

override public function deactivate():void
{
    super.deactivate();
    dispatcher.removeEventListener(NewProjectEvent.CREATE_NEW_PROJECT, createNewProjectHandler);
}
private function createNewProjectHandler(event:NewProjectEvent):void
{
    if (!canCreateProject(event))
    {
        return;
    }
    
    executeCreateProject = new CreateXYZProject(event);
}

private function canCreateProject(event:NewProjectEvent):Boolean
{
    var projectTemplateName:String = event.templateDir.fileBridge.name;
    return projectTemplateName.indexOf(ProjectTemplateType.XYZ) != -1;
}

CreateXYZProject will be a new class that displays a GUI with settings for creating a new project, such as the project's name and parent directory path. See the existing classes for details:

Add a new type to ProjectMenuTypes for the language. (This is a place where Moonshine still needs some refactoring)

public static const XYZ:String = "XYZ";

In TemplatingHelper.getTemplateMenuType(), add the names of the file templates to the switch body, and limit those templates to the new ProjectMenuTypes.XYZ type.

  • In ProjectMenu.getProjectMenuItems(), customize the Project menu items for the language. This typically includes build, clean, and other tasks.

The IProjectTypePlugin interface requires as projectClass getter to return the languages ProjectVO subclass.

public function get projectClass():Class
{
    return XYZProjectVO;
}

The interface also requires a getProjectMenuItems() method. This populates the menu items in the Project menu in Moonshine's GUI.

public function getProjectMenuItems(project:ProjectVO):Vector.<MenuItem>
{
    return null;
}

In this case, it returns null. However, if the language has a Build Plugin, it will return some MenuItem instances that dispatch events for the build commands. Typically, these menu items trigger events like XYZBuildEvent.BUILD_DEBUG, XYZBuildEvent.BUILD_RELEASE, and XYZBuildEvent.CLEAN. It's also common to include XYZBuildEvent.BUILD_AND_RUN. A more complete implementation might look like this:

private var _projectMenu:Vector.<MenuItem>;
private var resourceManager:IResourceManager = ResourceManager.getInstance();

public function getProjectMenuItems(project:ProjectVO):Vector.<MenuItem>
{
    if (_projectMenu == null)
    {
        var enabledTypes:Array = [ProjectMenuTypes.XYZ];

        _projectMenu = Vector.<MenuItem>([
            new MenuItem(null),
            new MenuItem(resourceManager.getString('resources', 'BUILD_PROJECT'), null, enabledTypes, XYZBuildEvent.BUILD_DEBUG,
                'b', [Keyboard.COMMAND],
                'b', [Keyboard.CONTROL]),
            new MenuItem(resourceManager.getString('resources', 'BUILD_AND_RUN'), null, enabledTypes, XYZBuildEvent.BUILD_AND_RUN,
                "\r\n", [Keyboard.COMMAND],
                "\n", [Keyboard.CONTROL]),
            new MenuItem(resourceManager.getString('resources', 'BUILD_RELEASE'), null, enabledTypes, XYZBuildEvent.BUILD_RELEASE),
            new MenuItem(resourceManager.getString('resources', 'CLEAN_PROJECT'), null, enabledTypes, XYZBuildEvent.CLEAN)
        ]);
        _projectMenu.forEach(function(item:MenuItem, index:int, vector:Vector.<MenuItem>):void
        {
            item.dynamicItem = true;
        });
    }

    return _projectMenu;
}

The IProjectTypePlugin interface has two more methods for opening directories containing a project of type XYZProjectVO.

The testProjectDirectory() method determines if the directory contains a project. Typically, this involves checking for an .xyzproj Moonshine project file. Depending on the language, it might also check for other types of files. For instance, pom.xml or build.gradle is necessary for Java projects.

public function testProjectDirectory(dir:FileLocation):FileLocation
{
    return XYZImporter.test(dir);
}

If testProjectDirectory() returns a file, parseProject() is called with the directory, an optional name, and the path to the file returned by testProjectDirectory().

public function parseProject(projectFolder:FileLocation, projectName:String = null, settingsFileLocation:FileLocation = null):ProjectVO
{
    return XYZImporter.parse(projectFolder, projectName, settingsFileLocation);
}

Language server plugin

A language server plugin launches a language server when a project for the new language is opened. It requires a project plugin to be implemented first.

A language server is an external program that provides code intelligence for an editor or IDE, such as Moonshine. They typically implement features like completion, function signature help, documentation on mouse hover, goto definition, renaming symbols, and more.

The language server plugin should extend the PluginBase class and implement the ILanguageServerPlugin interface.

package actionScripts.plugins.xyz
{
    import actionScripts.languageServer.ILanguageServerManager;
    import actionScripts.plugin.PluginBase;
    import actionScripts.plugin.ILanguageServerPlugin;
    import actionScripts.plugin.xyz.xyzproject.vo.XYZProjectVO;
    import actionScripts.valueObjects.ConstantsCoreVO;
    import actionScripts.valueObjects.ProjectVO;

    public class XYZLanguageServerPlugin extends PluginBase implements ILanguageServerPlugin
    {
        override public function get name():String
        {
            return "XYZ Language Server Plugin";
        }

        override public function get author():String
        {
            return ConstantsCoreVO.MOONSHINE_IDE_LABEL + " Project Team";
        }

        override public function get description():String
        {
            return "XYZ code intelligence provided by a language server";
        }
        
        public function XYZLanguageServerPlugin()
        {
            super();
        }
    }
}

The ILanguageServerPlugin interface requires certain fields.

The languageServerProjectType property returns the language's subclass of ProjectVO. This is used to determine which language server plugin should be associated with a particular project.

public function get languageServerProjectType():Class
{
    return XYZProjectVO;
}

The createLanguageServerManager() methods receives a ProjectVO (which is of type XYZProject) and returns an implementation of ILanguageServerManager, which will be implemented in the next section.

public function createLanguageServerManager(project:ProjectVO):ILanguageServerManager
{
    return new XYZLanguageServerManager(XYZProjectVO(project));
}

The majority of the code will be in the XYZLanguageServerManager class.

Language server manager

A language server manager launches the appropriate executable for the language server, and sets up a LanguageClient to handle communication with the language server.

Be sure to save or bookmark the Language Server Protocol Specification, and familiarize yourself with the "Base Protocol" a bit. While the LanguageClient class implements most of the protocol, there are still places where you may need to customize it, such as listening for custom notifications, or sending custom initialization options.

The XYZLanguageServerManager should implement ILanguageServerManager. Generally, it often also extends ConsoleOutputter to make it easier to write text to Moonshine's console.

package actionScripts.plugins.xyz
{
    import actionScripts.languageServer.ILanguageServerManager;
    import actionScripts.plugin.console.ConsoleOutputter;
    import actionScripts.plugin.xyz.xyzproject.vo.XYZProjectVO;

    public class XYZLanguageServerManager extends ConsoleOutputter implements ILanguageServerManager
    {
        private static const LANGUAGE_ID:String = "xyz";

        private var _project:XYZProjectVO;

        public function get project():ProjectVO
        {
            return _project;
        }

        public function XYZLanguageServerManager(project:XYZProjectVO)
        {
            super();
            _project = project;
        }
    }
}

There's quite a lot of code that goes into implementing a language server in Moonshine, and it's probably a good idea to reference the other ILanguageServerManager implementations to get an idea of what is required. However, be aware that each implementation has its own unique differences, so be sure to compare the implementations from multiple servers before simply copying and pasting a section of code from one of them.

Be aware that this section may contain specific code for certain tasks, but more of a higher-level overview for others. This is the nature of each language server working a bit differently, even if they all conform to the protocol.

Before starting the language server, there is usually a "bootstrapping" step. Typically, this involves checking if all required dependencies are configured in Moonshine's settings. If any are missing, the manager should display a message in Moonshine's console and open the relevant settings page.

For instance, many servers require a runtime like Node.js or the Java Virtual Machine. This runtime will need to be configured in the settings, and it often needs to meet a minimum version requirement. At the time of this writing, the Java language server requires JDK 17 or newer (and this changes from time to time, so it's important to check when updating to a new version of the language server). This typically involves running a native process using a -version option to print to stdout. Moonshine parses the version string and compares to the minimum version. If the minimum version requirement isn't met, a message should be displayed in the console, which mentions the current version and the required version.

Some language servers require all library dependencies to be installed before the language server will initialize correctly. You may need a multi-step bootstrapping step that also runs the appropriate command(s) in a terminal to handle installing all required libraries before attempting to start the language server.

Before each part of the bootstrapping step, it's a good idea to display what's currently happening in Moonshine's status bar:

_dispatcher.dispatchEvent(new StatusBarEvent(
    StatusBarEvent.LANGUAGE_SERVER_STATUS,
    project.name, "Checking XYZ version...", false
));

When bootstrapping is complete, clear the status:

_dispatcher.dispatchEvent(new StatusBarEvent(
    StatusBarEvent.LANGUAGE_SERVER_STATUS,
    project.name
));

After bootstrapping, the native process for the language server should be launched. The command line options probably won't be the same as other language servers, so this means referencing the language server's documentation or examples to determine what's required.

Additionally, reference the documentation or examples to determine if the language server requires a particular communication method (such as stdio or sockets), or how to force it to use a particular communication method.

The language server protocol is not strictly tied to a particular communication method for transferring data. Many language servers communicate over stdio. Some require TCP sockets. These two are most common, but other methods may exist. Generally, Moonshine has traditionally tried to use stdio with all language servers that it bundles. If your language server is implemented in Node.js using vscode-languageserver-node, then you may be able to specify that it should use stdio using the --stdio command line option. Something like this: node server.js --stdio. If your language server is implemented in Java or another runtime, then it probably requires a different method of configuration.

Be sure to use EnvironmentSetupUtils.getInstance().initCommandGenerationToSetLocalEnvironment() any time that you start a native process to ensure that the correct environment variables are configured.

Most likely, you will listen for a stderr event and an exit event from the native process.

_languageServerProcess.addEventListener(ProgressEvent.STANDARD_ERROR_DATA, languageServerProcess_standardErrorDataHandler);
_languageServerProcess.addEventListener(NativeProcessExitEvent.EXIT, languageServerProcess_exitHandler);

Write the stderr text to the debug console using trace(). It's mostly useful for Moonshine contributors and generally isn't displayed to users.

private function languageServerProcess_standardErrorDataHandler(e:ProgressEvent):void
{
    var output:IDataInput = _languageServerProcess.standardError;
    var data:String = output.readUTFBytes(output.bytesAvailable);
    trace(data);
}

The exit listener should clean up the native process (including shutting down the client, if it still exists):

private function languageServerProcess_exitHandler(e:NativeProcessExitEvent):void {
    if(_languageClient)
    {
        //this should have already happened, but if the process exits
        //abnormally, it might not have
        _languageClient.shutdown();
        
        warning("XYZ language server exited unexpectedly. Close the " + project.name + " project and re-open it to enable code intelligence.");
    }
    _languageServerProcess.removeEventListener(ProgressEvent.STANDARD_ERROR_DATA, languageServerProcess_standardErrorDataHandler);
    _languageServerProcess.removeEventListener(NativeProcessExitEvent.EXIT, languageServerProcess_exitHandler);
    _languageServerProcess.exit();
    _languageServerProcess = null;
}

Later, we'll add some more code to the exit listener to handle shutdown timeouts and restarting the language server.

Moonshine usually includes a "wrapper" script/executable around the real language server. It prints the process ID to stdout before starting the real language server so that it can be tracked in Moonshine's Language Server Monitor view. It's probably easiest to skip implementing this wrapper until you have the real language server working directly.

After starting the native process, create the LanguageClient to start communication.

_languageClient.addEventListener(Event.INIT, languageClient_initHandler);
_languageClient.addEventListener(Event.CLOSE, languageClient_closeHandler);

A basic implementation of these listeners looks like this:

private function languageClient_initHandler(event:Event):void
{
    dispatchEvent(new Event(Event.INIT));
}

private function languageClient_closeHandler(event:Event):void
{
    dispatchEvent(new Event(Event.CLOSE));
}

However, more code will often be required in these listeners.

For the Event.INIT listener, the workspace/didChangeConfiguration notification may need to be sent for some language servers. See the Java language server for an example.

The Event.CLOSE listener should also handle the case where the language server is being restarted. For instance, it may need to be restarted when an SDK path or some other setting changed. And if the language server is not being restarted, everything should be disposed.

You should also listen for the following notifications from the language client:

_languageClient.addEventListener(LspNotificationEvent.PUBLISH_DIAGNOSTICS, languageClient_publishDiagnosticsHandler);
_languageClient.addEventListener(LspNotificationEvent.REGISTER_CAPABILITY, languageClient_registerCapabilityHandler);
_languageClient.addEventListener(LspNotificationEvent.UNREGISTER_CAPABILITY, languageClient_unregisterCapabilityHandler);
_languageClient.addEventListener(LspNotificationEvent.LOG_MESSAGE, languageClient_logMessageHandler);
_languageClient.addEventListener(LspNotificationEvent.SHOW_MESSAGE, languageClient_showMessageHandler);
_languageClient.addEventListener(LspNotificationEvent.APPLY_EDIT, languageClient_applyEditHandler);

These notification listeners generally have the same implementation for all language servers:

private function languageClient_publishDiagnosticsHandler(event:LspNotificationEvent):void
{
    var params:PublishDiagnosticsParams = PublishDiagnosticsParams(event.params);
    var uri:String = params.uri;
    var diagnostics:Array = params.diagnostics;
    _dispatcher.dispatchEvent(new DiagnosticsEvent(DiagnosticsEvent.EVENT_SHOW_DIAGNOSTICS, uri, project, diagnostics));
}

private function languageClient_registerCapabilityHandler(event:LspNotificationEvent):void
{
    var params:RegistrationParams = RegistrationParams(event.params);
    var registrations:Array = params.registrations;
    for each(var registration:Registration in registrations)
    {
        var method:String = registration.method;
        switch(method)
        {
            case LanguageClient.METHOD_WORKSPACE__DID_CHANGE_WATCHED_FILES:
                var registerOptions:Object = registration.registerOptions;
                _watchedFiles[registration.id] = registerOptions.watchers.map(function(watcher:Object, index:int, source:Array):Object {
                    return GlobPatterns.toRegExp(watcher.globPattern);
                });
                break;
        }
        _dispatcher.dispatchEvent(new ProjectEvent(ProjectEvent.LANGUAGE_SERVER_REGISTER_CAPABILITY, _project, method));
    }
}

private function languageClient_unregisterCapabilityHandler(event:LspNotificationEvent):void
{
    var params:UnregistrationParams = UnregistrationParams(event.params);
    var unregistrations:Array = params.unregistrations;
    for each(var unregistration:Unregistration in unregistrations)
    {
        var method:String = unregistration.method;
        switch(method)
        {
            case LanguageClient.METHOD_WORKSPACE__DID_CHANGE_WATCHED_FILES:
                delete _watchedFiles[unregistration.id];
                break;
        }
        _dispatcher.dispatchEvent(new ProjectEvent(ProjectEvent.LANGUAGE_SERVER_UNREGISTER_CAPABILITY, _project, method));
    }
}

private function languageClient_logMessageHandler(event:LspNotificationEvent):void
{
    var params:LogMessageParams = LogMessageParams(event.params);
    var message:String = params.message;
    var type:int = params.type;
    var eventType:String = null;
    switch(type)
    {
        case 1: //error
        {
            eventType = ConsoleOutputEvent.TYPE_ERROR;
            break;
        }
        default:
        {
            eventType = ConsoleOutputEvent.TYPE_INFO;
        }
    }
    _dispatcher.dispatchEvent(
        new ConsoleOutputEvent(ConsoleOutputEvent.CONSOLE_PRINT, message, false, false, eventType)
    );
    trace(message);
}

private function languageClient_showMessageHandler(event:LspNotificationEvent):void
{
    var params:ShowMessageParams = ShowMessageParams(event.params);
    var message:String = params.message;
    var type:int = params.type;
    var eventType:String = null;
    switch(type)
    {
        case 1: //error
        {
            eventType = ConsoleOutputEvent.TYPE_ERROR;
            break;
        }
        default:
        {
            eventType = ConsoleOutputEvent.TYPE_INFO;
        }
    }
    
    Alert.show(message);
}

private function languageClient_applyEditHandler(event:LspNotificationEvent):void
{
    var params:Object = event.params;
    var workspaceEdit:WorkspaceEdit = WorkspaceEdit(params.edit);
    applyWorkspaceEdit(workspaceEdit)
}

Add a cleanup method where those LanguageClient listeners are removed:

protected function cleanupLanguageClient():void
{
    if(!_languageClient)
    {
        return;
    }
    _languageClient.removeEventListener(Event.INIT, languageClient_initHandler);
    _languageClient.removeEventListener(Event.CLOSE, languageClient_closeHandler);
    _languageClient.removeEventListener(LspNotificationEvent.PUBLISH_DIAGNOSTICS, languageClient_publishDiagnosticsHandler);
    _languageClient.removeEventListener(LspNotificationEvent.REGISTER_CAPABILITY, languageClient_registerCapabilityHandler);
    _languageClient.removeEventListener(LspNotificationEvent.UNREGISTER_CAPABILITY, languageClient_unregisterCapabilityHandler);
    _languageClient.removeEventListener(LspNotificationEvent.LOG_MESSAGE, languageClient_logMessageHandler);
    _languageClient.removeEventListener(LspNotificationEvent.SHOW_MESSAGE, languageClient_showMessageHandler);
    _languageClient.removeEventListener(LspNotificationEvent.APPLY_EDIT, languageClient_applyEditHandler);
    _languageClient = null;
}

Stopping the language server involves either informing the LanguageClient that it needs to shutdown. Or, if the language client hasn't yet been created or initialized, forcing the language server process to exit.

When telling the LanguageClient to shutdown, add a timeout of 8 seconds. If the shutdown process doesn't complete in that time, force the language server process to exit instead. Otherwise, Moonshine might not be able to properly exit because it could wait indefinitely for misbehaving language servers to close.

private function shutdown():void
{
    if(!_languageClient || !_languageClient.initialized)
    {
        if (_languageClient)
        {
            cleanupLanguageClient();
        }
        if (_languageServerProcess)
        {
            _languageServerProcess.exit(true);
        }
        return;
    }
    _shutdownTimeoutID = setTimeout(shutdownTimeout, LANGUAGE_SERVER_SHUTDOWN_TIMEOUT);
    _languageClient.shutdown();
}

private function shutdownTimeout():void
{
    _shutdownTimeoutID = uint.MAX_VALUE;
    if (!_languageServerProcess) {
        return;
    }
    var message:String = "Timed out while shutting down Java language server for project " + _project.name + ". Forcing process to exit.";
    warning(message);
    trace(message);
    _languageClient = null;
    _languageServerProcess.exit(true);
}

When the language server process dispatches NativeProcessExitEvent.EXIT, be sure to clear the timeout.

private function languageServerProcess_exitHandler(e:NativeProcessExitEvent):void
{
    if (_shutdownTimeoutID != uint.MAX_VALUE) {
        clearTimeout(_shutdownTimeoutID);
        _shutdownTimeoutID = uint.MAX_VALUE;
    }
    // ... the rest of the exit code
}

If the runtime or SDK path is changed in Moonshine's settings (or the project-specific settings) after the language server starts, the language server should be stopped and restarted.

private function restartLanguageServer():void
{
    if(_waitingToRestart)
    {
        //we'll just continue waiting
        return;
    }
    _waitingToRestart = false;
    if(_languageClient || _languageServerProcess)
    {
        _waitingToRestart = true;
        shutdown();
    }

    if(!_waitingToRestart)
    {
        bootstrapThenStartNativeProcess();
    }
}

At the end of the NativeProcessExitEvent.EXIT listener, handle the _waitingToRestart flag:

if(_waitingToRestart)
{
    _waitingToRestart = false;
    bootstrapThenStartNativeProcess();
}

Build plugin

A build plugin launches a compiler or other build tool. It requires a project plugin's getProjectMenuItems() to be implemented first, to expose the build events in the Moonshine user interface.

The build plugin should extend ConsoleBuildPluginBase. A few getters should always be overridden when creating a new plugin.

package actionScripts.plugins.xyz
{
    import actionScripts.plugins.build.ConsoleBuildPluginBase;
    import actionScripts.plugin.settings.ISettingsProvider;

    public class XYZBuildPlugin extends ConsoleBuildPluginBase implements ISettingsProvider
    {
        override public function get name():String
        {
            return "XYZ Build Setup";
        }

        override public function get author():String
        {
            return ConstantsCoreVO.MOONSHINE_IDE_LABEL + " Project Team";
        }

        override public function get description():String
        {
            return "XYZ Build Plugin.";
        }

        public function XYZBuildPlugin()
        {
            super();
        }
    }
}

Build plugins generally require settings for things like SDK paths and other command line tooling. This requires implementing the ISettingsProvider interface, which requires some methods, including getSettingsList() and onSettingsClose().

public function getSettingsList():Vector.<ISetting>
{
    onSettingsClose();

    return new <ISetting>[
        // settings go here
    ];
}

override public function onSettingsClose():void
{
    // clean up settings
}

Create an Event subclass that defines constants for building a project (example: HaxeBuildPlugin). Typically, these are BUILD_DEBUG, BUILD_RELEASE, BUILD_AND_RUN and CLEAN.

package actionScripts.plugins.xyz.events;

import openfl.events.Event;

class XYZBuildEvent extends Event {
	public static final BUILD_DEBUG:String = "xyzBuildDebug";
	public static final BUILD_RELEASE:String = "xyzBuildRelease";
	public static final BUILD_AND_RUN:String = "xyzBuildAndRun";
	public static final CLEAN:String = "xyzClean";

	public function new(type:String, bubbles:Bool = false, cancelable:Bool = false) {
		super(type, bubbles, cancelable);
	}
}

In an override of the activate() method, add listeners for the event constants. Also add a listener for ProjectActionEvent.BUILD_AND_DEBUG, if Moonshine has a debug adapter for the generated output. Similarly, in an override of the deactivate() method, remove the listeners for the same events added in activate().

override public function activate():void
{
    super.activate();
    dispatcher.addEventListener(XYZBuildEvent.BUILD_DEBUG, buildDebugHandler);
    dispatcher.addEventListener(XYZBuildEvent.BUILD_RELEASE, buildReleaseHandler);
    dispatcher.addEventListener(XYZBuildEvent.CLEAN, cleanHandler);
    dispatcher.addEventListener(XYZBuildEvent.BUILD_AND_RUN, buildAndRunHandler);
    dispatcher.addEventListener(ProjectActionEvent.BUILD_AND_DEBUG, buildAndDebugHandler);
}

override public function deactivate():void
{
    super.deactivate();
    dispatcher.removeEventListener(XYZBuildEvent.BUILD_DEBUG, buildDebugHandler);
    dispatcher.removeEventListener(XYZBuildEvent.BUILD_RELEASE, buildReleaseHandler);
    dispatcher.removeEventListener(XYZBuildEvent.CLEAN, cleanHandler);
    dispatcher.removeEventListener(XYZBuildEvent.BUILD_AND_RUN, buildAndRunHandler);
    dispatcher.removeEventListener(ProjectActionEvent.BUILD_AND_DEBUG, buildAndDebugHandler);
}

An example of a XYZBuildEvent.BUILD_DEBUG listener appears below.

private function buildDebugHandler(event:Event):void
{
    var project:XYZProjectVO = model.activeProject as XYZProjectVO;
    if (!project)
    {
        return;
    }
    clearOutput();

    var commandParts:Array = ["command", "arg1", "arg2", "arg3"];
    start(new <String>[commandParts.join(" ")], project.folderLocation);
}

The clearOutput() method comes from a superclass, and it clears the output of Moonshine's console.

The start() method comes from a superclass, and it runs a command in a terminal. The plugin doesn't necessarily need to run a command in a terminal. To clean a project, it could simply delete files in a particular directory using OpenFL's File class, if that's more appropriate for the new language. However, when doing it this way, be sure to check that the output directory is separate from the project/source directory. Avoid deleting the entire project accidentally!

Implementations of buildReleaseHandler and cleanHandler will be similar.

buildAndRunHandler and buildAndDebugHandler are more complex, and some state needs to be saved until after the compilation completes. This can be done in an override of onNativeProcessExit, originally defined in the superclass.

override protected function onNativeProcessExit(event:NativeProcessExitEvent):void
{
    super.onNativeProcessExit(event);
}

Project Menu Types

Warning: ProjectMenuTypes provides a central source of constants for all project types (all languages). This is one of the places where Moonshine still needs a bit of refactoring to decouple this functionality into separate plugins for each language.

Add constant to ProjectMenuTypes for the project menu type.

public static const XYZ:String = "xyz";

Add else if for XYZProjectVO to UtilsCore.setProjectMenuType()

else if (value is XYZProjectVO)
{
    currentMenuType = ProjectMenuTypes.XYZ;
}

In IFlexCoreBridgeImp, add ProjectMenuTypes.XYZ to the appropriate menu items in getWindowsMenu().

In CreateProject, add a new private member variable named isXYZProject:

private var isXYZProject:Boolean;

In CreateProject.setProjectType(), set the default value of false at the start of the method.

isXYZProject = false;

Then, set it to true, if the template name matches a particular string (yes, this really should be refactored to avoid potential conflicts).

else if (templateName.indexOf(ProjectTemplateType.XYZ) != -1)
{
    isXYZProject = true;
}

There may be other places in the file where isXYZProject should be considered. Search for isHaxeProject or isJavaProject to find them.

Integration

To include all of the new plugins in Moonshine, they should be referenced by the IProjectBridgeImpl class.

  • Add references to each of the new plugins to the array returned by IProjectBridgeImpl.getDefaultPlugins().
  • Add references to any of the plugins that don't have any settings to the array returned by IProjectBridgeImpl.getPluginsNotToShowInSettings(). In other words, if a plugin has any settings, don't add it here.
⚠️ **GitHub.com Fallback** ⚠️