microsoft/vscode

Exploration: SCM providers

joaomoreno opened this issue · 5 comments

The Inner Loop

Code has successfully survived so far using git as the sole source control solution. This is obviously a narrow worldview since there are many other popular SCMs which lack of support could be limiting our user base. A few of the popular alternatives are TFS, SVN, Mercurial and Perforce.

We still consider Code to be a pretty decent tool which tackles the tight inner loop of development with git as an SCM. This inner loop focus allows us to justify having a simple UI, which lets you do 80% of the tasks within Code, while suggesting to use the CLI or another GUI application for the rest of the tasks. Think about visualising history and managing stashes, as examples. Note that these are still fair feature requests, which are properly tracked, but the argument here is that Code is still a successful tool while catering for the inner loop.

While making sense for SCMs like SVN and Mercurial, this argument might not translate easily to other, such as TFS, in which suggesting the CLI is not practical (users simply don't know how to) and suggesting another tool, which in this case happens to be Visual Studio itself, defeats the whole purpose.

This creates our first question: does the current philosophy of Code as a lightweight editor with some SCM features fit other SCM providers? Will it be enough?


The Least Common Denominator

The ultimate problem with supporting multiple SCM systems is to discover and specify the least common denominator between the different systems, in terms of functionality, UI and API. There is an obvious analogy to the Debugger world, with its debug adapters.

This can go from taking a large API area by sharing common widgets, such as buttons, input boxes, the tree, all the way to narrowing down the commonality and invent a common SCM model. Both solutions have advantages and disadvantages. They also have very different implementation time horizons.


The Interface

One other issue that came up in discussions concerns the interface between the SCM systems and Code itself.

If the least common denominator problem is solved by sharing platform UI pieces, this can be addressed the same way custom trees were in #14048. The extension API would be augmented and used for this purpose.

If, on the other hand, we decide to go down the road of supporting a common SCM model there are two alternatives. The obvious one would be to add API to our extension API, making SCM contributions simple extensions which are in charge of either spawning processes or binding to libraries in JavaScript. The other one would be to create a new protocol, on top of JSON-RPC, similarly to how it's done in the Debug world. This would enable SCM providers to be implemented in any language of choice, the obvious candidate being TFS implementation, which is already implemented as a Java library in the TEE project.

There are several approaches to coming up with the least common denominator problem. I'll do my best to put them to prose.

Features

Here is the current set of features implemented by the git contribution:

  • Dirty diff renders glyph margin decorations in visible editors
  • Branch status bar is a single click affordance in the status bar which renders some parts of the git status (branch/tag/commit name, dirty status). Clicking it will invoke the Quick open feature.
  • Sync status bar is a single click affordance in the status bar which renders more parts of the git status (ahead & behind commits). Clicking it will execute the sync action.
  • Quick open is a very rich quick open implementation which allows the creation and checking out of branches.
  • Viewlet, which contains:
    • Actions, both atop the viewlet and inside the ... menu
    • Commit input box, a multi-line, decoration-able, input box.
    • Tree (or list), containing the collections of unstaged, staged and merged changes
      • Items within the tree contain inline and contextual actions
  • Output channel, visible in the panel, already supported by API
  • Editor actions
  • Global actions

Common Model

The toughest part of this problem is how to make the current viewlet be something that could be contributed externally. Let's approach this first by sharing a common SCM model across the multiple SCM providers.

Dirty diff, viewlet and actions

Revolving around the register*Provider pattern, let's break apart the SCM functionality into a set of providers:

// dirty diff is maybe a bad name...
// other names derivate from `original`, `baseline`, etc.
interface DirtyDiffProvider {
  // returns the URI of the baseline document for dirty diff to compare with
  getDirtyDiffTextDocument(uri: Uri): Uri | Thenable<Uri>;
}

interface SourceControlGroup {
  id: string;
  label: string;
  icon: string | Uri;
}

interface SourceControlStatus {
  groups: { [id: string]: Uri[]; };
}

interface SourceControlStatusProvider {

  // a static way to know the labels for each resource group
  groups: SourceControlGroup[];

  // need a more generic term than `commit`
  commit(message: string): void | Thenable<void>;

  getStatus(): SourceControlStatus | Thenable<SourceControlStatus>;

  // ugh, how to do drag and drop?
  drop(resource: Uri): void: Thenable<void>;

  // the status might change due to more than fs events, so we still need
  // to give the provider a chance to let us know that the status changed
  onStatusChange: Event<void>;
}

We'd have to come up with the following new menu locations:

  • scm/title, or scm/overflow, for the actions in the ... menu
  • scm/resource, for the inline actions on the resources
  • scm/resource/context, for the context actions on the resources

We'd need to have an SCM switcher in the upcoming SCM viewlet. We could create a context key to indicate which is the currently active SCM provider. This would require an ID.

Status bar

The status bar affordances could come in via the createStatusBarItem API, but this would disconnect them from the notion of the currently active SCM provider.

Alternatively, we could allow the provider to populate the current 2 status bar affordances via API and register commands that get executed when they are clicked.

Quick Open

All the quick open smart interactions can be dropped and transformed into simpler commands and simpler quick open interactions. We would basically let this go for now.

Common UI

Another approach to solving the viewlet problem could be taking #14048 another step further and widening the new API to accommodate for the UI currently needed by the git viewlet.

The current viewlet is composed of an input box placed above a tree widget. If one looks at those as a single widget, one sees that pattern everywhere around the workbench: git and search viewlets, quick open itself, problems view, etc; they are all trees with an input box above.

Here's what we would have to do:

  • add input box related API to the TreeExplorerNodeProvider (or successor) interface, to be able to get keypress events from the extension side.
  • add more methods, analogous to getLabel, to be able to render decorations, counters, the file name and file path for each file status.
  • Either let go the fact that each status section is non-collapsible, or add that option too the to API.
  • Figure out how to describe drag and drop in the API.
  • Figure out how to create dynamic menu locations for the global viewlet actions and contextual actions, or also add that to the API.
  • Figure out how to manage multiple SCM providers, is this even a desirable feature?

tbc

Common Model 2

We can also approach the problem from a push point of view. Here's how it could look:

export namespace scm {
    export function createSCMProvider(id: string, delegate: SCMDelegate): SCMProvider;
}

export interface SCMDelegate {
    commitCommand: string;
    clickCommand: string;
    dragCommand?: string;
    getOriginalResource(uri: Uri): Uri | Thenable<Uri>;
}

export interface SCMProvider extends Disposable {
    createResourceGroup(id: string, label: string): SCMResourceGroup;
}

export interface SCMResourceGroup extends Disposable {
    set(...resources: SCMResource[]): void;
    get(): SCMResource[];
}

export interface SCMResource {
    uri: Uri;
    // status type, icon decoration, etc
}

The SCM viewlet would be able to show the status given by a single SCM provider at once. Some ideas to select that provider include showing a dropdown picker or having API for providers to announce that they provide SCM info for each open file.

We would use the provider ID as a part of several environment context keys, which would be used to add commands to menus. We could add that context-aware functionality to the status bar contributions too, allowing the status bar items to be alive only when a specific scm context is set.

Closing the discussion.