Merge from vscode 1b314ab317fbff7d799b21754326b7d849889ceb

This commit is contained in:
ADS Merger
2020-07-15 23:51:18 +00:00
parent aae013d498
commit 9d3f12d0b7
554 changed files with 15159 additions and 8223 deletions

View File

@@ -48,6 +48,8 @@ import { ISplice } from 'vs/base/common/sequence';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { createStyleSheet } from 'vs/base/browser/dom';
import { ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode } from 'vs/workbench/common/editor';
class DiffActionRunner extends ActionRunner {
@@ -1012,14 +1014,16 @@ function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) =>
export class DirtyDiffModel extends Disposable {
private _originalModel: ITextModel | null = null;
get original(): ITextModel | null { return this._originalModel; }
get modified(): ITextModel | null { return this._editorModel; }
private _originalModel: IResolvedTextFileEditorModel | null = null;
private _model: ITextFileEditorModel;
get original(): ITextModel | null { return this._originalModel?.textEditorModel || null; }
get modified(): ITextModel | null { return this._model.textEditorModel || null; }
private diffDelayer: ThrottledDelayer<IChange[] | null> | null;
private diffDelayer = new ThrottledDelayer<IChange[] | null>(200);
private _originalURIPromise?: Promise<URI | null>;
private repositoryDisposables = new Set<IDisposable>();
private readonly originalModelDisposables = this._register(new DisposableStore());
private _disposed = false;
private readonly _onDidChange = new Emitter<{ changes: IChange[], diff: ISplice<IChange>[] }>();
readonly onDidChange: Event<{ changes: IChange[], diff: ISplice<IChange>[] }> = this._onDidChange.event;
@@ -1027,22 +1031,27 @@ export class DirtyDiffModel extends Disposable {
private _changes: IChange[] = [];
get changes(): IChange[] { return this._changes; }
private _editorModel: ITextModel | null;
constructor(
editorModel: ITextModel,
textFileModel: IResolvedTextFileEditorModel,
@ISCMService private readonly scmService: ISCMService,
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
@ITextModelService private readonly textModelResolverService: ITextModelService
) {
super();
this._editorModel = editorModel;
this.diffDelayer = new ThrottledDelayer<IChange[]>(200);
this._model = textFileModel;
this._register(editorModel.onDidChangeContent(() => this.triggerDiff()));
this._register(textFileModel.textEditorModel.onDidChangeContent(() => this.triggerDiff()));
this._register(scmService.onDidAddRepository(this.onDidAddRepository, this));
scmService.repositories.forEach(r => this.onDidAddRepository(r));
this._register(this._model.onDidChangeEncoding(() => {
this.diffDelayer.cancel();
this._originalModel = null;
this._originalURIPromise = undefined;
this.setChanges([]);
this.triggerDiff();
}));
this.triggerDiff();
}
@@ -1069,11 +1078,11 @@ export class DirtyDiffModel extends Disposable {
return this.diffDelayer
.trigger(() => this.diff())
.then((changes: IChange[] | null) => {
if (!this._editorModel || this._editorModel.isDisposed() || !this._originalModel || this._originalModel.isDisposed()) {
if (this._disposed || this._model.isDisposed() || !this._originalModel || this._originalModel.isDisposed()) {
return; // disposed
}
if (this._originalModel.getValueLength() === 0) {
if (this._originalModel.textEditorModel.getValueLength() === 0) {
changes = [];
}
@@ -1081,23 +1090,27 @@ export class DirtyDiffModel extends Disposable {
changes = [];
}
const diff = sortedDiff(this._changes, changes, compareChanges);
this._changes = changes;
this._onDidChange.fire({ changes, diff });
this.setChanges(changes);
});
}
private setChanges(changes: IChange[]): void {
const diff = sortedDiff(this._changes, changes, compareChanges);
this._changes = changes;
this._onDidChange.fire({ changes, diff });
}
private diff(): Promise<IChange[] | null> {
return this.getOriginalURIPromise().then(originalURI => {
if (!this._editorModel || this._editorModel.isDisposed() || !originalURI) {
if (this._disposed || this._model.isDisposed() || !originalURI) {
return Promise.resolve([]); // disposed
}
if (!this.editorWorkerService.canComputeDirtyDiff(originalURI, this._editorModel.uri)) {
if (!this.editorWorkerService.canComputeDirtyDiff(originalURI, this._model.resource)) {
return Promise.resolve([]); // Files too large
}
return this.editorWorkerService.computeDirtyDiff(originalURI, this._editorModel.uri, false);
return this.editorWorkerService.computeDirtyDiff(originalURI, this._model.resource, false);
});
}
@@ -1107,7 +1120,7 @@ export class DirtyDiffModel extends Disposable {
}
this._originalURIPromise = this.getOriginalResource().then(originalUri => {
if (!this._editorModel) { // disposed
if (this._disposed) { // disposed
return null;
}
@@ -1116,17 +1129,23 @@ export class DirtyDiffModel extends Disposable {
return null;
}
if (this._originalModel && this._originalModel.uri.toString() === originalUri.toString()) {
if (this._originalModel && this._originalModel.resource.toString() === originalUri.toString()) {
return originalUri;
}
return this.textModelResolverService.createModelReference(originalUri).then(ref => {
if (!this._editorModel) { // disposed
if (this._disposed) { // disposed
ref.dispose();
return null;
}
this._originalModel = ref.object.textEditorModel;
this._originalModel = ref.object as IResolvedTextFileEditorModel;
const encoding = this._model.getEncoding();
if (encoding) {
this._originalModel.setEncoding(encoding, EncodingMode.Decode);
}
this.originalModelDisposables.clear();
this.originalModelDisposables.add(ref);
@@ -1144,11 +1163,11 @@ export class DirtyDiffModel extends Disposable {
}
private async getOriginalResource(): Promise<URI | null> {
if (!this._editorModel) {
if (this._disposed) {
return Promise.resolve(null);
}
const uri = this._editorModel.uri;
const uri = this._model.resource;
const providers = this.scmService.repositories.map(r => r.provider);
const rootedProviders = providers.filter(p => !!p.rootUri);
@@ -1203,14 +1222,9 @@ export class DirtyDiffModel extends Disposable {
dispose(): void {
super.dispose();
this._editorModel = null;
this._disposed = true;
this._originalModel = null;
if (this.diffDelayer) {
this.diffDelayer.cancel();
this.diffDelayer = null;
}
this.diffDelayer.cancel();
this.repositoryDisposables.forEach(d => dispose(d));
this.repositoryDisposables.clear();
}
@@ -1235,15 +1249,15 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor
private enabled = false;
private viewState: IViewState = { width: 3, visibility: 'always' };
private models: ITextModel[] = [];
private items: { [modelId: string]: DirtyDiffItem; } = Object.create(null);
private items = new Map<IResolvedTextFileEditorModel, DirtyDiffItem>();
private readonly transientDisposables = this._register(new DisposableStore());
private stylesheet: HTMLStyleElement;
constructor(
@IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IConfigurationService private readonly configurationService: IConfigurationService,
@ITextFileService private readonly textFileService: ITextFileService
) {
super();
this.stylesheet = createStyleSheet();
@@ -1313,9 +1327,12 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor
}
this.transientDisposables.clear();
this.models.forEach(m => this.items[m.id].dispose());
this.models = [];
this.items = Object.create(null);
for (const [, dirtyDiff] of this.items) {
dirtyDiff.dispose();
}
this.items.clear();
this.enabled = false;
}
@@ -1337,37 +1354,39 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor
})
// remove nulls and duplicates
.filter((m, i, a) => !!m && !!m.uri && a.indexOf(m, i + 1) === -1) as ITextModel[];
.filter((m, i, a) => !!m && !!m.uri && a.indexOf(m, i + 1) === -1)
const newModels = models.filter(o => this.models.every(m => o !== m));
const oldModels = this.models.filter(m => models.every(o => o !== m));
// only want resolved text file service models
.map(m => this.textFileService.files.get(m!.uri))
.filter(m => m?.isResolved()) as IResolvedTextFileEditorModel[];
const set = new Set(models);
const newModels = models.filter(o => !this.items.has(o));
const oldModels = [...this.items.keys()].filter(m => !set.has(m));
oldModels.forEach(m => this.onModelInvisible(m));
newModels.forEach(m => this.onModelVisible(m));
this.models = models;
}
private onModelVisible(editorModel: ITextModel): void {
const model = this.instantiationService.createInstance(DirtyDiffModel, editorModel);
const decorator = new DirtyDiffDecorator(editorModel, model, this.configurationService);
this.items[editorModel.id] = new DirtyDiffItem(model, decorator);
private onModelVisible(textFileModel: IResolvedTextFileEditorModel): void {
const model = this.instantiationService.createInstance(DirtyDiffModel, textFileModel);
const decorator = new DirtyDiffDecorator(textFileModel.textEditorModel, model, this.configurationService);
this.items.set(textFileModel, new DirtyDiffItem(model, decorator));
}
private onModelInvisible(editorModel: ITextModel): void {
this.items[editorModel.id].dispose();
delete this.items[editorModel.id];
private onModelInvisible(textFileModel: IResolvedTextFileEditorModel): void {
this.items.get(textFileModel)!.dispose();
this.items.delete(textFileModel);
}
getModel(editorModel: ITextModel): DirtyDiffModel | null {
const item = this.items[editorModel.id];
if (!item) {
return null;
for (const [model, diff] of this.items) {
if (model.textEditorModel.id === editorModel.id) {
return diff.model;
}
}
return item.model;
return null;
}
dispose(): void {

View File

@@ -15,9 +15,6 @@
.scm-view .count {
display: flex;
}
.scm-view .count {
margin-left: 6px;
}
@@ -33,12 +30,27 @@
flex-flow: nowrap;
}
.scm-view.hide-provider-counts .scm-provider > .count,
.scm-view.auto-provider-counts .scm-provider > .count[data-count="0"] {
display: none;
}
.scm-view .scm-provider > .label {
display: flex;
flex-shrink: 1;
overflow: hidden;
}
.scm-view .scm-provider > .label > .name {
font-weight: bold;
}
.scm-view .scm-provider > .label > .description {
opacity: 0.7;
margin-left: 0.5em;
font-size: 0.9em;
}
.scm-view .scm-provider > .actions {
flex: 1;
padding-left: 10px;
@@ -46,27 +58,30 @@
justify-content: flex-end;
}
.scm-view .scm-provider > .actions .monaco-action-bar .action-item {
margin-left: 4px;
text-overflow: ellipsis;
/**
* The following rules are very specific because of inline drop down menus
* https://github.com/microsoft/vscode/issues/101410
*/
.scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item {
padding-left: 4px;
display: flex;
align-items: center;
min-width: 14px;
min-width: 16px;
}
.scm-view .scm-provider > .actions .monaco-action-bar .action-label {
text-overflow: ellipsis;
.scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label,
.scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .monaco-dropdown > .dropdown-label > .action-label {
display: flex;
align-items: center;
overflow: hidden;
min-width: 14px; /* for flex */
min-width: 16px; /* for flex */
height: 100%;
margin: 0;
background-repeat: no-repeat;
background-position: center;
}
.scm-view .scm-provider > .actions .monaco-action-bar .action-label .codicon {
vertical-align: sub;
display: inline-flex;
}
.scm-view .scm-provider > .actions .monaco-action-bar .action-item:last-of-type {
.scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item:last-of-type {
padding-right: 0;
}

View File

@@ -7,39 +7,24 @@ import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { DirtyDiffWorkbenchController } from './dirtydiffDecorator';
import { ShowViewletAction } from 'vs/workbench/browser/viewlet';
import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { SCMStatusController } from './activity';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { SCMService } from 'vs/workbench/contrib/scm/common/scmService';
import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views';
import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry, IViewsService } from 'vs/workbench/common/views';
import { SCMViewPaneContainer } from 'vs/workbench/contrib/scm/browser/scmViewPaneContainer';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { Codicon } from 'vs/base/common/codicons';
import { SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane';
class OpenSCMViewletAction extends ShowViewletAction {
static readonly ID = VIEWLET_ID;
static readonly LABEL = localize('toggleSCMViewlet', "Show SCM");
constructor(id: string, label: string, @IViewletService viewletService: IViewletService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService) {
super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService);
}
}
ModesRegistry.registerLanguage({
id: 'scminput',
extensions: [],
@@ -73,23 +58,32 @@ viewsRegistry.registerViews([{
ctorDescriptor: new SyncDescriptor(SCMViewPane),
canToggleVisibility: true,
workspace: true,
canMoveView: true
canMoveView: true,
containerIcon: Codicon.sourceControl.classNames
}], viewContainer);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored);
// Register Action to Open Viewlet
Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions).registerWorkbenchAction(
SyncActionDescriptor.from(OpenSCMViewletAction, {
primary: 0,
win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G }
}),
'View: Show SCM',
localize('view', "View")
);
// Register Action to Open View
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: VIEWLET_ID,
description: { description: localize('toggleSCMViewlet', "Show SCM"), args: [] },
weight: KeybindingWeight.WorkbenchContrib,
primary: 0,
win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G },
handler: accessor => {
const viewsService = accessor.get(IViewsService);
if (viewsService.isViewVisible(VIEW_PANE_ID)) {
viewsService.closeView(VIEW_PANE_ID);
} else {
viewsService.openView(VIEW_PANE_ID);
}
}
});
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'scm',
@@ -136,13 +130,24 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
type: 'string',
enum: ['all', 'focused', 'off'],
enumDescriptions: [
localize('scm.countBadge.all', "Show the sum of all Source Control Providers count badges."),
localize('scm.countBadge.all', "Show the sum of all Source Control Provider count badges."),
localize('scm.countBadge.focused', "Show the count badge of the focused Source Control Provider."),
localize('scm.countBadge.off', "Disable the Source Control count badge.")
],
description: localize('scm.countBadge', "Controls the Source Control count badge."),
description: localize('scm.countBadge', "Controls the count badge on the Source Control icon on the Activity Bar."),
default: 'all'
},
'scm.providerCountBadge': {
type: 'string',
enum: ['hidden', 'auto', 'visible'],
enumDescriptions: [
localize('scm.providerCountBadge.hidden', "Hide Source Control Provider count badges."),
localize('scm.providerCountBadge.auto', "Only show count badge for Source Control Provider when non-zero."),
localize('scm.providerCountBadge.visible', "Show Source Control Provider count badges.")
],
description: localize('scm.providerCountBadge', "Controls the count badges on Source Control Provider headers. These headers only appear when there is more than one provider."),
default: 'hidden'
},
'scm.defaultViewMode': {
type: 'string',
enum: ['tree', 'list'],

View File

@@ -47,10 +47,10 @@ import { flatten } from 'vs/base/common/arrays';
import { memoize } from 'vs/base/common/decorators';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { toResource, SideBySideEditor } from 'vs/workbench/common/editor';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_BORDER, PANEL_BACKGROUND, PANEL_INPUT_BORDER } from 'vs/workbench/common/theme';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorConstructionOptions } from 'vs/editor/common/config/editorOptions';
import { IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IModelService } from 'vs/editor/common/services/modelService';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
@@ -59,7 +59,7 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
import * as platform from 'vs/base/common/platform';
import { escape, compare, format } from 'vs/base/common/strings';
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder, registerColor, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { Schemas } from 'vs/base/common/network';
@@ -76,9 +76,11 @@ import { ContextSubMenu } from 'vs/base/browser/contextmenu';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { Command } from 'vs/editor/common/modes';
import { renderCodicons } from 'vs/base/common/codicons';
import { renderCodicons, Codicon } from 'vs/base/common/codicons';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
@@ -151,6 +153,7 @@ interface ISCMLayout {
interface RepositoryTemplate {
readonly name: HTMLElement;
readonly description: HTMLElement;
readonly countContainer: HTMLElement;
readonly count: CountBadge;
readonly toolBar: ToolBar;
@@ -178,6 +181,7 @@ class RepositoryRenderer implements ICompressibleTreeRenderer<ISCMRepository, Fu
const provider = append(container, $('.scm-provider'));
const label = append(provider, $('.label'));
const name = append(label, $('span.name'));
const description = append(label, $('span.description'));
const actions = append(provider, $('.actions'));
const toolBar = new ToolBar(actions, this.contextMenuService, { actionViewItemProvider: this.actionViewItemProvider });
const countContainer = append(provider, $('.count'));
@@ -188,7 +192,7 @@ class RepositoryRenderer implements ICompressibleTreeRenderer<ISCMRepository, Fu
const disposable = Disposable.None;
const templateDisposable = combinedDisposable(visibilityDisposable, toolBar, badgeStyler);
return { name, countContainer, count, toolBar, disposable, templateDisposable };
return { name, description, countContainer, count, toolBar, disposable, templateDisposable };
}
renderElement(node: ITreeNode<ISCMRepository, FuzzyScore>, index: number, templateData: RepositoryTemplate, height: number | undefined): void {
@@ -199,8 +203,10 @@ class RepositoryRenderer implements ICompressibleTreeRenderer<ISCMRepository, Fu
if (repository.provider.rootUri) {
templateData.name.textContent = basename(repository.provider.rootUri);
templateData.description.textContent = repository.provider.label;
} else {
templateData.name.textContent = repository.provider.label;
templateData.description.textContent = '';
}
let statusPrimaryActions: IAction[] = [];
@@ -216,6 +222,7 @@ class RepositoryRenderer implements ICompressibleTreeRenderer<ISCMRepository, Fu
updateToolbar();
const count = repository.provider.count || 0;
templateData.countContainer.setAttribute('data-count', String(count));
templateData.count.setCount(count);
};
disposables.add(repository.provider.onDidChange(onDidChangeProvider, null));
@@ -265,6 +272,7 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
constructor(
private outerLayout: ISCMLayout,
private updateHeight: (input: ISCMInput, height: number) => void,
private focusTree: () => void,
@IInstantiationService private instantiationService: IInstantiationService,
) { }
@@ -272,10 +280,16 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
// hack
addClass(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement, 'force-no-twistie');
const disposables = new DisposableStore();
const inputElement = append(container, $('.scm-input'));
const inputWidget = this.instantiationService.createInstance(SCMInputWidget, inputElement);
disposables.add(inputWidget);
return { inputWidget, disposable: Disposable.None, templateDisposable: inputWidget };
const onKeyDown = Event.map(domEvent(container, 'keydown'), e => new StandardKeyboardEvent(e));
const onEscape = Event.filter(onKeyDown, e => e.keyCode === KeyCode.Escape);
disposables.add(onEscape(this.focusTree));
return { inputWidget, disposable: Disposable.None, templateDisposable: disposables };
}
renderElement(node: ITreeNode<ISCMInput, FuzzyScore>, index: number, templateData: InputTemplate): void {
@@ -297,22 +311,19 @@ class InputRenderer implements ICompressibleTreeRenderer<ISCMInput, FuzzyScore,
this.contentHeights.set(input, contentHeight);
if (lastContentHeight !== contentHeight) {
if (lastContentHeight !== undefined) {
this.updateHeight(input, contentHeight + 10);
templateData.inputWidget.layout();
} else if (contentHeight !== InputRenderer.DEFAULT_HEIGHT) {
// first time render, we must rerender on the next stack frame
const timeout = setTimeout(() => {
this.updateHeight(input, contentHeight + 10);
templateData.inputWidget.layout();
});
disposables.add({ dispose: () => clearTimeout(timeout) });
}
this.updateHeight(input, contentHeight + 10);
templateData.inputWidget.layout();
}
};
disposables.add(templateData.inputWidget.onDidChangeContentHeight(onDidChangeContentHeight));
onDidChangeContentHeight();
const startListeningContentHeightChange = () => {
disposables.add(templateData.inputWidget.onDidChangeContentHeight(onDidChangeContentHeight));
onDidChangeContentHeight();
};
// Setup height change listener on next tick
const timeout = disposableTimeout(startListeningContentHeightChange, 0);
disposables.add(timeout);
// Layout the editor whenever the outer layout happens
const layoutEditor = () => templateData.inputWidget.layout();
@@ -720,20 +731,20 @@ class SCMResourceIdentityProvider implements IIdentityProvider<TreeElement> {
getId(element: TreeElement): string {
if (ResourceTree.isResourceNode(element)) {
const group = element.context;
return `folder:${group.provider.contextValue}/${group.id}/$FOLDER/${element.uri.toString()}`;
return `folder:${group.provider.id}/${group.id}/$FOLDER/${element.uri.toString()}`;
} else if (isSCMRepository(element)) {
const provider = element.provider;
return `repo:${provider.contextValue}`;
return `repo:${provider.id}`;
} else if (isSCMInput(element)) {
const provider = element.repository.provider;
return `input:${provider.contextValue}`;
return `input:${provider.id}`;
} else if (isSCMResource(element)) {
const group = element.resourceGroup;
const provider = group.provider;
return `resource:${provider.contextValue}/${group.id}/${element.sourceUri.toString()}`;
return `resource:${provider.id}/${group.id}/${element.sourceUri.toString()}`;
} else {
const provider = element.provider;
return `group:${provider.contextValue}/${element.id}`;
return `group:${provider.id}/${element.id}`;
}
}
}
@@ -816,6 +827,10 @@ class ViewModel {
private readonly _onDidChangeMode = new Emitter<ViewModelMode>();
readonly onDidChangeMode = this._onDidChangeMode.event;
private _onDidChangeRepositoryCollapseState = new Emitter<void>();
readonly onDidChangeRepositoryCollapseState: Event<void>;
private visible: boolean = false;
get mode(): ViewModelMode { return this._mode; }
set mode(mode: ViewModelMode) {
this._mode = mode;
@@ -848,10 +863,11 @@ class ViewModel {
private visibilityDisposables = new DisposableStore();
private scrollTop: number | undefined;
private firstVisible = true;
private repositoryCollapseStates: Map<ISCMRepository, boolean> | undefined;
private disposables = new DisposableStore();
constructor(
private repositories: ISequence<ISCMRepository>,
readonly repositories: ISequence<ISCMRepository>,
private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>,
private menus: SCMMenus,
private inputRenderer: InputRenderer,
@@ -859,12 +875,17 @@ class ViewModel {
private _sortKey: ViewModelSortKey,
@IEditorService protected editorService: IEditorService,
@IConfigurationService protected configurationService: IConfigurationService,
) { }
) {
this.onDidChangeRepositoryCollapseState = Event.any(
this._onDidChangeRepositoryCollapseState.event,
Event.signal(Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element)))
);
}
private onDidSpliceRepositories({ start, deleteCount, toInsert }: ISplice<ISCMRepository>): void {
private _onDidSpliceRepositories({ start, deleteCount, toInsert }: ISplice<ISCMRepository>): void {
const itemsToInsert = toInsert.map(repository => {
const disposable = combinedDisposable(
repository.provider.groups.onDidSplice(splice => this.onDidSpliceGroups(item, splice)),
repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)),
repository.input.onDidChangeVisibility(() => this.refresh(item))
);
const groupItems = repository.provider.groups.elements.map(group => this.createGroupItem(group));
@@ -886,7 +907,7 @@ class ViewModel {
this.refresh();
}
private onDidSpliceGroups(item: IRepositoryItem, { start, deleteCount, toInsert }: ISplice<ISCMResourceGroup>): void {
private _onDidSpliceGroups(item: IRepositoryItem, { start, deleteCount, toInsert }: ISplice<ISCMResourceGroup>): void {
const itemsToInsert: IGroupItem[] = toInsert.map(group => this.createGroupItem(group));
const itemsToDispose = item.groupItems.splice(start, deleteCount, ...itemsToInsert);
@@ -902,7 +923,7 @@ class ViewModel {
const resources: ISCMResource[] = [...group.elements];
const disposable = combinedDisposable(
group.onDidChange(() => this.tree.refilter()),
group.onDidSplice(splice => this.onDidSpliceGroup(item, splice))
group.onDidSplice(splice => this._onDidSpliceGroup(item, splice))
);
const item: IGroupItem = { element: group, resources, tree, disposable };
@@ -916,7 +937,7 @@ class ViewModel {
return item;
}
private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice<ISCMResource>): void {
private _onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice<ISCMResource>): void {
const before = item.resources.length;
const deleted = item.resources.splice(start, deleteCount, ...toInsert);
const after = item.resources.length;
@@ -941,8 +962,9 @@ class ViewModel {
setVisible(visible: boolean): void {
if (visible) {
this.visibilityDisposables = new DisposableStore();
this.repositories.onDidSplice(this.onDidSpliceRepositories, this, this.visibilityDisposables);
this.onDidSpliceRepositories({ start: 0, deleteCount: 0, toInsert: this.repositories.elements });
this.repositories.onDidSplice(this._onDidSpliceRepositories, this, this.visibilityDisposables);
this._onDidSpliceRepositories({ start: 0, deleteCount: 0, toInsert: this.repositories.elements });
this.repositoryCollapseStates = undefined;
if (typeof this.scrollTop === 'number') {
this.tree.scrollTop = this.scrollTop;
@@ -952,10 +974,21 @@ class ViewModel {
this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables);
this.onDidActiveEditorChange();
} else {
if (this.items.length > 1) {
this.repositoryCollapseStates = new Map();
for (const item of this.items) {
this.repositoryCollapseStates.set(item.element, this.tree.isCollapsed(item.element));
}
}
this.visibilityDisposables.dispose();
this.onDidSpliceRepositories({ start: 0, deleteCount: this.items.length, toInsert: [] });
this._onDidSpliceRepositories({ start: 0, deleteCount: this.items.length, toInsert: [] });
this.scrollTop = this.tree.scrollTop;
}
this.visible = visible;
this._onDidChangeRepositoryCollapseState.fire();
}
private refresh(item?: IRepositoryItem | IGroupItem): void {
@@ -966,6 +999,8 @@ class ViewModel {
} else {
this.tree.setChildren(null, this.items.map(item => this.render(item)));
}
this._onDidChangeRepositoryCollapseState.fire();
}
private render(item: IRepositoryItem | IGroupItem): ICompressedTreeElement<TreeElement> {
@@ -981,7 +1016,8 @@ class ViewModel {
children.push(...item.groupItems.map(i => this.render(i)));
}
return { element: item.element, children, incompressible: true, collapsible: true };
const collapsed = this.repositoryCollapseStates?.get(item.element);
return { element: item.element, children, incompressible: true, collapsed, collapsible: hasSomeChanges };
} else {
const children = this.mode === ViewModelMode.List
? Iterable.map(item.resources, element => ({ element, incompressible: true }))
@@ -1056,6 +1092,10 @@ class ViewModel {
}
getViewSecondaryActions(): IAction[] {
if (this.repositories.elements.length === 0) {
return [];
}
const viewAction = new SCMViewSubMenuAction(this);
if (this.repositories.elements.length !== 1) {
@@ -1080,6 +1120,38 @@ class ViewModel {
return this.repositories.elements[0].provider;
}
collapseAllProviders(): void {
for (const repository of this.repositories.elements) {
if (this.tree.isCollapsible(repository)) {
this.tree.collapse(repository);
}
}
}
expandAllProviders(): void {
for (const repository of this.repositories.elements) {
if (this.tree.isCollapsible(repository)) {
this.tree.expand(repository);
}
}
}
isAnyProviderCollapsible(): boolean {
if (!this.visible || this.repositories.elements.length === 1) {
return false;
}
return this.repositories.elements.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r));
}
areAllProvidersCollapsed(): boolean {
if (!this.visible || this.repositories.elements.length === 1) {
return false;
}
return this.repositories.elements.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)));
}
dispose(): void {
this.visibilityDisposables.dispose();
this.disposables.dispose();
@@ -1464,6 +1536,34 @@ class SCMInputWidget extends Disposable {
}
}
class SCMCollapseAction extends Action {
private allCollapsed = false;
constructor(private viewModel: ViewModel) {
super('scm.collapse', undefined, undefined, true);
this._register(viewModel.onDidChangeRepositoryCollapseState(this.update, this));
this.update();
}
async run(): Promise<void> {
if (this.allCollapsed) {
this.viewModel.expandAllProviders();
} else {
this.viewModel.collapseAllProviders();
}
}
private update(): void {
const isAnyProviderCollapsible = this.viewModel.isAnyProviderCollapsible();
this.enabled = isAnyProviderCollapsible;
this.allCollapsed = isAnyProviderCollapsible && this.viewModel.areAllProvidersCollapsed();
this.label = this.allCollapsed ? localize('expand all', "Expand All Providers") : localize('collapse all', "Collapse All Providers");
this.class = this.allCollapsed ? Codicon.expandAll.classNames : Codicon.collapseAll.classNames;
}
}
export class SCMViewPane extends ViewPane {
private _onDidLayout = new Emitter<void>();
@@ -1513,6 +1613,14 @@ export class SCMViewPane extends ViewPane {
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility);
updateActionsVisibility();
const updateProviderCountVisibility = () => {
const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge');
toggleClass(this.listContainer, 'hide-provider-counts', value === 'hidden');
toggleClass(this.listContainer, 'auto-provider-counts', value === 'auto');
};
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'))(updateProviderCountVisibility);
updateProviderCountVisibility();
const repositories = new SimpleSequence(this.scmService.repositories, this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository);
this._register(repositories);
@@ -1521,7 +1629,7 @@ export class SCMViewPane extends ViewPane {
this._register(repositories.onDidSplice(() => this.updateActions()));
this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, (input, height) => this.tree.updateElementHeight(input, height));
this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, (input, height) => this.tree.updateElementHeight(input, height), () => this.tree.domFocus());
const delegate = new ProviderListDelegate(this.inputRenderer);
const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action);
@@ -1634,7 +1742,14 @@ export class SCMViewPane extends ViewPane {
return [];
}
return this.viewModel.getViewActions();
if (this.viewModel.repositories.elements.length < 2) {
return this.viewModel.getViewActions();
}
return [
new SCMCollapseAction(this.viewModel),
...this.viewModel.getViewActions()
];
}
getSecondaryActions(): IAction[] {
@@ -1768,13 +1883,15 @@ export class SCMViewPane extends ViewPane {
}
}
export const scmProviderSeparatorBorderColor = registerColor('scm.providerBorder', { dark: '#454545', light: '#C8C8C8', hc: contrastBorder }, localize('scm.providerBorder', "SCM Provider separator border."));
registerThemingParticipant((theme, collector) => {
const inputBackgroundColor = theme.getColor(inputBackground);
if (inputBackgroundColor) {
collector.addRule(`.scm-view .scm-editor-container .monaco-editor-background,
.scm-view .scm-editor-container .monaco-editor,
.scm-view .scm-editor-container .monaco-editor .margin
{ background-color: ${inputBackgroundColor}; }`);
{ background-color: ${inputBackgroundColor} !important; }`);
}
const inputForegroundColor = theme.getColor(inputForeground);
@@ -1787,6 +1904,11 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`.scm-view .scm-editor-container { outline: 1px solid ${inputBorderColor}; }`);
}
const panelInputBorder = theme.getColor(PANEL_INPUT_BORDER);
if (panelInputBorder) {
collector.addRule(`.monaco-workbench .part.panel .scm-view .scm-editor-container { outline: 1px solid ${panelInputBorder}; }`);
}
const focusBorderColor = theme.getColor(focusBorder);
if (focusBorderColor) {
collector.addRule(`.scm-view .scm-editor-container.synthetic-focus { outline: 1px solid ${focusBorderColor}; }`);
@@ -1799,7 +1921,7 @@ registerThemingParticipant((theme, collector) => {
const inputValidationInfoBorderColor = theme.getColor(inputValidationInfoBorder);
if (inputValidationInfoBorderColor) {
collector.addRule(`.scm-view .scm-editor-container.validation-info { outline: 1px solid ${inputValidationInfoBorderColor}; }`);
collector.addRule(`.scm-view .scm-editor-container.validation-info { outline: 1px solid ${inputValidationInfoBorderColor} !important; }`);
collector.addRule(`.scm-editor-validation.validation-info { border-color: ${inputValidationInfoBorderColor}; }`);
}
@@ -1815,7 +1937,7 @@ registerThemingParticipant((theme, collector) => {
const inputValidationWarningBorderColor = theme.getColor(inputValidationWarningBorder);
if (inputValidationWarningBorderColor) {
collector.addRule(`.scm-view .scm-editor-container.validation-warning { outline: 1px solid ${inputValidationWarningBorderColor}; }`);
collector.addRule(`.scm-view .scm-editor-container.validation-warning { outline: 1px solid ${inputValidationWarningBorderColor} !important; }`);
collector.addRule(`.scm-editor-validation.validation-warning { border-color: ${inputValidationWarningBorderColor}; }`);
}
@@ -1831,7 +1953,7 @@ registerThemingParticipant((theme, collector) => {
const inputValidationErrorBorderColor = theme.getColor(inputValidationErrorBorder);
if (inputValidationErrorBorderColor) {
collector.addRule(`.scm-view .scm-editor-container.validation-error { outline: 1px solid ${inputValidationErrorBorderColor}; }`);
collector.addRule(`.scm-view .scm-editor-container.validation-error { outline: 1px solid ${inputValidationErrorBorderColor} !important; }`);
collector.addRule(`.scm-editor-validation.validation-error { border-color: ${inputValidationErrorBorderColor}; }`);
}