Prompts, notifications and UI
Quick picks, input boxes, messages, the status bar, progress and output channels
Everything an extension can put in front of the reader that is not a view or an editor: a
question, a message, a line in the status bar, a progress spinner, an output channel. All of it
hangs off vscode.window and vscode.env, handed to activate the same way the rest of the
namespace is.
#Asking a question
showQuickPick puts a list of choices to the reader and resolves with the one they took:
const choice = await vscode.window.showQuickPick(
[
{ label: "Small", description: "2 spaces" },
{ label: "Large", description: "4 spaces" },
],
{ placeHolder: "Indent size" },
);Escape or a click outside resolves undefined. Pass canPickMany: true for a tick per row and
the whole selection as the answer:
const chosen = await vscode.window.showQuickPick(items, {
canPickMany: true,
placeHolder: "Which files to include",
});showInputBox asks for a line of text. validateInput runs on every keystroke; returning a
string keeps Enter from doing anything until the field says something else:
const name = await vscode.window.showInputBox({
prompt: "Branch name",
placeHolder: "feature/…",
validateInput: (value) => (/^[\w./-]+$/.test(value) ? undefined : "No spaces or punctuation."),
});Nothing typed and Enter pressed answers ""; escape answers undefined — worth telling apart
if an empty value means something.
showWorkspaceFolderPick asks about the one folder there is:
const folder = await vscode.window.showWorkspaceFolderPick({ placeHolder: "Confirm the folder" });#Holding a prompt open
showQuickPick and showInputBox are one call each: they show, wait for an answer, and hide.
createQuickPick() and createInputBox() give you the same field without any of that — you own
its whole lifecycle and write to it as things change:
const picker = vscode.window.createQuickPick<vscode.QuickPickItem>();
picker.placeholder = "Search files…";
picker.busy = true;
picker.onDidChangeValue(async (query) => {
picker.busy = true;
picker.items = await search(query); // fills the list as results come back
picker.busy = false;
});
picker.onDidAccept(() => {
const [item] = picker.selectedItems;
if (item) open(item);
picker.hide();
});
picker.show();Reach for these over the one-shot calls whenever the list depends on what has been typed, whenever
you need a spinner while something loads, or whenever you want buttons on the field itself
(buttons, onDidTriggerButton) or on a row (QuickPickItem.buttons,
onDidTriggerItemButton). createInputBox() is the same idea with no list under it — value,
password, prompt, validationMessage written directly instead of read through options.
onDidHide fires however the field closes, and dispose() when you are done with it for good.
#Messages
Three calls, one per severity, each returning the button the reader clicked:
const answer = await vscode.window.showWarningMessage(
"Discard 3 unsaved files?",
{ modal: true, detail: "This cannot be undone." },
"Discard",
);
if (answer === "Discard") discard();Without items, the call still resolves — undefined if the reader dismisses it, whatever
string they clicked otherwise. Without { modal: true } a message is a toast in the corner the
reader can read past; with it, a dialog over the whole shell they have to answer before doing
anything else. Reserve modal for something that cannot be undone — detail is its second line
and only shows in a modal. Messages queue one at a time, oldest first, and if the extension that
raised one is stopped before it is answered, the promise resolves undefined rather than hanging.
#The status bar
const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
item.text = "$(check) All saved";
item.tooltip = "Every file is written to disk";
item.command = "myExtension.showDetails";
item.show();
context.subscriptions.push(item);Higher priority sits further left within its group (Left or Right). text reads $(name)
as one of codelet's icons, so "$(sync~spin) Syncing…" is a spinning icon before the words.
tooltip and color (a CSS string or a ThemeColor) are both honoured; backgroundColor takes
only statusBarItem.errorBackground or statusBarItem.warningBackground. command runs on
click — a string id, or a Command object if you need to pass arguments. show(), hide() and
dispose() are yours to call; nothing shows an item you have not called show() on.
For a message that should say itself once and go, setStatusBarMessage(text, hideAfterTimeout?)
is a status bar item and a timer in one call, returning a Disposable that takes it down early.
#Progress
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Window, title: "Publishing" },
async (progress) => {
progress.report({ message: "Uploading assets", increment: 20 });
await uploadAssets();
progress.report({ message: "Writing manifest", increment: 80 });
await writeManifest();
},
);ProgressLocation.Window is the only location — a line in the status bar, replaced by whatever
the task last reported. increment is a percentage added to what came before; report none and
the line shows a spinner with no bar, which is the honest shape for work that cannot say how far
along it is. There is no cancellation: the task runs to completion or throws, and nothing here
gives the reader a button to stop it early.
#Output channels
const output = vscode.window.createOutputChannel("My Extension", { log: true });
output.info("Starting up");
output.warn("Config missing a value, using the default");
output.error(new Error("Could not reach the server"));createOutputChannel(name) alone gives you an OutputChannel: append, appendLine,
replace, clear, show, hide, dispose. Passing { log: true } gives you a
LogOutputChannel with trace, debug, info, warn and error instead — trace lands as
debug, there being no fifth row to draw it at. A channel's lines show in the Logs panel
(Logs) when a workbench mounts it, and print to the console when
none is mounted — an extension does not need to know which.
#The theme
if (vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark) {
// …
}
context.subscriptions.push(vscode.window.onDidChangeActiveColorTheme(() => rebuildWebview()));activeColorTheme carries one member, kind — ColorThemeKind.Light or .Dark (.HighContrast
and .HighContrastLight are declared for an extension that compares against them, but neither is
ever answered). A webview is a whole document that styles itself, and it is rebuilt whenever the
theme changes rather than told about it — this event is how an extension notices and does the
same to anything else it drew that read the theme once.
#Icons and colors
new vscode.ThemeIcon(name) names one of codelet's own glyphs rather than drawing one —
ThemeIcon.File and ThemeIcon.Folder are built in, and a second argument colors it with a
ThemeColor. A name outside the set draws nothing, the same way an unknown id does in VS Code.
codelet ships a few dozen glyphs of its own — files, search, folder, gear, trash,
star, scm, terminal, chat, run, refresh, check, error, warning, info, add,
edit, copy, download, lock, plug, and more — plus a set of VS Code's own codicon names
aliased onto the same shapes (settings → gear, source-control → scm, play → run,
star-full → star, and so on). There is no published list to import; the fastest way to see what
is available is to look at the workbench's own chrome — every glyph in the activity bar, the
status bar and a tree row is one of these names, $(name) in a status bar item's text included.
new vscode.ThemeColor(id) names a color from a fixed table rather than VS Code's full theme —
an id outside it paints nothing. What is in the table:
| Id | What it colors |
|---|---|
gitDecoration.addedResourceForeground | An added file's badge and name. |
gitDecoration.modifiedResourceForeground | A modified file's badge and name. |
gitDecoration.deletedResourceForeground | A deleted file's badge and name. |
gitDecoration.renamedResourceForeground | A renamed file's badge and name. |
gitDecoration.untrackedResourceForeground | An untracked file's badge and name. |
gitDecoration.ignoredResourceForeground | An ignored file's badge and name. |
gitDecoration.conflictingResourceForeground | A conflicting file's badge and name. |
list.deemphasizedForeground | Muted row text. |
list.errorForeground / list.warningForeground | Row text at that severity. |
statusBarItem.errorBackground | StatusBarItem.backgroundColor. |
statusBarItem.warningBackground | StatusBarItem.backgroundColor. |
problemsErrorIcon.foreground | An error glyph. |
problemsWarningIcon.foreground | A warning glyph. |
problemsInfoIcon.foreground | An info glyph. |
editorGutter.addedBackground | The quick diff gutter's added bar. |
editorGutter.modifiedBackground | The quick diff gutter's modified bar. |
editorGutter.deletedBackground | The quick diff gutter's deleted wedge. |
#The clipboard and external links
await vscode.env.clipboard.writeText(url);
const copied = await vscode.env.clipboard.readText();
const opened = await vscode.env.openExternal(vscode.Uri.parse("https://example.com"));
if (!opened) {
// a popup the browser blocked, with no gesture behind it
}env.clipboard wraps navigator.clipboard; a browser that offers none, or a read the reader has
not allowed, rejects rather than answering with nothing. env.openExternal opens a new tab and
resolves whether it actually opened — false is a blocked popup, worth telling the reader about
rather than swallowing.