Syntax Highlighted Text Editor
This composable is marked experimental (ExperimentalHighlightApi). Call sites must opt in with @OptIn(ExperimentalHighlightApi::class) or propagate the annotation. The API surface (parameters, defaults, behavior) may change in future releases.
A syntax-highlighted code editor composable built on BasicTextField.
As the user types, the visible text is re-highlighted in the background using the same HighlightEngine pipeline as SyntaxHighlightedCode. Keystrokes are debounced by debounceMs to avoid firing a highlight call on every character. While a new highlight result is in flight, the previously highlighted spans (or plain text on first render) remain visible with no flicker.
Cursor position and selection are always preserved: the highlighting only replaces the AnnotatedString content inside the TextFieldValue, never the cursor or selection.
This composable reads the active theme from LocalHighlightTheme, so a HighlightThemeProvider ancestor must exist, or you must pass an explicit theme.
For custom layout or third-party text fields, use rememberSyntaxHighlightedEditorValue directly to obtain the highlighted TextFieldValue without the Surface wrapper.
Usage - inside HighlightThemeProvider (recommended)
HighlightThemeProvider(
lightHighlightTheme = rememberTomorrowTheme(),
darkHighlightTheme = rememberTomorrowNightTheme(),
) {
var editorValue by remember { mutableStateOf(TextFieldValue("fun hello() = println(\"Hello!\")")) }
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "kotlin",
modifier = Modifier.fillMaxWidth().border(1.dp, Color.Gray, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
contentPadding = PaddingValues(12.dp),
)
}Usage - with an explicit theme
var editorValue by remember { mutableStateOf(TextFieldValue("SELECT * FROM users")) }
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "sql",
theme = rememberTomorrowTheme(),
)Parameters
The current TextFieldValue, including text, cursor position, and selection.
Called whenever the user edits the text or moves the cursor.
Highlight.js language identifier (e.g. "kotlin", "python", "sql").
Modifier applied to the outer Surface container (background, border, size, etc.). Do not include padding here - use contentPadding instead. Padding applied via modifier would shrink the Surface layout area, leaving a gap between the border and the theme background.
Padding applied inside the Surface, between the background edge and the text. Defaults to PaddingValues of 0.dp (no padding). Use this instead of adding .padding() to modifier so the theme background fills the full bordered area.
Shape used to clip the Surface background. Must match the shape used in any .border() applied via modifier so the background and border align. Defaults to RectangleShape (no rounding).
The highlight theme to apply. Defaults to LocalHighlightTheme.
Text style for the editor. Defaults to a monospace style. The theme's foreground color is applied on top of this style when a highlight result is available.
Milliseconds to wait after the last keystroke before triggering a new highlight call. Defaults to 150 ms - a good balance between responsiveness and avoiding unnecessary WebView calls on fast typists. If debounceMs changes, the new value is used on the next keystroke. The currently running debounce window is unaffected (the original delay completes with its captured-at-suspension value).
Optional callback invoked each time a highlight cycle completes successfully. Receives the resulting AnnotatedString with syntax spans applied. Useful for testing (wait until the first result arrives) and for observing the highlight output without owning the editor's text state. Defaults to null (no callback).
Optional callback invoked with the HighlightException when a highlight cycle fails. The editor falls back to plain text on failure regardless of whether this callback is set - it is purely observational. Use it to log failures, show a snackbar, or record analytics. Defaults to null (no callback).
Note on shape: if you pass a custom Shape (e.g. RoundedCornerShape(8.dp)), wrap it in remember at the call site so that a new instance is not created on every recomposition, which would defeat Compose's skipping optimisation.