Workbench
The VS Code-shaped shell around one editor — options, methods, the panel, the status bar and server rendering
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.
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.
#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. |
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. |
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. |
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:
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) reports nothing here.
See Editing, tabs and saving for how tabs, dirty marks and saving behave.
Warning
open, active, cursors, view, panel, secondary, theme, extensions and themes are
markup — see 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 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, 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).
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.
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:
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.
#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:
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().
#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.
import { Editor } from "codelet";
const editor = new Editor({
parent: document.getElementById("editor")!,
doc: "console.log('hello')\n",
lang: "ts",
});#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 — justdocin andvalueout. 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.
Editortakes noextensionsoption, 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.
#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 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), and theme takes the same Theme object the workbench does
(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:
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