Editor & Intellisense
The Soma editor is a fully custom, lightweight code editor built without third-party libraries. It layers a transparent <textarea> over a syntax-highlighted <pre> element and ships autocomplete, hover tooltips, and per-user colour customisation — all in-process inside the node panel.
Editor Architecture
The editor is implemented in SomaEditor.tsx as a single React component. It intentionally avoids Monaco, CodeMirror, and similar heavy editors to keep the bundle small and keep the UI fully integrated with the node panel layout.
Overlay technique
A contenteditable <textarea> and a <pre> element are stacked on top of each other with identical font, line-height, padding, and size. The textarea is transparent — the user types into it and their raw input is captured normally. The <pre> is rendered behind the textarea and contains the same text, but HTML-escaped and wrapped in <span> elements with inline colour styles. The visible caret comes from the textarea; the visible text colours come from the <pre>.
Syntax highlighter
highlightSoma(source, colors) is a single-pass inline tokeniser. It splits the source into lines and processes each character: whitespace is passed through, # starts a comment span to the end of the line, a digit starts a number token, an alphabetic character starts an identifier (then a lookahead peek checks for ( to classify it as a function name), and every operator character gets its own span. It produces an HTML string that is injected into the <pre> via dangerouslySetInnerHTML.
Input handling
The textarea's onInput handler reads the new value and calls the parent onChange callback, which triggers a React re-render. Tab key press is intercepted to insert two spaces instead of focus-shifting. Scroll sync: the pre element's scrollTop and scrollLeft are kept in sync with the textarea's on every scroll event, ensuring both layers always show the same viewport.
Font
The editor renders using the font stack: 'Fira Code', 'Cascadia Code', 'Consolas', monospace — matching common developer fonts in order of preference. No custom font is loaded; the stack falls back gracefully to Consolas on Windows and the system monospace on other platforms.
Colour System
Syntax colours are managed by SomaConfig.ts through the SomaColors interface. Two palettes are provided out of the box — one for dark mode and one for light mode. Users can override any token colour; preferences are persisted to localStorage under the key traderoid_soma_editor_settings.
SomaColors interface
| Field | Dark default | Light default | Applied to |
|---|---|---|---|
| keyword | #c792ea | #7c3aed | input, output, param, let, as, for, in, if, else, while, and, or, not, true, false, break, continue, int, float, bool |
| function | #82aaff | #2563eb | Identifiers followed by ( — built-in and user-defined function calls |
| variable | #d1d4dc | #333333 | All other identifiers — local variables, input/output names |
| number | #f78c6c | #ea580c | Numeric literals: integers and decimals |
| comment | #546e7a | #9ca3af | # comment text through to end of line |
| operator | #89ddff | #0891b2 | Operators and punctuation: + − * / = [ ] ( ) , { } > < ! & | |
| plain | #d1d4dc | #333333 | Any character not matched by the above rules |
Settings persistence
Settings are read from localStorage on component mount via getSomaEditorSettings(). When a user changes a colour in the node property panel, saveSomaEditorSettings(newSettings) serialises the full settings object to JSON and writes it to localStorage. It then dispatches a custom DOM event (traderoid-soma-editor-settings-change) so that all open Soma editors on the page update simultaneously without a page reload.
// Subscribe to live colour changes inside SomaEditor
useEffect(() => {
setColors(getSomaEditorSettings().colors[theme]);
return subscribeToSomaEditorSettings(s => setColors(s.colors[theme]));
}, [theme]);Intellisense Autocomplete
Autocomplete is implemented in SomaIntellisense.tsx as a custom React hook (useSomaIntellisense). It triggers on every keystroke inside the editor and renders a floating suggestion popup near the text cursor.
Trigger conditions
- →A continuous run of alphabetic characters, digits, or underscores at the cursor position. Minimum 1 character typed.
- →The popup is dismissed if the prefix drops to empty (backspace past start) or if the user types a non-word character such as a space or operator.
- →Escape always closes the popup without inserting.
Navigation & insertion
- →↑ / ↓ arrow keys move the highlighted row. The list wraps at both ends.
- →Tab or Enter insert the selected suggestion: the current prefix is replaced with the suggestion's
insertText. Keyword snippets include a trailing space; function snippets include()and move the cursor inside the parens. - →Clicking a suggestion row has the same effect as pressing Tab.
Suggestion sources
Three pools are searched on every keystroke, all filtered by prefix match (case-sensitive from the start of the token):
| Pool | Size | Detail column | Insert |
|---|---|---|---|
| Keywords | 17 | Short description (e.g. "declare an input channel") | Keyword text + trailing space or snippet |
| Built-in functions | 43 | Full function signature (e.g. "clamp(x, lo, hi)") | function name + () |
| User-declared variables | Dynamic | "variable" badge | Variable name |
User-declared variables are extracted from the current script text by scanning the source for let, input, output, and param declarations every time the popup opens. This means newly typed names appear as suggestions immediately.
Hover Tooltips
The useSomaHover hook and SomaHoverTooltip component provide inline documentation when the user hovers over a token.
Detection
On mousemove over the <pre> highlight layer, the hook uses document.caretPositionFromPoint() (or caretRangeFromPoint() for WebKit) to determine the character offset under the cursor. It then scans left and right from that offset to extract the full token (letters, digits, underscores).
Lookup
The extracted token is looked up in the FUNCTIONS map. If found, the tooltip shows the full function signature on the first line and the one-line description below it. Keywords and user variables do not show a tooltip.
Positioning
The tooltip is absolutely positioned just above the hovered token using the bounding rect of the hovered element. It clamps to the viewport edges to avoid overflow.
Soma Logger
The logger panel (SomaLogger.tsx) displays the output of all print() calls executed during the most recent graph evaluation. It is rendered beneath the script editor inside the node property panel.
Array display
Arrays are shown up to 8 elements: [1.2, 3.4, 5.6, … (100 total)]. Scalars are formatted with up to 6 decimal places; trailing zeros are trimmed. NaN and Infinity are displayed literally.
Log lifecycle
Logs are cleared at the start of each evaluation and re-populated as the script runs. The panel shows a scrollable list of all print() calls in source order. Each row includes the argument labels (the source-text of each argument) alongside the computed values.