Developing a source code editor in JavaFX (without e4 and OSGi)


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.

app

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

maven

maven2

maven3

maven4

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

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.

ldef_2

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

ldef_3

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.

parser_stuff

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 editor
  • org.eclipse.jface.text.IDocumentPartitioner: The component responsible to partition the source file
  • org.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;
}
This entry was posted in e(fx)clipse. Bookmark the permalink.

19 Responses to Developing a source code editor in JavaFX (without e4 and OSGi)

  1. maxd says:

    Maven snapshot repository doesn’t have JAR files ( look at here: https://oss.sonatype.org/content/repositories/snapshots/at/bestsolution/eclipse/org.eclipse.fx.code.editor.fx/2.1.0-SNAPSHOT/ ) This is cause the following error:

    [ERROR] Failed to execute goal on project application: Could not resolve dependencies for project keylord:application:jar:1.3.0: The following artifacts could not be resolved: at.bestsolution.eclipse:org.eclipse.fx.code.editor.fx:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.code.editor:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.core.di:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.text:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.text.ui:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.ui.controls:jar:2.1.0-SNAPSHOT, at.bestsolution.eclipse:org.eclipse.fx.core:jar:2.1.0-SNAPSHOT: Could not find artifact at.bestsolution.eclipse:org.eclipse.fx.code.editor.fx:jar:2.1.0-SNAPSHOT in sonatype-snapshots (https://oss.sonatype.org/content/repositories/snapshots/) -> [Help 1]

    Could you please check it?

    • Tom Schindl says:

      Sorry – I was certain that all stuff is available but somehow things vanished. I’ve uploaded once more the snapshots, wiped my local directory and now things resolve fine

  2. oreoshake says:

    How can we set a wrapping width on the content of the source area?

    • Tom Schindl says:

      We don’t provide word wrap a the moment – the problem is that we have to fix the cell height for now because of problems with the ListView implementation we are useing. If we’d manage to get rid of that we could implement word wrap in a breeze because a line is made up of a TextFlow who would manage that for us. Feel free to file a bug

  3. Pingback: Access Dart Analysis Server from Java | Tomsondev Blog

  4. Pingback: e(fx)clipse 2.1.0 released | Tomsondev Blog

  5. rhain says:

    hi Tom ,I can’t get org.eclipse.fx.code.editor.fx-2.1.0-SNAPSHOT.jar,but I get org.eclipse.fx.code.editor.fx-2.1.0.jar.But It seems the api has changed. because I get those error on my eclipse.
    DartEditor editor = new DartEditor(new SourceFileInput(path,StandardCharsets.UTF_8));
    —> Cannot instantiate the type SourceFileInput error!!

    ReadOnlyBooleanProperty modifiedProperty = editor.modifiedProperty();
    —> Method undefined for type error.

    How do I can fix those error,and run the appliaction.

    Thanks!

  6. maxd says:

    Have you any plans to release latest version of editor control (without e4 and OSGi)? I have looked to commit log, seems like since version 2.1.0 you have made a great work: added new features and fix a lot of bugs. Will be very interesting to look at it.

  7. Adrodoc55 says:

    This looks really cool, I am propably going to rewrite my mini IDE for a custom language using this. I am also new to JavaFX so this might be a dump question, but how do I add undo-redo functionality for ctrl+z and ctrl+y.
    Also how do Ito set the font family for the whole source (including whitespace)? I tried the following, but it didn’t work for whitespace:
    “`
    .styled-text-area {
    -fx-font-family: Consolas;
    }

    .styled-text-area .list-view {
    -fx-font-family: Consolas;
    -fx-background-color: white;
    }

    .styled-text-area .mpl.mpl_default {
    -fx-font-family: Consolas;
    -styled-text-color: rgb(0, 0, 0);
    }

    .styled-text-area .mpl.mpl_keyword {
    -fx-font-family: Consolas;
    -styled-text-color: rgb(127, 0, 85);
    -fx-font-weight: bold;
    }

    “`

    • Tom Schindl says:

      * You can get the IUndoManager from SourceViewerConfiguration#getUndoManager
      * Your CSS looks correct not sure why it does not work

  8. Akshata SHANBHAG says:

    Hi Tom, I am creating a JavaFX e4 application. I wanted to add project explorer view. What approach do you suggest I do not find org.eclipse.ui in my dependencies.

  9. Guillaume Pichon says:

    Hi Tom, i have some trouble to find a maven build for the Ldef code generation. I didn’t find anything to let maven generate the source code.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.