mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-07 15:26:49 +01:00
wip: scm folder menus
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user