
# Views

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:

```ts
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](/api/extension).

## Tree views

```ts
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](/api/commands). |
| `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.

```ts
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:

```ts
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](/extensions/search)) is a worked tree view with a
filter box and a two-level tree of files and matches:

::codelet-playground{mode="workbench" active="README.md" extensions="search" view="search" height="420"}

```md [README.md]
# Hello

A search for "hello" finds this file.
```

```ts [greet.ts]
export const greet = () => "hello";
```

::

### Drag and drop

`createTreeView`'s `dragAndDropController` makes a tree's rows draggable and lets it accept a
drop:

```ts
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)`:

```ts
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](/extensions/logs)) uses both, alongside a `select` dropdown for severity:

::codelet-playground{mode="workbench" extensions="logs" view="logs" height="360"}

```md [README.md]
Open your browser's DevTools console and run `console.warn("careful")` — a coloured row appears
below, and the dropdown over the rows narrows what severity shows.
```

::

## `viewsWelcome`

Markdown shown in place of a view's rows while it has none:

```ts
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:

```ts
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](/extensions/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.

```tsx
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:

```tsx
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](/extensions/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](/extensions/extensions)) is a simpler one, listing what the workbench is
running:

::codelet-playground{mode="workbench" extensions="extensions-view search" view="extensions" height="360"}

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

::

## File decorations

`registerFileDecorationProvider` badges and colours a file wherever the explorer, or a
contributed tree whose rows carry `resourceUri`, draws it:

```ts
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:

```ts
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:

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

::codelet-playground{mode="workbench" active="index.ts" extensions="search lsp-typescript" view="search" height="420"}

```ts [greet.ts]
export function greet(name: string) {
  return `hi ${name}`;
}
```

```ts [index.ts]
import { greet } from "./greet";

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

::

Right-click `greet` in either file and choose **Go to References** — the Search view swaps to
the list `registerLocationsView` was handed.
