
# Workbench

`Workbench` is a whole IDE shell around one editor. Down the left, an **activity bar** switches
between containers shown in the **primary side bar** — the built-in one is the **explorer**, a
file tree over a `FileSystem`. The middle is the **editor area**: **tabs** across the top, a
**breadcrumb** trail under them, one editor below. A **panel** sits at the foot for things like
Problems, and a **secondary side bar** can appear down the far edge. A **status bar** runs along
the very bottom, and `Mod-P`/`Mod-K` open the **command palette** from anywhere.

```ts
import { FileSystem, Workbench } from "codelet/workbench";

const fs = new FileSystem({
  "/README.md": "# Hello\n\nEdit me.",
  "/src/index.ts": "console.log('hi');",
});

const workbench = new Workbench({
  parent: document.getElementById("app")!,
  fs,
});
```

`parent` is the only required option. Everything else has a default.

## Try it

Switch between the tabs, browse the tree in the explorer, then press `Mod-P` for the command
palette and jump to a file by name.

::codelet-playground{mode="workbench" active="src/index.ts" height="420"}

```md [README.md]
# Hello

Edit me, or open src/index.ts.
```

```ts [src/index.ts]
export const greet = (name: string) => `hi ${name}`;

console.log(greet("codelet"));
```

```json [package.json]
{
  "name": "codelet-demo",
  "private": true
}
```

::

## Options

| Option        | Type                                          | Default                                      | Description                                                                                               |
| ------------- | --------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `fs`          | `FileSystem`                                  | a new, empty one                             | The filesystem the workbench shows and edits.                                                             |
| `name`        | `string`                                      | none                                         | What the tree is called. Not shown anywhere in the chrome.                                                |
| `open`        | `string \| string[]`                          | none                                         | Paths opened as tabs on mount.                                                                            |
| `active`      | `string`                                      | the last path in `open`                      | Which of `open` has focus.                                                                                |
| `onOpen`      | `(open: string[], active: string) => void`    | —                                            | Called with the open tabs and the active one whenever either changes.                                     |
| `cursors`     | `Record<string, Cursor>`                      | none                                         | Where each path was looking, read once when that tab opens.                                               |
| `onCursor`    | `(path: string, cursor: Cursor) => void`      | —                                            | Called with a path and its cursor whenever it moves.                                                      |
| `onSave`      | `(path: string, text: string) => void`        | —                                            | Called with a path and its text when the reader saves with `Mod-S`.                                       |
| `view`        | `string`                                      | explorer if the tree has a file, else closed | Which container the side bar shows. `""` closes it.                                                       |
| `onView`      | `(view: string) => void`                      | —                                            | Called with the side bar's container, or `""`, whenever the reader changes it.                            |
| `panel`       | `string`                                      | none (closed)                                | Which view the panel at the foot of the editor shows.                                                     |
| `onPanel`     | `(panel?: string) => void`                    | —                                            | Called with the panel's view, or nothing, whenever it changes.                                            |
| `secondary`   | `string`                                      | none (closed)                                | Which container the secondary side bar shows.                                                             |
| `onSecondary` | `(secondary?: string) => void`                | —                                            | Called with the secondary bar's container, or nothing, whenever it changes.                               |
| `extensions`  | `readonly Extension[]`                        | none                                         | The extensions to activate — what fills the activity bar, panel and command palette beyond the built-ins. |
| `settings`    | `SettingsStore`                               | `localSettings()`                            | Where an extension's settings are kept. See [Settings](/guide/settings).                                  |
| `state`       | `SettingsStore`                               | `localSettings()`, a second document         | Where an extension's own `globalState`/`workspaceState` are kept.                                         |
| `secrets`     | `SecretsStore`                                | none (the page only)                         | Where an extension's `context.secrets` are kept. See [Settings](/guide/settings).                         |
| `externalUri` | `(uri: Uri) => string \| PromiseLike<string>` | —                                            | Where a link that has to leave the page comes back to. Without one, `env.asExternalUri` rejects.          |
| `theme`       | `Theme`                                       | `lightTheme`                                 | The active theme.                                                                                         |
| `themes`      | `readonly Theme[]`                            | none                                         | Which themes the reader may choose between. See [Themes](/guide/themes).                                  |
| `onTheme`     | `(theme: Theme) => void`                      | —                                            | Called with the new theme whenever the reader switches.                                                   |

A path in `open` that the filesystem does not hold is dropped rather than opened as an empty tab:

```ts
new Workbench({
  parent,
  fs,
  open: ["/README.md", "/src/index.ts"],
  active: "/src/index.ts",
  cursors: { "/src/index.ts": { from: [3, 1] } },
  onOpen: (open, active) => console.log("tabs:", open, "active:", active),
  onSave: (path, text) =>
    fetch("/api/save", { method: "POST", body: JSON.stringify({ path, text }) }),
});
```

`onOpen`, `onCursor` and `onSave` only ever report the reader's own tree — a workspace an
extension opened over it (see [Multiple workspaces](#multiple-workspaces)) reports nothing here.
See [Editing, tabs and saving](/guide/editing) for how tabs, dirty marks and saving behave.

::warning
`open`, `active`, `cursors`, `view`, `panel`, `secondary`, `theme`, `extensions` and `themes` are
markup — see [Server rendering](#server-rendering).
::

## Methods and properties

| Member                                | Type                           | Description                                                                                             |
| ------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `workbench.fs`                        | `FileSystem`                   | The filesystem the workbench is showing.                                                                |
| `workbench.parent`                    | `HTMLElement`                  | The element it mounted into.                                                                            |
| `workbench.setTheme(theme)`           | `(theme: Theme) => void`       | Switches the active theme. Does nothing if it's the same object already in use.                         |
| `workbench.showPanel(panel?)`         | `(panel?: string) => void`     | Opens the panel onto a view, or closes it given nothing.                                                |
| `workbench.showSecondary(secondary?)` | `(secondary?: string) => void` | Opens the secondary side bar onto a container, or closes it given nothing.                              |
| `workbench.handleUri(uri)`            | `(uri: Uri) => boolean`        | Hands a link that arrived to the extension its authority names. Answers whether one was waiting for it. |
| `workbench.destroy()`                 | `() => void`                   | Unmounts the shell.                                                                                     |

There is no `showView` for the primary side bar: pass `view` on construction, or leave it to the
reader — an extension reaches it through its own commands instead.

## The activity bar and side bars

The primary side bar has exactly one built-in container: the **explorer**, the file tree over
`fs`. Everything else in the activity bar — Search, Extensions, and whatever an `extensions`
entry contributes — is an extension's own container, in the order it was passed. See
[Using extensions](/guide/extensions) for how a manifest adds one.

At the foot of the bar sits the **gear**, which is everything about the workbench rather than
about a view: the [theme control](/guide/themes), and any command a manifest contributed to
`globalActivity` — where the settings extension puts its own Settings row, and where anything
of yours that is the reader's preference rather than the project's belongs
([Commands and menus](/api/commands)).

The **secondary side bar** has **nothing built in**. It exists only once an extension
contributes a container to it — a workbench with no such extension shows no toggle and no
column there at all.

```ts
workbench.showSecondary("chat");
```

## The panel

The panel at the foot of the editor area always has **Problems** built in, with no extension
required. Every other tab — a terminal, logs, source control — comes from an extension.
`panel`/`onPanel` on construction and `showPanel()` afterwards choose which view it shows, or
close it given nothing:

```ts
workbench.showPanel("problems");
```

## The status bar

The workbench itself puts three things in the status bar: an error/warning counter once
something has reported a problem (click it to open Problems), a line for whichever background
task is running longest, and, for the active file, its line count and its language name. Read-only
files get a lock icon instead of the language being clickable.

Extensions add their own items on either side of these through `window.createStatusBarItem` and
`languages.createLanguageStatusItem` — see [API: Prompts, notifications and UI](/api/ui).

## Multiple workspaces

An extension can open a workspace of its own — an empty tree, shown in place of the reader's
until it closes — with `codelet.workspace.open()`. It is a stack, not a picker: only the top
tree is showing, and there is no UI to switch between them.

Nothing about the reader's own tree is lost while another is showing. Their open tabs, carets and
unsaved edits are held aside and come back exactly as they were the moment the extension's
workspace closes. `name` on `Workbench` is what names the reader's own tree in that stack — an
extension-opened one is always named by whoever opened it.

## Server rendering

`renderWorkbench()` (from `codelet/workbench/server`) returns the same markup `Workbench` would
produce on the client, so the page has something to show before the bundle loads:

```ts
import { renderWorkbench } from "codelet/workbench/server";

const html = renderWorkbench({
  fs,
  open: "/README.md",
  active: "/README.md",
});
```

::warning
`fs`, `open`, `active`, `cursors`, `view`, `panel`, `secondary`, `theme`, `extensions` and
`themes` are **markup**. Pass the exact same values to `renderWorkbench()` and to `Workbench`,
or the client has nothing to match on the first render and hydration rebuilds the shell instead
of taking over. `settings`, `state`, `secrets` and `externalUri` render nothing, so
`renderWorkbench()` takes none of them. A value only the client can know — a panel remembered in
`localStorage`, say — has to be applied after mount instead, through `showPanel()` or
`showSecondary()`.
::

:read-more{to="/guide/files"}
:read-more{to="/guide/editing"}
:read-more{to="/guide/settings"}
:read-more{to="/guide/extensions"}

## The standalone editor

The editor the workbench mounts also ships on its own, as `Editor` from `codelet`, for a page
that needs one code field rather than an IDE — a config box, a snippet, a single file in a
playground. If you are building a workbench you need none of this: it makes its own editors.

```ts
import { Editor } from "codelet";

const editor = new Editor({
  parent: document.getElementById("editor")!,
  doc: "console.log('hello')\n",
  lang: "ts",
});
```

::codelet-playground

```ts [hello.ts]
export const greet = (name: string) => `hi ${name}`;

console.log(greet("codelet"));
```

::

### What you give up

`Editor` is one code field and nothing around it. Everything the rest of this guide describes is
the workbench's, not the editor's:

- **No files.** There is no `FileSystem`, no explorer and no concept of a path — just `doc` in
  and `value` out. One editor is one document.
- **No tabs, panel, side bars or status bar.** Nothing to open a second file into.
- **No command palette**, and no Search or Problems view.
- **No extensions.** `Editor` takes no `extensions` option, so no language servers, no
  diagnostics, no go-to-definition, no hover, no previews, no terminal. Completion is limited to
  the words already in the document.
- **No settings, secrets or stored state.**

What it does keep is the editing itself, which is the same editor the workbench uses.

### Options

| Option     | Type                                          | Default      | Description                                                                                                                 |
| ---------- | --------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `parent`   | `HTMLElement`                                 | —            | Element the editor mounts into. Required.                                                                                   |
| `doc`      | `string`                                      | `""`         | Initial document text.                                                                                                      |
| `lang`     | `string`                                      | —            | rangi grammar name, e.g. `"ts"`, `"python"`, `"yml"`. Plain text when omitted.                                              |
| `theme`    | `Theme`                                       | `lightTheme` | Chrome and token colors. Pass a stable object reference.                                                                    |
| `readOnly` | `boolean`                                     | `false`      | Blocks edits when `true`.                                                                                                   |
| `onChange` | `(value: string, update: ViewUpdate) => void` | —            | Called when the document changes.                                                                                           |
| `cursor`   | `Cursor`                                      | —            | Selected and scrolled to once, when the editor is created.                                                                  |
| `onCursor` | `(cursor: Cursor) => void`                    | —            | Called whenever the selection changes. Deduplicated: a drag firing many selection changes over the same lines reports once. |

`Cursor` is the same `{ from: Place, to?: Place }` the workbench uses — see
[Restoring where the reader was](/guide/editing#restoring-where-the-reader-was).

### Methods and properties

| Member        | Signature                     | Description                                                                                                     |
| ------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `view`        | `EditorView`                  | The underlying CodeMirror view. Read-only.                                                                      |
| `value`       | `string`                      | The current document text.                                                                                      |
| `setValue`    | `(next: string) => void`      | Replaces the document. No-op when `next` already matches it.                                                    |
| `setCursor`   | `(cursor: Cursor) => void`    | Selects and scrolls to a position. No-op when the selection is already there.                                   |
| `setLang`     | `(lang?: string) => void`     | Switches the grammar. No-op when it is already that language, since reconfiguring re-parses the whole document. |
| `setTheme`    | `(theme: Theme) => void`      | Swaps the theme. No-op when it is already that theme.                                                           |
| `setReadOnly` | `(readOnly: boolean) => void` | Toggles read-only mode.                                                                                         |
| `destroy`     | `() => void`                  | Tears down the view. Call it when the element is removed.                                                       |

### What comes for free

Every codelet editor — the workbench's tabs included — ships these with nothing to configure:
bracket and quote pairing with match highlighting, indentation-based code folding, auto-indent,
click-and-drag line selection in the gutter, find and replace (`Mod-F`, `Mod-H`), multiple
cursors and column selection (`Mod-click`, Alt-drag, `Mod-D`), word-based autocomplete, undo and
redo, active-line and selection-match highlighting, and scroll-past-end.

`lang` takes a [rangi](https://github.com/pi0/rangi) grammar name — 46 of them, `ts`, `js`, `tsx`,
`py`, `go`, `rs`, `java`, `cs`, `cpp`, `css`, `html`, `json`, `yml`, `md`, `sql` and `bash` among
them. An unrecognized name highlights nothing rather than throwing, and a document with no `lang`
renders as plain text. `editor.setLang(lang)` switches it later.

The font size shrinks a step on a phone-width viewport and grows back on rotation
([Small screens](/guide/mobile)), and `theme` takes the same `Theme` object the workbench does
([Themes](/guide/themes)).

### The escape hatch

`editor.view` is the CodeMirror `EditorView` underneath. Reach it for anything codelet does not
expose directly. There is deliberately no `extensions` option — configure the view itself through
`editor.view`.

### Server rendering

`renderEditorHTML` returns static markup that stands in for the editor until the client bundle
mounts the real one:

```ts
import { renderEditorHTML } from "codelet";

const html = renderEditorHTML({
  doc: "console.log('hello')\n",
  lang: "ts",
  cursor: { from: [3] },
});
```

| Option     | Type     | Default      | Description                                                                    |
| ---------- | -------- | ------------ | ------------------------------------------------------------------------------ |
| `doc`      | `string` | `""`         | Text to render.                                                                |
| `lang`     | `string` | —            | Same grammar name as `Editor`, tokenized and colored the same way.             |
| `theme`    | `Theme`  | `lightTheme` | Must match the theme the real `Editor` mounts with.                            |
| `maxLines` | `number` | `200`        | How many lines around `cursor` are rendered; the rest are left out.            |
| `cursor`   | `Cursor` | —            | Where the real view will scroll to, so the stand-in is already scrolled there. |

Unlike `Workbench`, `Editor` does not adopt this markup. Mount the editor into a sibling or
replacement element and remove the stand-in once the real view exists. It carries
`aria-hidden="true"` for that reason — it is a picture of an editor, not one a screen reader
should announce.

### React and Vue

`<CodeEditor />` wraps `Editor` for both frameworks, with `initialValue`/`value` for uncontrolled
and controlled use. [Getting started](/guide/usage)
