In my last blog post I introduced the DSL we’ll ship with e(fx)clipse 2.1 in August 2015.
Our main deployment platform is of course e4 on JavaFX but because we have a clean architecture based on IoC and services our components don’t know about OSGi and hence can be used in any Java(FX) application no matter if you run on OSGi or not.
To show you that those are not idle words the first blog showing the code editor components in action uses plain Java and maven as build tool – for those who want to use them in OSGi a blog will follow soon.
The following video demonstrates the final application in action
Step 1: Install e(fx)clipse 2.1 or later
At the time of this writing e(fx)clipse 2.1 has not been released so the best option to get started is to download our all-in-one nightly build.
Step 2: Create a new maven project
Step 3: Modify the pom.xml
First we need modify the source and target version for the Java compiler by adding:
<!-- ... --> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <!-- ... -->
Because the dependencies have not yet been released we need to add the Sonatype snapshot repository with:
<!-- ... --> <repositories> <repository> <id>sonatype-snapshots</id> <url>https://oss.sonatype.org/content/repositories/snapshots/</url> </repository> </repositories> <!-- ... -->
And finally we add the JavaFX-Code editor component with:
<!-- ... --> <dependencies> <dependency> <groupId>at.bestsolution.eclipse</groupId> <artifactId>org.eclipse.fx.code.editor.fx</artifactId> <version>2.1.0-SNAPSHOT</version> </dependency> </dependencies> <!-- ... -->
At the end your pom.xml should look like this:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>at.bestsolution.sample</groupId> <artifactId>at.bestsolution.sample.code</artifactId> <version>0.0.1-SNAPSHOT</version> <repositories> <repository> <id>sonatype-snapshots</id> <url>https://oss.sonatype.org/content/repositories/snapshots/</url> </repository> </repositories> <dependencies> <dependency> <groupId>at.bestsolution.eclipse</groupId> <artifactId>org.eclipse.fx.code.editor.fx</artifactId> <version>2.1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
Step 4: Language definition
Now that we have configured our project appropriately we can start defining our application. We start with creating a package like “at.bestsolution.sample.code” and there we add a file named “dart.ldef”
After the file is created. Eclipse will prompt you for adding the Xtext nature to your project because of course the DSL is implemented with the help of Xtext.
The last step before we start defining our language is to make some modifications to the project. So open the project properties and navigate to LDef/Compiler check the “Enable project specific settings” and modify the Output Folder/Directory value to src/main/java
Now paste the following content to the dart.ldef file:
package at.bestsolution.sample.code
dart {
partitioning {
partition __dftl_partition_content_type
partition __dart_singlelinedoc_comment
partition __dart_multilinedoc_comment
partition __dart_singleline_comment
partition __dart_multiline_comment
partition __dart_string
rule {
single_line __dart_string "'" => "'"
single_line __dart_string '"' => '"'
single_line __dart_singlelinedoc_comment '///' => ''
single_line __dart_singleline_comment '//' => ''
multi_line __dart_multilinedoc_comment '/**' => '*/'
multi_line __dart_multiline_comment '/*' => '*/'
}
}
lexical_highlighting {
rule __dftl_partition_content_type whitespace javawhitespace {
default dart_default
dart_operator {
character [ ';', '.', '=', '/', '\\', '+', '-', '*', '<', '>', ':', '?', '!', ',', '|', '&', '^', '%', '~' ]
}
dart_bracket {
character [ '(', ')', '{', '}', '[', ']' ]
}
dart_keyword {
keywords [ "break", "case", "catch", "class", "const", "continue", "default"
, "do", "else", "enum", "extends", "false", "final", "finally", "for"
, "if", "in", "is", "new", "null", "rethrow", "return", "super"
, "switch", "this", "throw", "true", "try", "var", "void", "while"
, "with" ]
}
dart_keyword_1 {
keywords [ "abstract", "as", "assert", "deferred"
, "dynamic", "export", "external", "factory", "get"
, "implements", "import", "library", "operator", "part", "set", "static"
, "typedef" ]
}
dart_keyword_2 {
keywords [ "async", "async*", "await", "sync*", "yield", "yield*" ]
}
dart_builtin_types {
keywords [ "num", "String", "bool", "int", "double", "List", "Map" ]
}
}
rule __dart_singlelinedoc_comment {
default dart_doc
dart_doc_reference {
single_line "[" => "]"
}
}
rule __dart_multilinedoc_comment {
default dart_doc
dart_doc_reference {
single_line "[" => "]"
}
}
rule __dart_singleline_comment {
default dart_single_line_comment
}
rule __dart_multiline_comment {
default dart_multi_line_comment
}
rule __dart_string {
default dart_string
dart_string_inter {
single_line "${" => "}"
//TODO We need a $ => IDENTIFIER_CHAR rule
}
}
}
integration {
javafx {
java "at.bestsolution.sample.code.generated"
}
}
}
I won’t explain most of the files content because I’ve done that already in the last blog post “Defining source editors with a DSL” but in brief it holds the paritioning and tokenizing rules required to provide lexical highlighting for Google Dart.
The only new section is:
package org.eclipse.fx.code.dart
dart {
....
integration {
javafx {
java "at.bestsolution.sample.code.generated"
}
}
}
This section holds the configuration for the code generator. In our case we instruct it to generate some java code for us and your project explorer should show something similar to this.
For us the 2 most important classes are:
DartPartitioner: This one is responsible for partitioning your source file into eg comment-sections, code-sections, string-sections, …DartPresentationReconciler: This one is responsible to tokenize the different partitions eg to create keyword-tokens, ….
Step 5: Setup an editor control
Now that the parsing infrastructure is in place we can create our setup for the editor control. For that we create a new class “DartEditor” in “at.bestsolution.sample.code” and make it extend “org.eclipse.fx.code.editor.fx.TextEditor“.
package at.bestsolution.sample.code;
import org.eclipse.fx.code.editor.StringInput;
public class DartEditor extends TextEditor {
public DartEditor(StringInput input) {
setInput(input);
setDocument(new InputDocument(input));
setPartitioner(new DartPartitioner());
setSourceViewerConfiguration(
new DefaultSourceViewerConfiguration(input, new DartPresentationReconciler(), null, null, null)
);
}
}
The above is the minimal configuration you need when creating an editor:
org.eclipse.fx.code.editor.Input: Is the abstraction for the retrieving and storing the content eg on the filesystem, …org.eclipse.jface.text.Document: Is the text buffer behind the text editororg.eclipse.jface.text.IDocumentPartitioner: The component responsible to partition the source fileorg.eclipse.jface.text.source.SourceViewerConfiguration: The component responsible for syntax highlighting, error displaying and auto-completion
The final application
Now that we have an editor component we can build our final application which needs to have a filesystem browser on the left and a tab folder with editors on the right.
package at.bestsolution.sample.code;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.eclipse.fx.code.editor.SourceFileInput;
import org.eclipse.fx.ui.controls.filesystem.FileItem;
import org.eclipse.fx.ui.controls.filesystem.ResourceEvent;
import org.eclipse.fx.ui.controls.filesystem.ResourceItem;
import org.eclipse.fx.ui.controls.filesystem.ResourceTreeView;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
public class DartEditorSample extends Application {
private TabPane tabFolder;
private ResourceTreeView viewer;
static class EditorData {
final Path path;
final DartEditor editor;
public EditorData(Path path, DartEditor editor) {
this.path = path;
this.editor = editor;
}
}
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane p = new BorderPane();
p.setTop(createMenuBar());
viewer = new ResourceTreeView();
viewer.addEventHandler(ResourceEvent.openResourceEvent(), this::handleOpenResource);
p.setLeft(viewer);
tabFolder = new TabPane();
p.setCenter(tabFolder);
Scene s = new Scene(p, 800, 600);
s.getStylesheets().add(getClass().getResource("default.css").toExternalForm());
primaryStage.setScene(s);
primaryStage.show();
}
private MenuBar createMenuBar() {
MenuBar bar = new MenuBar();
Menu fileMenu = new Menu("File");
MenuItem rootDirectory = new MenuItem("Select root folder ...");
rootDirectory.setOnAction(this::handleSelectRootFolder);
MenuItem saveFile = new MenuItem("Save");
saveFile.setAccelerator(new KeyCodeCombination(KeyCode.S,KeyCombination.META_DOWN));
saveFile.setOnAction(this::handleSave);
fileMenu.getItems().addAll(rootDirectory, saveFile);
bar.getMenus().add(fileMenu);
return bar;
}
private void handleSelectRootFolder(ActionEvent e) {
DirectoryChooser chooser = new DirectoryChooser();
File directory = chooser.showDialog(viewer.getScene().getWindow());
if( directory != null ) {
viewer.setRootDirectories(
FXCollections.observableArrayList(ResourceItem.createObservedPath(Paths.get(directory.getAbsolutePath()))));
}
}
private void handleSave(ActionEvent e) {
Tab t = tabFolder.getSelectionModel().getSelectedItem();
if( t != null ) {
((EditorData)t.getUserData()).editor.save();
}
}
private void handleOpenResource(ResourceEvent<ResourceItem> e) {
e.getResourceItems()
.stream()
.filter( r -> r instanceof FileItem)
.map( r -> (FileItem)r)
.filter( r -> r.getName().endsWith(".dart"))
.forEach(this::handle);
}
private void handle(FileItem item) {
Path path = (Path) item.getNativeResourceObject();
Tab tab = tabFolder.getTabs().stream().filter( t -> ((EditorData)t.getUserData()).path.equals(path) ).findFirst().orElseGet(() -> {
return createAndAttachTab(path, item);
});
tabFolder.getSelectionModel().select(tab);
}
private Tab createAndAttachTab(Path path, FileItem item) {
BorderPane p = new BorderPane();
DartEditor editor = new DartEditor(new SourceFileInput(path, StandardCharsets.UTF_8));
editor.initUI(p);
ReadOnlyBooleanProperty modifiedProperty = editor.modifiedProperty();
StringExpression titleText = Bindings.createStringBinding(() -> {
return modifiedProperty.get() ? "*" : "";
}, modifiedProperty).concat(item.getName());
Tab t = new Tab();
t.textProperty().bind(titleText);
t.setContent(p);
t.setUserData(new EditorData(path, editor));
tabFolder.getTabs().add(t);
return t;
}
public static void main(String[] args) {
Application.launch(args);
}
}
The last thing that needs to be done is to fill the “at/bestsolution/sample/code/default.css” with life:
.styled-text-area .list-view {
-fx-background-color: white;
}
.styled-text-area .dart.dart_default {
-styled-text-color: rgb(0, 0, 0);
}
.styled-text-area .dart.dart_operator {
-styled-text-color: rgb(0, 0, 0);
}
.styled-text-area .dart.dart_bracket {
-styled-text-color: rgb(0, 0, 0);
}
.styled-text-area .dart.dart_keyword {
-styled-text-color: rgb(127, 0, 85);
-fx-font-weight: bold;
}
.styled-text-area .dart.dart_keyword_1 {
-styled-text-color: rgb(127, 0, 85);
-fx-font-weight: bold;
}
.styled-text-area .dart.dart_keyword_2 {
-styled-text-color: rgb(127, 0, 85);
-fx-font-weight: bold;
}
.styled-text-area .dart.dart_single_line_comment {
-styled-text-color: rgb(63, 127, 95);
}
.styled-text-area .dart.dart_multi_line_comment {
-styled-text-color: rgb(63, 127, 95);
}
.styled-text-area .dart.dart_string {
-styled-text-color: rgb(42, 0, 255);
}
.styled-text-area .dart.dart_string_inter {
-styled-text-color: rgb(42, 0, 255);
-fx-font-weight: bold;
}
.styled-text-area .dart.dart_builtin_types {
-styled-text-color: #74a567;
-fx-font-weight: bold;
}
.styled-text-area .dart.dart_doc {
-styled-text-color: rgb(63, 95, 191);
}
.styled-text-area .dart.dart_doc_reference {
-styled-text-color: rgb(63, 95, 191);
-fx-font-weight: bold;
}



















