Rich Text And Code Editing: RichTextArea And A Syntax-Highlighting CodeEditor

Friday’s post covered how we fund this work without the bait-and-switch; today is the engineering side of the same week, with two new visual editors.

PR #5272 adds two components: RichTextArea, a WYSIWYG rich text editor, and CodeEditor, an IDE-style code editor with syntax highlighting. They are different on the surface and identical underneath, which is the part worth explaining first.

One abstraction, two backends

Both editors extend AbstractEditorComponent. That base class does not draw anything itself. It speaks a small semantic channel of commands (“apply bold”, “set language to java”), queries (“give me the current HTML”) and events (“the text changed”) to whatever backend is attached. The component never assumes how the editor is rendered, only what it can ask for and what it gets told back.

There are two backends behind that channel, and they are interchangeable.

The first is a pure Codename One engine. It paints text, selection, carets, diagnostics, images, and formatting with lightweight components, while the port’s low-level text-input bridge supplies virtual-keyboard and IME events. That keeps the document and interaction model consistent across canvas-based ports without embedding a web editor.

The second backend is optional and supplied by a port. A platform implementation can return a native editor peer through CodenameOneImplementation#createNativeEditorPeer, then handle editorPeerCommand and editorPeerQuery. The same semantic channel drives it, so the component code does not change. Applications can also point CodeEditor#setEngineURL(...) at a compatible custom engine when they need a specialized integration.

flowchart TD A["RichTextArea / CodeEditor"] --> B["AbstractEditorComponent
(command / query / event channel)"] B --> C["Pure Codename One engine
lightweight rendering + text input bridge"] B --> D["Optional native peer
createNativeEditorPeer /
editorPeerCommand / editorPeerQuery"] C --> E["Canvas rendering
keyboard + IME input from the port"]

CodeEditor

CodeEditor highlights eight languages: java, kotlin, js, python, css, xml, json, and c. It draws a line-number gutter, ships light and dark themes, and auto-closes brackets and quotes as you type.

CodeEditor editor = new CodeEditor();
editor.setLanguage("java");
editor.setTheme("dark");
editor.setText("public class Main {\n\n}");

Form f = new Form("Editor", new BorderLayout());
f.add(BorderLayout.CENTER, editor);
f.show();

Code completion is asynchronous, which matters because a real completion source may have to look something up. You register a CodeCompletionProvider, and it hands results back through a callback rather than returning them inline, so the editor never blocks while you compute suggestions:

editor.setCompletionProvider((ed, code, cursorPosition, results) -> {
    // Inspect code up to cursorPosition, build suggestions, then deliver.
    List<CodeCompletion> out = new ArrayList<>();
    out.add(new CodeCompletion("println", "println(String x)"));
    out.add(new CodeCompletion("print", "print(String x)"));
    // Hand them back when ready, on or off the EDT (onSucess is CN1's spelling).
    results.onSucess(out);
});

Diagnostics work the same way. You push CodeDiagnostic entries at the editor and it renders them as squiggly underlines, gutter markers, and tooltips on hover:

editor.setDiagnostics(Arrays.asList(
    new CodeDiagnostic(3, 5, 3, 12, "cannot find symbol: prinln")
        .setSeverity(CodeDiagnostic.ERROR)
));

Here is the editor with Java highlighting, the gutter, and the completion popup offering println and print:

CodeEditor showing Java syntax highlighting, a line-number gutter, and an async code-completion popup with println and print suggestions

The dark theme is a one-line switch, not a separate component:

CodeEditor rendered in its dark theme

RichTextArea

RichTextArea is the WYSIWYG side. It edits formatted text and gives you HTML in and out. You get bold, italic and underline, ordered and unordered lists, links, foreground and highlight colors, and headings. It fires change events as the user edits, and it shows a placeholder when empty.

The content model is HTML, so you load and save with two calls:

RichTextArea rich = new RichTextArea();
rich.setPlaceholder("Trip notes...");
rich.setHtml("<p>Itinerary: <b>Day 1</b> arrive, see "
    + "<a href=\"https://example.com\">the museum</a></p>");

rich.addChangeListener(e -> rich.getHtml(html -> {
    // getHtml is async; the markup arrives in the callback.
    Storage.getInstance().writeObject("note", html);
}));

The toolbar drives the same command channel described earlier; pressing the bold button sends an “apply bold” command to the backend rather than editing a string directly:

RichTextArea WYSIWYG editor with a formatting toolbar (bold, italic, underline, lists, link, color) editing a trip itinerary note that contains bold text, a link, and strikethrough

You only pay for it if you use it

CodeEditor can be backed by a heavier engine, and CodeMirror assets are not free weight to carry around. So the Android and iOS builders scan your app for use of com.codename1.ui.CodeEditor. The optional CodeMirror assets are bundled only when that API actually appears, gated by CN1_USE_CODEMIRROR. An app that never touches CodeEditor ships nothing extra.

The Playground opts its multiline TextArea source panes into setLightweightEditingEnabled(true), so the same caret, selection, scrolling, clipboard, keyboard, and IME machinery is dogfooded in a real application. There are 33 deterministic unit tests across RichTextAreaTest and CodeEditorTest, backed by an editor SPI in the test implementation, plus a dedicated developer-guide chapter with screenshots.

The tradeoff

The default engine is web-view-backed. That is what makes it portable, and it is also the limit: it needs a web view to exist on the platform. iOS, Android, the desktop and the web all have one, so all four are covered. Apple Watch and tvOS do not have a usable web view, so the editors are not enabled there. If a port supplies a native editor peer, that constraint goes away for that port, which is exactly why the second backend exists.

Wrapping up

Two editors, one abstraction. The shared AbstractEditorComponent is the reason a WYSIWYG HTML editor and a syntax-highlighting code editor could land in the same PR: the hard part was the command/query/event channel and the cross-platform engine behind it, and both components reuse it. The web-view engine gives you something that works everywhere a web view exists, and the native peer hook gives a port room to do better when it can. Try CodeEditor and RichTextArea in the next build and tell us where they fall short.


Discussion

Join the conversation via GitHub Discussions.