KlabMonacoEditor embeds the Monaco Editor in a JavaFX
WebView. It can be used as a standalone text editor or connected to the k.LAB language server
through LSP4J.
The Monaco JavaScript, CSS, and bridge resources are packaged in the library, so the editor runs without downloading web assets at runtime.
- Java 21
- JavaFX 21
- Maven 3.9 or the included Maven wrapper
<dependency>
<groupId>org.integratedmodelling</groupId>
<artifactId>klab-editor</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>For a modular application, add the editor module:
requires org.integratedmodelling.klabeditor;The module exports both org.integratedmodelling.klabeditor and
org.integratedmodelling.klabeditor.lsp.
Create the view on the JavaFX application thread, provide an optional document URI and save callback, and then load the text:
String documentUri = "inmemory:///examples/hello.txt";
MonacoEditorView editor =
new MonacoEditorView(
documentUri,
text -> {
// Persist the complete document text here.
});
editor.loadEditor("Hello, Monaco!\n", "plaintext", "vs-dark");The document URI identifies Monaco's internal model. Use a stable, unique URI for every document.
Both real file URIs such as path.toUri().toString() and virtual URIs such as
inmemory:///project/document.kim are supported.
The built-in themes are vs, vs-dark, hc-black, and hc-light. The language argument is a
Monaco language identifier or a supported k.LAB language identifier. Unknown identifiers fall back
to plain text behavior.
editor.setOnSave(text -> save(text));
editor.setOnDirtyChanged(dirty -> updateTabTitle(dirty));
editor.setCursorPositionListener(offset -> selectAssetAt(offset));
editor.setChangeListener(text -> inspectLocally(text));setOnSave is invoked by Ctrl+S or Cmd+S. The callback
receives the complete document text. A successful save command also resets the editor's dirty
baseline.
Useful imperative operations include:
editor.setText(updatedText);
String currentText = editor.getText();
editor.setCursorPosition(characterOffset);
editor.requestEditorFocus();
editor.setLineNumbers(false);
editor.setMinimapVisible(false);
editor.setTheme("vs");The built-in Ctrl+S/Cmd+S command advances the dirty-state
baseline before invoking setOnSave. If a toolbar button or another external action performs the
save, call editor.markSaved() after persistence succeeds.
For an asynchronous save, retain the submitted snapshot and call editor.markSaved(savedText) when
that exact snapshot is confirmed; edits made while the request is in flight will remain dirty.
Calls that affect Monaco can be made before its JavaScript bridge is ready; supported pending state is replayed when initialization completes.
Subclass the editor and return fully initialized JavaFX nodes for either side of the top or bottom bar. A bar is not created when its callback returns no components, so the base editor retains its original borderless appearance.
public final class ProjectEditor extends MonacoEditorView {
@Override
protected Collection<BarComponent> createHeaderBarComponents() {
Button save = new Button("Save");
save.setOnAction(event -> {
save(getText());
markSaved();
});
return List.of(new BarComponent(save, BarSide.LEFT));
}
@Override
protected Collection<BarComponent> createStatusBarComponents() {
Label status = new Label("Ready");
return List.of(new BarComponent(status, BarSide.RIGHT));
}
}For components created after the callbacks run, subclasses can call
installBarComponent(EditorBar.STATUS, new BarComponent(node, BarSide.LEFT)) on the JavaFX thread.
The bars use AtlantaFX semantic color lookups and otherwise leave child-control styling to the
active application theme.
Diagnostics can be supplied directly using LSP4J's Diagnostic model:
editor.setDiagnostics(diagnostics);For simple markers, use createMarker or createMarkerByOffset:
editor.createMarker(4, "Check this statement", "warning");
editor.createMarkerByOffset(120, 8, "Unknown identifier", "error");Review markers use Monaco's glyph margin, the column to the left of line numbers. The margin and markers are hidden unless review mode is enabled:
editor.setReviewMarkers(List.of(
new MonacoEditorView.ReviewMarker(
"comment-42", 4, "!", "#e67e22", 18,
"Architecture review", "open-comment", "platform-team"),
new MonacoEditorView.ReviewMarker(
"approval-7", 12, "✓", "seagreen", 16,
"Approved", "show-approval", "maria")));
editor.setOnReviewMarkerClicked(click -> {
openReviewAction(click.id(), click.action(), click.responsibility(), click.lineNumber());
});
editor.setOnReviewMarginDoubleClicked(lineNumber -> {
createReviewAt(lineNumber);
});
editor.setReviewMode(true);Marker ids are unique. putReviewMarker adds or replaces one marker, removeReviewMarker removes
one, and clearReviewMarkers removes all of them. The icon is a text glyph, the color accepts CSS
colors, and sizes are clamped to 8–32 pixels. Marker state survives review-mode toggles and editor
initialization. The margin double-click callback only fires for empty glyph cells, not when the user
double-clicks an existing marker.
KlabLspService is a process bridge for a running k.LAB language-server LocalInstance. Initialize
the singleton once, then create one LspDocumentSession per open editor document:
String documentUri = "inmemory:///project/example.kim";
String languageId = "kim";
String initialText = loadDocument();
MonacoEditorView editor = new MonacoEditorView(documentUri, this::saveDocument);
editor.loadEditor(initialText, languageId, "vs-dark");
KlabLspService lsp = KlabLspService.getInstance();
LspDocumentSession lspSession = null;
if (lsp.ensureInitialized(languageServerInstance, userScope)) {
lspSession = new LspDocumentSession(editor, languageId, initialText);
}The session:
- sends
textDocument/didOpenwith the initial text; - forwards complete editor contents through
textDocument/didChange; - routes diagnostics for the editor's URI back to Monaco on the JavaFX thread;
- rejects versioned diagnostics that do not match the current document version; and
- unregisters its callbacks and sends
textDocument/didClosewhen closed.
LspDocumentSession requires the editor to have a nonblank document URI and requires
ensureInitialized(...) to have succeeded. It throws an exception when either precondition is not
met instead of creating a silently disconnected session.
Close the session only when the document editor is genuinely closed:
if (lspSession != null) {
lspSession.close();
lspSession = null;
}Do not use Node.sceneProperty() becoming null as a disposal signal. JavaFX controls such as
TabPane can temporarily detach content while switching tabs or views. Closing the session at that
point sends didClose even though the editor remains active. Tie close() to the application's real
tab, document, or window-close lifecycle instead. Repeated close() calls are safe.
The editor can use a highlighter service and preload k.LAB keyword or concept metadata:
editor.setHighlighterServiceUrl("http://localhost:8765");
editor.preloadKeywordHighlighterCache("kim", keywords);
editor.preloadConceptHighlighterCache(conceptCategories);These facilities are independent of LspDocumentSession; applications may use them with or without
LSP document synchronization.
On Linux or macOS:
./mvnw testOn Windows:
.\mvnw.cmd testThe Maven build installs Node.js and npm locally when needed, compiles monaco-bridge.ts, packages
the browser resources, compiles the Java module, and runs the tests.
To launch the included demonstration application:
./mvnw javafx:runRegister setOnComposeObservable(() -> completionStage) to handle Ctrl+Shift+Space
(Cmd+Shift+Space on macOS). The supplier runs on the JavaFX thread and returns an asynchronous
Observable, or null on cancellation. The library inserts its URN at the invoking cursor as one
undoable edit, using the existing content/dirty/LSP notification path. It never marks the edit saved.
Hosts that need editor context can instead register the overload accepting an
ObservableCompositionContext; it supplies the active selection and any concept identifier under
the cursor.
Repeated shortcuts while a request is pending are ignored. A result is discarded if the model was
replaced, edited, disposed or made read-only; a page reload also invalidates its Java callback.
The IDE supplies the UI and decides whether composition has any action beyond returning a value.
npm test compiles the bridge and runs its focused Node regression tests.