scm: make tree rendering opt-in via proposed API

fix #82203
This commit is contained in:
Joao Moreno 2019-10-10 18:37:28 +02:00
parent e80d909439
commit afa911d942
8 changed files with 92 additions and 28 deletions

View file

@ -691,7 +691,9 @@ export class Repository implements Disposable {
this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, onDotGitFileChange, outputChannel));
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
this._sourceControl = scm.createSourceControl('git', 'Git', root, {
treeRendering: true
});
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;

View file

@ -764,6 +764,35 @@ declare module 'vscode' {
//#endregion
//#region Joao: SCM tree rendering
/**
* Options for creating a [SourceControl](#SourceControl) instance.
*/
export interface SourceControlOptions {
/**
* Whether tree rendering is supported by the [SourceControl](#SourceControl) instance.
*/
readonly treeRendering?: boolean;
}
export namespace scm {
/**
* Creates a new [source control](#SourceControl) instance.
*
* @param id An `id` for the source control. Something short, e.g.: `git`.
* @param label A human-readable string for the source control. E.g.: `Git`.
* @param rootUri An optional Uri of the root of the source control. E.g.: `Uri.parse(workspaceRoot)`.
* @param options Additional options for creating the source control.
* @return An instance of [source control](#SourceControl).
*/
export function createSourceControl(id: string, label: string, rootUri?: Uri, options?: SourceControlOptions): SourceControl;
}
//#endregion
//#region Joao: SCM Input Box
/**

View file

@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { assign } from 'vs/base/common/objects';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation } from 'vs/workbench/contrib/scm/common/scm';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMProviderProps, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../common/extHost.protocol';
import { Command } from 'vs/editor/common/modes';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { ISplice, Sequence } from 'vs/base/common/sequence';
@ -128,12 +128,15 @@ class MainThreadSCMProvider implements ISCMProvider {
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
get treeRendering(): boolean { return this._props.treeRendering; }
constructor(
private readonly proxy: ExtHostSCMShape,
private readonly _handle: number,
private readonly _contextValue: string,
private readonly _label: string,
private readonly _rootUri: URI | undefined,
private readonly _props: SCMProviderProps,
@ISCMService scmService: ISCMService
) { }
@ -287,8 +290,8 @@ export class MainThreadSCM implements MainThreadSCMShape {
this._disposables.dispose();
}
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void {
const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.revive(rootUri), this.scmService);
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, props: SCMProviderProps): void {
const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.revive(rootUri), props, this.scmService);
const repository = this.scmService.registerSCMProvider(provider);
this._repositories.set(handle, repository);

View file

@ -709,8 +709,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
get inputBox() {
return extHostSCM.getLastInputBox(extension)!; // Strict null override - Deprecated api
},
createSourceControl(id: string, label: string, rootUri?: vscode.Uri) {
return extHostSCM.createSourceControl(extension, id, label, rootUri);
createSourceControl(id: string, label: string, rootUri?: vscode.Uri, opts?: vscode.SourceControlOptions) {
return extHostSCM.createSourceControl(extension, id, label, rootUri, opts);
}
};

View file

@ -656,6 +656,10 @@ export interface MainThreadExtensionServiceShape extends IDisposable {
$onExtensionHostExit(code: number): void;
}
export interface SCMProviderProps {
readonly treeRendering: boolean;
}
export interface SCMProviderFeatures {
hasQuickDiffProvider?: boolean;
count?: number;
@ -689,7 +693,7 @@ export type SCMRawResourceSplices = [
];
export interface MainThreadSCMShape extends IDisposable {
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void;
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, props: SCMProviderProps): void;
$updateSourceControl(handle: number, features: SCMProviderFeatures): void;
$unregisterSourceControl(handle: number): void;

View file

@ -9,7 +9,7 @@ import { debounce } from 'vs/base/common/decorators';
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { asPromise } from 'vs/base/common/async';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto } from './extHost.protocol';
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, SCMProviderProps } from './extHost.protocol';
import { sortedDiff, equals } from 'vs/base/common/arrays';
import { comparePaths } from 'vs/base/common/comparers';
import * as vscode from 'vscode';
@ -452,10 +452,15 @@ class ExtHostSourceControl implements vscode.SourceControl {
private _commands: ExtHostCommands,
private _id: string,
private _label: string,
private _rootUri?: vscode.Uri
private _rootUri: vscode.Uri | undefined,
_props: SCMProviderProps
) {
if (!_extension.enableProposedApi && _props.treeRendering) {
throw new Error(`[${_extension.identifier.value}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${_extension.identifier.value}`);
}
this._inputBox = new ExtHostSCMInputBox(_extension, this._proxy, this.handle);
this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri);
this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, _props);
}
private updatedResourceGroups = new Set<ExtHostSourceControlResourceGroup>();
@ -517,6 +522,12 @@ class ExtHostSourceControl implements vscode.SourceControl {
}
}
function asProps(options: vscode.SourceControlOptions | undefined): SCMProviderProps {
return {
treeRendering: options && !!options.treeRendering || false
};
}
export class ExtHostSCM implements ExtHostSCMShape {
private static _handlePool: number = 0;
@ -576,11 +587,11 @@ export class ExtHostSCM implements ExtHostSCMShape {
});
}
createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined): vscode.SourceControl {
createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined, options?: vscode.SourceControlOptions): vscode.SourceControl {
this.logService.trace('ExtHostSCM#createSourceControl', extension.identifier.value, id, label, rootUri);
const handle = ExtHostSCM._handlePool++;
const sourceControl = new ExtHostSourceControl(extension, this._proxy, this._commands, id, label, rootUri);
const sourceControl = new ExtHostSourceControl(extension, this._proxy, this._commands, id, label, rootUri, asProps(options));
this._sourceControls.set(handle, sourceControl);
const sourceControls = this._sourceControlsByExtension.get(ExtensionIdentifier.toKey(extension.identifier)) || [];

View file

@ -11,7 +11,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm';
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@ -36,7 +36,7 @@ import { ThrottledDelayer, disposableTimeout } from 'vs/base/common/async';
import { INotificationService } from 'vs/platform/notification/common/notification';
import * as platform from 'vs/base/common/platform';
import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { ISequence, ISplice } from 'vs/base/common/sequence';
import { ISplice } from 'vs/base/common/sequence';
import { ResourceTree, IBranchNode, INode } from 'vs/base/common/resourceTree';
import { ObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree';
import { Iterator } from 'vs/base/common/iterator';
@ -402,6 +402,12 @@ class ViewModel {
get mode(): ViewModelMode { return this._mode; }
set mode(mode: ViewModelMode) {
mode = this.provider.treeRendering ? mode : ViewModelMode.List;
if (mode === this._mode) {
return;
}
this._mode = mode;
for (const item of this.items) {
@ -425,7 +431,7 @@ class ViewModel {
private disposables = new DisposableStore();
constructor(
private groups: ISequence<ISCMResourceGroup>,
private provider: ISCMProvider,
private tree: ObjectTree<TreeElement, FuzzyScore>,
private _mode: ViewModelMode,
@IEditorService protected editorService: IEditorService,
@ -484,8 +490,8 @@ class ViewModel {
setVisible(visible: boolean): void {
if (visible) {
this.visibilityDisposables = new DisposableStore();
this.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables);
this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.groups.elements });
this.provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables);
this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.provider.groups.elements });
if (typeof this.scrollTop === 'number') {
this.tree.scrollTop = this.scrollTop;
@ -533,8 +539,8 @@ class ViewModel {
}
// go backwards from last group
for (let i = this.groups.elements.length - 1; i >= 0; i--) {
const group = this.groups.elements[i];
for (let i = this.provider.groups.elements.length - 1; i >= 0; i--) {
const group = this.provider.groups.elements[i];
for (const resource of group.elements) {
if (isEqual(uri, resource.sourceUri)) {
@ -759,19 +765,25 @@ export class RepositoryPanel extends ViewletPanel {
this._register(this.tree.onContextMenu(this.onListContextMenu, this));
this._register(this.tree);
let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree;
let mode: ViewModelMode;
const rootUri = this.repository.provider.rootUri;
if (!this.repository.provider.treeRendering) {
mode = ViewModelMode.List;
} else {
mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree;
if (typeof rootUri !== 'undefined') {
const storageMode = this.storageService.get(`scm.repository.viewMode:${rootUri.toString()}`, StorageScope.WORKSPACE) as ViewModelMode;
const rootUri = this.repository.provider.rootUri;
if (typeof storageMode === 'string') {
mode = storageMode;
if (typeof rootUri !== 'undefined') {
const storageMode = this.storageService.get(`scm.repository.viewMode:${rootUri.toString()}`, StorageScope.WORKSPACE) as ViewModelMode;
if (typeof storageMode === 'string') {
mode = storageMode;
}
}
}
this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, mode);
this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider, this.tree, mode);
this._register(this.viewModel);
addClass(this.listContainer, 'file-icon-themable-tree');
@ -781,8 +793,10 @@ export class RepositoryPanel extends ViewletPanel {
this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this));
this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this));
this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel);
this._register(this.toggleViewModelModeAction);
if (this.repository.provider.treeRendering) {
this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel);
this._register(this.toggleViewModelModeAction);
}
this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this));

View file

@ -62,6 +62,7 @@ export interface ISCMProvider extends IDisposable {
readonly acceptInputCommand?: Command;
readonly statusBarCommands?: Command[];
readonly onDidChange: Event<void>;
readonly treeRendering: boolean;
getOriginalResource(uri: URI): Promise<URI | null>;
}