Codelet logoCodelet

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

MemberWhere it lands
labelthe heading over that provider's groups
inputBoxthe message box, Mod-Enter accepts
commitTemplatewritten into the box when it changes and the box is empty
acceptInputCommandthe button under the box, drawn from its title
statusBarCommandsreal status bar items, each a button running its command
countthe badge on the activity bar icon — every group's rows added up if unset
group.label / hideWhenEmptya foldable section, and whether an empty one draws at all
state.resourceUria file icon, its name, and its folder dimmed after it
state.commandwhat a click on the row runs
state.decorationstooltip, 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.inputBox is the real one, and the only one this reads.
  • git itself. This is the pane, vscode.diff and 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.
Read more in API > Integrations.
Read more in Guide > Editing.