Source control
A Source Control pane in the activity bar, drawing whatever registers as a provider
scm adds a Source Control pane to the activity bar: a group of changed files per provider, a
commit message box above them, and a click on a row that opens what changed.
import { Workbench, FileSystem } from "codelet/workbench";
import { scm } from "codelet/extensions/scm";
const workbench = new Workbench({
parent: document.getElementById("app")!,
fs: new FileSystem({ "/src/index.ts": "export const hi = 1;\n" }),
extensions: [scm()],
});Warning
This is the pane, and only the pane. It brings no git and no provider — what fills it is a
SourceControl some other extension registers against vscode.scm, the workbench's own
namespace. A workbench that mounts scm and no provider gets no icon in the activity bar at
all: the container is only there while something is tracking, so nothing shows until something
does. Register a provider and the icon, the pane and the badge all arrive together.
One provider ships: codelet/extensions/github
keeps the tarball it unpacked as a base revision, so mounting the two together is a real,
read-only source control over any repository on GitHub — no git anywhere.
#Registering a provider
Write against vscode.scm — VS Code's own namespace and
objects, member for member, so a provider written for real VS Code compiles here unmodified.
import { defineExtension } from "codelet/extensions";
export const git = defineExtension({
manifest: { name: "git", displayName: "Git" },
activate(context, vscode) {
const control = vscode.scm.createSourceControl("git", "Git", vscode.Uri.file("/"));
control.acceptInputCommand = { command: "git.commit", title: "Commit" };
control.inputBox.placeholder = "Message (⌘Enter to commit)";
control.statusBarCommands = [{ command: "git.checkout", title: "$(scm) main" }];
const changes = control.createResourceGroup("working", "Changes");
changes.resourceStates = [
{
resourceUri: vscode.Uri.file("/src/index.ts"),
command: {
command: "vscode.diff",
title: "Open Changes",
arguments: [
vscode.Uri.parse("git:/HEAD/src/index.ts"),
vscode.Uri.file("/src/index.ts"),
"index.ts (Working Tree)",
],
},
decorations: {
tooltip: "Modified",
iconPath: new vscode.ThemeIcon(
"scm",
new vscode.ThemeColor("gitDecoration.modifiedResourceForeground"),
),
},
},
];
context.subscriptions.push(control);
},
});Writing a member is what repaints the pane: assign group.resourceStates and the rows redraw,
set control.count and the badge on the activity bar icon changes. There's no second call to
make — the same as VS Code's own workbench.
#What the pane draws
| Member | Where it lands |
|---|---|
label | the heading over that provider's groups |
inputBox | the message box, Mod-Enter accepts |
commitTemplate | written into the box when it changes and the box is empty |
acceptInputCommand | the button under the box, drawn from its title |
statusBarCommands | real status bar items, each a button running its command |
count | the badge on the activity bar icon — every group's rows added up if unset |
group.label / hideWhenEmpty | a foldable section, and whether an empty one draws at all |
state.resourceUri | a file icon, its name, and its folder dimmed after it |
state.command | what a click on the row runs |
state.decorations | tooltip, strikeThrough, faded, and a ThemeIcon whose colour is used |
decorations.light / decorations.dark override the base for the theme showing. Colour lands
on one letter at the end of the row and nowhere else — names stay plain foreground text, folders
are dimmed, and a whole row painted in colour would leave nothing standing out. That letter is
your FileDecorationProvider's badge, the same one the explorer draws beside that file:
vscode.window.registerFileDecorationProvider({
onDidChangeFileDecorations: changed.event,
provideFileDecoration: (uri) =>
new vscode.FileDecoration(
"M",
"Modified",
new vscode.ThemeColor("gitDecoration.modifiedResourceForeground"),
),
});Register no decoration provider and a row falls back to the ThemeIcon on
state.decorations instead.
#Buttons
Four places, read from your manifest: scm/title over the provider's heading,
scm/inputBox beside the message box, scm/resourceGroup/context on a group, and
scm/resourceState/context on a row.
contributes: {
commands: [{ command: "git.stage", title: "Stage Changes", icon: "add" }],
menus: {
"scm/resourceState/context": [
{ command: "git.stage", group: "inline", when: "scmProvider == git && scmResourceGroup == working" },
],
commandPalette: [{ command: "git.stage", when: "false" }],
},
}when reads VS Code's own keys — scmProvider is the source control's id, scmResourceGroup a
group's id, scmResourceGroupState and scmResourceState whatever contextValue you set on the
group or the row. A command's icon is the button; without one, its title is.
#The diff view and the quick-diff gutter
These belong to the workbench, not to this extension — they work as soon as anything registers a
source control, with or without scm mounted.
vscode.diff opens a tab with the two documents side by side, highlighted for the file's own
language, read-only, with unchanged runs collapsed. Either side may be a real file or a document
one of your own providers serves — which is where a HEAD version comes from:
await vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse("git:/HEAD/src/index.ts"), // left
vscode.Uri.file("/src/index.ts"), // right
"index.ts (Working Tree)", // title, optional
);Without a title the tab's bar shows the two addresses instead. A document a provider serves is read once while its tab is open — close it and run the command again for a fresh comparison.
The quick-diff gutter paints a bar beside every changed line of the file you're editing,
sourced from SourceControl.quickDiffProvider:
control.quickDiffProvider = {
provideOriginalResource: (uri) => vscode.Uri.parse(`git:/HEAD${uri.path}`),
};The first registered source control that answers a Uri for the file showing is the one the
gutter is drawn against. No provider, or a provider that answers nothing for that file, means no
gutter — there's no bar guessing at a change nothing said was one.
#What's not here
scm.inputBox, VS Code's deprecated global for "whichever source control was made last".SourceControl.inputBoxis the real one, and the only one this reads.- git itself. This is the pane,
vscode.diffand the gutter — what knows about commits and history is a provider's to bring. The one that ships reads a tarball, which is a base revision with no history under it: read-only, and clear about it.