SyntaxHighlightedTextEditor¶
Experimental API
This composable is annotated with @ExperimentalHighlightApi.
The API surface may change without a deprecation cycle.
SyntaxHighlightedTextEditor is an editable code field (built on BasicTextField) with live
syntax highlighting. As users type, highlighting updates after a short debounce while preserving
cursor and selection.
Full API in Dokka:
SyntaxHighlightedTextEditorrememberSyntaxHighlightedEditorValueSyntaxHighlightedTextEditorDefaultsExperimentalHighlightApi
When to use it¶
- You want an editable code field with built-in async highlighting.
- You want a ready-made
SurfaceplusBasicTextFieldwrapper. - You want to keep editor state (
TextFieldValue) while highlighting updates in background.
Key parameters¶
valueandonValueChange- editor state and updates.language- highlight.js language id.theme- explicit theme or value fromHighlightThemeProvider.debounceMs- typing pause before highlight call.contentPaddingandshape- layout and clipping of editor surface.horizontalScrollStateandverticalScrollState- hoisted scroll states for horizontal and vertical scrolling. Defaults tonull. WhenhorizontalScrollStateisnull, code lines soft-wrap to fit the container width. Passing arememberScrollState()disables soft-wrapping and enables horizontal panning across long code lines.keyboardOptions- keyboard / IME behavior. Defaults toSyntaxHighlightedTextEditorDefaults.CodeKeyboardOptions(autocorrect off, autocapitalization off, Ascii keyboard) - the right defaults for source code. Override via.copy(imeAction = ...)to keep the code-friendly settings while customising one field.cursorBrush- text cursor color. Defaults tonull, which derives the cursor from the theme's text color so it stays visible on both light and dark themes. Pass an explicitBrush(e.g.SolidColor(MaterialTheme.colorScheme.primary)) to override.onHighlightCompleteandonError- observability hooks.onHighlightCompletereceives aHighlightResult(timing, span count, language); see the API docs.indentation- indentation string to insert when the Tab key is pressed. Defaults to 4 spaces (" ").autoIndentEnabled- whether to automatically copy the leading indentation of the previous line when inserting a newline. Defaults totrue.tabKeyInterceptionEnabled- whether to intercept the hardware Tab key to insert spaces instead of shifting focus to the next view. Defaults totrue. Note thatShift+Tabmoves focus backwards andCtrl+Tabadvances focus so keyboard navigation is preserved, while arrow keys (Up, Down, Left, Right) are intercepted to prevent focus from escaping the editor when boundaries are reached.escapeKeyClearsFocus- whether pressing the hardware Escape key clears focus from the editor, preventing keyboard focus traps (WCAG 2.1.2) for keyboard and switch-access users. Defaults totrue(SyntaxHighlightedTextEditorDefaults.ESCAPE_KEY_CLEARS_FOCUS).
For anything else BasicTextField exposes (enabled, readOnly, singleLine,
maxLines, decorationBox, etc.) drop one level down to
rememberSyntaxHighlightedEditorValue
and render your own field - the editor is intentionally opinionated.
Opting in¶
Both SyntaxHighlightedTextEditor and SyntaxHighlightedTextEditorDefaults are annotated
with @ExperimentalHighlightApi, so accessing either - including static members like
SyntaxHighlightedTextEditorDefaults.DefaultTextStyle or CodeKeyboardOptions - requires the
same opt-in.
// Option 1 - opt in at the call site
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun MyScreen() {
SyntaxHighlightedTextEditor(...)
}
// Option 2 - propagate to your own API
@ExperimentalHighlightApi
@Composable
fun MyEditorScreen() {
SyntaxHighlightedTextEditor(...)
}
// Option 3 - file-wide opt-in (before package statement)
@file:OptIn(ExperimentalHighlightApi::class)
Recommended usage¶
With HighlightThemeProvider (recommended)¶
import dev.hossain.highlight.ui.ExperimentalHighlightApi
import dev.hossain.highlight.ui.HighlightThemeProvider
import dev.hossain.highlight.ui.SyntaxHighlightedTextEditor
import dev.hossain.highlight.ui.rememberTomorrowNightTheme
import dev.hossain.highlight.ui.rememberTomorrowLightTheme
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun EditorScreen() {
var editorValue by remember { mutableStateOf(TextFieldValue("fun hello() = println(\"Hello!\")")) }
HighlightThemeProvider(
lightHighlightTheme = rememberTomorrowLightTheme(),
darkHighlightTheme = rememberTomorrowNightTheme(),
) {
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "kotlin",
modifier = Modifier
.fillMaxWidth()
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
contentPadding = PaddingValues(12.dp),
)
}
}
With an explicit theme (no provider)¶
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun SqlEditor() {
var editorValue by remember { mutableStateOf(TextFieldValue("SELECT * FROM users")) }
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "sql",
theme = rememberTomorrowLightTheme(),
)
}
Customize the editor text style¶
SyntaxHighlightedTextEditorDefaults.DefaultTextStyle is the pre-allocated singleton
the editor uses by default (monospace family). Build on top of it with .copy(...)
to override only what you need - using the singleton avoids allocating a fresh
TextStyle on every recomposition while typing.
import dev.hossain.highlight.ui.SyntaxHighlightedTextEditorDefaults
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun LargerEditor() {
var editorValue by remember { mutableStateOf(TextFieldValue("")) }
// Wrap in remember so typing recompositions don't allocate a fresh TextStyle
// every frame. Add keys (e.g. remember(fontSize) { ... }) when the derived
// style depends on changing inputs.
val editorStyle = remember {
SyntaxHighlightedTextEditorDefaults.DefaultTextStyle.copy(fontSize = 15.sp)
}
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "kotlin",
textStyle = editorStyle,
)
}
Disable line wrapping (enable horizontal scrolling)¶
By default, the editor soft-wraps lines when they exceed the horizontal bounds of the screen.
To disable line wrapping and allow horizontal panning across long code lines, pass a hoisted
ScrollState via horizontalScrollState:
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun HorizontallyScrollableEditor() {
var editorValue by remember { mutableStateOf(TextFieldValue("")) }
val horizontalScrollState = rememberScrollState()
SyntaxHighlightedTextEditor(
value = editorValue,
onValueChange = { editorValue = it },
language = "kotlin",
horizontalScrollState = horizontalScrollState,
)
}
How the highlight pipeline behaves¶
SyntaxHighlightedTextEditor delegates pipeline logic to
rememberSyntaxHighlightedEditorValue() and renders the returned value in a Surface plus
BasicTextField wrapper.
Core behavior:
- A debounce window waits
debounceMsafter typing stops, then callsHighlightEngine.highlight(). - Rapid keystrokes cancel prior coroutine work, so only the latest text is highlighted.
- Stale snapshot checks keep language and theme changes safe.
- While new results are in flight, previously computed spans are reused where valid, so only the actively edited region is briefly unstyled.
Common pitfalls¶
- Use
contentPaddingfor inner spacing;.padding()onmodifierchanges outer layout instead. - Keep
shapealigned with any border shape to avoid background bleed at rounded corners. - Outside
HighlightThemeProvider, a standalone engine/WebView is created and managed by lifecycle.
Why aren't enabled, readOnly, singleLine, etc. exposed?¶
SyntaxHighlightedTextEditor is a deliberate convenience composable: it ships code-friendly
defaults (keyboardOptions, cursorBrush, monospace textStyle, debounce window, theme-aware
surface) and exposes only the parameters needed to tune them. It is not a parameterized
clone of BasicTextField.
For any other BasicTextField parameter you might want - enabled, readOnly, singleLine,
maxLines, minLines, keyboardActions, visualTransformation, onTextLayout,
interactionSource, decorationBox - drop one level down to
rememberSyntaxHighlightedEditorValue and render
your own field. The helper returns a TextFieldValue with highlighting applied and cursor /
selection preserved; you compose it into whatever field shape you need.
This split keeps the convenience composable's surface small (and stable - it doesn't grow as
Compose Foundation adds new BasicTextField parameters) while still giving you full control
when you need it.
Lower-level helper for custom text fields¶
Use rememberSyntaxHighlightedEditorValue() when you want your own field component
(OutlinedTextField, third-party editor, etc.) and only need highlighted TextFieldValue output.
@OptIn(ExperimentalHighlightApi::class)
@Composable
fun MyCustomEditor() {
var editorValue by remember { mutableStateOf(TextFieldValue("")) }
val displayValue = rememberSyntaxHighlightedEditorValue(
value = editorValue,
language = "kotlin",
)
OutlinedTextField(
value = displayValue,
onValueChange = { editorValue = it },
label = { Text("Code") },
)
}