Skip to content

Latest commit

 

History

60 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KlabMonacoEditor

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.

Requirements

  • Java 21
  • JavaFX 21
  • Maven 3.9 or the included Maven wrapper

Add the dependency

<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.

Use the editor without 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.

Listen for editor activity

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.

Add optional command and status bars

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.

Show diagnostics without LSP

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");

Add clickable review markers

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.

Use the editor with the k.LAB LSP service

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/didOpen with 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/didClose when 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.

Dispose the LSP session correctly

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.

Highlighting support

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.

Build and test

On Linux or macOS:

./mvnw test

On Windows:

.\mvnw.cmd test

The 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:run

Host-provided observable composer

Register 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.

About

Modernized Monaco/JavaFX integration

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages