Views
Tree views, webviews, component views, decorations and the chrome codelet adds
A view is a pane inside a container — the activity bar, the panel, or the secondary side bar. This page covers the three kinds a view can be, the chrome around one, and the two calls that show something outside any container at all: file decorations and a list of locations.
#Containers and views
contributes.viewsContainers declares where a container sits — activitybar, panel or
secondary — and contributes.views, keyed by container id, declares what's in it:
contributes: {
viewsContainers: {
activitybar: [{ id: "todo", title: "To Do", icon: "check" }],
},
views: {
todo: [{ id: "todo.list", name: "To Do", type: "tree" }],
},
},type picks the kind: "tree" (the default) fills a view from a TreeDataProvider,
"webview" from a sandboxed document, "component" from a real component the workbench mounts
directly. A container is its views — one whose every view fails its when shows no icon and no
tab at all. The rest of a container's and a view's fields are on
Anatomy of an extension.
#Tree views
import { defineExtension } from "codelet/extensions";
export const todo = defineExtension({
manifest: {
name: "todo",
contributes: {
viewsContainers: { activitybar: [{ id: "todo", title: "To Do", icon: "check" }] },
views: { todo: [{ id: "todo.list", name: "To Do" }] },
},
},
activate(context, vscode) {
context.subscriptions.push(
vscode.window.registerTreeDataProvider("todo.list", {
getChildren: () => ["Buy milk", "Write docs"],
getTreeItem: (label) => new vscode.TreeItem(label),
}),
);
},
});registerTreeDataProvider(viewId, provider) fills a "tree" view and returns a Disposable.
createTreeView(viewId, options) does the same and hands back a TreeView you can also set
message, title, description or badge on — the same split registerComponentView /
createComponentView follows for a component view.
A TreeDataProvider<T> has four members:
| Member | Description |
|---|---|
getChildren(element?) | The root's children with no argument, or that element's. |
getTreeItem(element) | The TreeItem to draw for it. |
getParent(element)? | Its parent, for TreeView.reveal to walk up. Without it, reveal only finds root rows. |
resolveTreeItem(item, element, token)? | Fills in the rest of a row once the pointer reaches it — a tooltip, a command. |
onDidChangeTreeData? | Fires to redraw. undefined/an element/an array redraws everything. |
A TreeItem carries the row itself:
| Field | Type | Description |
|---|---|---|
label | string \| TreeItemLabel | The row's text, or { label, highlights } for ranges to underline. |
id | string? | Keys expansion state across a refetch, else the row's path from root does. |
iconPath | string \| ThemeIcon | A name, or a ThemeIcon — never an image. |
description | string \| boolean | Muted text after the label. |
resourceUri | Uri? | Gives the row a file icon and picks up file decorations. |
tooltip | string? | Plain text. |
command | Command? | Runs on click. |
collapsibleState | TreeItemCollapsibleState | None, Collapsed or Expanded. |
contextValue | string? | Read as viewItem by a view/item/context menu — see Commands and menus. |
checkboxState | TreeItemCheckboxState? | Unchecked or Checked — a checkbox before the row. |
accessibilityInformation | { label, role? }? | What a screen reader says, when the label alone isn't it. |
Checking a row's box fires TreeView.onDidChangeCheckboxState — nothing redraws it for you, the
provider's onDidChangeTreeData still has to.
const changed = new vscode.EventEmitter<void>();
vscode.window.registerTreeDataProvider("todo.list", {
onDidChangeTreeData: changed.event,
getChildren: () => items,
getTreeItem: (item) => {
const row = new vscode.TreeItem(item.label, vscode.TreeItemCollapsibleState.None);
row.checkboxState = item.done
? vscode.TreeItemCheckboxState.Checked
: vscode.TreeItemCheckboxState.Unchecked;
row.contextValue = item.done ? "done" : "open";
return row;
},
});
// Later, once something changes:
changed.fire();A TreeView returned by createTreeView carries the chrome above the rows and the state a
mounted pane holds:
const view = vscode.window.createTreeView("todo.list", { treeDataProvider: provider });
view.message = "3 open";
view.badge = { value: 3, tooltip: "3 open items" };message is a line above the rows, title renames the heading, description reads beside it,
and badge marks the container's icon. selection, onDidChangeSelection, visible and
onDidChangeVisibility all describe the pane while it's mounted, and read as empty/false while
it isn't — a folded sidebar has no rows to select.
codelet/extensions/search (Search) is a worked tree view with a
filter box and a two-level tree of files and matches:
#Drag and drop
createTreeView's dragAndDropController makes a tree's rows draggable and lets it accept a
drop:
vscode.window.createTreeView("todo.list", {
treeDataProvider: provider,
dragAndDropController: {
dropMimeTypes: ["application/vnd.code.tree.todo.list"],
dragMimeTypes: [],
handleDrop(target, dataTransfer) {
const dragged = dataTransfer.get("application/vnd.code.tree.todo.list")?.value;
// …
},
},
});Any controller makes the rows draggable, whether or not it implements handleDrag: the tree's
own mime type, application/vnd.code.tree.<viewId>, always carries the elements themselves —
nothing is serialized, since drag and drop happens inside one page. A drop only lands on a row,
never on empty space below the tree.
#View chrome codelet adds
filter and select in a manifest add a search box and a dropdown above a view's rows, read
back through codelet.views.filter(viewId) and codelet.views.select(viewId):
views: {
todo: [{
id: "todo.list",
name: "To Do",
filter: { placeholder: "Filter" },
select: { options: [{ value: "all", label: "All" }, { value: "open", label: "Open" }] },
}],
},
activate(context, vscode, codelet) {
const filter = codelet.views.filter("todo.list");
const select = codelet.views.select("todo.list");
context.subscriptions.push(
filter!.onDidChange(() => refresh()),
select!.onDidChange(() => refresh()),
);
},Both are { value, onDidChange } — filter/select return undefined for a view that didn't
declare one. select's first option is what the dropdown opens on.
tagged and colored change how a row is drawn rather than adding chrome: tagged says a row's
highlighted range is a name it leads with rather than a search match, so it's brought forward out
of the muted text instead of underlined; colored paints a row whose ThemeIcon names a colour
in that colour entirely, label included, rather than the glyph alone. codelet/extensions/logs
(Logs) uses both, alongside a select dropdown for severity:
#viewsWelcome
Markdown shown in place of a view's rows while it has none:
viewsWelcome: [
{ view: "todo.list", contents: "No items yet.\n\n[Add one](command:todo.add)" },
],[text](command:id) runs a command when clicked. when scopes a welcome the same way any other
context expression does, and group@order decides which shows first where more than one holds —
ungrouped comes last. view can also name "explorer", the shell's own file tree, for a welcome
over the workspace itself rather than a contributed view.
#Webview views
A "webview" view is filled by a WebviewViewProvider, resolved the first time it mounts:
views: { todo: [{ id: "todo.list", name: "To Do", type: "webview" }] },
activate(context, vscode) {
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("todo.list", {
resolveWebviewView(view) {
view.webview.options = { enableScripts: true };
view.webview.html = `<button onclick="alert('hi')">Click</button>`;
view.webview.onDidReceiveMessage((message) => console.log(message));
},
}),
);
},webview.html is a whole document in a sandboxed frame with no access to the page around it —
postMessage(message) from either side and onDidReceiveMessage on the other are the only way
across. WebviewOptions controls what the sandbox allows:
| Option | Type | Default | Description |
|---|---|---|---|
enableScripts | boolean | false | Lets the document run scripts. |
enableForms | boolean | enableScripts's value | Lets a form in the document submit. |
enableCommandUris | boolean \| readonly string[] | false | command: links: every command, or only these. |
localResourceRoots | Uri[]? | everything workspace.fs reaches | Narrows what asWebviewUri may resolve. |
webview.asWebviewUri(uri) turns a file: path into an address this document can load — an
extension's own stylesheet or image. There's no server behind it: every such address found in
the document is rewritten, as the frame is built, into a data: URI of the file's actual bytes,
so cspSource (for a Content-Security-Policy the document writes) is literally data:. That's
also its limit — an address built at runtime and handed to the frame over postMessage, rather
than written directly into the document, resolves nothing: there's no real directory behind any
of this for such a request to reach.
codelet/extensions/terminal (Terminal) is a webview view, one
xterm per tab, filled this way.
#Component views
The third kind, and codelet's own: type: "component" plus
codelet.window.registerComponentView, mounting a real component where the workbench would
otherwise mount a frame.
views: { todo: [{ id: "todo.list", name: "To Do", type: "component" }] },
activate(context, vscode, codelet) {
context.subscriptions.push(codelet.window.registerComponentView("todo.list", TodoPane));
},TodoPane is a plain component of your own — JSX, types shared with the extension that owns it,
no postMessage between the two halves of one view. It takes no props, since a view's own state
belongs to the extension that registered it:
import { useState } from "preact/hooks";
function TodoPane() {
const [items, setItems] = useState<string[]>([]);
return (
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
);
}createComponentView(viewId, component) is the same registration, returning a ComponentView
you can also set description and badge on — there's no title (rename the container in the
manifest instead) and no message (a component draws its own).
Warning
A component view only works where preact is one instance. Today that means an extension
bundled with the workbench's own build, or a build that resolves codelet to this repo's
source — not an extension installed from npm, whose own preact is a second copy that breaks
hooks against the workbench's. WebviewView is the surface to use for an extension published on
its own.
codelet/extensions/scm (Scm) draws its whole pane this way — a
component rather than a tree, since a commit box and a row's own buttons are chrome a
TreeDataProvider has no way to ask for. codelet/extensions/extensions
(Extensions) is a simpler one, listing what the workbench is
running:
#File decorations
registerFileDecorationProvider badges and colours a file wherever the explorer, or a
contributed tree whose rows carry resourceUri, draws it:
context.subscriptions.push(
vscode.window.registerFileDecorationProvider({
provideFileDecoration(uri) {
if (!modified.has(uri.fsPath)) return;
return new vscode.FileDecoration(
"M",
"Modified",
new vscode.ThemeColor("gitDecoration.modifiedResourceForeground"),
);
},
}),
);A FileDecoration is a badge (a letter or two), a tooltip, a color (a ThemeColor, not a
raw value), and propagate — whether a directory takes the colour of a decoration under it.
Answer synchronously and a row is decorated on first paint; answer with a promise and the row
stays plain until you fire onDidChangeFileDecorations.
codelet.window.decorationOf(path) is the reading half, for a pane drawing its own rows of files
that should wear the same badges the explorer does — the letter a source control pane draws
beside a changed file's name is read this way, off whichever provider answered first:
const badge = codelet.window.decorationOf(path)?.badge;#A list of locations
codelet.window.registerLocationsView is where a list of places goes — the one language gesture
that doesn't open a single file, References being the case. There's no built-in peek widget; the
reader gets whatever pane registers here instead:
context.subscriptions.push(
codelet.window.registerLocationsView((title, locations) => {
// title: "References to greet", locations: readonly Location[]
}),
);codelet/extensions/search is the registrant that ships — the same tree the Search view already
draws, so a reference search lands in familiar rows. Registering nothing here means "Go to
References" simply has nowhere to send its answer, so the row is missing from the editor's
context menu rather than doing nothing when clicked.
Right-click greet in either file and choose Go to References — the Search view swaps to
the list registerLocationsView was handed.