Codelet logoCodelet

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

OptionTypeDefaultDescription
fsFileSystema new, empty oneThe filesystem the workbench shows and edits.
namestringnoneWhat the tree is called. Not shown anywhere in the chrome.
openstring \| string[]nonePaths opened as tabs on mount.
activestringthe last path in openWhich of open has focus.
onOpen(open: string[], active: string) => voidCalled with the open tabs and the active one whenever either changes.
cursorsRecord<string, Cursor>noneWhere each path was looking, read once when that tab opens.
onCursor(path: string, cursor: Cursor) => voidCalled with a path and its cursor whenever it moves.
onSave(path: string, text: string) => voidCalled with a path and its text when the reader saves with Mod-S.
viewstringexplorer if the tree has a file, else closedWhich container the side bar shows. "" closes it.
onView(view: string) => voidCalled with the side bar's container, or "", whenever the reader changes it.
panelstringnone (closed)Which view the panel at the foot of the editor shows.
onPanel(panel?: string) => voidCalled with the panel's view, or nothing, whenever it changes.
secondarystringnone (closed)Which container the secondary side bar shows.
onSecondary(secondary?: string) => voidCalled with the secondary bar's container, or nothing, whenever it changes.
extensionsreadonly Extension[]noneThe extensions to activate — what fills the activity bar, panel and command palette beyond the built-ins.
settingsSettingsStorelocalSettings()Where an extension's settings are kept. See Settings.
stateSettingsStorelocalSettings(), a second documentWhere an extension's own globalState/workspaceState are kept.
secretsSecretsStorenone (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.
themeThemelightThemeThe active theme.
themesreadonly Theme[]noneWhich themes the reader may choose between. See Themes.
onTheme(theme: Theme) => voidCalled 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

MemberTypeDescription
workbench.fsFileSystemThe filesystem the workbench is showing.
workbench.parentHTMLElementThe element it mounted into.
workbench.setTheme(theme)(theme: Theme) => voidSwitches the active theme. Does nothing if it's the same object already in use.
workbench.showPanel(panel?)(panel?: string) => voidOpens the panel onto a view, or closes it given nothing.
workbench.showSecondary(secondary?)(secondary?: string) => voidOpens the secondary side bar onto a container, or closes it given nothing.
workbench.handleUri(uri)(uri: Uri) => booleanHands a link that arrived to the extension its authority names. Answers whether one was waiting for it.
workbench.destroy()() => voidUnmounts 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().

Read more in Guide > Files.
Read more in Guide > Editing.
Read more in Guide > Settings.
Read more in 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.

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

OptionTypeDefaultDescription
parentHTMLElementElement the editor mounts into. Required.
docstring""Initial document text.
langstringrangi grammar name, e.g. "ts", "python", "yml". Plain text when omitted.
themeThemelightThemeChrome and token colors. Pass a stable object reference.
readOnlybooleanfalseBlocks edits when true.
onChange(value: string, update: ViewUpdate) => voidCalled when the document changes.
cursorCursorSelected and scrolled to once, when the editor is created.
onCursor(cursor: Cursor) => voidCalled 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

MemberSignatureDescription
viewEditorViewThe underlying CodeMirror view. Read-only.
valuestringThe current document text.
setValue(next: string) => voidReplaces the document. No-op when next already matches it.
setCursor(cursor: Cursor) => voidSelects and scrolls to a position. No-op when the selection is already there.
setLang(lang?: string) => voidSwitches the grammar. No-op when it is already that language, since reconfiguring re-parses the whole document.
setTheme(theme: Theme) => voidSwaps the theme. No-op when it is already that theme.
setReadOnly(readOnly: boolean) => voidToggles read-only mode.
destroy() => voidTears 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] },
});
OptionTypeDefaultDescription
docstring""Text to render.
langstringSame grammar name as Editor, tokenized and colored the same way.
themeThemelightThemeMust match the theme the real Editor mounts with.
maxLinesnumber200How many lines around cursor are rendered; the rest are left out.
cursorCursorWhere 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