wip: scm folder menus

This commit is contained in:
Joao Moreno
2019-09-18 09:49:35 +02:00
parent 701b16af3b
commit bcd7bcedff
6 changed files with 150 additions and 60 deletions
+47
View File
@@ -850,6 +850,53 @@
"group": "inline"
}
],
"scm/resourceFolder/context": [
{
"command": "git.stage",
"when": "scmProvider == git && scmResourceGroup == merge",
"group": "1_modification"
},
{
"command": "git.stage",
"when": "scmProvider == git && scmResourceGroup == merge",
"group": "inline"
},
{
"command": "git.unstage",
"when": "scmProvider == git && scmResourceGroup == index",
"group": "1_modification"
},
{
"command": "git.unstage",
"when": "scmProvider == git && scmResourceGroup == index",
"group": "inline"
},
{
"command": "git.stage",
"when": "scmProvider == git && scmResourceGroup == workingTree",
"group": "1_modification"
},
{
"command": "git.clean",
"when": "scmProvider == git && scmResourceGroup == workingTree && !gitFreshRepository",
"group": "1_modification"
},
{
"command": "git.clean",
"when": "scmProvider == git && scmResourceGroup == workingTree && !gitFreshRepository",
"group": "inline"
},
{
"command": "git.stage",
"when": "scmProvider == git && scmResourceGroup == workingTree",
"group": "inline"
},
{
"command": "git.ignore",
"when": "scmProvider == git && scmResourceGroup == workingTree",
"group": "1_modification@3"
}
],
"scm/resourceState/context": [
{
"command": "git.stage",
+14 -1
View File
@@ -20,6 +20,7 @@ export interface IBranchNode<T> {
readonly name: string;
readonly size: number;
readonly children: Iterator<INode<T>>;
readonly parent: IBranchNode<T> | undefined;
get(childName: string): INode<T> | undefined;
}
@@ -47,6 +48,10 @@ class BranchNode<T> extends Node implements IBranchNode<T> {
return Iterator.fromIterableIterator(this._children.values());
}
constructor(path: string, readonly parent: IBranchNode<T> | undefined = undefined) {
super(path);
}
get(path: string): BranchNode<T> | LeafNode<T> | undefined {
return this._children.get(path);
}
@@ -75,6 +80,14 @@ export class ResourceTree<T extends NonNullable<any>> {
return obj instanceof BranchNode;
}
static getRoot<T>(node: IBranchNode<T>): IBranchNode<T> {
while (node.parent) {
node = node.parent;
}
return node;
}
constructor(private rootURI: URI = URI.file('/')) { }
add(uri: URI, element: T): void {
@@ -91,7 +104,7 @@ export class ResourceTree<T extends NonNullable<any>> {
if (!child) {
if (i < parts.length - 1) {
child = new BranchNode(path);
child = new BranchNode(path, node);
node.set(name, child);
} else {
child = new LeafNode(path, element);
@@ -87,6 +87,7 @@ export const enum MenuId {
ProblemsPanelContext,
SCMChangeContext,
SCMResourceContext,
SCMResourceFolderContext,
SCMResourceGroupContext,
SCMSourceControl,
SCMTitle,
@@ -39,8 +39,9 @@ namespace schema {
case 'menuBar/file': return MenuId.MenubarFileMenu;
case 'scm/title': return MenuId.SCMTitle;
case 'scm/sourceControl': return MenuId.SCMSourceControl;
case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext;
case 'scm/resourceState/context': return MenuId.SCMResourceContext;
case 'scm/resourceFolder/context': return MenuId.SCMResourceFolderContext;
case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext;
case 'scm/change/title': return MenuId.SCMChangeContext;
case 'statusBar/windowIndicator': return MenuId.StatusBarWindowIndicatorMenu;
case 'view/title': return MenuId.ViewTitle;
+25 -18
View File
@@ -5,7 +5,7 @@
import 'vs/css!./media/scmViewlet';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
import { IAction } from 'vs/base/common/actions';
@@ -20,13 +20,15 @@ function actionEquals(a: IAction, b: IAction): boolean {
return a.id === b.id;
}
interface ISCMResourceGroupMenuEntry extends IDisposable {
interface ISCMResourceGroupMenuEntry {
readonly group: ISCMResourceGroup;
readonly disposable: IDisposable;
}
interface ISCMMenus {
readonly resourceGroupMenu: IMenu;
readonly resourceMenu: IMenu;
readonly resourceFolderMenu: IMenu;
}
export function getSCMResourceContextKey(resource: ISCMResourceGroup | ISCMResource): string {
@@ -48,7 +50,7 @@ export class SCMMenus implements IDisposable {
private readonly resourceGroupMenuEntries: ISCMResourceGroupMenuEntry[] = [];
private readonly resourceGroupMenus = new Map<ISCMResourceGroup, ISCMMenus>();
private readonly disposables: IDisposable[] = [];
private readonly disposables = new DisposableStore();
constructor(
provider: ISCMProvider | undefined,
@@ -68,7 +70,7 @@ export class SCMMenus implements IDisposable {
}
this.titleMenu = this.menuService.createMenu(MenuId.SCMTitle, this.contextKeyService);
this.disposables.push(this.titleMenu);
this.disposables.add(this.titleMenu);
this.titleMenu.onDidChange(this.updateTitleActions, this, this.disposables);
this.updateTitleActions();
@@ -109,6 +111,10 @@ export class SCMMenus implements IDisposable {
return this.getActions(MenuId.SCMResourceContext, resource).secondary;
}
getResourceFolderContextActions(group: ISCMResourceGroup): IAction[] {
return this.getActions(MenuId.SCMResourceFolderContext, group).secondary;
}
private getActions(menuId: MenuId, resource: ISCMResourceGroup | ISCMResource): { primary: IAction[]; secondary: IAction[]; } {
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('scmResourceGroup', getSCMResourceContextKey(resource));
@@ -141,6 +147,14 @@ export class SCMMenus implements IDisposable {
return this.resourceGroupMenus.get(group)!.resourceMenu;
}
getResourceFolderMenu(group: ISCMResourceGroup): IMenu {
if (!this.resourceGroupMenus.has(group)) {
throw new Error('SCM Resource Group menu not found');
}
return this.resourceGroupMenus.get(group)!.resourceFolderMenu;
}
private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice<ISCMResourceGroup>): void {
const menuEntriesToInsert = toInsert.map<ISCMResourceGroupMenuEntry>(group => {
const contextKeyService = this.contextKeyService.createScoped();
@@ -149,30 +163,23 @@ export class SCMMenus implements IDisposable {
const resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService);
const resourceMenu = this.menuService.createMenu(MenuId.SCMResourceContext, contextKeyService);
const resourceFolderMenu = this.menuService.createMenu(MenuId.SCMResourceFolderContext, contextKeyService);
const disposable = combinedDisposable(contextKeyService, resourceGroupMenu, resourceMenu, resourceFolderMenu);
this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu });
return {
group,
dispose() {
contextKeyService.dispose();
resourceGroupMenu.dispose();
resourceMenu.dispose();
}
};
this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu, resourceFolderMenu });
return { group, disposable };
});
const deleted = this.resourceGroupMenuEntries.splice(start, deleteCount, ...menuEntriesToInsert);
for (const entry of deleted) {
this.resourceGroupMenus.delete(entry.group);
entry.dispose();
entry.disposable.dispose();
}
}
dispose(): void {
dispose(this.disposables);
dispose(this.resourceGroupMenuEntries);
this.resourceGroupMenus.clear();
this.disposables.dispose();
this.resourceGroupMenuEntries.forEach(e => e.disposable.dispose());
}
}
@@ -54,8 +54,8 @@ interface ResourceGroupTemplate {
name: HTMLElement;
count: CountBadge;
actionBar: ActionBar;
elementDisposable: IDisposable;
dispose: () => void;
elementDisposables: IDisposable;
disposables: IDisposable;
}
class ResourceGroupRenderer implements ICompressibleTreeRenderer<ISCMResourceGroup, FuzzyScore, ResourceGroupTemplate> {
@@ -77,18 +77,14 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer<ISCMResourceGro
const countContainer = append(element, $('.count'));
const count = new CountBadge(countContainer);
const styler = attachBadgeStyler(count, this.themeService);
const elementDisposable = Disposable.None;
const elementDisposables = Disposable.None;
const disposables = combinedDisposable(actionBar, styler);
return {
name, count, actionBar, elementDisposable, dispose: () => {
actionBar.dispose();
styler.dispose();
}
};
return { name, count, actionBar, elementDisposables, disposables };
}
renderElement(node: ITreeNode<ISCMResourceGroup, FuzzyScore>, index: number, template: ResourceGroupTemplate): void {
template.elementDisposable.dispose();
template.elementDisposables.dispose();
const group = node.element;
template.name.textContent = group.label;
@@ -102,7 +98,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer<ISCMResourceGro
disposables.add(group.onDidSplice(updateCount, null));
updateCount();
template.elementDisposable = disposables;
template.elementDisposables = disposables;
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ISCMResourceGroup>, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void {
@@ -110,11 +106,11 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer<ISCMResourceGro
}
disposeElement(group: ITreeNode<ISCMResourceGroup, FuzzyScore>, index: number, template: ResourceGroupTemplate): void {
template.elementDisposable.dispose();
template.elementDisposables.dispose();
}
disposeTemplate(template: ResourceGroupTemplate): void {
template.dispose();
template.disposables.dispose();
}
}
@@ -124,8 +120,8 @@ interface ResourceTemplate {
fileLabel: IResourceLabel;
decorationIcon: HTMLElement;
actionBar: ActionBar;
elementDisposable: IDisposable;
dispose: () => void;
elementDisposables: IDisposable;
disposables: IDisposable;
}
class MultipleSelectionActionRunner extends ActionRunner {
@@ -175,18 +171,15 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IBran
});
const decorationIcon = append(element, $('.decoration-icon'));
const disposables = combinedDisposable(actionBar, fileLabel);
return {
element, name, fileLabel, decorationIcon, actionBar, elementDisposable: Disposable.None, dispose: () => {
actionBar.dispose();
fileLabel.dispose();
}
};
return { element, name, fileLabel, decorationIcon, actionBar, elementDisposables: Disposable.None, disposables };
}
renderElement(node: ITreeNode<ISCMResource, FuzzyScore> | ITreeNode<IBranchNode<ISCMResource>, FuzzyScore>, index: number, template: ResourceTemplate): void {
template.elementDisposable.dispose();
template.elementDisposables.dispose();
const elementDisposables = new DisposableStore();
const resource = node.element;
const theme = this.themeService.getTheme();
const icon = ResourceTree.isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark);
@@ -201,13 +194,18 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IBran
fileKind,
matches: createMatches(node.filterData)
});
template.actionBar.clear();
template.actionBar.context = resource;
const disposables = new DisposableStore();
if (ResourceTree.isBranchNode(resource)) {
const group = viewModel.getResourceGroupOf(resource);
if (!ResourceTree.isBranchNode(resource)) {
disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar));
if (group) {
elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(group), template.actionBar));
}
} else {
elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar));
toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough);
toggleClass(template.element, 'faded', resource.decorations.faded);
}
@@ -224,43 +222,50 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IBran
}
template.element.setAttribute('data-tooltip', tooltip);
template.elementDisposable = disposables;
template.elementDisposables = elementDisposables;
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ISCMResource> | ICompressedTreeNode<IBranchNode<ISCMResource>>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void {
template.elementDisposable.dispose();
template.elementDisposables.dispose();
const disposables = new DisposableStore();
const compressed = node.element as ICompressedTreeNode<IBranchNode<ISCMResource>>;
const resource = compressed.elements[compressed.elements.length - 1];
const branchNode = compressed.elements[compressed.elements.length - 1];
const label = compressed.elements.map(e => e.name).join('/');
const uri = URI.file(resource.path);
const uri = URI.file(branchNode.path);
const fileKind = FileKind.FOLDER;
template.fileLabel.setResource({ resource: uri, name: label }, {
fileDecorations: { colors: false, badges: true },
fileKind,
matches: createMatches(node.filterData)
});
template.actionBar.clear();
template.actionBar.context = resource;
const disposables = new DisposableStore();
template.actionBar.clear();
template.actionBar.context = 'what'; // TODO
const viewModel = this.viewModelProvider();
const group = viewModel.getResourceGroupOf(branchNode);
if (group) {
disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(group), template.actionBar));
}
template.decorationIcon.style.display = 'none';
template.decorationIcon.style.backgroundImage = '';
template.element.setAttribute('data-tooltip', resource.path);
template.elementDisposable = disposables;
template.element.setAttribute('data-tooltip', branchNode.path);
template.elementDisposables = disposables;
}
disposeElement(resource: ITreeNode<ISCMResource, FuzzyScore> | ITreeNode<IBranchNode<ISCMResource>, FuzzyScore>, index: number, template: ResourceTemplate): void {
template.elementDisposable.dispose();
template.elementDisposables.dispose();
}
disposeTemplate(template: ResourceTemplate): void {
template.elementDisposable.dispose();
template.dispose();
template.elementDisposables.dispose();
template.disposables.dispose();
}
}
@@ -459,6 +464,18 @@ class ViewModel {
}
}
getResourceGroupOf(node: IBranchNode<ISCMResource>): ISCMResourceGroup | undefined {
const root = ResourceTree.getRoot(node);
for (const item of this.items) {
if (item.tree.root === root) {
return item.group;
}
}
return undefined;
}
dispose(): void {
this.visibilityDisposables.dispose();
this.disposables.dispose();
@@ -766,10 +783,14 @@ export class RepositoryPanel extends ViewletPanel {
}
const element = e.element;
let actions: IAction[];
let actions: IAction[] = [];
if (ResourceTree.isBranchNode(element)) {
actions = [];
const group = this.viewModel.getResourceGroupOf(element);
if (group) {
actions = this.menus.getResourceFolderContextActions(group);
}
} else if (isSCMResource(element)) {
actions = this.menus.getResourceContextActions(element);
} else {