Code editor
ProA small editor for code or a query a person writes or corrects: line numbers, the same token colours as CodeListing and Payload drawn under the text, Tab and Shift+Tab to indent, and a validation message from your app tied to its line and read out with the field (aria-describedby). The text is a plain textarea underneath, so typing, undo, selection and screen readers behave as they do in any field. It reads left to right whatever the page's direction unless dir says otherwise, and its colours come from the theme's tokens.
It isn't a full code editor: no autocomplete, linting, folding, search or multiple cursors, and its colours come from a light tokenizer rather than a parser. For those, embed Monaco or CodeMirror themed from the same tokens (the "Code editor" pattern, /patterns/code-editor-embedding). Nothing in the text is run or rendered as markup; what the person types reaches your app as a string, and your server decides whether to run it.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0 and 2.8.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/code-editor
Brings in: bidi, code-listing, locale, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { CodeEditor, type CodeEditorError } from "@/components/candor/code-editor"
import { PermissionPrompt, type PermissionRequest } from "@/components/candor/permission-prompt"
const start = `{
"table": "orders",
"where": { "status": "settled" },
"limit": 50
}`
/** Your app's validation: here, JSON.parse, with the line worked out from the position it reports. */
function check(text: string): CodeEditorError | undefined {
try {
const v = JSON.parse(text) as unknown
return v && typeof v === "object" && !Array.isArray(v) ? undefined : { message: "That isn’t a JSON object, so it can’t be used." }
} catch (e) {
const at = /position (\d+)/.exec(String(e))
const line = at ? text.slice(0, Number(at[1])).split("\n").length : undefined
return { message: "That isn’t valid JSON.", line }
}
}
const request: PermissionRequest = {
id: "p1",
tool: "Bash",
title: "The agent wants to run npm test -- invoices",
reason: "No rule allows Bash commands yet.",
input: { command: "npm test -- invoices", timeout: 120000 },
}
export default function Example() {
const [value, setValue] = React.useState(start)
const [said, setSaid] = React.useState<string>()
return (
<div className="flex max-w-lg flex-col gap-6">
<div className="flex flex-col gap-2">
<CodeEditor highlight label="Query, as JSON" language="json" value={value} onValueChange={setValue} error={check(value)} minLines={6} />
<p className="text-xs text-muted-foreground">Delete a comma to see the message tied to its line.</p>
</div>
{/* PermissionPrompt (free) edits the input in a plain field; pass CodeEditor and it edits in this one. */}
<PermissionPrompt
request={request}
editable
jsonEditor={CodeEditor}
onAllow={(a) => setSaid(`Allowed${a.updatedInput ? " as edited" : ""}.`)}
onDeny={() => setSaid("Denied.")}
/>
<p role="status" className="text-sm text-muted-foreground">{said}</p>
</div>
)
}<CodeEditor>
| Prop | Type | Default | Description |
|---|---|---|---|
| valuerequired | string | — | The text, as the person has it. |
| onValueChangerequired | (value: string) => void | — | Called with the new text on each edit. |
| languagerequired | string | — | The language, for its colours: "json", "sql", "yaml", "ts"; "text" for none. Never run. |
| labelrequired | string | — | The field's name, shown above it. |
| labelHidden | boolean | undefined | false | Keeps the label for screen readers only, for a frame that already names the field. |
| error | CodeEditorError | undefined | — | What's wrong, from your app's validation, and its line. The field is marked invalid and the message is read with it. |
| wrap | boolean | undefined | false | Wrap long lines instead of scrolling sideways, for prose such as instructions. Defaults to false: code keeps its lines. |
| dir | "ltr" | "rtl" | "auto" | undefined | "ltr" | The text's direction. Code reads left to right in every language, so it defaults to "ltr"; "auto" follows the text, for prose. |
| indent | string | undefined | " " | What Tab inserts and Shift+Tab removes. Defaults to two spaces. |
| minLines | number | undefined | 4 | The fewest lines tall it is. Defaults to 4. |
| maxLines | number | undefined | 20 | The most lines tall it grows before it scrolls. Defaults to 20. |
| maxLength | number | undefined | — | Most characters allowed. |
| placeholder | string | undefined | — | Shown while the field is empty. |
| footer | React.ReactNode | undefined | — | Inside the frame, under the text: a toolbar or a count. |
| highlight | boolean | undefined | — | Colour the code by its syntax. Left out, it follows the nearest `data-syntax` attribute (off by default). |
| ref | React.Ref<HTMLTextAreaElement> | undefined | — | The textarea, for placing the caret or inserting text from outside. |
| className | string | undefined | — | Extra classes for the outer element. |
Functions
indentEdit(value: string, start: number, end: number, indent: string, outdent: boolean)The edit that Tab (or Shift+Tab, with outdent) makes: the range it replaces, what goes there, and the selection after. With nothing selected Tab types one indent at the caret; with lines selected, or for Shift+Tab, each line in the selection moves by one indent.
Strings
The fixed words, in English by default. Change any of them under codeEditor in <LocaleProvider strings={{ … }}>; the table is exported as codeEditorStrings.
| Key | English |
|---|---|
| hint | Tab indents and Shift+Tab outdents. To move on with the keyboard, press Esc, then Tab (or Shift+Tab to go back). |
| lineError | Line {line}: {message} |
Types
- CodeEditorError
- What's wrong with the text, in the app's words, and the line it's on when the app knows.
{ /** The app's sentence: "That isn't a JSON object, so it can't be used." Shown as text. */ message: string; /** The line it's on, counting from 1. Left out, the message is about the whole text. */ line?: number | undefined; }