# Code editor

A 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.

- Registry name: `code-editor` (`@candor/code-editor`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/code-editor

## Install

```bash
npx candor-ui add code-editor
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/code-editor`.
It brings in: code-listing, locale, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { CodeEditor, type CodeEditorError } from "@/components/candor/code-editor"

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 }
  }
}

export default function Example() {
  const [value, setValue] = React.useState(start)
  return (
    <div className="flex max-w-lg flex-col gap-2">
      <CodeEditor highlight label="Query, as JSON" language="json" value={value} onChange={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>
  )
}
```

## <CodeEditor>

Props:

- `value: string`: The text, as the person has it.
- `onChange: (value: string) => void`: Called with the new text on each edit.
- `language: string`: The language, for its colours: "json", "sql", "yaml", "ts"; "text" for none. Never run.
- `label: string`: The field's name, shown above it.
- `labelHidden?: boolean | undefined` (default `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` (default `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` (default `"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` (default `" "`): What Tab inserts and Shift+Tab removes. Defaults to two spaces.
- `minLines?: number | undefined` (default `4`): The fewest lines tall it is. Defaults to 4.
- `maxLines?: number | undefined` (default `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`

## Functions

### indentEdit

```ts
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 (`codeEditor`)

The fixed words, English by default. Override any of them under `codeEditor` in `<LocaleProvider strings={{ codeEditor: { … } }}>`; the table is exported as `codeEditorStrings`.

- `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.

```ts
{
  /** 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;
}
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
